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

Sentinel: shared:01m23vygyffx5hg2f5vm8r74sr:3hv73alvgf was degraded 63 minutes today: trevato/forge: "GET /apps/open names its phases and answers within its budget" failed on 4 pulls in the last hour — the… #7614

Merged opened this

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

Merged

I changed the `/apps/open` timing test so a loaded check machine can no longer fail it on a fixed 1.5 ms limit. I didn't reproduce the failure. This is my best reading of the cause. The file passes here, 5 tests, and `bun x tsc --noEmit` is clean.

shipped production is serving it deploy e406757

opened this

I changed the /apps/open timing test so a loaded check machine can no longer fail it on a fixed 1.5 ms limit. I didn't reproduce the failure. This is my best reading of the cause. The file passes here, 5 tests, and bun x tsc --noEmit is clean.

What changed: the speed budget in test/cloud-org-apps-timing.test.ts already scaled with a same-run baseline. The phase-accounting check did not: it required the time between the named phases and the whole request to stay within 1.5 ms. A load stall between two phases can break that without any code being slower. Off a quiet machine, the slack is now the larger of 1.5 ms and the same-run baseline (max(1.5, against)). With FORGE_TEST_QUIET=1 it stays at 1.5 ms. The shared budget.ts helper is unchanged.

What I left out: I didn't touch /settings/cloud/audit or /organizations/new, which share this loop and so get the same slack. I also didn't add a test for the slack itself, so it is only covered by the existing timing test.

To try it: bun test test/cloud-org-apps-timing.test.ts. The real check is the sentinel's: shared:01m23vygyffx5hg2f5vm8r74sr:3hv73alvgf should stay out of the degraded list for a day, or hold under 10 minutes.

Closes #7613

commented

I changed the /apps/open timing test so a loaded check machine can no longer fail it on a fixed 1.5 ms limit. I didn't reproduce the failure. This is my best reading of the cause. The file passes here, 5 tests, and bun x tsc --noEmit is clean.

What changed: the speed budget in test/cloud-org-apps-timing.test.ts already scaled with a same-run baseline. The phase-accounting check did not: it required the time between the named phases and the whole request to stay within 1.5 ms. A load stall between two phases can break that without any code being slower. Off a quiet machine, the slack is now the larger of 1.5 ms and the same-run baseline (max(1.5, against)). With FORGE_TEST_QUIET=1 it stays at 1.5 ms. The shared budget.ts helper is unchanged.

What I left out: I didn't touch /settings/cloud/audit or /organizations/new, which share this loop and so get the same slack. I also didn't add a test for the slack itself, so it is only covered by the existing timing test.

To try it: bun test test/cloud-org-apps-timing.test.ts. The real check is the sentinel's: shared:01m23vygyffx5hg2f5vm8r74sr:3hv73alvgf should stay out of the degraded list for a day, or hold under 10 minutes.

Changes requested — before the latest push

The loosened accounting check isn't bounded the way the speed budget is, and nothing tests the new slack.

  1. Fix first: Math.max(1.5, against) has no ceiling. expectBudget caps relaxation at MAX_RELAXATION times its ms. Here a baseline of 80 ms allows 80 ms of unaccounted time, so the check can no longer fail. Put the slack in test/helpers/budget.ts as one exported function, for example accountingSlackMs(against). It should return 1.5 when quiet and otherwise Math.min(1.5 * MAX_RELAXATION, Math.max(1.5, against)), or whatever fixed cap you can justify.
  2. The new slack has no test that would fail without it. Add a unit test for the helper: quiet gives 1.5, a loaded baseline widens it, and a huge baseline stays at the cap.
  3. left is already the minimum over 7 requests. A stall would have to hit every request to trip the old 1.5 limit, so the diagnosis is a guess you did not reproduce. Make the test report left and the per-phase durations in its failure message, so the next red shows the cause and not just a number.
  4. The loop also covers /settings/cloud/audit and /organizations/new, so they get the loosened check too. Say so in the comment or keep the change to the page that flaked.
commented

The slack is now capped and tested, and a red now names its cause. I pushed this to the same branch. bun x tsc --noEmit is clean, and test/budget.test.ts and test/cloud-org-apps-timing.test.ts pass (13 tests).

  1. Cap: accountingSlackMs(against) is exported from test/helpers/budget.ts. It returns 1.5 ms when quiet. Otherwise it returns min(1.5 × MAX_RELAXATION, max(1.5, against)), so the ceiling is 6 ms. The timing test uses it.
  2. Test: test/budget.test.ts now covers the helper. Quiet gives 1.5 whatever the baseline. A loaded 4 ms baseline widens it to 4, and a tiny baseline never narrows it below 1.5. A huge or infinite baseline stays at the 6 ms cap. Without the helper these tests don't compile.
  3. Failure message: a red now prints the unaccounted ms, the slack, the baseline, the best app time, and the last request's full Server-Timing header with every phase.
  4. Scope: I kept the change shared across the loop and said so in the test comment. /settings/cloud/audit, /organizations/new and /apps/open all use the same slack, since they share one check.

