Uh oh!
There was an error while loading. Please reload this page.
fix(ci): stall guard reads real liveness — --log-order=stream under every guard-wrapped turbo, guard refuses grouped, shard timings re-measured - #11868
Conversation
…stream under every guard-wrapped turbo The guard measures output flushes; turbo's CI default is grouped log order, which flushes a task's output only when the task ends. A shard whose tail is one task longer than --stall-minutes was therefore killed as a stall while perfectly healthy. Pin --log-order=stream at all four guard-wrapped turbo call sites, and make the guard refuse a turbo invocation without the flag so the class cannot regress silently (self-test pins both directions). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
Regenerated per the dataset's own provenance recipe (4-vCPU box, --concurrency=4 --summarize, generator merge across six run summaries with disjoint filter sets — every test task a genuine MISS, zero cached). cli 392.11s -> 458.15s on this box (>601s alive-at-kill on the CI runner), the ~2x under-weight that was skewing every shard split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
os-steve
commented
Aug 24, 2026
ACCEPT — PM review, This was the shift's It closed the class, not the instance — verified by classifying every occurrenceThe card named one call site. I counted
Four wiring sites, exactly as reported, and the merge-base carries zero — so the fix is real rather than pre-existing. The three sites beyond the card's would each have produced the same silent kill on a different lane. ⭐ The part that makes it not decay: the guard now refuses
Pinned by 5 new self-test cases, and the refusal ablation is non-vacuous in the way that matters: mutation proven on disk (anchor 1→0, mutant 0→1), The fences held
The card's own figure corrected, honestlyZone 2 asked you to re-measure the
Evidence qualityFail-before/pass-after with the real signature. At 1/100 scale ( The dress rehearsal is the reading I'd have asked for. The exact new Test Core wiring (guard, 10-min budget, stream, summarize, NODE_OPTIONS plumbing) over a 19-package cli-anchored chunk: zero stalls, exit 0, 8m08s, cli solo tail 429s streamed throughout — the precise shape that was being killed. Plus the completeness guard on that real stream log: The flake was handled by rule, not by judgment call. The showcase #10293 race fixture failed to reproduce its race under load and passed solo; its contended sample was discarded by the generator's own failed-task rule and the solo 22.87s recorded, with the caveat declared in the PR body. Discarding a sample under a pre-existing rule and saying so is the opposite of chasing a green. On landing — the unlock consumersRecorded so this does not get lost: #11635 is Flipping to ready; arming once every check run completes green.
Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11808
What was wrong
scripts/run-with-stall-guard.mjsmeasures output flushes. Turbo's default log order in CI is grouped — a task's output is buffered and flushed only when the task ends — so a shard whose tail is a single task longer than--stall-minutesemits zero bytes while perfectly healthy and is killed at exit 75 by construction. Three occurrences in one shift, two lanes (PR #11785 twice onTest Core (1/6); PR #11845 / #11844 withTest Files 173 passed (173)arriving in the very flush the kill forced, stamped ~40ms after it). The red reproduces on rerun, so standard flake triage reads it as "not a flake, therefore the diff" — the exact false trail.What this PR does
Correction (a) from the card — turbo under the guard now streams (
--log-order=streamrestores the pre-grouped behaviour the guard was written against):.github/workflows/ci.yml— Test Core shard invocation and the dogfood invocation (also a guard-wrapped turbo: a single task whose output would flush only at its end)..github/workflows/rerun-safety-nightly.yml— both guard-wrapped full-suite passes.The class is closed, not just the instances: the guard now refuses to wrap a turbo invocation that does not pin
--log-order=stream(exit 1 at startup, before anything spawns, with the mechanism named in the error). Five new--self-testcases pin both directions (refusal on missing flag and on explicit--log-order=grouped; acceptance of=streamand split--log-order stream; no false trip onturboinside a flag value).Correction (b) (a second liveness probe) is deliberately NOT here — filed as #11855 with the naive-CPU inversion hazard written into the card: a probe that reads CPU ticks as "alive" never fires on sync-spinning hangs (half the #4250 population), converting "kills healthy suites" into "never fires", which no green can distinguish from success. Route (a) is smaller, reversible, and measured sufficient (below).
scripts/test-shard-timings.jsonrefreshed per its own provenance recipe (full cold local re-measure on a 4-vCPU box,--concurrency=4 --summarize, generator merge — six run summaries from one tree, disjoint filters so every test task is a genuine MISS andskippedAsCachedstays empty).@objectstack/clire-measured (Zone-2 falsification of the card's ~710s): 458.2s under the dataset's own conditions (concurrency-4 chunk on this container), vs 392.11s recorded and vs >601s alive-at-kill on the actual CI runner (the card's ~710s was a solo run under uncontrolled contention — the claim's direction holds, its magnitude was environment-specific). What the dataset needs is relative weight on one box, one day, one method — which this refresh restores for every package at once.Measurements (all on this branch's tree; environment: 4-vCPU container, turbo 2.10.10,
CI=true GITHUB_ACTIONS=true)1. Grouped log order is the CI default and buffers until task end — chunk-arrival instrument on
turbo run test --filter=@objectstack/sdui-parser(single task, cold): run header at t=808ms, then silence until one burst at t=4037ms on a 4071ms run (maxGap 3229ms ≈ the task's whole runtime, 86% of bytes in the final second).2.
--log-order=streamrestores real liveness under the same env — same command + flag: 13 chunks spread across the run, maxGap 957ms.3. Fail-before / pass-after at 1/100 scale — the guard wrapping a healthy
@objectstack/formulasuite (~15s) with--stall-minutes 0.1(6s):last line : ::endgroup::— byte-identical signature to the CI occurrence (the flushed group of the previous task is the last thing seen);--log-order=stream: exit 0, no stall, summaries stream with@objectstack/formula:test:prefixes.4. Full-scale dress rehearsal of the exact new Test Core wiring — guard +
--stall-minutes 10+--log-order=stream --summarize --concurrency=4(same NODE_OPTIONS report plumbing) over a 19-package chunk anchored on@objectstack/cli— the composition that killed CI: zero stalls, exit 0, 8m08s wall, 19/19 test tasks exit 0; cli ran a 429s solo tail after the last other task ended, streaming per-file lines throughout, and the guard stayed silent. Under stream order the kill condition is no longer "a task longer than the budget" but "a single test file silent for 10 minutes" — which is a genuine hang, i.e. exactly what the guard exists to kill.5. The completeness guard still attributes under stream order —
check-test-completeness.mjsagainst the dress rehearsal's real stream-order log:OK (19 of 19 scheduled package(s) reported, 0 had nothing to run, 0 never reached; 4268 test(s) declared and all accounted for), exit 0. (Stream attribution is a pinned first-class branch of itsparseSummaries— the<pkg>:test:prefix — with its own self-test cases; this run is the live control.)6. Refusal ablation (non-vacuous, both directions proven on disk) — with the fix committed, the refusal call was neutralized (
if (false && …); anchor count 1→0, mutant count 0→1,node --checkgreen so the red is not a load error), self-test went red on exactly the 3 refusal-direction cases and nothing else; restore proven byte-identical (shaf6712480e8dfpristine == restored == HEAD), self-test back to 27/27. The mutation/restore script carriedtrap restore EXIT INT TERM.7. pnpm's recursive runner streams — chunk instrument on
pnpm --filter A --filter B test(the temporal-leg shape): 12 chunks, maxGap 691ms — which is why the pnpm-wrapped guard call sites need no change.Why the guard fired at all under grouped in the fail legs but CI's shards were green for months: on a full-list shard, other tasks end and flush regularly; only an affected-set shard composed as {one heavy package + small ones} produces a solo tail past the budget — exactly the card's composition analysis.
Gates
Derived via
node scripts/pm/dispatch-gates.mjs(no hand-fed paths; it reads the change set itself from the merge base) and run atd81806a4f(the final commit): all 22 derived families exit 0, exits captured pre-pipe — check:agent-test-spelling, check:cross-package-test-inputs, check:entry-guard, check:node-version, check:parse-guard, check:pnpm-acquisition, check:pnpm-filter-targets, check:required-contexts, check:shard-attestation, check:stall-guard (✓ 27 case(s) passed), check:workflow-status-functions, check-aggregator-roster, check-ci-filter-parity, check-step-collectors, docs-audit/check-drift-comment, pm/ci-failure --self-test, check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, pm/release-rehearsal-clone --self-test. check:nul-bytes:OK (scanned 6598 text file(s) … no raw ASCII control bytes). Scoped eslint: population pereslint.config.mjsis**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}— of this diff onlyscripts/run-with-stall-guard.mjsis in it (workflows/JSON are outside the population);--format jsonreports 1 file, 0 errors, 0 warnings, exit 0; the config runs no type-aware linting (its own note: noparserOptions.projectanywhere), so this diff cannot move an untouched file's verdict.Timings-refresh provenance caveat, stated rather than hidden: the re-measure ran as six disjoint-filter chunk invocations rather than one monolithic run (bounded shared-container lock holds), so per-package contention varies:
@objectstack/spec/@objectstack/objectqlre-ran as a 2-task leg (a mid-chunk flake had canceled them: the #10293 race-reproduction fixture in example-showcase failed to reproduce its race under load, passed clean on solo re-run) andexample-showcase's recorded 22.87s is a solo-run sample. Heavy tails run near-solo in any methodology (cli's 458s window included a 429s solo tail — same shape in CI shards); the dataset is a relative-balancing input that medians toward truth across refreshes.This PR touches only CI workflows, a CI guard script, and a generated CI dataset — nothing published:
skip-changeset.For the unlock consumers
#11635 / PR #11785: unlock is merge
main(with this landed) and let CI re-run — no code change owed there. PR #11845 / #11844 (domain:services): same; theirTest Corereds carry no information about their diffs.Generated by Claude Code