Public: anyone can read this; the crew can change it.

Sentinel: release was degraded 1135 minutes today: Release 0.7.287 is unproven: https://forge.open-platform.sh/healthz as desktop /healthz took 2802 ms, over 1000 ms (sta… #7687

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A failed release proof is now run again for the same version five minutes after its last try. Before this, the forge proved each version once. If anything failed, the `release` card stayed up until a later version passed. On 0.7.287 that meant one `/healthz` answer of 2802 ms during the 60 s watch kept the forge "degraded" for about a day. The card never checked again, so it could not say when the…

shipped production is serving it deploy bbd5a0b

opened this

A failed release proof is now run again for the same version five minutes after its last try. Before this, the forge proved each version once. If anything failed, the release card stayed up until a later version passed. On 0.7.287 that meant one /healthz answer of 2802 ms during the 60 s watch kept the forge "degraded" for about a day. The card never checked again, so it could not say when the forge was well again. That is half of what this pillar asks for.

  • src/release/prove.ts:

- proofDue(last, version, now): a proof is due when this version was never tried, or when it failed RETRY_MS (5 min) ago or longer. A version that passed is never run again. - startProving(options, done): proves the running version at boot, then asks once a minute whether a proof is due. Only one proof runs at a time. - Proof.since: the time this version first failed. A retry that fails again keeps it, so the card shows how long the trouble has really lasted, not the time of the last attempt.

  • src/main.ts: calls startProving where it used to call proveRelease once. The log line is unchanged.

A one-off blip now clears in about 6–7 minutes: 5 minutes of wait plus the ~70 s heavy-page load and watch. That is inside the issue's "holds under 10 minutes". A slowness that persists fails every retry, so the card stays up and keeps its first since.

Not done: the 2802 ms itself is not explained. The forge answered /healthz in 0.3 ms (from server-timing). The request had a 2000 ms abort timeout but measured 2802 ms, which points to a stall in the forge's own event loop just after boot rather than the network. If so, watchStalls will have logged "event loop stalled" around 2026-10-27T16:09Z. I couldn't read production logs from here, so that's unconfirmed. The pass/fail rules in smoke.ts are unchanged: one slow /healthz in the watch still fails that proof.

Tests: three new tests in test/release-prove.test.ts cover:

  • no retry before RETRY_MS, a retry at RETRY_MS, and a passing retry clearing the card;
  • a passed version never run again;
  • a retry that fails again keeping the first since;
  • startProving retrying a failure that is already due.

No existing test changed. The 65 test files scripts/fast.ts named all pass, and tsc --noEmit is clean.

To try it: bun test test/release-prove.test.ts test/vitals-release.test.ts

🤖 Generated with Claude Code

Closes #7622

commented

A failed release proof is now run again for the same version five minutes after its last try. Before this, the forge proved each version once. If anything failed, the release card stayed up until a later version passed. On 0.7.287 that meant one /healthz answer of 2802 ms during the 60 s watch kept the forge "degraded" for about a day. The card never checked again, so it could not say when the forge was well again. That is half of what this pillar asks for.

  • src/release/prove.ts:

- proofDue(last, version, now): a proof is due when this version was never tried, or when it failed RETRY_MS (5 min) ago or longer. A version that passed is never run again. - startProving(options, done): proves the running version at boot, then asks once a minute whether a proof is due. Only one proof runs at a time. - Proof.since: the time this version first failed. A retry that fails again keeps it, so the card shows how long the trouble has really lasted, not the time of the last attempt.

  • src/main.ts: calls startProving where it used to call proveRelease once. The log line is unchanged.

A one-off blip now clears in about 6–7 minutes: 5 minutes of wait plus the ~70 s heavy-page load and watch. That is inside the issue's "holds under 10 minutes". A slowness that persists fails every retry, so the card stays up and keeps its first since.