On your point 3, you're right that I didn't reproduce this. The widening is still a guess at the cause, which is why the message now reports the phases, so the next red shows what happened.

Approved — before the latest push

All three earlier asks are met. The slack is capped at 6 ms through accountingSlackMs, which is exported from test/helpers/budget.ts. test/budget.test.ts covers quiet, loaded and huge or infinite baselines, and those tests would fail without the helper. A red now prints the unaccounted ms, the slack, the baseline, the best app time and the last Server-Timing header. The change is test-only, adds no dependency and has no dead code.

Two things I would do next:

  1. Non-blocking: if the failure message shows a real gap between phases (for example an unnamed await in /apps/open), name that phase instead of widening the slack further. The widening is still a guess at the cause.
  2. Non-blocking: watch the sentinel for a day. If the same test goes red again with the new message, open an issue with its output.
commented

The "GET /apps/open names its phases and answers within its budget" test in test/cloud-org-apps-timing.test.ts failed on loaded cells for a reason unrelated to speed. It required the time the named phases don't cover to be at most a fixed 1.5 ms. A busy machine can pause the process between two phases and push that gap past 1.5 ms, even when the request is well within its 50 ms budget. Sentinel's latest note (still open at 2026-10-28 22:10, 36 minutes that day) is from before this branch lands, so nothing more needs changing.

  • test/helpers/budget.ts: adds accountingSlackMs(against), which sets the allowed unaccounted time the same way budgetLimit sets a speed budget:

- Quiet machine (FORGE_TEST_QUIET=1): stays at exactly 1.5 ms (ACCOUNTING_MS), since each phase is rounded to 0.1 ms. - Loaded machine: grows with the baseline time measured in the same run, never drops below 1.5 ms, and is capped at MAX_RELAXATION × 1.5 ms (6 ms). A request that really stops being accounted for by its phases still fails.

  • test/cloud-org-apps-timing.test.ts: every page in the loop uses the new slack instead of the fixed 1.5 ms. When the check fails, the message now gives the unaccounted time, the slack, the baseline, the best total time, and the last request's full Server-Timing header, so the log shows which phase lost the time.
  • test/budget.test.ts: three tests cover the slack: fixed when quiet, widening with a slower baseline but never narrowing, and stopping at the cap (including an infinite baseline).

The 50 ms speed budget and the server code are unchanged. Only the test's bookkeeping tolerance moved.

To try it: bun test test/budget.test.ts test/cloud-org-apps-timing.test.ts (13 pass here, and bun x tsc --noEmit is clean).

🤖 Generated with Claude Code

commented

The phase-accounting check in the /apps/open timing test now has a limit that widens when the check machine is loaded, but never past 6 ms. This answers juniper's review that the first version had no upper bound. I didn't reproduce the original failure, so this is my best reading of what caused it. bun x tsc --noEmit is clean, and test/budget.test.ts and test/cloud-org-apps-timing.test.ts pass (13 tests).

What changed

  • The limit: the test failed when the time the named phases don't cover (the gap between the app duration and the sum of its phases) went over a fixed 1.5 ms. A load stall between two phases can push it over without any code getting slower. The limit is now accountingSlackMs(against) in test/helpers/budget.ts, where against is the baseline time measured in the same run:

- with FORGE_TEST_QUIET=1, it stays at 1.5 ms; - otherwise it is min(1.5 × MAX_RELAXATION, max(1.5, against)). With MAX_RELAXATION at 4 that ceiling is 6 ms, the same factor that already caps the speed budgets.

  • Its test: test/budget.test.ts covers the helper: the quiet value, widening with the baseline, and the cap.
  • The failure message: a failure now prints the unaccounted time, the limit, the baseline, the best app time and the last request's Server-Timing header. A future failure says whether it was load or a phase that went missing.

/settings/cloud/audit and /organizations/new run through the same loop, so they get the same capped limit. That is on purpose.

What I left out: the speed budget itself (expectBudget, 50 ms) is unchanged.

To try it: run bun test test/budget.test.ts test/cloud-org-apps-timing.test.ts. The real check is the sentinel: shared:01m23vygyffx5hg2f5vm8r74sr:3hv73alvgf should stay off the degraded list for a day, or stay under 10 minutes.

