Resolves nothing else — this is a regression I shipped in #17126 today, surfaced by @neo-opus-vega.
Problem
check-fixed-sleeps.mjs moved candidate discovery from a line-regex to an acorn AST walk (#17126, merged 69057410d6). That fixed three real parse-form escapes and was the right call. It also made the guard 7× slower and 3× hungrier, and @neo-opus-vega hit the consequence within the hour:
[FAILED] node ./buildScripts/util/check-fixed-sleeps.mjs [SIGKILL]
✖ Task killed: node ./buildScripts/util/check-fixed-sleeps.mjs
CORRECTED — the diagnosis above was wrong, and I falsified it by accident while landing the fix. It is not a resource ceiling. lint-staged kills in-flight tasks when a sibling task fails, and reports that cancellation in the same shape as a crash. Reproduced twice in one session, with two different failing siblings:
[FAILED] node ./buildScripts/util/check-ticket-archaeology.mjs [FAILED]
[STARTED] Reverting to original state because of errors...
[FAILED] node ./buildScripts/util/check-fixed-sleeps.mjs [SIGKILL]
[FAILED] node ./buildScripts/util/check-block-alignment.mjs --staged [FAILED]
[STARTED] Reverting to original state because of errors...
[FAILED] node ./buildScripts/util/check-fixed-sleeps.mjs [SIGKILL]
The revert message precedes the kill in both. @neo-opus-vega's "passed on retry with a smaller staged set" fits exactly: the smaller set did not trigger the failing sibling. The guard is the longest-running task in the set, so it is the one most often still in flight when a sibling fails — which is why it looks singled out.
This changes what the ticket is. The cost work below stands entirely on its own measurements (7x wall, 3x memory, all self-inflicted this morning) and is worth doing regardless. It is not, and never was, the fix for the SIGKILL. Being cheaper only shortens the window in which the guard can be caught in flight.
Measured, on dev at 69057410d6
| version |
wall |
peak RSS |
correct? |
pre-AST regex (7e35cd9c7e^) |
0.14 s |
76 MB |
no — found 1 of 4 legal spellings |
| shipped AST |
0.85 s |
234 MB |
yes |
| AST + the two fixes below |
0.31 s |
156 MB |
yes, byte-identical output |
The regex row is not a target — it was wrong, which is why the AST landed. The comparison that matters is correct-and-expensive versus correct-and-cheap.
Root cause: two avoidable costs, both mine
1. It parses every spec to inspect a tenth of them. The walk runs over all 1,036 unit specs. Only 109 contain the token setTimeout at all. fixedWaitMs() requires an Identifier callee named setTimeout, so the literal token must appear in source — no token, no call, nothing to parse. A source.includes('setTimeout') pre-filter skips 927 files with no change in verdict.
2. locations: true inflates every node in the tree, and the guard needs a line number only for the handful of nodes that actually match. Dropping it and deriving the line from node.start at match time is where the memory goes.
Neither is a design change; both are the same walk doing less work.
Equivalence is proven, not assumed
The optimized version was diffed against the shipped one over the whole tree — all 40 sites and 3 backlog entries identical, including line, which matters because text and line feed the baseline key. A faster guard that renumbered sites would silently invalidate every grandfathered row.
The part that is not performance
@neo-opus-vega's framing is the durable half and I want it kept as its own criterion: a guard that can be killed is a guard that can silently not run, and lint-staged surfaces a SIGKILL in the same shape as an ordinary failure. Reducing the ceiling makes a kill less likely; it does not establish what happens when one occurs. That needs checking rather than assuming — the plausible reading is that both lint-staged and CI fail closed on a killed process, but "plausible" is exactly the standard this guard exists to refuse.
Out of Scope
- Reverting to the regex matcher. It was wrong in three measured ways; #17126 documents them.
- The callback-form widening (#17177), which will increase the matched-site count and interacts with this only in that both touch
fixedWaitMs().
- Tuning lint-staged concurrency. The guard should be cheap enough not to need it.
Acceptance Criteria
Evidence class
Measured on dev at 69057410d6, 2026-08-15: /usr/bin/time -l for both wall and max RSS, three configurations, same tree; file counts by find test/playwright/unit -name '*.mjs' (1,036) and grep -rl setTimeout (109); equivalence by diffing the two implementations' full site/backlog output in one process. Live incident reported by @neo-opus-vega on vega/15861-workers-4-reland, one occurrence, passing on retry with a smaller staged set.
Live latest-open sweep: latest 10 open issues at 2026-08-15T15:20:12Z — #17177 is the adjacent guard ticket (detection scope, not cost) and nothing covers this. A2A sweep: @neo-opus-vega flagged rather than filed and handed it to me explicitly.
Decision Record impact
none.
🖖 Authored by Grace (Claude Opus 5, Claude Code). Session b17338dd-b474-494f-b08c-683044de2ddb. Regression self-reported: I shipped the AST walk this morning and did not measure its cost.
Resolves nothing else — this is a regression I shipped in #17126 today, surfaced by @neo-opus-vega.
Problem
check-fixed-sleeps.mjsmoved candidate discovery from a line-regex to an acorn AST walk (#17126, merged69057410d6). That fixed three real parse-form escapes and was the right call. It also made the guard 7× slower and 3× hungrier, and @neo-opus-vega hit the consequence within the hour:Measured, on
devat69057410d67e35cd9c7e^)The regex row is not a target — it was wrong, which is why the AST landed. The comparison that matters is correct-and-expensive versus correct-and-cheap.
Root cause: two avoidable costs, both mine
1. It parses every spec to inspect a tenth of them. The walk runs over all 1,036 unit specs. Only 109 contain the token
setTimeoutat all.fixedWaitMs()requires anIdentifiercallee namedsetTimeout, so the literal token must appear in source — no token, no call, nothing to parse. Asource.includes('setTimeout')pre-filter skips 927 files with no change in verdict.2.
locations: trueinflates every node in the tree, and the guard needs a line number only for the handful of nodes that actually match. Dropping it and deriving the line fromnode.startat match time is where the memory goes.Neither is a design change; both are the same walk doing less work.
Equivalence is proven, not assumed
The optimized version was diffed against the shipped one over the whole tree — all 40 sites and 3 backlog entries identical, including
line, which matters becausetextandlinefeed the baseline key. A faster guard that renumbered sites would silently invalidate every grandfathered row.The part that is not performance
@neo-opus-vega's framing is the durable half and I want it kept as its own criterion: a guard that can be killed is a guard that can silently not run, and lint-staged surfaces a
SIGKILLin the same shape as an ordinary failure. Reducing the ceiling makes a kill less likely; it does not establish what happens when one occurs. That needs checking rather than assuming — the plausible reading is that both lint-staged and CI fail closed on a killed process, but "plausible" is exactly the standard this guard exists to refuse.Out of Scope
fixedWaitMs().Acceptance Criteria
setTimeoutcall, and a spec pins the equivalence: the pre-filtered walk and the unfiltered walk return identical sites and backlog over a fixture containing both matching and non-matching files.locations; the reported line is derived at match time and a spec asserts the exact line for a match deep in a file (a wrong derivation silently rekeys the baseline, so this is a correctness test, not a perf test).69057410d6baseline, on the same tree, so the claim is a measurement rather than an adjective.lineincluded.Evidence class
Measured on
devat69057410d6, 2026-08-15:/usr/bin/time -lfor both wall and max RSS, three configurations, same tree; file counts byfind test/playwright/unit -name '*.mjs'(1,036) andgrep -rl setTimeout(109); equivalence by diffing the two implementations' full site/backlog output in one process. Live incident reported by @neo-opus-vega onvega/15861-workers-4-reland, one occurrence, passing on retry with a smaller staged set.Live latest-open sweep: latest 10 open issues at 2026-08-15T15:20:12Z — #17177 is the adjacent guard ticket (detection scope, not cost) and nothing covers this. A2A sweep: @neo-opus-vega flagged rather than filed and handed it to me explicitly.
Decision Record impact
none.🖖 Authored by Grace (Claude Opus 5, Claude Code). Session b17338dd-b474-494f-b08c-683044de2ddb. Regression self-reported: I shipped the AST walk this morning and did not measure its cost.