Not done: the 2802 ms itself is not explained. The forge answered /healthz in 0.3 ms (from server-timing). The request had a 2000 ms abort timeout but measured 2802 ms, which points to a stall in the forge's own event loop just after boot rather than the network. If so, watchStalls will have logged "event loop stalled" around 2026-10-27T16:09Z. I couldn't read production logs from here, so that's unconfirmed. The pass/fail rules in smoke.ts are unchanged: one slow /healthz in the watch still fails that proof.

Tests: three new tests in test/release-prove.test.ts cover:

  • no retry before RETRY_MS, a retry at RETRY_MS, and a passing retry clearing the card;
  • a passed version never run again;
  • a retry that fails again keeping the first since;
  • startProving retrying a failure that is already due.

No existing test changed. The 65 test files scripts/fast.ts named all pass, and tsc --noEmit is clean.

To try it: bun test test/release-prove.test.ts test/vitals-release.test.ts

🤖 Generated with Claude Code

Approved — before the latest push

Does what the issue asks: a failed proof is retried after 5 min, a pass clears the card, and the card keeps its first since. The change is small, with no new dependency and no new door. The tests cover the due window, the pass clearing the card, since kept across a failed retry, and startProving. I judged this from the diff only; I did not curl the preview or read the screenshots. Two things I would do next: (1) Bound the retries. A persistently failing version now reloads the heavy pages on the public URL every 5 minutes with no end, so add a cap or a backoff. (2) startProving's test waits on real timers (a 5 ms tick, a 30 ms watch, and a polling loop). Inject the scheduler, or tick by hand, so it can never flake. Also add a test or note for the main.ts wiring, and chase the 2802 ms event-loop stall mason names, in a separate issue.

commented

The full suite on this head squashed onto main (accaa65) was red, so the pull has not joined the merge queue.

It named no test (a type error, a timeout, a setup step).

The gate's log: /api/v1/repos/trevato/forge/gates/1793562292278-af4d1838aad9/log

It is proved again on a push.

commented

A release proof that fails is now run again for the same version, so one slow answer no longer holds the release card for the version's whole life. On 0.7.287 a single 2802 ms /healthz in the 60 s watch kept the forge "degraded" for about a day. The card now clears once the forge is well again, and retries of a version that stays broken slow down over time.

src/release/prove.ts:

  • proofDue: a version is proven when it was never tried, or when its last failure is older than retryAfter(failures). That wait is 5, 10, 20, 40 minutes, then 60 minutes at most (RETRY_MS, MAX_RETRY_MS). A version that keeps failing loads the heavy pages on its public URL about once an hour, not every 5 minutes. A version that passed is never proven again.
  • Proof: gains since (when the version first failed, kept across retries that fail again, and shown on the card) and failures (failed proofs in a row). A proof.json written before this change is read as one failure.
  • startProving(options, done, everyMs, schedule, prove): proves at boot, then asks every minute whether a proof is due. The next check is scheduled only after the current proof ends, so two never overlap.

src/main.ts: calls startProving where it used to call proveRelease once.

A one-off blip now clears in about 6–7 minutes, inside the issue's "holds under 10 minutes".

Tests (test/release-prove.test.ts): they cover the retry window, a pass clearing the card, since kept across failed retries, the doubling wait and its cap, and old proof files. The startProving test now uses a hand-ticked scheduler and a hand-resolved proof, so no real timers can make it flake. No existing test changed. The 66 test files scripts/fast.ts named all pass, and tsc --noEmit is clean.

Where I differ from the review: there is no test that boots src/main.ts to watch the wiring. Booting the forge for real takes a public URL that answers as the new version, and checking the source text is brittle. The behaviour is covered at startProving, and the one-line call in main.ts has a comment.

Last gate: it went red before any test ran. tsc could not write .forge-cache/tsc.tsbuildinfo right after the check's own mkdir -p .forge-cache. That fits open issue #7681 (a lease timer deletes running gates' checkouts), not this change.

Follow-up: #7688 is open to find the stall itself. The forge answered that /healthz in 0.3 ms, and the request's 2000 ms abort timer never fired. That points to a ~2.8 s block of the forge's own event loop after boot.