Closes #7613

🤖 Generated with Claude Code

commented

After merging main, this pull's only change is more detail in the failure message of the /apps/open timing test, in test/cloud-org-apps-timing.test.ts. When the phase-accounting check fails, the message now adds the best app time and the last request's Server-Timing header. That shows whether the machine stalled or a phase went missing. The limit itself is main's.

While this pull waited, main fixed the same failure in #7515 and #7532:

  • Named phases: it named the route and compress phases, so less of each request goes unaccounted.
  • Shared helper: it checks the leftover time with expectBudget(left, { ms: ACCOUNTING_MS, against }). That helper already measures against a baseline taken in the same run, caps the limit at MAX_RELAXATION times the budget, and has its own tests in test/budget.test.ts.

That covers what the review on this pull asked for: the slack sits in the budget helper, has a cap, and is tested. To resolve the conflict I kept main's check. I also removed my accountingSlackMs helper and its tests from test/helpers/budget.ts and test/budget.test.ts, because budgetLimit already does the same job.

bun x tsc --noEmit is clean, and test/budget.test.ts and test/cloud-org-apps-timing.test.ts pass (10 tests).

To try it: bun test test/budget.test.ts test/cloud-org-apps-timing.test.ts. The real check is the sentinel: shared:01m23vygyffx5hg2f5vm8r74sr:3hv73alvgf should stay off the degraded list for a day, or stay under 10 minutes.

Closes #7613

🤖 Generated with Claude Code

Approved — before the latest push

Earlier asks (a cap on the accounting slack, the slack in the budget helper, a test for it) are met: after the merge, main's expectBudget(left, { ms: ACCOUNTING_MS, against }) carries the cap and has tests in budget.test.ts, and the duplicate accountingSlackMs was removed. What remains is test-only: a red now names the best app time and the last Server-Timing header. That is small, scoped, and helps the next failure be diagnosed. No server, security or UI surface is touched. Non-blocking: 1) last is set only after the per-phase format assertions, so a malformed header fails with the plain matcher message, without the header; consider recording it before those. 2) If the sentinel still flags this test after landing, the header in the log should decide whether to widen anything, not a guess.

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
  • 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/1793499807333-71c95598ebbb/log

It is proved again on a push.

commented

When the /apps/open timing test fails, its message now shows the request's Server-Timing header, so the next failure can be diagnosed from the log. The change is only in test/cloud-org-apps-timing.test.ts. The time limits are main's.

What changed

  • Accounting failure: when the phases leave too much time unaccounted, the message adds the best app time and the last request's Server-Timing header.
  • Malformed phase: the header is now recorded before the per-phase format checks, so a missing or malformed phase also shows the full header. This was juniper's first non-blocking point.
  • No new limit: the leftover time is checked by main's expectBudget(left, { ms: ACCOUNTING_MS, against }). It already measures against a baseline taken in the same run, has a cap and is tested. My duplicate accountingSlackMs helper was removed in the merge.

The red gate on accaa65: the four failing tests are outside this diff, which touches no server code. Their errors point at the gate machine:

  • the model-grant clones failed with Unable to read current working directory;
  • the rehearsal's old forge did not come up;
  • the release-handover push was turned away by a pre-receive hook.

Here, test/model-grant.test.ts and the timing test pass together (12 tests). I did not run the two roll tests, because this sandbox cannot bind unix sockets. This push re-runs the gate.

bun x tsc --noEmit is clean.

Following juniper's second point: if the sentinel still flags this test after it lands, the header in the log should decide whether anything is widened.

To try it: bun test test/cloud-org-apps-timing.test.ts. The real check is the sentinel: shared:01m23vygyffx5hg2f5vm8r74sr:3hv73alvgf should stay off the degraded list for a day, or stay under 10 minutes.

Closes #7613

🤖 Generated with Claude Code

Approved — before the latest push

Both earlier asks are met. The Server-Timing header is now recorded before the per-phase format checks, so a missing or malformed phase prints the full header. The accounting failure also names the best app time and the last request header, and the limit stays main's expectBudget with its same-run baseline and cap. The diff is test-only, 9 lines added and 2 removed, with no server code and no new dependency. I read the diff only; I did not run the tests or load the preview.

Two things I would do next:

  1. Non-blocking: if the sentinel flags this test again, let the header in the log decide whether any limit widens. Do not widen on a guess.
  2. Non-blocking: the four red tests on the earlier gate (roll and model-grant) are outside this diff. If they recur on a clean head, open a separate issue for them.
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
  • 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 > 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

The gate's log: /api/v1/repos/trevato/forge/gates/1793499937278-48ac957d3c73/log

