Queue worker results before settlement cleanup, claim terminal and death boundaries before provider callbacks, and close late-message admission.
Make child cancellation and disposal reentrancy-safe across the workflow bridge and generic subagent wrapper, with adversarial regression coverage and RFC documentation.
Make Cordis construction and teardown ownership reentrancy-safe, then carry caller and provider ownership through reservation, setup, publication, quiescence, and sentinel retirement.
Stabilize registry carriers and factory/workflow boundaries, add adversarial lifecycle regressions, and align the rewritten RFC plus generated contracts with the enforced behavior.
Drain idle injection flushes before agent teardown, snapshot approval and subagent provider inputs, and gate subagent lifecycle events on real child readiness. Align the RFCs and generated contracts with the hardened behavior.
The keyless Loader-path smokes killed the child 10s after spawn, but a
loaded CI e2e runner routinely needs longer just to boot the unbuilt tsx
tree: on this branch's run the coding-agent smoke burned both retries and
failed at 30s wall-clock, and the sibling smokes passed only on retry x2
(master's latest run shows the same near-misses). The budget guards
against a HANG, not slowness — raise kill to 30s and the vitest test
timeout to 45s so a slow boot no longer masquerades as one.
formatSystemPromptSnapshot's insert-join ternary had its already-newline-
terminated arm unexercised (a delta whose insert ends in a blank line),
failing the per-file 100% branch gate on suite.ts (99.24%).
Two independent multipliers were pushing bare `pnpm run lint` past node's
default heap:
1. parserOptions.project expanded to every package tsconfig plus the root
one — each matched config built its OWN ts.Program, and the dev paths
map pulls sibling package SOURCES (plus the vendored Cordis stack) into
every such program, so resident memory grew as sum-of-closures, not
repo size: ~4.6 GB peak for 425 repo files. projectService shares one
tsserver-style graph: ~2.0 GB peak, ~28 s → ~14 s wall.
2. `eslint .` traversed .claude/ harness-local state — stale worktree
checkouts there carry tens of thousands of additional .ts files
(whole-repo copies), roughly tripling the work again even under the
project service. Other checkouts are not this one's sources; ignore
them like node_modules. (#169 carries the identical ignore line inside
its chain; the hunks dedupe on its next rebase.)
Type-aware rules verified live under the service: a floating-promise
probe still trips no-floating-promises.
Review follow-up: the blanket 10s default correctly targets worker-thread
cold-start races (starting, first-script-line, async child-registration
messages — genuinely CPU-bound under CI contention), but four waits assert
something different — that the HOST reacted PROMPTLY to an event that
already happened (a settled result, an observed worker death). Those had no
cold-start left to wait on, so the generous default just widened the window
a real regression could hide in.
Verified by injecting a 6s delay into the settle-reap's abort call: the
un-overridden helper's test still passed in ~6s. The same mutation now fails
in ~1s with the explicit 1000ms override restored on all four sites (the
abort-on-settle test's own assertion, the two worker-death cancel/dispose
reap checks, and the dispose-ack-race check). The other 12 waits keep the
10s default — they run BEFORE a result is awaited, waiting on the worker to
actually start rather than on a host reaction. Doc comment corrected to
describe the split instead of claiming every wait is a cold-start race.
The spec's 16 vi.waitFor sites used the 1s default timeout to wait for
worker-thread startup and child registration — CPU-bound work that blows
past 1s on a contended runner. The CI coverage lane (4 vitest workers plus
suites that spawn real subprocesses) hit this 3 times across 4 recent PR
runs, each a different subset of the cancellation/worker-death tests, each
green on rerun.
Every wait now goes through a shared helper with a 10s bound, and the file
sets a 30s test timeout to make room for it. The one deliberately tight
wait keeps its 800ms bound through the helper's override — it proves the
host (not the wedged worker's later loop turn) delivered the cancel, so a
generous bound would erase what it tests. No behavior under test changed.
spec.onError is a caller-supplied callback boundary, but the flattening
catch invoked it unguarded: a throwing sink rejected the whole async result,
breaking the seam's "result never rejects on a child-level failure"
contract (and docs/defensive-patterns.md's contain-callback-exceptions
rule). The sink's own throw is now swallowed — named as such — while the
original child failure still settles as stopReason 'error'. Regression test
drives a spawn failure through a throwing sink and asserts result resolves.
Same defect as the codex backend's, fixed there on PR #240; this is the
symmetric fix for the already-merged ACP backend.