Decision records
ADR 0057 — Multi-gate suspend passthrough: the ordered _resumePayloads fold (EVENT_SCHEMA 1.16.0)
- Status: Accepted — shipped (fold list + counted passthrough + legacy fallback)
- Date: 2026-07-15
- Builds on: the suspend/resume machinery (P2 determinism), K5 event-schema governance,
found by the perpetual-agents W8 audit (
loom/docs/requirements/2026-07-15-perpetual-agents-audit.md) (first flagged during ADR 0056’s testing). - Part of: the perpetual-agents series (W8, issue #172) — a pre-existing kernel bug fix, orthogonal to the series’ features.
Context — the silent bug
A flow re-runs from the top on every resume; recorded steps replay from frames, and a
suspend gate that was already resumed must pass through instead of re-parking. The
passthrough was built for one gate: the fold kept only the latest resume payload
(variables._resumePayload, overwritten by each EXECUTION_RESUMED) and the context had
a single-shot consumed flag.
For a flow with two sequential bare gates, the second resume replayed like this: gate 1 passed through — with gate 2’s payload (the latest) — then gate 2 found the passthrough consumed and re-parked. Net effect: wrong data through gate 1, and the run never progresses past gate 2 (for timer gates, an infinite re-wake loop). Silent — no error, no warning. No flow in the repo used two sequential bare gates, which is why it survived; the audit’s mechanism trace confirmed it as a real bug, not a documented limitation.
Decision
The fold records the ordered list; the context consumes by index.
EXECUTION_RESUMEDnow also appends its payload tovariables._resumePayloads(ordered, append-only — the sameappendAuditidiom as_migrations). The single-latest_resumePayloadis kept for compatibility.ctx.suspendmaintains a consumed count: the i-th suspend call in a re-run passes through with payload i; a gate beyond the recorded resumes throws and parks, exactly as before. Each gate now receives its own recorded payload.- Legacy fallback: a pre-1.16 snapshot folded only
_resumePayload; the context treats it as a one-entry list, so single-gate flows resume byte-identically. (Pre-fix multi-gate logs were already broken — there is nothing behavior-preserving to keep.)
EVENT_SCHEMA 1.15.0 → 1.16.0: additive fold key derived from existing events — no new
event kinds, no payload changes, knownEventTypes()/PINNED untouched. A log that never
resumes twice folds one extra list entry alongside the legacy key; single-gate replay
behavior is regression-tested unchanged.
Alternatives considered
- Clear
_resumePayloadonEXECUTION_SUSPENDED— fixes the wrong-payload half but not ordering (gate 1 would get no payload on the second re-run), and mutates the meaning of an existing fold key for old logs. Rejected. - Key passthrough by the suspend condition (
on) — breaks flows that legitimately reuse a condition name across gates, and requires pairing logic the ordered list gets for free (suspends and resumes strictly alternate per execution). Rejected.
Consequences
- Multi-gate human/approval flows are now correct — three regression tests (two-gate payload ordering, three-gate completion, legacy single-payload fallback).
ctx.sleepchains (multiple timer gates in one flow) are fixed by the same mechanism.- The W8 audit’s other fixes ship alongside: iterative cycle-safe gc ref walk, depth-
capped
stableStringify, symlink-skipping lint walk (see the audit doc for the full disposition table, including the DS&A suggestions rejected with rationale).
Source: docs/adr/0057-multi-gate-suspend-passthrough.md in the loom repository.