Public: anyone can read this; the crew can change it.
Sentinel: release was degraded 768 minutes today: Release 0.7.359 is unproven: https://forge.open-platform.sh/healthz as desktop /healthz took 2032 ms, over 1000 ms (sta… #9258
Check · Review · Land · Live done — Landed and live in production.
A release that fails its proof only because one answer was slow is now proven again within a minute per failure, instead of waiting out the full backoff. I haven't confirmed why `/healthz` took 2032 ms on 0.7.359. I looked only in the code and have no logs from that moment.
shipped production is serving it deploy 309b41e
A release that fails its proof only because one answer was slow is now proven again within a minute per failure, instead of waiting out the full backoff. I haven't confirmed why /healthz took 2032 ms on 0.7.359. I looked only in the code and have no logs from that moment.
The card stayed open for 768 minutes because a failed proof waits 5 minutes, then doubles each time up to an hour. The failure was a latency sample, /healthz took 2032 ms, over 1000 ms (status 200), so one load spike could keep the release marked unproven for hours.
- Change:
retryAfter(failures, failure?)insrc/release/prove.tsnow recognises a failure that is only a "took N ms, over M ms" answer. It retries aftermin(backoff, 60 s × failures). Any other failure keeps the existing 5-minute doubling backoff.proofNeedandproofDueboth pass the failure text. - Tests:
test/release-prove.test.tsgains one case for the slow and non-slow timings, and its existing.map(retryAfter)call is now.map((n) => retryAfter(n)). The release-proof tests pass,tsc --noEmitis clean andscripts/gate.tsprinted nothing. - Not done: I did not loosen the 1000 ms threshold or the smoke watch itself.
- Not done: I did not diagnose the 2032 ms; the next slow release's log will show whether the forge or the network was slow.
- Cost: a release that stays slow now costs its public URL a minute-long proof every few minutes, until the backoff takes over.
To check it after merge, watch whether release stays out of the degraded list for a day or holds under 10 minutes.
The fix is small and aimed at the right thing, but the wiring is untested and the slow-answer match is a loose regex over prose. Please fix in one round:
- Test the callers, not just
retryAfter. The new test callsretryAfter(n, slow)directly, so it still passes ifproofDueorproofNeedstops passingfailure, and that wiring is the whole fix. Add a case where a slow-only proof is due after about 60 s viaproofDue, and one whereproofNeed's sentence shows the shortened re-prove time. Both should fail without the change. - Tighten the match.
SLOW_FAILUREis an unanchored regex on the human-readable failure line (checkLineoutput). Anchor it to the shape smoke.ts produces, or better, recordslow: trueon theProofwhen the first failure is the slow kind. Then a reworded message or a URL containing that text cannot silently flip the backoff. If you keep the regex, add a test with a non-slow failure that mentions timing. - Move
SLOW_FAILUREaboveretryAfter. It is a const used before its declaration; it works only because the function runs later. - Update the doc comment on
retryAfter, which still says only 'doubled per failure'. The README and docs should describe the slow-answer rule if the release proof is documented anywhere (check docs/features).
Not a blocker: with the cap at min(backoff, 60 s × failures), a persistently slow release is re-proven at 1, 2, 3, 4 minutes until the doubling backoff takes over at failure 5. That is bounded, which is fine. State the intended cap in the comment.
I've pushed all four changes to the branch. The release-proof and vitals tests, tsc --noEmit and scripts/gate.ts all passed on the final tree; I didn't run the full suite.
What changed
- Slow flag instead of a regex:
- Failure in src/release/smoke.ts now carries slow?: boolean, set only where the smoke check reports "took N ms, over M ms".
- proveRelease writes slow: true onto the Proof.
- retryAfter(failures, slow) takes that boolean, so a reworded message or a URL containing "took … ms" cannot change the backoff. SLOW_FAILURE is deleted, which also settles the declaration-order point.
- Callers tested:
- proofDue has a case where a slow proof is due after 61 s, and the same proof with slow: false is not.
- proofNeed has a case where a slow proof shows "proven again at 03:01 UTC".
- In the smoke tests, a slow /healthz failure has slow === true, and a run with no failures leaves it unset.
- Comment and docs: The
retryAftercomment states the cap,min(backoff, 60 s × failures), which is 1, 2, 3, 4 minutes and then the doubling.docs/OPERATING.md("A release proves itself") describes the rule.
What I did not do
- No check for a non-slow failure whose text mentions timing: with a flag instead of a regex, that case cannot occur.
- I did not loosen the 1000 ms threshold or the smoke watch.
Approved. Slow-only proofs are now retried at 1, 2, 3 and 4 minutes before the doubling takes over. The slow flag is set on Failure in smoke.ts and carried through Proof into retryAfter. It does not parse the message text. I read the wiring: the watch runs only when no earlier check failed, so failures[0] is the slow one. proofDue and proofNeed both pass the flag, and the tests cover both callers and the unflagged case. OPERATING.md describes the rule. No UI or terminal surface is touched, and there is no new dependency. I did not run the tests or open the preview.
Two things I would do next:
- Record the forge-side time (Server-Timing) in the failure line, so the next slow release shows whether the forge or the network was slow.
- Add a vitals metric for how long a release stays unproven, so the day-later check Mason described is measurable.
A release that fails its proof only because one answer was slow is now proven again within a minute per failure, instead of waiting out the full backoff. I haven't confirmed why
/healthztook 2032 ms on 0.7.359. I looked only in the code and have no logs from that moment.The card stayed open for 768 minutes because a failed proof waits 5 minutes, then doubles each time up to an hour. The failure was a latency sample,
/healthz took 2032 ms, over 1000 ms (status 200), so one load spike could keep the release marked unproven for hours.retryAfter(failures, failure?)insrc/release/prove.tsnow recognises a failure that is only a "took N ms, over M ms" answer. It retries aftermin(backoff, 60 s × failures). Any other failure keeps the existing 5-minute doubling backoff.proofNeedandproofDueboth pass the failure text.test/release-prove.test.tsgains one case for the slow and non-slow timings, and its existing.map(retryAfter)call is now.map((n) => retryAfter(n)). The release-proof tests pass,tsc --noEmitis clean andscripts/gate.tsprinted nothing.To check it after merge, watch whether
releasestays out of the degraded list for a day or holds under 10 minutes.Closes #9238