Platform stall repros, consolidated: parked states need a wake condition
Status summary#
Consolidation branch: merges the three open repro PRs (#2518, #2513, #2486), adds one new expected-fail spec (the eviction-churn LLM re-dial hang found while investigating #2529's preview failure), and names the mechanism they share plus a fix direction that covers all of them. Repro-only apart from the halt self-heal fix carried in from #2486. The three source PRs close in favor of this one.
Interactive walkthrough (timelines for every hat, real journal timestamps,
and the fix replayed): explainers/parked-states-need-a-wake.html.
The five threads#
- Mid-stream LLM stall never settles — #2510 (already merged):
apps/os/src/domains/agents/agent-llm-stall.test.ts. A hung attempt never fails, so nothing settles until the keepalive wedge breaker (~15min). - Eviction-churn re-dial hang (new, this branch): second expected-fail
in the same file. Eviction mid-attempt recovers fine (revival + re-dial
within ~10s — #2480's fix works), but when the re-dial goes out during
the same churn window and also hangs, no third attempt and no settle
happen for ~15m10s on a quiet stream. On busy preview streams,
deliveries shorten that to the observed 150s/183s first turns (PR #2529
investigation; Depot run w1hcwnlc3q; projects
agent-script-reuse-mtbkj6o8-c56ce7a9/-mtbkmi8q-b6f3c4e3on preview_6). The 10/20/40s retry ladder (#1826) never engages because a hung attempt never fails. - Halted fan-out subscription is parked forever — from #2486 (with its fix): a halt had no wake condition until the deploy-version antidote; pre-existing halts still need manual resume, and a halt under an unchanged deploy still waits indefinitely.
- Transient registry-propagation failure kills bootstrap permanently —
from #2513:
build-backend-transient-resolution.test.ts. A retryable egress failure is classified terminal; the saga parks with no retry deadline. - Facet source-version pin false-alarms — from #2518:
userspace-facet-recycle-false-alarm.e2e.test.ts. A coincidental recycle is indistinguishable from the commit-triggered rebuild because rebuilds carry no provenance (no "why did I rebuild").
The common mechanism#
Threads 1–4 are one gap wearing four hats: a parked or in-flight state
whose progress depends on an external push that may never come. The
platform re-drives obligations on delivery; no state carries its own
deadline. An open LLM attempt has no chunk-idle budget; the 10-minute
expiry is a comparison, not a timer (tasks/agent-llm-deadline-alarm.md,
proven live with FOUR-DAY orphans on prd); a halted subscription had no
wake until #2486 taught it one specific wake (deploy version change); a
bootstrap saga hit by weather parks terminally instead of parking with a
retry time.
Thread 5 is the observability twin: states change without recording why
— the same shape #2486 had to fix by stamping workerVersion on halts
before the antidote could be decidable.
Fix direction (one primitive, four applications)#
Extend tasks/agent-llm-deadline-alarm.md's sketch from "arm an alarm at
the expiry horizon" to a general rule: every parked/in-flight state in
reduced state carries a nextDeadlineAt, and the processor host arms its
DO alarm at min(keepalive, nextDeadlineAt over all obligations);
firing runs the ordinary catch-up/reconcile. Because the deadline derives
from reduced state, it is recomputed on revival and survives eviction by
construction.
Applications:
- LLM attempts (threads 1, 2): deadline =
min(lastProgressAt + chunkIdleBudget, expiresAt)where progress = dial or chunk (both already journaled). On fire: abort the in-flight slot, settle the attemptfailed— the existing 10/20/40s ladder then owns re-dialing. This turns a churn-window severance from "30s–15min of dead air per cycle" into "chunkIdleBudget + ladder step", and makes first-turn latency on preview boundable — which is what unblocks deleting spec warm-ups (#2529). - Halted subscriptions (thread 3): keep #2486's deploy antidote, add
haltedAt + reprobeBackoffas a deadline so a halt self-heals (paced) even without a deploy, and pre-existing versionless halts stop needing an operator. - Bootstrap saga (thread 4): classify registry-propagation failures as
transient; park with
retryAtinstead of terminal failure. Same primitive, different obligation. - Provenance (thread 5): facet rebuilds record their trigger
("source-commit
" vs "cold-boot"), the way halts now record workerVersion. The pin then asserts causality instead of coincidence.
The budgets (chunkIdleBudget, reprobeBackoff, retry caps) are product decisions the fix makes; the expected-fail specs deliberately pin placeholder numbers (60s) the same way #2510 did.
Live confirmation of the thesis: the halt fix's own edge#
Bugbot found (on the consolidation PR itself) that #2486's antidote-resume
retry has this exact bug one level down: an interrupted resume append arms
a bare in-memory alarm (#resumeHaltFromAntidoteDeploy), which
#armAlarmFromStore — recomputing purely from durable rows, and skipping
halted rows — can immediately clear. The owed retry then waits for an
unrelated wake. Real, verified, and strictly-no-worse than pre-#2486
("parked forever"), so it ships as-is; the fix belongs to the primitive:
a halt whose workerVersion differs from the current one IS durable
evidence of an owed resume, so the deadline derivation should produce a
wake from that state directly, instead of trusting a bare armAlarm.
Caution for the implementer: a naive durable nextAttemptAt on a halted
row hot-loops the alarm against a paused stream (the resume append
rejects while paused; the catch path deliberately defers to the unpause
delivery) — the deadline must distinguish interrupted-append from
rejected-while-paused.
Quarantines this task owns#
Applied on this branch per docs/testing.md#flaky-test-quarantine-protocol, after three consecutive 2026-08-27 preview runs (Depot w1hcwnlc3q on #2529, bqk06tf2kc and p2blkx8cx5 on #2530) went red on tests unrelated to either PR's diff:
specs/agent-script-reuse.spec.ts(both tests): failed 3/3 runs (test 1) and 1/3 (test 2, at turn 2 — past its warm-up), always the churn-wedge signature ("Spinner was still visible after N ms", turns completing server-side in 150-183s). Exit criteria: attempt-progress deadlines (threads 1-2) landed, and one preview run green with the warm-up removed (#2529's change) — un-skip and merge #2529 together.apps/os/e2e/vitest/userspace-facet-source-version.e2e.test.ts: the documented coincidental-recycle false alarm fired twice more (7+ total). The blind-spot repro (userspace-facet-recycle-false-alarm.e2e.test.ts) stays active. Exit: achieved differently (tasks/failing-test-helper.md) — the pin is live again onfailingTestwith round-based coincidence immunity: only same-boot evidence concludes; a recycle makes the round inconclusive and a fresh revision is committed against the new boot. Rebuild provenance (thread 5) is still a nice-to-have for the platform, no longer this test's gate.
Checklist#
- merge
facet-recycle-false-alarm-repro(#2518) clean merge - merge
worker-build-registry-race-spec(#2513) clean merge - merge
stream-fanin-stall-repro(#2486) one additive conflict in the sender test harness args (both sides added options — kept both); generated itx api files regenerated and confirmed identical - new expected-fail spec for the eviction-churn re-dial hang sibling
test in
agent-llm-stall.test.ts, budget measured with a throwaway probe first: re-dial at +10s works, then nothing until +15m10s - verify the new spec fails for the intended reason when unmarked
{settled: 0, attemptsDialed: 2}a minute after revival - close #2518, #2513, #2486 pointing here
- checks green, PR open
Implementation log#
- The eviction-churn spec's numbers come from a probe run before writing the assertion: crash at t0 → revival fact + re-dial at t0+10s (first keepalive alarm) → no further state change until t0+15m10s, when the wedge breaker settles the request. See PR #2529's investigation for the live preview evidence this models.