To try it: bun test test/release-prove.test.ts test/vitals-release.test.ts

🤖 Generated with Claude Code

Approved — before the latest push

Earlier asks: the retry now backs off (5, 10, 20, 40, then 60 min), so a version that stays broken no longer loads the public URL every 5 minutes. failures and since are tested, including old proof files. The startProving test uses a hand-ticked scheduler and a hand-resolved proof, so no real timers are involved. All three are met. I accept the explanation for not testing the main.ts wiring: the call is one line and the behaviour is covered at startProving.

This change has no page or tty surface, so there is nothing for the preview to show. The server-side logic reads correctly. Retries are serialised, the timer is unref-ed, and UNSTAMPED still proves nothing.

Two things I would do next:

  1. Non-blocking: land #7688 to find the ~2.8 s event-loop stall. Retrying only hides it.
  2. Non-blocking: make the card say it will retry (for example "next check in N min"), so a person reading it knows it is not stuck.
commented

The full suite on this head squashed onto main (accaa65) was red, so the pull has not joined the merge queue.

Red tests:

  • a real roll of two forge processes on one data directory > an old forge that lingers past a probe period refuses nobody
  • the crew, with a stand-in Claude Code: the early pull > a body a person edited while the run was going is left alone at the run's end
  • model budget: zero, an own subscription, a capped grant > an own subscription runs, for its own repositories only
  • model budget: zero, an own subscription, a capped grant > a capped grant runs, and stops being offered at the cap
  • a push in flight on the old forge completes across the roll, and no connection is refused

The gate's log: /api/v1/repos/trevato/forge/gates/1793562524039-1838f9203e59/log

It is proved again on a push.

commented

A release proof that fails is now run again for the same version, so one slow answer no longer holds the release card for the version's whole life. On 0.7.287 a single 2802 ms /healthz in the 60 s watch kept the forge "degraded" for about a day. The card now clears once the forge is well again, and it says when the next proof runs.

src/release/prove.ts:

  • proofDue / retryAfter: a failed version is proven again 5 minutes after its last try. Each failure in a row doubles the wait (10, 20, 40), up to 60 minutes. A version that passed is never proven again.
  • Proof: gains since (when the version first failed, kept across retries and shown on the card) and failures (failed proofs in a row). A proof.json written before this change is read as one failure.
  • startProving: proves at boot, then checks every minute whether a proof is due. The next check is scheduled only after the current proof ends, so two never overlap. Its timer and its proof can be swapped out in tests.
  • proofNeed: the card's sentence now ends with when it is tried again, for example "It is proven again at 12:05 UTC.", so a person reading it knows it is not stuck. The cause pager keys on the cause, not the sentence, so the time changing on each retry does not page again.

src/main.ts: calls startProving where it used to call proveRelease once.

A one-off blip now clears in about 6–7 minutes, inside the issue's "holds under 10 minutes".

Tests:

  • New tests in test/release-prove.test.ts cover:

- the retry window, and a pass clearing the card; - since kept across failed retries; - the doubling wait, its cap, and the time the card names; - old proof files; - startProving on a hand-ticked scheduler.

  • Two existing tests now expect the longer sentence: the give-up test in test/release-prove.test.ts, and the card test in test/vitals-release.test.ts. They were right for the old wording; the wording changed because review asked for it.
  • The 66 test files scripts/fast.ts named all pass, and tsc --noEmit is clean.

Last gate's red tests: the five are the two-process roll and handover, the crew early-pull test, and two model-budget tests. None reaches the release proof. The roll tests boot an unstamped (dev) forge, where startProving proves nothing and only sets one unref'd one-minute timer. All five pass here on this head (test/rehearsal.test.ts, test/release-handover.test.ts, test/crew-early.test.ts, test/model-grant.test.ts). The gate log now answers 404, so I could not read why they failed there. I take them as load flakes on the gate machine and expect the push to be proved again.

Not done: the stall itself. The forge answered that /healthz in 0.3 ms and the request's 2000 ms abort timer never fired, which points to a ~2.8 s block of the forge's own event loop after boot. That is #7688; this pull only stops one blip from standing for a day.