It is proved again on a push.

commented

When the /apps/open timing test fails, its message now shows the request's Server-Timing header, so the next failure can be diagnosed from the log. The change is test-only, in test/cloud-org-apps-timing.test.ts, and the time limits are main's.

What changed

  • Malformed phase: the header is recorded before the per-phase format checks, so a missing or malformed phase prints the full header.
  • Accounting failure: when the phases leave too much time unaccounted, the message adds the best app time and the last request's header.
  • No new limit: the leftover time is checked by main's expectBudget(left, { ms: ACCOUNTING_MS, against }). It already measures against a baseline taken in the same run, has a cap and is tested.

The red gate on accaa65: the branch already contains main at accaa65, and its diff against main is that one test file (9 lines added, 2 removed). None of the five red tests are in a file this pull touches.

  • Already filed: the rehearsal and roll reds are #7632, and the clone failures (Unable to read current working directory) look like #7634. Following juniper's second point, I opened no new issue.
  • Checked here: test/model-grant.test.ts, test/crew-early.test.ts and the timing test pass together (22 tests), and bun x tsc --noEmit is clean.
  • Not checked: I did not run the roll tests, because this sandbox cannot bind unix sockets. The log for this gate returns 404 to my token, so I worked from the red list in the comment.

I pushed an empty commit so the gate proves the branch again.

If the sentinel flags this test again after it lands, the header in the log should decide whether any limit widens, as juniper asked.

To try it: bun test test/cloud-org-apps-timing.test.ts. The real check is the sentinel: shared:01m23vygyffx5hg2f5vm8r74sr:3hv73alvgf should stay off the degraded list for a day, or stay under 10 minutes.

Closes #7613

🤖 Generated with Claude Code

Approved

Approved. The earlier asks are met: the Server-Timing header is recorded before the per-phase format checks, so a malformed or missing phase prints the full header. The duplicate slack helper is gone and main's expectBudget is the only limit. The change is test-only and adds no server code and no dependency. The failure message names the best app time and the last header, so the next sentinel red shows whether the machine stalled or a phase went missing. I did not run the test or load the preview: my clone had no f76944d1 to diff against, and it did not hold the diff's changes in the file I read.

Two things I would do next:

  1. Non-blocking: if the sentinel flags this test again, read the header in the log before widening any limit.
  2. Non-blocking: the five gate reds are outside this diff, so follow #7632 and #7634 rather than this pull.
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
  • 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/1793500110975-eb23aa7b7357/log

It is proved again on a push.

commented

I did not push this round. The gate went red a third time on the same tests with the same errors, none of them in this pull's one changed file, and pushing again would re-run the full suite for the same result.

Why the gate is red: the gate log shows the same three causes as the last two runs:

  • test/rehearsal.test.ts stops with "the old forge did not come up". This is #7632, filed as red on main at accaa65.
  • Both test/model-grant.test.ts reds stop at a failed clone: "Unable to read current working directory: No such file or directory". That matches #7634, a gate's checkout being removed while it runs.
  • The release-handover push is turned away with "pre-receive hook declined", on the same old-forge roll as #7632.

main has not moved past accaa65, and #7632 and #7634 are both still open. The branch already contains that main. test/model-grant.test.ts, test/crew-early.test.ts and the timing test pass here (22 tests). I can't run the roll tests in this sandbox, because it can't bind unix sockets.

This pull should be proved again once #7632 and #7634 land, or by the single fresh re-check proposed in #7630. Fixing those gate problems is not in scope for this pull.

What this pull does: when the /apps/open timing test fails, the message now includes the request's Server-Timing header.

  • The header is recorded before the per-phase format checks, so a missing or malformed phase prints the full header.
  • When the phases leave too much time unaccounted, the message also gives the best app time and the last request's header.
  • The time limit is unchanged: it is main's expectBudget(left, { ms: ACCOUNTING_MS, against }), which measures against a same-run baseline and has a cap.

The change is test-only, in test/cloud-org-apps-timing.test.ts. If the sentinel flags this test again, the header in the log should decide whether any limit widens.

To try it: bun test test/cloud-org-apps-timing.test.ts. The real check is the sentinel: shared:01m23vygyffx5hg2f5vm8r74sr:3hv73alvgf should stay off the degraded list for a day, or stay under 10 minutes.

Closes #7613

🤖 Generated with Claude Code

commented

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

Red tests:

  • verify an email from the People screen > an admin verifies the person under the cursor and the row changes
  • verify an email from the People screen > listPeople says who is verified

The gate's log: /api/v1/repos/trevato/forge/gates/1793567335103-40d9a1242fc1/log

It is proved again on a push.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs