Deadline-derived wakes: waiting states arm their own alarm
Status summary#
In progress, stacked on #2530 (which holds the repros, the analysis, and
the quarantines this fix un-parks). Goal: the minimal implementation of the
fix direction in tasks/platform-stall-repros.md — every waiting state
derives a nextDeadlineAt from reduced state; the processor host arms its
Durable Object alarm at min(keepalive, deadline); firing runs the
ordinary reconcile. Acceptance is mostly already written: the expected-fail
specs on the base branch flip to plain passing tests.
The rule#
One primitive, per tasks/platform-stall-repros.md and extending
tasks/agent-llm-deadline-alarm.md:
- Each processor exposes a pure
nextDeadlineAt(reducedState): number|nullnext to its reduce. - The host arms the DO alarm at
min(keepaliveAt, nextDeadlineAt), persists the desire beside the keepalive record, re-arms after every commit and every fire. Imperative "arm an alarm and hope" is exactly the bug class (see the Bugbot edge on #2530): in-memory intent gets wiped; a derivation is recomputed identically by every incarnation. - On fire: run the ordinary catch-up/reconcile — no new recovery
entrypoint. The reconcile compares
nowto the same derivation and acts. - Facts needed by a derivation get journaled ("if it happened and we might care, journal it" — Misha, 2026-08-27). Chatty facts ride the ephemeral lane; machinery events get no model/UI face unless they earn one.
Scope: minimal per hat#
-
Host primitive: deadline slice on the processor hostnot needed for the minimal fix — scouting showed each hat has a smaller honest seam (below). The registry'ssetAlarmSlicemachinery exists for a future hat that genuinely needs a durable per-processor deadline; the scheduler and device processors already use it - Hats 1+2 — LLM attempt progress watchdog (agent processor):
chunks are contract-forced ephemeral (can't fold a durable
lastProgressAt), so the watchdog lives on the in-flight slot instead:
lastProgressAtMsrefreshed at dial + every chunk, ahost.sleeploop in the same background dispatch settles the attemptfailedat 45s idle (LLM_ATTEMPT_IDLE_BUDGET_MS, argued in its comment) → the 10/20/40s ladder owns re-dialing. Sound despite being in-memory: every loss path is covered — eviction loses attempt AND watchdog together, keepalive revival's adopt re-dial arms a fresh one. Both stall pins flipped to plain passing tests; new false-trip guard test (slow-but-chunking attempt survives); 3 agent-processor tests updated where they held manual-respond attempts idle past 45s; the #2498 wedge pin now documents watchdog-first settlement (horizon stays the outer backstop, still covered by the adopt-expiry test) - Hat 3 — halted subscriptions (stream event sender): a
stale-version halt IS durable evidence of an owed antidote resume, so
#armAlarmFromStorenow derives the wake from it (1s lifecycle pace), gated on!state.paused; the!recordedbranch's bare in-memoryarmAlarm— the #2530 Bugbot edge — is deleted in favor of the derivation. Two new sender tests: interrupted-append retry wake survives recomputation and lands the resume; paused stream derives no wake. Deliberate cut: same-version halts still wake only on the next deploy (#2486's shipped design) — ahaltedAt + reprobeBackoffself-heal needs a timestamp and a probe counter in reduced state, deferred as not repro-backed - Hat 4 — bootstrap transient (worker build backend):
executeWorkerBuildre-runs the install/bundle up to 3 attempts when every failure line is a version-resolution shape (the two propagation -window patterns only; parse/install crashes stay one-shot). No artificial pause — the bundler call itself takes seconds, which is the pacing. #2513's pin flipped to a plain passing test; the pattern-parameterized loud-failure test updated to model a PERSISTENT warning (mockResolvedValue, not Once). Deliberate cut: minute-scale propagation windows still fail loudly — the saga-level journaledretryAtpark is a follow-up, per the two-seam analysis - Hat 5 — facet rebuild provenance:
implement heredeferred: distinct subsystem (dynamic worker build/probe surface), e2e-only verification, no unit pin to flip. Its quarantine's exit criteria (rebuilds carry their trigger) is unchanged and still owned by tasks/platform-stall-repros.md - Tests: all via the existing node harness (advanceTime drives the watchdog through the virtualized host.sleep); agents + streams + workers domains fully green (854 passed / 7 unrelated expected-fail pins)
- Un-quarantine sequencing: the script-reuse spec's exit criteria also wants a green preview run with #2529's warm-up removed — un-skip lands with that merge train, not here; explained in the PR body
- Full checks + CI green on the stacked PR
Assumptions (worktreeified while Misha is present but not specifying)#
- Budget defaults: chunkIdleBudget ~10s (matches the harness-measured revival cadence and the ladder's first rung), reprobeBackoff for halts ~5min with cap — flagged for review, argued in comments.
- Hat 5 may be deferred out of this PR; the other four hats are the core.
- The expected-fail specs' 60s placeholder budgets become the real assertion budgets unless the chosen product budgets demand adjustment.
Implementation log#
- Three scout maps (host/keepalive/alarm machinery; agent turn-loop
lifecycle; build-backend failure path) reshaped the plan from "one grand
host primitive" to three small local seams. Key findings that drove it:
chunk events are contract-forced ephemeral and not consumed (no durable
lastProgressAt is derivable),
#atHeadruns only on delivery, a hung attempt never fails so the ladder never starts, and the sender's#armAlarmFromStoreis already the derive-from-durable-state pattern the fix generalizes. - The "derive from reduced state" principle survives in amended form: derive from durable state where the wake must outlive an incarnation (hat 3), and use an in-incarnation watchdog where every incarnation-loss path is already covered by an existing durable wake (hats 1-2: keepalive revival → adopt re-dial → fresh watchdog).
- Collateral test updates were all of one shape: suites that held manual-respond attempts idle across large advanceTime windows now meet the watchdog; tightened their timelines rather than widening the budget.
- Spec-1's original tail (
openRequestends null) was under-specified — the true contract is stronger (the ladder re-dials, so a fresh request legitimately opens); rewritten with a comment saying so.