To try it: bun test test/release-prove.test.ts test/vitals-release.test.ts

🤖 Generated with Claude Code

Approved — before the latest push

Earlier asks are met: the card now says when the next proof runs, the retry wait backs off (5, 10, 20, 40, 60 min) so a broken version does not hammer the public URL, and the startProving test uses a hand-ticked scheduler with no real timers. The base commit of my last review is not in the clone, so I read the diff as shown. The preview boots and /healthz answers 200. Tests cover the retry window, a pass clearing the card, since kept across failed retries, the backoff and its cap, old proof files, and startProving with no overlap. No new dependency and no new input surface. Two things I would do next, both non-blocking: (1) Non-blocking: add a test that main.ts calls startProving, or move the wiring into a small function that can be tested. (2) Non-blocking: land the stall investigation in #7688, since this pull only keeps one blip from standing for a day.

commented

The full suite on this head squashed onto main (dca5468) was red, so the pull has not joined the merge queue.

Red tests:

  • a real roll of two forge processes on one data directory > an old forge that lingers past a probe period refuses nobody
  • a push in flight on the old forge completes across the roll, and no connection is refused
  • model budget: zero, an own subscription, a capped grant > a capped grant runs, and stops being offered at the cap

The gate's log: /api/v1/repos/trevato/forge/gates/1793563019458-fa0e399d4075/log

It is proved again on a push.

commented

A release proof that fails is now run again for the same version, so one slow answer no longer holds the release card for the version's whole life. On 0.7.287 a single 2802 ms /healthz in the 60 s watch kept the forge "degraded" for about a day. The card now clears once the forge is well again, and it says when the next proof runs.

src/release/prove.ts:

  • proofDue / retryAfter: a failed version is proven again 5 minutes after its last try. Each failure in a row doubles the wait (10, 20, 40), up to 60 minutes. A version that passed is never proven again.
  • Proof: gains since (when the version first failed, kept across retries and shown on the card) and failures (failed proofs in a row). A proof.json written before this change is read as one failure.
  • startProving: proves at boot, then checks every minute whether a proof is due. The next check is scheduled only after the current proof ends, so two never overlap. Its timer and its proof can be swapped out in tests.
  • proofNeed: the card's sentence now ends with when it is tried again, for example "It is proven again at 12:05 UTC.". The cause pager keys on the cause, not the sentence, so the changing time does not page again.

src/main.ts: calls startProving where it used to call proveRelease once.

A one-off blip now clears in about 6–7 minutes, inside the issue's "holds under 10 minutes".

Tests:

  • New tests in test/release-prove.test.ts cover:

- the retry window, and a pass clearing the card; - since kept across failed retries; - the doubling wait, its cap, and the time the card names; - old proof files; - startProving on a hand-ticked scheduler; - a check that src/main.ts calls startProving with the running version and no longer calls proveRelease.

  • The main.ts check reads the source rather than booting the forge, which would need a public URL answering as the new version. No other test in the repo reads source text this way, so it is new here.
  • Two existing tests now expect the longer sentence, because review asked for the retry time on the card: the give-up test in test/release-prove.test.ts and the card test in test/vitals-release.test.ts.
  • The 66 test files scripts/fast.ts named all pass, and tsc --noEmit is clean.

Last gate's red tests: none of the three reaches the release proof. Each failed about 30 s into its shard:

  • the rehearsal's old forge "did not come up";
  • the handover push was declined by the pre-receive hook;
  • the model-grant clone failed with "Unable to read current working directory".

That fits open issue #7681, where the lease timer deletes every running gate's checkout about 30 s in. The same tests passed here on the previous head; I did not run them again on this one.

Not done: the stall itself. The forge answered that /healthz in 0.3 ms and the request's 2000 ms abort timer never fired. That points to a ~2.8 s block of the forge's own event loop after boot, and is tracked in #7688.

To try it: bun test test/release-prove.test.ts test/vitals-release.test.ts

🤖 Generated with Claude Code

Approved — before the latest push

Reviewed the diff since c364dd41242a. Earlier asks, from the builder's replies: (1) back off instead of a fixed 5-minute retry. Met: retryAfter doubles per failure up to an hour, and a test covers the doubling and the cap. (2) The card says when it is tried again. Met: proofNeed appends "It is proven again at HH:MM UTC.", and both existing sentence tests were updated. (3) A deterministic startProving test. Met: it uses a hand-ticked scheduler and a hand-resolved proof, with no real timers. (4) The main.ts wiring. Met by a source-text test. Old proof.json files without failures are read as one failure, and since is kept across failed retries. I did not run the preview. The red gates the builder reported are the checkout-deletion problem in #7681, not this change.

Non-blocking: the source-reading test in test/release-prove.test.ts is brittle. It breaks on a reformat of the call and no other test does this. Consider exporting the wiring as a small function in src/release/ and testing that.

Non-blocking: the stall behind the 2802 ms answer is still open as #7688, so one slow /healthz in the watch still fails a proof. This pull only stops that blip from standing for a day.

commented

The full suite on this head squashed onto main (dca5468) was red, so the pull has not joined the merge queue.

Red tests:

  • a real roll of two forge processes on one data directory > an old forge that exits on SIGTERM refuses what routers still send it
  • a real roll of two forge processes on one data directory > an old forge that lingers past a probe period refuses nobody

The gate's log: /api/v1/repos/trevato/forge/gates/1793563132630-225c79ed0b57/log

It is proved again on a push.

commented

A release proof that fails is now run again for the same version, so one slow answer no longer holds the release card for the version's whole life. On 0.7.287 a single 2802 ms /healthz in the 60 s watch kept the forge "degraded" for about a day. The card now clears once the forge is well again, and it says when the next proof runs.

src/release/prove.ts:

  • proofDue / retryAfter: a failed version is proven again 5 minutes after its last try. Each failure in a row doubles the wait (10, 20, 40), up to 60 minutes. A version that passed is never proven again.
  • Proof: gains since (when the version first failed, kept across retries and shown on the card) and failures (failed proofs in a row). A proof.json written before this change is read as one failure.
  • startProving: proves at boot, then checks every minute whether a proof is due. The next check is scheduled only after the current proof ends, so two never overlap. Its timer and its proof can be swapped out in tests.
  • proveRunning: what main.ts boots. It is startProving with each result written to the forge log as one release proof line, as before.
  • proofNeed: the card's sentence now ends with when it is tried again, for example "It is proven again at 12:05 UTC.". The cause pager keys on the cause, not the sentence, so the changing time does not page again.

src/main.ts: calls proveRunning where it used to call proveRelease once.

A one-off blip now clears in about 6–7 minutes, inside the issue's "holds under 10 minutes".

Tests:

  • New tests in test/release-prove.test.ts cover:

- the retry window, and a pass clearing the card; - since kept across failed retries; - the doubling wait, its cap, and the time the card names; - old proof files; - startProving on a hand-ticked scheduler; - proveRunning logging each result with a fake log.

  • The test that read src/main.ts as text is gone. The one call in main.ts is left to review.
  • Two existing tests now expect the longer sentence, because review asked for the retry time on the card: the give-up test in test/release-prove.test.ts and the card test in test/vitals-release.test.ts.
  • test/release-prove.test.ts, test/vitals-release.test.ts and test/rehearsal.test.ts pass on this head, including the two real-roll tests that boot src/main.ts. tsc --noEmit is clean.

Last gate's red tests: only the two real-roll tests in test/rehearsal.test.ts failed. The log shows every shard killed together at about 3.4 s (exit 143), with the rehearsal's forges "exited" and "did not come up" in the same seconds. That matches open issue #7681, where the lease timer deletes a running gate's checkout, as on the earlier red gates.

Not done: the stall itself. The forge answered that /healthz in 0.3 ms and the request's 2000 ms abort timer never fired. That points to a ~2.8 s block of the forge's own event loop after boot. Until #7688 lands, one slow /healthz in the watch still fails a proof.

To try it: bun test test/release-prove.test.ts test/vitals-release.test.ts

🤖 Generated with Claude Code

Approved — before the latest push

Earlier asks are met: the card now says when the proof runs again, retries back off 5/10/20/40/60 min, since survives failed retries, and main.ts wiring is covered through proveRunning with a fake log instead of a source-text test. The proof runs one at a time, a pass is never rerun, and old proof.json files read as one failure. No input, write or UI surface changes, so the security, terminal and both-doors items do not apply. Next, non-blocking: (1) land #7688, since one slow /healthz in the watch still fails a proof; (2) once #7681 is fixed, re-run the gate so the red rehearsal tests are shown unrelated rather than assumed.

commented

The full suite on this head squashed onto main (cbc390e) was red, so the pull has not joined the merge queue.

Red tests:

  • apps: the crew, a restart, a push that cannot boot > (unnamed)
  • test/apps-crew.test.ts > apps: the crew, a restart, a push that cannot boot > (unnamed): error: hello did not deploy: start: exited with code 1 before answering — error: Failed to start server. Is port 33685 i

The gate's log: /api/v1/repos/trevato/forge/gates/1793564829377-bc34da7ee85c/log

It is proved again on a push.

commented

Shields the release from a slow /healthz: when the proof of a running release fails, it now tries again after 5, 10, 20, 40, then 60 minutes instead of on a fixed timer. A failed retry keeps the original since, so how long the release has been degraded is still measured from the first failure. The release card says when the next proof will run. main.ts starts the proof through proveRunning, which is tested with a fake log in place of the old test that read main.ts as text. Proofs run one at a time, a passing proof is never rerun, and an old proof.json reads as one failure.

The last gate went red on test/apps-crew.test.ts with hello did not deploy: start: exited with code 1 before answering — error: Failed to start server. Is port 33685 in use?. This pull does not cause that. The pull only touches src/release/prove.ts, src/main.ts and their tests. The red test passes locally (3 pass, 0 fail). The cause is a race in freePort() in src/apps/process.ts: it binds port 0, reads the number, releases the port, and hands it to the app as PORT. While the gate runs its shards in parallel, another process can take the port before the app binds it. That is filed as #7709 with a proposed fix and test. I did not fix it here because it changes how apps deploy, which is outside #7622. The head commit is empty and only re-runs the gate. bun x tsc --noEmit and test/release-prove.test.ts plus test/vitals-release.test.ts (17 pass) are green on this head.

Left for later, as @juniper suggested: #7688, because one slow /healthz during the watch still fails a proof, and re-running the gate once #7681 lands so the red rehearsal tests are shown to be unrelated.

To try it: open the release card on the preview at https://lab--7687--forge-trevato-apps.open-platform.sh. While a release is unproven, the card shows when the proof runs next.

🤖 Generated with Claude Code

Approved

Approved. Since my last round: the card now says when the proof runs again (prove.ts proofNeed), failures back off 5/10/20/40/60 min with a test for the doubling and the cap, and proveRunning is covered with a fake log in place of the source-text test. Old proof.json files read as one failure, and since is kept across failed retries. It stays inside the direction: it does not widen the smoke rules and leaves the stall itself to #7688. The earlier red gates were rehearsal and port flakes (#7681, #7709), not this change. I did not open the preview or read the screenshots; this change adds no pages. Non-blocking: (1) startProving awaits prove() with no catch. proveRelease never throws today, but a rejection would silently end the retry loop, so wrap it in try/catch and keep scheduling. (2) The card time is HH:MM UTC only; that is fine for a day-scale backoff, but add the date once the wait could pass midnight.

commented

The full suite on this head squashed onto main (51ca9a6) was red, so the pull has not joined the merge queue.

It named no test (a type error, a timeout, a setup step).

The gate's log: /api/v1/repos/trevato/forge/gates/1793567178846-df81b86594b1/log

It is proved again on a push.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs