@loomwork/replay-vm 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1760 @@
1
+ import type { EventEnvelope } from "@loomwork/core";
2
+ import type { ExecutionFrame, ExecutionState, SuspendWait } from "@loomwork/core";
3
+
4
+ type Handler = (state: ExecutionState, event: EventEnvelope) => ExecutionState;
5
+
6
+ // Copy-on-write a single frame into the frames container, with per-fold ownership
7
+ // (mirrors `appendAudit` below). The first step event of a fold pass spreads the
8
+ // caller's input container once — O(S) key refs, untouched frames shared — and marks
9
+ // the result fold-owned; every later step event in the same pass writes in place onto
10
+ // that owned container (O(1)). `foldEvents` resets ownership at the start of each call,
11
+ // and the input state (snapshot / prior state) is never mutated, so copy-on-write —
12
+ // and the async snapshot saves that hold prior states — are preserved. Byte-identical
13
+ // output; per-replay frame allocations drop from O(S²) to O(S). Handlers never mutate a
14
+ // frame in place; they always write a new frame object, so sharing untouched frames is safe.
15
+ //
16
+ // Uses the assignment form (`frames[stepId] = frame`), so reserved keys must be rejected
17
+ // (see assertSafeKey) — unlike a computed-key object literal, `frames["__proto__"] = …`
18
+ // would invoke the prototype setter and corrupt this execution's own fold.
19
+ function setFrame(
20
+ frames: Record<string, ExecutionFrame>,
21
+ stepId: string,
22
+ frame: ExecutionFrame
23
+ ): Record<string, ExecutionFrame> {
24
+ assertSafeKey(stepId);
25
+ if (foldOwned.has(frames)) {
26
+ frames[stepId] = frame;
27
+ return frames;
28
+ }
29
+ const next = { ...frames, [stepId]: frame };
30
+ foldOwned.add(next);
31
+ return next;
32
+ }
33
+
34
+ // Defense-in-depth: reject prototype-related keys as fold container keys. They are never
35
+ // valid stepIds/decisionIds, and the assignment form (`frames[key] = …`) would otherwise
36
+ // invoke the prototype setter / shadow `constructor` and corrupt that execution's own fold.
37
+ // (No global pollution is reachable — see the audit — but fail-closed on a reserved key is
38
+ // cheap and clear.)
39
+ const RESERVED_FOLD_KEYS = new Set(["__proto__", "constructor", "prototype"]);
40
+ function assertSafeKey(key: string): void {
41
+ if (RESERVED_FOLD_KEYS.has(key)) {
42
+ throw new Error(`fold: reserved key "${key}" is not a valid identifier`);
43
+ }
44
+ }
45
+
46
+ // ── O(1)-amortized audit-list append (per-fold ownership) ────────────────────
47
+ //
48
+ // The audit handlers below append to durable lists (`_outcomes`, `_timers`, …). A naive
49
+ // `[...existing, item]` per event makes a full-log fold O(k²) for k events of one type.
50
+ // Instead we track the arrays THIS fold pass created in `foldOwned` and push in place onto
51
+ // those, copying only when the array came from the caller's input state (snapshot / prior
52
+ // state) — which we must never mutate. `foldEvents` resets the set at the start of every
53
+ // call; folding is synchronous and non-reentrant, so a module-level set is safe, and the
54
+ // returned state's arrays are always fold-owned (never aliased into the input). Output is
55
+ // byte-identical; only intermediate allocations drop from O(k²) to O(k).
56
+ let foldOwned = new WeakSet<object>();
57
+
58
+ export function resetFoldOwnership(): void {
59
+ foldOwned = new WeakSet<object>();
60
+ }
61
+
62
+ /**
63
+ * R1a — install the ownership set a fold pass writes against. A one-shot fold (`foldEvents`
64
+ * with no set) starts empty every call. A LIVE fold (`ReplayVM.applyEvent`) hands in the set
65
+ * its VM keeps between commits, so containers the VM itself created stay writable in place
66
+ * across commits (O(1) per event) instead of being re-copied by every single-event fold
67
+ * (O(size) per event — quadratic over a run). The VM must drop the set the moment it lets a
68
+ * state escape to a caller (`ReplayVM.getState`), so an escaped state is never written to.
69
+ * Folding is synchronous and non-reentrant, so swapping the module-level reference is safe.
70
+ */
71
+ export function useFoldOwnership(owned: WeakSet<object>): void {
72
+ foldOwned = owned;
73
+ }
74
+
75
+ function appendAudit<T>(existing: T[] | undefined, item: T): T[] {
76
+ if (existing !== undefined && foldOwned.has(existing)) {
77
+ existing.push(item);
78
+ return existing;
79
+ }
80
+ const next = existing === undefined ? [item] : [...existing, item];
81
+ foldOwned.add(next);
82
+ return next;
83
+ }
84
+
85
+ // ── 1.19.0 (ADR 0102) — `_flowEmits`: stamped flow-level emits counted per digest ──
86
+ //
87
+ // Shape: a fixed-depth, 16-way trie over the digest's LAST three hex digits (FNV-1a's
88
+ // best-mixed bits). Inner nodes are 16-slot arrays (`null` = empty), leaves are
89
+ // `{ [digest]: count }`:
90
+ //
91
+ // _flowEmits[hex(d[15])][hex(d[14])][hex(d[13])][d] = count
92
+ //
93
+ // Why a trie and not a flat `{ [digest]: count }`: a live commit is its own fold pass, so
94
+ // copy-on-write must copy what it touches, and a flat map costs O(k) per emit (worse in
95
+ // practice: a many-key object is a dictionary, and its spread is slow). The trie copies
96
+ // the root-to-leaf path — three 16-slot arrays (`slice`, no hidden-class churn) plus one
97
+ // leaf of ~k/4096 entries — O(1) for any realistic k, still plain JSON (it rides snapshots
98
+ // and survives `truncatePrefix` compaction), and O(1) amortized within a multi-event
99
+ // replay pass, where fold-owned nodes are updated in place (`appendAudit`'s ownership
100
+ // rule). Size is one leaf entry per DISTINCT stamped emit plus the touched inner arrays.
101
+ export const FLOW_EMIT_DIGEST = /^[0-9a-f]{16}$/;
102
+ export type FlowEmitLeaf = Record<string, number>;
103
+ /** A 16-slot inner node of the `_flowEmits` trie; the third level holds leaves. */
104
+ export type FlowEmitCounts = Array<FlowEmitCounts | FlowEmitLeaf | null>;
105
+ const TRIE_PATH = [15, 14, 13] as const;
106
+
107
+ function ownedNode(existing: FlowEmitCounts | null | undefined): FlowEmitCounts {
108
+ if (existing != null && foldOwned.has(existing)) return existing;
109
+ const next: FlowEmitCounts = existing == null ? new Array(16).fill(null) : existing.slice();
110
+ foldOwned.add(next);
111
+ return next;
112
+ }
113
+
114
+ function ownedLeaf(existing: FlowEmitLeaf | null | undefined): FlowEmitLeaf {
115
+ if (existing != null && foldOwned.has(existing)) return existing;
116
+ const next: FlowEmitLeaf = existing == null ? {} : { ...existing };
117
+ foldOwned.add(next);
118
+ return next;
119
+ }
120
+
121
+ export function countFlowEmit(
122
+ variables: Record<string, unknown>,
123
+ digest: string
124
+ ): Record<string, unknown> {
125
+ const root = variables._flowEmits as FlowEmitCounts | undefined;
126
+ const nextRoot = ownedNode(root);
127
+ const a = Number.parseInt(digest[15] as string, 16);
128
+ const b = Number.parseInt(digest[14] as string, 16);
129
+ const c = Number.parseInt(digest[13] as string, 16);
130
+ const mid = ownedNode(nextRoot[a] as FlowEmitCounts | null);
131
+ nextRoot[a] = mid;
132
+ const low = ownedNode(mid[b] as FlowEmitCounts | null);
133
+ mid[b] = low;
134
+ const leaf = ownedLeaf(low[c] as FlowEmitLeaf | null);
135
+ low[c] = leaf;
136
+ leaf[digest] = (leaf[digest] ?? 0) + 1;
137
+ // A fold-owned root is already inside a fold-created `variables`; only a fresh root
138
+ // needs a new `variables` object (never mutate the caller's).
139
+ return nextRoot === root ? variables : { ...variables, _flowEmits: nextRoot };
140
+ }
141
+
142
+ /** How many stamped flow-level emits with this digest the folded log holds. */
143
+ export function flowEmitCount(variables: Record<string, unknown>, digest: string): number {
144
+ let node: unknown = variables._flowEmits;
145
+ for (const i of TRIE_PATH) {
146
+ if (!Array.isArray(node)) return 0;
147
+ node = node[Number.parseInt(digest[i] as string, 16)];
148
+ }
149
+ if (node == null || typeof node !== "object" || !Object.hasOwn(node, digest)) return 0;
150
+ const count = (node as FlowEmitLeaf)[digest];
151
+ return typeof count === "number" ? count : 0;
152
+ }
153
+
154
+ /**
155
+ * 1.20.0 (ADR 0104) — a `null` EXECUTION_RESUMED that answers no suspend point re-opens the
156
+ * execution and records NO gate passthrough. The kernel writes one on `resumeAs` onto a
157
+ * terminal execution and on an in-doubt verdict (whose answer is recorded as a decision):
158
+ * `null` because every store round-trips it (SQLite's payload column is NOT NULL, so an
159
+ * absent payload cannot be written). Keyed on the state being answered, not on `null` alone,
160
+ * so a caller's `null` approval of a real gate is still that gate's answer. The one pre-0104 log
161
+ * this changes is one where a caller resumed `null` into either position: that entry answered
162
+ * no suspend point and would have been served to the NEXT gate as `null` — the stray this fixes.
163
+ */
164
+ const IN_DOUBT_GATE = "EffectInDoubtResolved";
165
+ /**
166
+ * ADR 0113 (R7 fix round 1) — the reserved gate a REFUSED re-run parks on. Like the in-doubt gate it
167
+ * is answered by the kernel's own status-only `null` resume (a re-drive after the code is fixed):
168
+ * the answer must NOT enter `_resumePayloads`, or the body's next gate would take it as its approval.
169
+ */
170
+ export const FLOW_DIVERGED_GATE = "__loom_diverged__";
171
+
172
+ /**
173
+ * 1.21.0 (ADR 0105) — reserved fold key: `true` while the execution is PARKED and a
174
+ * `STEP_FAILED` has landed since it parked (a parallel sibling failing after the flow reached
175
+ * its gate). Set only by that event in that state; removed by whatever ends the park — the next
176
+ * `EXECUTION_SUSPENDED` or `EXECUTION_RESUMED`, or a terminal `EXECUTION_COMPLETED` /
177
+ * `_FAILED` / `_CANCELLED`; absent otherwise, so a log without such a failure never gains it.
178
+ *
179
+ * It exists to keep one rule the 1.20.0 fold had implicitly. 1.20.0 folded that failure to
180
+ * "failed", so a following `null` resume was status-only (it answered no gate, and a 1.20.0
181
+ * `resumeAs` wrote exactly that). Since 1.21.0 the execution stays `suspended`; without this
182
+ * key the same `null` would become the gate's answer, and a replay's outcome would depend on
183
+ * whether it was seeded from a 1.20.0 snapshot — breaking snapshot + tail ≡ full log. So a
184
+ * `null` resume in this position stays status-only (fail-closed: the gate re-parks rather than
185
+ * passing without an approval). The cost: a caller's explicit `null` answer to such a gate is
186
+ * not recorded as its answer; the gate re-parks and takes the next non-null one.
187
+ */
188
+ export const FAILED_WHILE_PARKED = "_failedWhileParked";
189
+
190
+ function isStatusOnlyResume(state: ExecutionState, payload: unknown): boolean {
191
+ return (
192
+ payload === null &&
193
+ (state.status !== "suspended" ||
194
+ state.suspendedOn?.on === IN_DOUBT_GATE ||
195
+ state.suspendedOn?.on === FLOW_DIVERGED_GATE ||
196
+ state.variables[FAILED_WHILE_PARKED] === true)
197
+ );
198
+ }
199
+
200
+ /** `variables` without the {@link FAILED_WHILE_PARKED} key (the same object when absent). */
201
+ function withoutFailedWhileParked(variables: Record<string, unknown>): Record<string, unknown> {
202
+ if (!Object.hasOwn(variables, FAILED_WHILE_PARKED)) return variables;
203
+ const { [FAILED_WHILE_PARKED]: _cleared, ...rest } = variables;
204
+ return rest;
205
+ }
206
+
207
+ // ── 1.23.0 (Wave 2, W2-0) — reserved shapes, folded only from what the kernel wrote ──────────
208
+ //
209
+ // The new kinds (`EXECUTION_CONTINUED`, `MESSAGE_CONSUMED`) and the PARALLEL_COMPLETED payload
210
+ // fold ONLY an envelope carrying `metadata.kernel === true`. An application event that already
211
+ // carries one of those names in an existing log therefore folds as the no-op it always was
212
+ // (byte-identically), even though a handler now exists and no kernel writer does yet (ADR 0110
213
+ // "Marker"; design §3). The journal stamps the marker for {@link KERNEL_STAMPED_EVENT_TYPES}.
214
+ // Four more shapes ride the same stamp but are stamped by their WRITER through commit
215
+ // `opts.metadata` (the kind also has an unstamped, userspace form, so the journal cannot stamp by
216
+ // type): a wait-addressed EXECUTION_RESUMED (ADR 0110 D3), an EXECUTION_SUSPENDED carrying
217
+ // `condition.waits` (D4 — the R2 writer adds `waits` AFTER the commit site's strip and stamps
218
+ // the event; an unstamped park's `waits` is dropped by the fold), and an OUTCOME_RECORDED
219
+ // carrying `recordedBy` / `late` (ADR 0116; the `recordOutcome` writer).
220
+
221
+ /** Kinds whose fold requires the kernel stamp, and which the journal stamps when it writes one. */
222
+ export const KERNEL_STAMPED_EVENT_TYPES: ReadonlySet<string> = new Set([
223
+ "EXECUTION_CONTINUED",
224
+ "MESSAGE_CONSUMED",
225
+ "PARALLEL_COMPLETED",
226
+ // 1.25.0 (ADR 0130 residuals D9): written by quarantineInbox / releaseInbox / auto-release (slice S5).
227
+ "INBOX_QUARANTINED",
228
+ "INBOX_RELEASED",
229
+ ]);
230
+
231
+ function isKernelWritten(event: EventEnvelope): boolean {
232
+ return event.metadata?.kernel === true;
233
+ }
234
+
235
+ function isPlain(v: unknown): v is Record<string, unknown> {
236
+ return v !== null && typeof v === "object" && !Array.isArray(v);
237
+ }
238
+
239
+ /** Every inbox dedupe window the fold keeps is capped here; the runtime may apply a smaller one. */
240
+ export const INBOX_DEDUPE_CAP = 1024;
241
+ /**
242
+ * 1.25.0 (ADR 0130 residuals D9) — the fold keeps at most this many quarantined entries in total
243
+ * (`_quarantined`, across topics). A fold cannot read a runtime setting (the lesson of ADR 0110 D6),
244
+ * so the ceiling is a fold constant; the runtime's own `quarantineMax` (default 256) may be raised
245
+ * to it, never above. A fact that would exceed it is ignored.
246
+ */
247
+ export const QUARANTINE_FOLD_CAP = 1024;
248
+ /** ADR 0115 D2 — custom metering dimension names, count and per-value ceiling. */
249
+ export const CUSTOM_DIMENSION_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/;
250
+ export const CUSTOM_DIMENSION_MAX = 32;
251
+ export const CUSTOM_DIMENSION_CEILING = 1e15;
252
+
253
+ /** A fold-owned copy-on-write record (the same ownership rule as `_decisions`). */
254
+ function ownedRecord<T>(prior: Record<string, T> | undefined): Record<string, T> {
255
+ if (prior !== undefined && foldOwned.has(prior)) return prior;
256
+ const next: Record<string, T> = prior === undefined ? {} : { ...prior };
257
+ foldOwned.add(next);
258
+ return next;
259
+ }
260
+
261
+ function safeMapKey(key: unknown): key is string {
262
+ return typeof key === "string" && key.length > 0 && !RESERVED_FOLD_KEYS.has(key);
263
+ }
264
+
265
+ /**
266
+ * ADR 0115 D2 — add the valid entries of a `custom` metering map onto the running one. Never
267
+ * throws: a bad name, a prototype name, a non-finite / negative / over-ceiling value, or the
268
+ * 33rd distinct name is skipped deterministically and the rest folds, so every replica folds the
269
+ * same state from a log it did not write the guard for. Returns `prior` when nothing changed.
270
+ */
271
+ function addCustomDimensions(
272
+ prior: Record<string, number> | undefined,
273
+ raw: unknown
274
+ ): Record<string, number> | undefined {
275
+ if (!isPlain(raw)) return prior;
276
+ let next: Record<string, number> | undefined;
277
+ let names = prior === undefined ? 0 : Object.keys(prior).length;
278
+ for (const [name, v] of Object.entries(raw)) {
279
+ if (!CUSTOM_DIMENSION_PATTERN.test(name) || RESERVED_FOLD_KEYS.has(name)) continue;
280
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > CUSTOM_DIMENSION_CEILING) {
281
+ continue;
282
+ }
283
+ const known = (next ?? prior) !== undefined && Object.hasOwn((next ?? prior) as object, name);
284
+ if (!known) {
285
+ if (names >= CUSTOM_DIMENSION_MAX) continue;
286
+ names++;
287
+ }
288
+ next ??= { ...prior };
289
+ next[name] = (Object.hasOwn(next, name) ? (next[name] as number) : 0) + v;
290
+ }
291
+ return next ?? prior;
292
+ }
293
+
294
+ /**
295
+ * ADR 0115 D3 — the valid entries of a decision's descriptive `estimatedCost` map (a name
296
+ * matching the dimension pattern or a built-in, a finite non-negative value, at most 32), or
297
+ * `undefined` when none survive. Never throws, so a log written without the guard folds the same
298
+ * on every replica; null-prototype so a caller-chosen name cannot reach a prototype member.
299
+ */
300
+ function sanitizeEstimatedCost(raw: unknown): Record<string, number> | undefined {
301
+ if (!isPlain(raw)) return undefined;
302
+ let out: Record<string, number> | undefined;
303
+ let names = 0;
304
+ for (const [name, v] of Object.entries(raw)) {
305
+ if (names >= CUSTOM_DIMENSION_MAX) break;
306
+ if (!BUILTIN_DIMENSION_NAMES.has(name)) {
307
+ if (!CUSTOM_DIMENSION_PATTERN.test(name) || RESERVED_FOLD_KEYS.has(name)) continue;
308
+ }
309
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) continue;
310
+ out ??= {};
311
+ out[name] = v;
312
+ names++;
313
+ }
314
+ return out;
315
+ }
316
+
317
+ export const BUILTIN_DIMENSION_NAMES: ReadonlySet<string> = new Set([
318
+ "tokens",
319
+ "costUsd",
320
+ "ms",
321
+ "memBytes",
322
+ "wallMs",
323
+ "netBytes",
324
+ ]);
325
+
326
+ /** A validated `_resources` seed from `continuedFrom.carried.resources`; invalid parts are ignored. */
327
+ function seedResources(raw: unknown): Record<string, unknown> | undefined {
328
+ if (!isPlain(raw)) return undefined;
329
+ const num = (v: unknown): number =>
330
+ typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
331
+ const custom = addCustomDimensions(undefined, raw.custom);
332
+ const memBytes = num(raw.memBytes);
333
+ const wallMs = num(raw.wallMs);
334
+ const netBytes = num(raw.netBytes);
335
+ // A quota denial survives a rotation: the successor of an execution that exceeded its budget
336
+ // starts exceeded, with the reason it was given (a continue-as-new cannot reset a quota).
337
+ const exceeded = raw.exceeded === true;
338
+ const reason = exceeded && typeof raw.reason === "string" ? raw.reason : undefined;
339
+ return {
340
+ tokens: num(raw.tokens),
341
+ costUsd: num(raw.costUsd),
342
+ ms: num(raw.ms),
343
+ count: num(raw.count),
344
+ ...(memBytes > 0 && { memBytes }),
345
+ ...(wallMs > 0 && { wallMs }),
346
+ ...(netBytes > 0 && { netBytes }),
347
+ ...(custom !== undefined && { custom }),
348
+ ...(exceeded && { exceeded: true }),
349
+ ...(reason !== undefined && { reason }),
350
+ };
351
+ }
352
+
353
+ /** A validated `_inbox` seed from `continuedFrom.carried.inbox`; invalid parts are ignored. */
354
+ function seedInbox(
355
+ raw: unknown
356
+ ): { cursors: Record<string, number>; dedupe: string[] } | undefined {
357
+ if (!isPlain(raw)) return undefined;
358
+ const cursors: Record<string, number> = {};
359
+ if (isPlain(raw.cursors)) {
360
+ for (const [topic, seq] of Object.entries(raw.cursors)) {
361
+ if (safeMapKey(topic) && Number.isSafeInteger(seq) && (seq as number) >= 0) {
362
+ cursors[topic] = seq as number;
363
+ }
364
+ }
365
+ }
366
+ const dedupe = Array.isArray(raw.dedupe)
367
+ ? raw.dedupe.filter((id): id is string => typeof id === "string").slice(-INBOX_DEDUPE_CAP)
368
+ : [];
369
+ return { cursors, dedupe };
370
+ }
371
+
372
+ /** One held (or released, not yet delivered) inbox message of a topic's quarantine queue (1.25.0). */
373
+ interface QuarantineEntry {
374
+ key: string;
375
+ inboxSeq: number;
376
+ heldAt: number;
377
+ state: "held" | "released";
378
+ }
379
+ type Quarantined = Record<string, QuarantineEntry[]>;
380
+
381
+ function quarantinedOf(state: ExecutionState): Quarantined | undefined {
382
+ const q = state.variables._quarantined;
383
+ return isPlain(q) ? (q as Quarantined) : undefined;
384
+ }
385
+
386
+ /** `variables` with `_quarantined` set to `q`, or ABSENT when `q` is empty (never-quarantined shape). */
387
+ function withQuarantined(
388
+ variables: Record<string, unknown>,
389
+ q: Quarantined
390
+ ): Record<string, unknown> {
391
+ const { _quarantined: _dropped, ...rest } = variables;
392
+ return Object.keys(q).length === 0 ? rest : { ...rest, _quarantined: q };
393
+ }
394
+
395
+ function quarantineTotal(q: Quarantined | undefined): number {
396
+ let n = 0;
397
+ if (q !== undefined) for (const queue of Object.values(q)) n += queue.length;
398
+ return n;
399
+ }
400
+
401
+ /**
402
+ * A validated `_quarantined` seed from `continuedFrom.carried.quarantined` (1.25.0); invalid parts
403
+ * are ignored, never thrown. A topic's queue must read `released? held*` (at most ONE released entry: the fold can only produce one) (the shape release
404
+ * preserves); a queue that does not is dropped whole. The total is capped at {@link QUARANTINE_FOLD_CAP}.
405
+ */
406
+ function seedQuarantined(raw: unknown): Quarantined | undefined {
407
+ if (!isPlain(raw)) return undefined;
408
+ const out: Quarantined = {};
409
+ let total = 0;
410
+ for (const [topic, queue] of Object.entries(raw)) {
411
+ if (!safeMapKey(topic) || !Array.isArray(queue)) continue;
412
+ const entries: QuarantineEntry[] = [];
413
+ let sawHeld = false;
414
+ let sawReleased = false;
415
+ let ok = true;
416
+ for (const e of queue as unknown[]) {
417
+ if (
418
+ !isPlain(e) ||
419
+ typeof e.key !== "string" ||
420
+ e.key.length === 0 ||
421
+ !Number.isSafeInteger(e.inboxSeq) ||
422
+ (e.inboxSeq as number) < 0 ||
423
+ !Number.isSafeInteger(e.heldAt) ||
424
+ (e.heldAt as number) < 0 ||
425
+ (e.state !== "held" && e.state !== "released") ||
426
+ (sawHeld && e.state === "released") ||
427
+ (sawReleased && e.state === "released")
428
+ ) {
429
+ ok = false;
430
+ break;
431
+ }
432
+ if (e.state === "held") sawHeld = true;
433
+ else sawReleased = true;
434
+ entries.push({
435
+ key: e.key,
436
+ inboxSeq: e.inboxSeq as number,
437
+ heldAt: e.heldAt as number,
438
+ state: e.state,
439
+ });
440
+ }
441
+ if (!ok || entries.length === 0 || total + entries.length > QUARANTINE_FOLD_CAP) continue;
442
+ total += entries.length;
443
+ out[topic] = entries;
444
+ }
445
+ return total > 0 ? out : undefined;
446
+ }
447
+
448
+ /** The chain identity a `forkChain` stamp or the start payload names; `undefined` if malformed. */
449
+ function chainIdentity(v: unknown): { chainId: string; generation: number } | undefined {
450
+ if (!isPlain(v)) return undefined;
451
+ const { chainId, generation } = v;
452
+ if (typeof chainId !== "string" || chainId.length === 0) return undefined;
453
+ if (!Number.isSafeInteger(generation) || (generation as number) < 0) return undefined;
454
+ return { chainId, generation: generation as number };
455
+ }
456
+
457
+ /** ADR 0110 D3 — a wait-addressed resume: kernel-stamped, payload `{ waitId, payload }`. */
458
+ type WaitResume =
459
+ | { answer: true; waitId: string; wait: SuspendWait; inner: unknown }
460
+ | { answer: false };
461
+ function parseWaitResume(state: ExecutionState, event: EventEnvelope): WaitResume | undefined {
462
+ if (!isKernelWritten(event)) return undefined;
463
+ const p = event.payload;
464
+ if (!isPlain(p) || typeof p.waitId !== "string" || !Object.hasOwn(p, "payload")) {
465
+ return undefined;
466
+ }
467
+ // The table is the fold-owned `suspendedOn.waits`: it exists ONLY when the park was a
468
+ // kernel-stamped EXECUTION_SUSPENDED (the handler drops `waits` from an unstamped one), and
469
+ // holds only well-formed entries, so a userspace park can never be answered as a multi-wait.
470
+ // Total: a non-array or malformed entry is skipped, never thrown.
471
+ const table: unknown = state.status === "suspended" ? state.suspendedOn?.waits : undefined;
472
+ const wait = Array.isArray(table)
473
+ ? (table as unknown[]).find((w): w is SuspendWait => isPlain(w) && w.waitId === p.waitId)
474
+ : undefined;
475
+ if (wait === undefined || !safeMapKey(p.waitId)) return { answer: false };
476
+ return { answer: true, waitId: p.waitId, wait, inner: p.payload };
477
+ }
478
+
479
+ const handlers: Record<string, Handler> = {
480
+ EXECUTION_STARTED: (state, event) => {
481
+ // `flowHash` (K11.1, additive — absent on legacy logs) is the content hash
482
+ // of the flow code this execution started under; resume/recovery compares
483
+ // it against the registered flow so changed code never silently folds an
484
+ // in-flight log. Reserved fold key: `variables._flowHash`.
485
+ //
486
+ // 1.23.0 (Wave 2) — further additive fields, each folded to a reserved key and each absent
487
+ // from every older log: `principal` → `_principal` (ADR 0111; a fork's `forkedBy` stamp
488
+ // wins), `parent` → `_parent`, `chainId` + `generation` (+ `continuedFrom`) → `_chain`
489
+ // (ADR 0109; a fork's `forkChain` stamp wins and drops `continuedFrom`), `codeHash` →
490
+ // `_codeHash` (ADR 0113). A successor's validated `continuedFrom.carried` seeds `_resources`
491
+ // and `_inbox` (never for a fork, which starts a fresh chain); an invalid carry is ignored,
492
+ // never thrown.
493
+ const payload = event.payload as {
494
+ input?: unknown;
495
+ flowHash?: string;
496
+ principal?: unknown;
497
+ parent?: unknown;
498
+ chainId?: unknown;
499
+ generation?: unknown;
500
+ continuedFrom?: unknown;
501
+ codeHash?: unknown;
502
+ };
503
+ const meta = event.metadata;
504
+ const vars: Record<string, unknown> = {};
505
+ if (payload.input !== undefined) vars._input = payload.input;
506
+ if (payload.flowHash !== undefined) vars._flowHash = payload.flowHash;
507
+ const principal = isPlain(meta?.forkedBy) ? meta.forkedBy : payload.principal;
508
+ if (isPlain(principal)) vars._principal = principal;
509
+ if (typeof payload.parent === "string" && payload.parent.length > 0) {
510
+ vars._parent = payload.parent;
511
+ }
512
+ const forkChain = chainIdentity(meta?.forkChain);
513
+ let carriedFrom: Record<string, unknown> | undefined;
514
+ let forkResources: unknown;
515
+ const own = forkChain ?? chainIdentity(payload);
516
+ if (own !== undefined) {
517
+ const from = payload.continuedFrom;
518
+ const continuedFrom =
519
+ forkChain === undefined &&
520
+ isPlain(from) &&
521
+ typeof from.executionId === "string" &&
522
+ (from.carried === undefined || isPlain(from.carried))
523
+ ? {
524
+ executionId: from.executionId,
525
+ ...(from.carried !== undefined && { carried: from.carried }),
526
+ }
527
+ : undefined;
528
+ vars._chain = { ...own, ...(continuedFrom !== undefined && { continuedFrom }) };
529
+ carriedFrom = continuedFrom?.carried as Record<string, unknown> | undefined;
530
+ // A fork of a chain member leaves the chain (no `continuedFrom`, no carried inbox cursors or
531
+ // deadline), but it keeps what is the execution's OWN history rather than the chain's
532
+ // membership: the running resource totals, so a carried `exceeded` quota is not shed by
533
+ // forking (a fork of an ordinary execution keeps it through its copied prefix).
534
+ if (forkChain !== undefined && isPlain(payload.continuedFrom)) {
535
+ const carried = (payload.continuedFrom as { carried?: unknown }).carried;
536
+ if (isPlain(carried)) forkResources = carried.resources;
537
+ }
538
+ }
539
+ if (typeof payload.codeHash === "string") vars._codeHash = payload.codeHash;
540
+ const resources = seedResources(carriedFrom?.resources ?? forkResources);
541
+ if (resources !== undefined) vars._resources = resources;
542
+ const inbox = seedInbox(carriedFrom?.inbox);
543
+ if (inbox !== undefined) vars._inbox = inbox;
544
+ // 1.25.0 (ADR 0130 residuals D9) — the quarantine queues ride the same carry as the inbox.
545
+ const quarantined = seedQuarantined(carriedFrom?.quarantined);
546
+ if (quarantined !== undefined) vars._quarantined = quarantined;
547
+ return {
548
+ ...state,
549
+ status: "running",
550
+ startedAt: event.timestamp,
551
+ updatedAt: event.timestamp,
552
+ ...(Object.keys(vars).length > 0 && { variables: { ...state.variables, ...vars } }),
553
+ };
554
+ },
555
+
556
+ STEP_STARTED: (state, event) => {
557
+ const payload = event.payload as { stepId: string; input?: unknown; scope?: unknown };
558
+ const existing = state.frames[payload.stepId];
559
+ // ADR 0113 D3 — the log's first-start order, and (kernel-stamped only) the scope it started in.
560
+ const scope =
561
+ existing?.scope ??
562
+ (isKernelWritten(event) && typeof payload.scope === "string" ? payload.scope : undefined);
563
+ const firstSeq = existing === undefined ? event.sequence : existing.firstSeq;
564
+ const frame: ExecutionFrame = {
565
+ stepId: payload.stepId,
566
+ // Set ONLY when the frame is new: a frame folded before `firstSeq` existed (an old snapshot)
567
+ // that is re-started in the tail must not be stamped with the LATER sequence, or it would
568
+ // rank last and an unchanged body would be refused. It stays unranked (sorts first).
569
+ ...(firstSeq !== undefined && { firstSeq }),
570
+ ...(scope !== undefined && { scope }),
571
+ status: "running",
572
+ attempts: (existing?.attempts ?? 0) + 1,
573
+ startedAt: event.timestamp,
574
+ // K8.3 — the first attempt's logged start survives re-runs, so deadlines
575
+ // anchored here are never reset by crash recovery.
576
+ firstStartedAt: existing?.firstStartedAt ?? existing?.startedAt ?? event.timestamp,
577
+ ...(payload.input !== undefined && { input: payload.input }),
578
+ };
579
+ return {
580
+ ...state,
581
+ frames: setFrame(state.frames, payload.stepId, frame),
582
+ updatedAt: event.timestamp,
583
+ };
584
+ },
585
+
586
+ STEP_COMPLETED: (state, event) => {
587
+ const payload = event.payload as { stepId: string; output?: unknown };
588
+ const existing = state.frames[payload.stepId];
589
+ const frame: ExecutionFrame = {
590
+ stepId: payload.stepId,
591
+ status: "completed",
592
+ attempts: existing?.attempts ?? 1,
593
+ ...(existing?.firstSeq !== undefined && { firstSeq: existing.firstSeq }),
594
+ ...(existing?.scope !== undefined && { scope: existing.scope }),
595
+ completedAt: event.timestamp,
596
+ ...(existing?.startedAt !== undefined && { startedAt: existing.startedAt }),
597
+ ...(existing?.firstStartedAt !== undefined && { firstStartedAt: existing.firstStartedAt }),
598
+ ...(payload.output !== undefined && { output: payload.output }),
599
+ };
600
+ return {
601
+ ...state,
602
+ frames: setFrame(state.frames, payload.stepId, frame),
603
+ updatedAt: event.timestamp,
604
+ };
605
+ },
606
+
607
+ STEP_FAILED: (state, event) => {
608
+ const payload = event.payload as {
609
+ stepId: string;
610
+ error?: unknown;
611
+ // DE-4 (1.17.0) — present only for typed failures on a DECLARED channel;
612
+ // absence folds byte-identically to pre-1.17 logs (K5 payload-extension rule).
613
+ channel?: string;
614
+ payload?: unknown;
615
+ };
616
+ const existing = state.frames[payload.stepId];
617
+ const frame: ExecutionFrame = {
618
+ stepId: payload.stepId,
619
+ status: "failed",
620
+ attempts: existing?.attempts ?? 1,
621
+ ...(existing?.firstSeq !== undefined && { firstSeq: existing.firstSeq }),
622
+ ...(existing?.scope !== undefined && { scope: existing.scope }),
623
+ ...(existing?.startedAt !== undefined && { startedAt: existing.startedAt }),
624
+ ...(existing?.firstStartedAt !== undefined && { firstStartedAt: existing.firstStartedAt }),
625
+ ...(payload.error !== undefined && { error: payload.error }),
626
+ ...(payload.channel !== undefined && {
627
+ errorChannel: payload.channel,
628
+ errorPayload: payload.payload,
629
+ }),
630
+ };
631
+ // ADR 0105 — the STEP failed, not the execution: leave execution `status` alone. A flow
632
+ // may catch the throw (try/catch, a saga compensation, a typed-failure fallback) and carry
633
+ // on; folding "failed" here made a live execution read terminal, so a crash after the
634
+ // catch was never re-driven and `cancel` refused it. A failure that escapes the runner is
635
+ // recorded by the EXECUTION_FAILED the kernel commits after it, which owns the terminal
636
+ // status (as EXECUTION_CANCELLED does for STEP_CANCELLED below). A failure that lands while
637
+ // the execution is parked (a parallel sibling) marks FAILED_WHILE_PARKED — see there.
638
+ const parked = state.status === "suspended" && state.variables[FAILED_WHILE_PARKED] !== true;
639
+ return {
640
+ ...state,
641
+ frames: setFrame(state.frames, payload.stepId, frame),
642
+ ...(parked && { variables: { ...state.variables, [FAILED_WHILE_PARKED]: true } }),
643
+ updatedAt: event.timestamp,
644
+ };
645
+ },
646
+
647
+ STEP_CANCELLED: (state, event) => {
648
+ // Marks the in-flight step as cancelled WITHOUT touching execution status —
649
+ // the surrounding EXECUTION_CANCELLED event owns the execution-level status,
650
+ // and this must not overwrite it back to a failure/other state.
651
+ const payload = event.payload as { stepId: string };
652
+ const existing = state.frames[payload.stepId];
653
+ const frame: ExecutionFrame = {
654
+ stepId: payload.stepId,
655
+ status: "cancelled",
656
+ attempts: existing?.attempts ?? 1,
657
+ ...(existing?.firstSeq !== undefined && { firstSeq: existing.firstSeq }),
658
+ ...(existing?.scope !== undefined && { scope: existing.scope }),
659
+ ...(existing?.startedAt !== undefined && { startedAt: existing.startedAt }),
660
+ ...(existing?.firstStartedAt !== undefined && { firstStartedAt: existing.firstStartedAt }),
661
+ };
662
+ return {
663
+ ...state,
664
+ frames: setFrame(state.frames, payload.stepId, frame),
665
+ updatedAt: event.timestamp,
666
+ };
667
+ },
668
+
669
+ STEP_RETRYING: (state, event) => {
670
+ const payload = event.payload as { stepId: string; attempt: number };
671
+ const existing = state.frames[payload.stepId];
672
+ const frame: ExecutionFrame = {
673
+ stepId: payload.stepId,
674
+ status: "pending",
675
+ attempts: payload.attempt,
676
+ ...(existing?.firstSeq !== undefined && { firstSeq: existing.firstSeq }),
677
+ ...(existing?.scope !== undefined && { scope: existing.scope }),
678
+ ...(existing?.input !== undefined && { input: existing.input }),
679
+ };
680
+ return {
681
+ ...state,
682
+ frames: setFrame(state.frames, payload.stepId, frame),
683
+ updatedAt: event.timestamp,
684
+ };
685
+ },
686
+
687
+ PARALLEL_STARTED: (state, event) => {
688
+ const payload = event.payload as { stepIds: string[] };
689
+ // One fresh container, then write each new frame into it (the container is
690
+ // not shared, so in-place writes are safe). Mark it fold-owned so subsequent
691
+ // step events in the same fold pass mutate it in place instead of re-spreading.
692
+ const frames = { ...state.frames };
693
+ foldOwned.add(frames);
694
+ for (const stepId of payload.stepIds) {
695
+ assertSafeKey(stepId);
696
+ frames[stepId] = {
697
+ stepId,
698
+ status: "pending",
699
+ attempts: 0,
700
+ startedAt: event.timestamp,
701
+ };
702
+ }
703
+ return { ...state, frames, updatedAt: event.timestamp };
704
+ },
705
+
706
+ // 1.23.0 (ADR 0112 D2) — the journaled fail-fast scope outcome. Folds `_scopes[scopePath]` ONLY
707
+ // from a kernel-stamped envelope; an unstamped one keeps the pre-1.23.0 behaviour (no variable
708
+ // change, `updatedAt` only), so a log that already holds such an event replays identically.
709
+ PARALLEL_COMPLETED: (state, event) => {
710
+ if (!isKernelWritten(event)) return { ...state, updatedAt: event.timestamp };
711
+ const p = event.payload as {
712
+ scopePath?: unknown;
713
+ failedBranch?: unknown;
714
+ error?: unknown;
715
+ cancelled?: unknown;
716
+ errorName?: unknown;
717
+ errorChannel?: unknown;
718
+ errorPayload?: unknown;
719
+ launched?: unknown;
720
+ consumed?: unknown;
721
+ cursor?: unknown;
722
+ };
723
+ if (!isPlain(p) || !safeMapKey(p.scopePath)) return { ...state, updatedAt: event.timestamp };
724
+ const scopes = ownedRecord(state.variables._scopes as Record<string, unknown> | undefined);
725
+ // Amended 2026-10-06 (R6 review, user decision) — ADDITIVE optional fields, each folded only
726
+ // when well-formed; a record without them folds exactly as before (and a re-run serves it
727
+ // as before: it launches every branch and takes gate answers by position).
728
+ const nonNegInt = (v: unknown): v is number => Number.isSafeInteger(v) && (v as number) >= 0;
729
+ const consumed = Array.isArray(p.consumed)
730
+ ? p.consumed.filter(
731
+ (e): e is [string, number, number] =>
732
+ Array.isArray(e) &&
733
+ e.length === 3 &&
734
+ typeof e[0] === "string" &&
735
+ nonNegInt(e[1]) &&
736
+ nonNegInt(e[2]) &&
737
+ e[2] >= 1
738
+ )
739
+ : undefined;
740
+ scopes[p.scopePath] = {
741
+ ...(typeof p.failedBranch === "number" && { failedBranch: p.failedBranch }),
742
+ ...(p.error !== undefined && { error: p.error }),
743
+ ...(typeof p.errorName === "string" && { errorName: p.errorName }),
744
+ ...(typeof p.errorChannel === "string" && { errorChannel: p.errorChannel }),
745
+ ...(p.errorPayload !== undefined && { errorPayload: p.errorPayload }),
746
+ cancelled: Array.isArray(p.cancelled)
747
+ ? p.cancelled.filter((id): id is string => typeof id === "string")
748
+ : [],
749
+ ...(nonNegInt(p.launched) && { launched: p.launched }),
750
+ ...(consumed !== undefined && { consumed }),
751
+ ...(nonNegInt(p.cursor) && { cursor: p.cursor }),
752
+ };
753
+ return {
754
+ ...state,
755
+ variables: { ...state.variables, _scopes: scopes },
756
+ updatedAt: event.timestamp,
757
+ };
758
+ },
759
+
760
+ // 1.23.0 (ADR 0109 D2) — the predecessor of a continue-as-new ends, naming its successor. A NEW
761
+ // terminal status, `continued` (never `completed`: there is no output), and `_continuedTo`.
762
+ // Kernel-stamped envelopes only; an unstamped one is the no-op an unknown kind always was.
763
+ EXECUTION_CONTINUED: (state, event) => {
764
+ if (!isKernelWritten(event)) return state;
765
+ const p = event.payload as { successorId?: unknown };
766
+ if (!isPlain(p) || typeof p.successorId !== "string" || p.successorId.length === 0) {
767
+ return state;
768
+ }
769
+ return {
770
+ ...state,
771
+ status: "continued",
772
+ variables: { ...withoutFailedWhileParked(state.variables), _continuedTo: p.successorId },
773
+ updatedAt: event.timestamp,
774
+ };
775
+ },
776
+
777
+ // 1.23.0 (ADR 0110 D5/D6) — the owner consumed an inbox message: the per-topic cursor advances,
778
+ // the bounded dedupe window remembers the id, and the delivered payload answers its wait
779
+ // (`_waitAnswers[waitId]`, first answer wins). A `messageId` already in the window is a
780
+ // duplicate: the cursor advances, nothing is delivered. Kernel-stamped envelopes only.
781
+ MESSAGE_CONSUMED: (state, event) => {
782
+ if (!isKernelWritten(event)) return state;
783
+ const p = event.payload as {
784
+ waitId?: unknown;
785
+ topic?: unknown;
786
+ messageId?: unknown;
787
+ inboxSeq?: unknown;
788
+ payload?: unknown;
789
+ };
790
+ if (
791
+ !isPlain(p) ||
792
+ !safeMapKey(p.waitId) ||
793
+ !safeMapKey(p.topic) ||
794
+ typeof p.messageId !== "string" ||
795
+ !Number.isSafeInteger(p.inboxSeq) ||
796
+ (p.inboxSeq as number) < 0
797
+ ) {
798
+ return state;
799
+ }
800
+ const prior = state.variables._inbox as
801
+ | { cursors?: Record<string, number>; dedupe?: string[] }
802
+ | undefined;
803
+ const cursors = ownedRecord(prior?.cursors);
804
+ // Own-property read: a topic named `toString` / `valueOf` must not read a prototype member.
805
+ const was = Object.hasOwn(cursors, p.topic) ? (cursors[p.topic] as number) : -1;
806
+ cursors[p.topic] = Math.max(was, p.inboxSeq as number);
807
+ const window = prior?.dedupe;
808
+ const duplicate = window?.includes(p.messageId) === true;
809
+ let dedupe = window;
810
+ if (!duplicate) {
811
+ dedupe = appendAudit(window, p.messageId);
812
+ if (dedupe.length > INBOX_DEDUPE_CAP) dedupe.splice(0, dedupe.length - INBOX_DEDUPE_CAP);
813
+ }
814
+ const variables: Record<string, unknown> = {
815
+ ...state.variables,
816
+ _inbox: { cursors, dedupe: dedupe ?? [] },
817
+ };
818
+ if (!duplicate) {
819
+ const answers = ownedRecord(
820
+ state.variables._waitAnswers as Record<string, unknown> | undefined
821
+ );
822
+ if (!Object.hasOwn(answers, p.waitId)) {
823
+ answers[p.waitId] = { kind: "inbox", name: p.topic, payload: p.payload };
824
+ }
825
+ variables._waitAnswers = answers;
826
+ }
827
+ // 1.25.0 (ADR 0130 residuals D9) — gated on `_quarantined`: a log that never quarantines folds
828
+ // byte-identically. Delivering the topic's first RELEASED entry (matched by its dedupe key, which
829
+ // is this fact's `messageId`) drops it from the queue; the cursor already sits at or above its
830
+ // original `inboxSeq`, so it does not move.
831
+ const held = quarantinedOf(state);
832
+ if (held !== undefined && Object.hasOwn(held, p.topic)) {
833
+ const queue = held[p.topic] as QuarantineEntry[];
834
+ const first = queue[0];
835
+ if (first !== undefined && first.state === "released" && first.key === p.messageId) {
836
+ const next = ownedRecord(held);
837
+ if (queue.length === 1) delete next[p.topic];
838
+ else next[p.topic] = queue.slice(1);
839
+ // An emptied record is deleted, so a full hold/release/consume cycle folds to the
840
+ // never-quarantined shape.
841
+ return {
842
+ ...state,
843
+ variables: withQuarantined(variables, next),
844
+ updatedAt: event.timestamp,
845
+ };
846
+ }
847
+ }
848
+ return { ...state, variables, updatedAt: event.timestamp };
849
+ },
850
+
851
+ // 1.25.0 (ADR 0130 residuals D9; written by `quarantineInbox`) — the topic's head message is HELD: the
852
+ // cursor advances past the original entry (so it no longer pins the stream), the entry joins the
853
+ // topic's FIFO queue `_quarantined[topic]`, and the dedupe window is NOT touched (the release
854
+ // delivery must not read as a duplicate). Kernel-stamped envelopes only; a malformed fact, or one
855
+ // that would take the total past QUARANTINE_FOLD_CAP, is ignored.
856
+ INBOX_QUARANTINED: (state, event) => {
857
+ if (!isKernelWritten(event)) return state;
858
+ const p = event.payload as Record<string, unknown> | null;
859
+ if (
860
+ !isPlain(p) ||
861
+ !safeMapKey(p.topic) ||
862
+ !Number.isSafeInteger(p.inboxSeq) ||
863
+ (p.inboxSeq as number) < 0 ||
864
+ typeof p.key !== "string" ||
865
+ p.key.length === 0 ||
866
+ typeof p.messageId !== "string" ||
867
+ !Number.isSafeInteger(p.heldAt) ||
868
+ (p.heldAt as number) < 0 ||
869
+ !isPlain(p.operator)
870
+ ) {
871
+ return state;
872
+ }
873
+ const prior = quarantinedOf(state);
874
+ if (quarantineTotal(prior) + 1 > QUARANTINE_FOLD_CAP) return state;
875
+ const inbox = state.variables._inbox as
876
+ | { cursors?: Record<string, number>; dedupe?: string[] }
877
+ | undefined;
878
+ const cursors = ownedRecord(inbox?.cursors);
879
+ const was = Object.hasOwn(cursors, p.topic) ? (cursors[p.topic] as number) : -1;
880
+ cursors[p.topic] = Math.max(was, p.inboxSeq as number);
881
+ const quarantined = ownedRecord(prior);
882
+ const queue = Object.hasOwn(quarantined, p.topic)
883
+ ? (quarantined[p.topic] as QuarantineEntry[])
884
+ : [];
885
+ quarantined[p.topic] = [
886
+ ...queue,
887
+ {
888
+ key: p.key,
889
+ inboxSeq: p.inboxSeq as number,
890
+ heldAt: p.heldAt as number,
891
+ state: "held",
892
+ },
893
+ ];
894
+ return {
895
+ ...state,
896
+ variables: {
897
+ ...state.variables,
898
+ _inbox: { cursors, dedupe: inbox?.dedupe ?? [] },
899
+ _quarantined: quarantined,
900
+ },
901
+ updatedAt: event.timestamp,
902
+ };
903
+ },
904
+
905
+ // 1.25.0 (ADR 0130 residuals D9; written by `releaseInbox` and the capable-node auto-release) — the topic's OLDEST held entry is
906
+ // released or dropped. EXACTLY ONE of `operator` (a manual release) / `auto` (an automatic one,
907
+ // only with `deliver`) is present; a fact with both or neither is ignored, as is one that does not
908
+ // name the topic's first entry while it is `held`. "deliver" marks it `released`; "discard" removes
909
+ // it and adds its key to the dedupe window (a sender's retry of that id is then a duplicate).
910
+ INBOX_RELEASED: (state, event) => {
911
+ if (!isKernelWritten(event)) return state;
912
+ const p = event.payload as Record<string, unknown> | null;
913
+ if (
914
+ !isPlain(p) ||
915
+ !safeMapKey(p.topic) ||
916
+ typeof p.key !== "string" ||
917
+ p.key.length === 0 ||
918
+ typeof p.messageId !== "string" ||
919
+ (p.disposition !== "deliver" && p.disposition !== "discard")
920
+ ) {
921
+ return state;
922
+ }
923
+ const hasOperator = p.operator !== undefined;
924
+ const hasAuto = p.auto !== undefined;
925
+ if (hasOperator === hasAuto) return state; // both, or neither
926
+ if (hasOperator && !isPlain(p.operator)) return state;
927
+ if (hasAuto && (p.auto !== true || p.disposition !== "deliver")) return state;
928
+ const prior = quarantinedOf(state);
929
+ if (prior === undefined || !Object.hasOwn(prior, p.topic)) return state;
930
+ const queue = prior[p.topic] as QuarantineEntry[];
931
+ const first = queue[0];
932
+ if (first === undefined || first.state !== "held" || first.key !== p.key) return state;
933
+ const quarantined = ownedRecord(prior);
934
+ if (p.disposition === "deliver") {
935
+ quarantined[p.topic] = [{ ...first, state: "released" }, ...queue.slice(1)];
936
+ return {
937
+ ...state,
938
+ variables: { ...state.variables, _quarantined: quarantined },
939
+ updatedAt: event.timestamp,
940
+ };
941
+ }
942
+ if (queue.length === 1) delete quarantined[p.topic];
943
+ else quarantined[p.topic] = queue.slice(1);
944
+ const inbox = state.variables._inbox as
945
+ | { cursors?: Record<string, number>; dedupe?: string[] }
946
+ | undefined;
947
+ const window = inbox?.dedupe;
948
+ let dedupe = window;
949
+ if (window?.includes(first.key) !== true) {
950
+ dedupe = appendAudit(window, first.key);
951
+ if (dedupe.length > INBOX_DEDUPE_CAP) dedupe.splice(0, dedupe.length - INBOX_DEDUPE_CAP);
952
+ }
953
+ const variables = withQuarantined(
954
+ { ...state.variables, _inbox: { cursors: inbox?.cursors ?? {}, dedupe: dedupe ?? [] } },
955
+ quarantined
956
+ );
957
+ return { ...state, variables, updatedAt: event.timestamp };
958
+ },
959
+
960
+ EXECUTION_SUSPENDED: (state, event) => {
961
+ const payload = event.payload as {
962
+ condition: {
963
+ on: string;
964
+ timeout?: number;
965
+ correlationKey?: string;
966
+ waits?: SuspendWait[];
967
+ };
968
+ };
969
+ let variables = withoutFailedWhileParked(state.variables);
970
+ // 1.23.0 (ADR 0110 D4) — `condition.waits` is honoured ONLY on a kernel-stamped park (the R2
971
+ // writer stamps `metadata.kernel` on every EXECUTION_SUSPENDED that carries waits; the
972
+ // runtime's commit site strips `waits` from every userspace park, so no unstamped event
973
+ // carries them — this is the fold's own guard for a log it did not write). An unstamped
974
+ // park's `waits` is dropped from `suspendedOn` too, so nothing downstream (the wait-addressed
975
+ // resume's lookup, `loom` introspection) can read a table userspace wrote.
976
+ let condition: unknown = payload.condition;
977
+ if (isPlain(condition) && Object.hasOwn(condition, "waits")) {
978
+ const { waits: raw, ...rest } = condition;
979
+ condition = rest;
980
+ if (isKernelWritten(event) && Array.isArray(raw)) {
981
+ const clean = (raw as unknown[]).filter(
982
+ (w): w is SuspendWait =>
983
+ isPlain(w) &&
984
+ safeMapKey(w.waitId) &&
985
+ (w.kind === "gate" || w.kind === "inbox" || w.kind === "timer")
986
+ );
987
+ condition = { ...rest, waits: clean };
988
+ // A timer arm's `wakeAt` is recorded at its FIRST registration and served on every
989
+ // later one, so sibling traffic cannot push a deadline out.
990
+ let recorded: Record<string, unknown> | undefined;
991
+ for (const w of clean) {
992
+ if (w.kind !== "timer") continue;
993
+ if (typeof w.wakeAt !== "number" || !Number.isFinite(w.wakeAt)) continue;
994
+ recorded ??= ownedRecord(variables._waits as Record<string, unknown> | undefined);
995
+ if (!Object.hasOwn(recorded, w.waitId)) {
996
+ recorded[w.waitId] = { kind: "timer", wakeAt: w.wakeAt };
997
+ }
998
+ }
999
+ if (recorded !== undefined) variables = { ...variables, _waits: recorded };
1000
+ // A park lists EVERY unanswered arm of every pending select, so a recorded timer whose
1001
+ // waitId this park does not list belongs to a select another arm already settled: drop
1002
+ // it, or a loop of `select([inbox, timer])` grows `_waits` (state and every snapshot)
1003
+ // by one entry per iteration, forever.
1004
+ const priorWaits = variables._waits;
1005
+ if (isPlain(priorWaits)) {
1006
+ const listed = new Set(clean.map((w) => w.waitId));
1007
+ const stale = Object.keys(priorWaits).filter((id) => !listed.has(id));
1008
+ if (stale.length > 0) {
1009
+ const kept = ownedRecord(priorWaits as Record<string, unknown>);
1010
+ for (const id of stale) delete kept[id];
1011
+ variables = { ...variables, _waits: kept };
1012
+ }
1013
+ }
1014
+ }
1015
+ }
1016
+ return {
1017
+ ...state,
1018
+ status: "suspended",
1019
+ suspendedOn: condition as typeof payload.condition,
1020
+ ...(variables !== state.variables && { variables }),
1021
+ updatedAt: event.timestamp,
1022
+ };
1023
+ },
1024
+
1025
+ EXECUTION_RESUMED: (state, event) => {
1026
+ const { suspendedOn: _removed, ...rest } = state;
1027
+ // 1.23.0 (ADR 0110 D3) — a wait-addressed resume answers ONE wait of a multi-wait park: its
1028
+ // payload is `{ waitId, payload }` and the fold unwraps it into `_waitAnswers[waitId]`
1029
+ // (kind and name from the park's wait; first answer wins) and drops the wait's pinned
1030
+ // `_waits` entry. It never enters the positional `_resumePayloads` (the two mechanisms do
1031
+ // not share entries). One naming no wait of the current park is a status-only re-open.
1032
+ const addressed = parseWaitResume(state, event);
1033
+ if (addressed !== undefined) {
1034
+ const variables = { ...withoutFailedWhileParked(state.variables) };
1035
+ if (addressed.answer) {
1036
+ const { waitId, wait, inner } = addressed;
1037
+ const answers = ownedRecord(variables._waitAnswers as Record<string, unknown> | undefined);
1038
+ if (!Object.hasOwn(answers, waitId)) {
1039
+ answers[waitId] = {
1040
+ kind: wait.kind,
1041
+ name: wait.on ?? wait.topic ?? wait.kind,
1042
+ payload: inner,
1043
+ };
1044
+ }
1045
+ variables._waitAnswers = answers;
1046
+ if (isPlain(variables._waits) && Object.hasOwn(variables._waits, waitId)) {
1047
+ const waits = ownedRecord(variables._waits as Record<string, unknown>);
1048
+ delete waits[waitId];
1049
+ variables._waits = waits;
1050
+ }
1051
+ }
1052
+ return { ...rest, status: "running", variables, updatedAt: event.timestamp };
1053
+ }
1054
+ return {
1055
+ ...rest,
1056
+ status: "running",
1057
+ variables: {
1058
+ ...withoutFailedWhileParked(state.variables),
1059
+ ...(event.payload !== undefined &&
1060
+ !isStatusOnlyResume(state, event.payload) && {
1061
+ // Latest payload (legacy key, kept for compat with pre-1.16 snapshots)…
1062
+ _resumePayload: event.payload,
1063
+ // …and the ORDERED list of every resume payload (1.16.0): a flow with
1064
+ // N suspend gates passes gate i through with payload i on replay —
1065
+ // the single-latest key silently fed gate 1 the LAST gate's payload.
1066
+ _resumePayloads: appendAudit(
1067
+ state.variables._resumePayloads as unknown[] | undefined,
1068
+ event.payload
1069
+ ),
1070
+ }),
1071
+ },
1072
+ updatedAt: event.timestamp,
1073
+ };
1074
+ },
1075
+
1076
+ // Loom 2.0 (P9) — a durable record of an advisory/optimizer choice. Stored in a
1077
+ // reserved `_decisions` map keyed by decisionId so replay serves the recorded
1078
+ // choice instead of re-consulting the optimizer, keeping the fold pure (P2).
1079
+ DECISION_RECORDED: (state, event) => {
1080
+ const payload = event.payload as {
1081
+ decisionId: string;
1082
+ choice: unknown;
1083
+ rationale?: unknown;
1084
+ source?: string;
1085
+ // D1 (ADR 0030) — descriptive materialization fields; folded verbatim but NEVER
1086
+ // read by getDecision, so `choice` remains the sole replay input (P9). Absence = legacy.
1087
+ alternatives?: unknown;
1088
+ estimatedCostUsd?: number;
1089
+ // ADR 0115 D3 — the open spelling of `estimatedCostUsd`: descriptive, never a replay input.
1090
+ estimatedCost?: unknown;
1091
+ estimatedLatencyMs?: number;
1092
+ confidence?: number;
1093
+ // ADR 0079 — the observation the policy saw and which policy saw it. Same
1094
+ // descriptive contract as the fields above: folded verbatim, never a replay
1095
+ // input. Absence = legacy (or a decision no policy produced).
1096
+ context?: Record<string, unknown>;
1097
+ policy?: { id: string; version: string; propensity?: number };
1098
+ // ADR 0113 D3 (fix round 1) — the step ids the decider's own logic ran. Kernel-stamped only
1099
+ // (an app event naming it folds as before); a served decision exempts exactly these from the
1100
+ // replay-divergence detector. Descriptive: never a replay input.
1101
+ innerSteps?: unknown;
1102
+ // Past the inner-step cap: the cap's worth of ids plus the journal sequence the decider began at
1103
+ // (a served overflow decision also exempts the frames first started between it and this record).
1104
+ innerStepsOverflow?: unknown;
1105
+ innerFrom?: unknown;
1106
+ };
1107
+ assertSafeKey(payload.decisionId);
1108
+ // R1a — the decision map is a per-fold-owned container, written in place (the same rule
1109
+ // as `setFrame`/`appendAudit`): spreading it per event made a fold of D decisions O(D²)
1110
+ // (20k decisions = 42 s). The caller's map (snapshot / prior state / an escaped state) is
1111
+ // copied once, then owned. Insertion order is unchanged: a new id appends, a repeated id
1112
+ // keeps its slot — exactly what `{ ...existing, [id]: v }` did.
1113
+ const prior = state.variables._decisions as Record<string, unknown> | undefined;
1114
+ let decisions: Record<string, unknown>;
1115
+ if (prior !== undefined && foldOwned.has(prior)) {
1116
+ decisions = prior;
1117
+ } else {
1118
+ decisions = prior === undefined ? {} : { ...prior };
1119
+ foldOwned.add(decisions);
1120
+ }
1121
+ const estimatedCost = sanitizeEstimatedCost(payload.estimatedCost);
1122
+ // 1.24.0 (ADR 0128) — the record's own envelope sequence, so the ORDER decisions were made in
1123
+ // survives a checkpoint (the map's key order does not: array-index-like ids sort first, and a
1124
+ // normalising snapshot store such as Postgres `jsonb` reorders keys). From the ENVELOPE, never
1125
+ // the payload; a repeated id keeps its FIRST `seq` (as it keeps its slot). A repeat of a legacy
1126
+ // entry (folded before 1.24.0, no `seq`) stays without one: its first sequence is unknowable.
1127
+ const had = Object.hasOwn(decisions, payload.decisionId)
1128
+ ? (decisions[payload.decisionId] as { seq?: unknown } | undefined)
1129
+ : undefined;
1130
+ const seq = had === undefined ? event.sequence : had.seq;
1131
+ decisions[payload.decisionId] = {
1132
+ choice: payload.choice,
1133
+ ...(payload.rationale !== undefined && { rationale: payload.rationale }),
1134
+ ...(payload.source !== undefined && { source: payload.source }),
1135
+ ...(payload.alternatives !== undefined && { alternatives: payload.alternatives }),
1136
+ ...(payload.estimatedCostUsd !== undefined && {
1137
+ estimatedCostUsd: payload.estimatedCostUsd,
1138
+ }),
1139
+ ...(estimatedCost !== undefined && { estimatedCost }),
1140
+ ...(payload.estimatedLatencyMs !== undefined && {
1141
+ estimatedLatencyMs: payload.estimatedLatencyMs,
1142
+ }),
1143
+ ...(payload.confidence !== undefined && { confidence: payload.confidence }),
1144
+ ...(payload.context !== undefined && { context: payload.context }),
1145
+ ...(payload.policy !== undefined && { policy: payload.policy }),
1146
+ ...(isKernelWritten(event) &&
1147
+ Array.isArray(payload.innerSteps) && {
1148
+ innerSteps: payload.innerSteps.filter((id): id is string => typeof id === "string"),
1149
+ ...(payload.innerStepsOverflow === true &&
1150
+ Number.isSafeInteger(payload.innerFrom) && {
1151
+ innerStepsOverflow: true,
1152
+ innerFrom: payload.innerFrom as number,
1153
+ recordedAtSeq: event.sequence,
1154
+ }),
1155
+ }),
1156
+ ...(Number.isSafeInteger(seq) && { seq: seq as number }),
1157
+ };
1158
+ return {
1159
+ ...state,
1160
+ variables: { ...state.variables, _decisions: decisions },
1161
+ updatedAt: event.timestamp,
1162
+ };
1163
+ },
1164
+
1165
+ // Loom 2.0 (K2) — a capability denial: the EffectRuntime refused an ungranted
1166
+ // effect. Recorded as a durable audit fact in a reserved `_denials` list (the
1167
+ // step itself still fails via the STEP_FAILED that follows). Pure audit — it does
1168
+ // not change frame/execution status.
1169
+ EFFECT_DENIED: (state, event) => {
1170
+ const payload = event.payload as { toolId: string; stepId?: string; reason?: string };
1171
+ return {
1172
+ ...state,
1173
+ variables: {
1174
+ ...state.variables,
1175
+ _denials: appendAudit(state.variables._denials as unknown[] | undefined, {
1176
+ toolId: payload.toolId,
1177
+ ...(payload.stepId !== undefined && { stepId: payload.stepId }),
1178
+ ...(payload.reason !== undefined && { reason: payload.reason }),
1179
+ }),
1180
+ },
1181
+ updatedAt: event.timestamp,
1182
+ };
1183
+ },
1184
+
1185
+ // C.1 (execution-lifecycle authz) — an EXECUTION_DENIED fact: an ExecutionAuthorizer
1186
+ // refused a lifecycle operation (resume/cancel) on an existing execution. Recorded as
1187
+ // a durable audit fact in a reserved `_executionDenials` list, symmetric to
1188
+ // EFFECT_DENIED (which gates effects). Pure audit — the op is refused by a thrown
1189
+ // error; this fold does not change frame/execution status.
1190
+ EXECUTION_DENIED: (state, event) => {
1191
+ const payload = event.payload as { op: string; reason?: string; actor?: string };
1192
+ return {
1193
+ ...state,
1194
+ variables: {
1195
+ ...state.variables,
1196
+ _executionDenials: appendAudit(state.variables._executionDenials as unknown[] | undefined, {
1197
+ op: payload.op,
1198
+ ...(payload.reason !== undefined && { reason: payload.reason }),
1199
+ ...(payload.actor !== undefined && { actor: payload.actor }),
1200
+ }),
1201
+ },
1202
+ updatedAt: event.timestamp,
1203
+ };
1204
+ },
1205
+
1206
+ // K10.1 (R5) — retention control facts on a dedicated lifecycle stream. When
1207
+ // `applyRetention` archives or deletes a terminal execution it journals the action
1208
+ // so the audit trail records its own pruning (P9): the values are gone but the FACT
1209
+ // that they were pruned survives. Pure audit into `_lifecycle`; replay is byte-identical
1210
+ // for any log that never prunes.
1211
+ EXECUTION_ARCHIVED: (state, event) => {
1212
+ const p = event.payload as { executionId: string };
1213
+ return {
1214
+ ...state,
1215
+ variables: {
1216
+ ...state.variables,
1217
+ _lifecycle: appendAudit(state.variables._lifecycle as unknown[] | undefined, {
1218
+ op: "archived",
1219
+ executionId: p.executionId,
1220
+ at: event.timestamp,
1221
+ }),
1222
+ },
1223
+ updatedAt: event.timestamp,
1224
+ };
1225
+ },
1226
+ EXECUTION_DELETED: (state, event) => {
1227
+ const p = event.payload as { executionId: string };
1228
+ return {
1229
+ ...state,
1230
+ variables: {
1231
+ ...state.variables,
1232
+ _lifecycle: appendAudit(state.variables._lifecycle as unknown[] | undefined, {
1233
+ op: "deleted",
1234
+ executionId: p.executionId,
1235
+ at: event.timestamp,
1236
+ }),
1237
+ },
1238
+ updatedAt: event.timestamp,
1239
+ };
1240
+ },
1241
+ // ADR 0107 — an operator repair re-homed events the pre-0106 compaction bug had written to
1242
+ // the blank execution `""` into the execution they belong to. Journaled on the lifecycle
1243
+ // stream beside the retention facts (P9): the log records its own repair. Pure audit into
1244
+ // `_lifecycle`; a log that is never repaired replays byte-identically.
1245
+ EVENTS_REHOMED: (state, event) => {
1246
+ const p = event.payload as {
1247
+ executionId: string;
1248
+ fromSequence: number;
1249
+ throughSequence: number;
1250
+ proof: string;
1251
+ };
1252
+ return {
1253
+ ...state,
1254
+ variables: {
1255
+ ...state.variables,
1256
+ _lifecycle: appendAudit(state.variables._lifecycle as unknown[] | undefined, {
1257
+ op: "rehomed",
1258
+ executionId: p.executionId,
1259
+ fromSequence: p.fromSequence,
1260
+ throughSequence: p.throughSequence,
1261
+ proof: p.proof,
1262
+ at: event.timestamp,
1263
+ }),
1264
+ },
1265
+ updatedAt: event.timestamp,
1266
+ };
1267
+ },
1268
+ // T3-R4.5 — an erasure was requested but deferred because an execution of the subject was
1269
+ // still live (never shred a fold in flight). Folding the erasure control stream recovers the
1270
+ // set of subjects awaiting completion, so a parked erasure survives a restart.
1271
+ ERASURE_PENDING: (state, event) => {
1272
+ const p = event.payload as { subjectId: string };
1273
+ return {
1274
+ ...state,
1275
+ variables: {
1276
+ ...state.variables,
1277
+ _erasure: appendAudit(state.variables._erasure as unknown[] | undefined, {
1278
+ op: "pending",
1279
+ subjectId: p.subjectId,
1280
+ at: event.timestamp,
1281
+ }),
1282
+ },
1283
+ updatedAt: event.timestamp,
1284
+ };
1285
+ },
1286
+
1287
+ // Loom 2.0 (learning seam) — an OUTCOME_RECORDED fact: how a prior choice (a
1288
+ // route/capability/tool, by `key`) actually turned out. The symmetric counterpart
1289
+ // to DECISION_RECORDED ("what was chosen"): this records "how it went", so
1290
+ // @loomwork/analytics can mine it into a ranking fed back to the router — closing the
1291
+ // loop while keeping every future route a logged decision (P9). Folded into a
1292
+ // reserved `_outcomes` audit list; pure audit, does not change status.
1293
+ OUTCOME_RECORDED: (state, event) => {
1294
+ const kernel = isKernelWritten(event);
1295
+ const p = event.payload as {
1296
+ key: string;
1297
+ success?: boolean;
1298
+ score?: number;
1299
+ decisionId?: string;
1300
+ // 1.23.0 (ADR 0116) — written only by `recordOutcome`, which stamps `metadata.kernel`;
1301
+ // the fold reads them ONLY from a stamped envelope (a userspace emit is refused by
1302
+ // `guardUserspaceEmit`, and a log it did not write is not trusted either). Absent = a
1303
+ // live, in-run outcome.
1304
+ recordedBy?: string;
1305
+ late?: boolean;
1306
+ };
1307
+ return {
1308
+ ...state,
1309
+ variables: {
1310
+ ...state.variables,
1311
+ _outcomes: appendAudit(state.variables._outcomes as unknown[] | undefined, {
1312
+ key: p.key,
1313
+ ...(p.success !== undefined && { success: p.success }),
1314
+ ...(p.score !== undefined && { score: p.score }),
1315
+ ...(p.decisionId !== undefined && { decisionId: p.decisionId }),
1316
+ ...(kernel && typeof p.recordedBy === "string" && { recordedBy: p.recordedBy }),
1317
+ ...(kernel && p.late === true && { late: true }),
1318
+ }),
1319
+ },
1320
+ updatedAt: event.timestamp,
1321
+ };
1322
+ },
1323
+
1324
+ // Loom 2.0 (K6) — a behavioral-safety block: a safety validator refused an
1325
+ // effect's input or output. Recorded as a durable audit fact in a reserved
1326
+ // `_safety` list (the step itself still fails via the STEP_FAILED that follows).
1327
+ // Pure audit — it does not change frame/execution status, so a blocked run
1328
+ // reconstructs its safety verdict exactly on replay.
1329
+ SAFETY_BLOCKED: (state, event) => {
1330
+ const p = event.payload as {
1331
+ validatorId: string;
1332
+ stepId?: string;
1333
+ phase?: string;
1334
+ reason?: string;
1335
+ category?: string;
1336
+ };
1337
+ return {
1338
+ ...state,
1339
+ variables: {
1340
+ ...state.variables,
1341
+ _safety: appendAudit(state.variables._safety as unknown[] | undefined, {
1342
+ validatorId: p.validatorId,
1343
+ ...(p.stepId !== undefined && { stepId: p.stepId }),
1344
+ ...(p.phase !== undefined && { phase: p.phase }),
1345
+ ...(p.reason !== undefined && { reason: p.reason }),
1346
+ ...(p.category !== undefined && { category: p.category }),
1347
+ }),
1348
+ },
1349
+ updatedAt: event.timestamp,
1350
+ };
1351
+ },
1352
+
1353
+ // Loom 2.1 (K9) — an in-doubt effect: recovery found a tool declared
1354
+ // `idempotent: false` whose prior attempt is in an UNKNOWN state (STEP_STARTED
1355
+ // with no terminal step event) and refused to silently re-run it. Recorded in a
1356
+ // reserved `_indoubt` audit list; the EXECUTION_SUSPENDED that follows owns the
1357
+ // status change. Pure audit — replay reconstructs the parking exactly, and the
1358
+ // list length per step is the durable epoch for fresh idempotency keys after a
1359
+ // "retry-fresh" verdict.
1360
+ EFFECT_INDOUBT: (state, event) => {
1361
+ const p = event.payload as {
1362
+ stepId: string;
1363
+ toolId?: string;
1364
+ attempts?: number;
1365
+ idempotencyKey?: string;
1366
+ };
1367
+ return {
1368
+ ...state,
1369
+ variables: {
1370
+ ...state.variables,
1371
+ _indoubt: appendAudit(state.variables._indoubt as unknown[] | undefined, {
1372
+ stepId: p.stepId,
1373
+ ...(p.toolId !== undefined && { toolId: p.toolId }),
1374
+ ...(p.attempts !== undefined && { attempts: p.attempts }),
1375
+ ...(p.idempotencyKey !== undefined && { idempotencyKey: p.idempotencyKey }),
1376
+ }),
1377
+ },
1378
+ updatedAt: event.timestamp,
1379
+ };
1380
+ },
1381
+
1382
+ // Loom 2.1 (K11.3) — a durable migration verdict: the execution was explicitly
1383
+ // resumed on DIFFERENT flow code (resumeAs, replay-check or operator-ack).
1384
+ // Re-points the recorded flow-code fingerprint (`_flowHash`) and the flow
1385
+ // resolution pointer (`_migratedTo` — the flow id this execution now resolves
1386
+ // to; unconditional so the fold never depends on seed identity), and appends
1387
+ // an audit entry to the reserved `_migrations` list. Replay re-applies the
1388
+ // same verdict at the same sequence (G1).
1389
+ EXECUTION_MIGRATED: (state, event) => {
1390
+ const p = event.payload as {
1391
+ toFlowId: string;
1392
+ toHash?: string;
1393
+ fromHash?: string;
1394
+ strategy?: string;
1395
+ migratedInput?: unknown;
1396
+ // ADR 0113 D2 (fix round 1) — the target flow's body fingerprint ("" = it has none): re-arms
1397
+ // the `_codeHash` comparison against the code the execution now runs.
1398
+ toCodeHash?: unknown;
1399
+ };
1400
+ return {
1401
+ ...state,
1402
+ variables: {
1403
+ ...state.variables,
1404
+ ...(p.toHash !== undefined && { _flowHash: p.toHash }),
1405
+ ...(typeof p.toCodeHash === "string" && { _codeHash: p.toCodeHash }),
1406
+ // K11.5 — contract-versioned state migration: the recorded TRANSFORMED
1407
+ // input replaces `_input`, so every later resume/restore/replay folds
1408
+ // the same migrated value (the outcome is durable, not the function).
1409
+ ...(p.migratedInput !== undefined && { _input: p.migratedInput }),
1410
+ _migratedTo: p.toFlowId,
1411
+ _migrations: appendAudit(state.variables._migrations as unknown[] | undefined, {
1412
+ toFlowId: p.toFlowId,
1413
+ ...(p.toHash !== undefined && { toHash: p.toHash }),
1414
+ ...(p.fromHash !== undefined && { fromHash: p.fromHash }),
1415
+ ...(p.strategy !== undefined && { strategy: p.strategy }),
1416
+ ...(p.migratedInput !== undefined && { stateMigrated: true }),
1417
+ }),
1418
+ },
1419
+ updatedAt: event.timestamp,
1420
+ };
1421
+ },
1422
+
1423
+ // Loom 2.1 (K12) — a VERIFIED approval: a ResumeAuthenticator confirmed the
1424
+ // identity behind an authority-gating resume, so "who approved" is durable
1425
+ // evidence, not an unverified claim. Folded into a reserved `_approvals`
1426
+ // audit list. Pure audit — the accompanying EXECUTION_RESUMED owns the
1427
+ // lifecycle; a REJECTED approval never reaches the log (the resume is refused
1428
+ // before any event is committed).
1429
+ APPROVAL_GRANTED: (state, event) => {
1430
+ const p = event.payload as { actor?: string; verifiedBy?: string; on?: string };
1431
+ return {
1432
+ ...state,
1433
+ variables: {
1434
+ ...state.variables,
1435
+ _approvals: appendAudit(state.variables._approvals as unknown[] | undefined, {
1436
+ ...(p.actor !== undefined && { actor: p.actor }),
1437
+ ...(p.verifiedBy !== undefined && { verifiedBy: p.verifiedBy }),
1438
+ ...(p.on !== undefined && { on: p.on }),
1439
+ at: event.timestamp,
1440
+ }),
1441
+ },
1442
+ updatedAt: event.timestamp,
1443
+ };
1444
+ },
1445
+
1446
+ // Loom 2.1 (K10) — a crypto-shred tombstone: the subject's encryption key was
1447
+ // destroyed, so this execution's sensitive payload fields are permanently
1448
+ // unrecoverable (GDPR erasure of an append-only log). Written AT SHRED TIME by
1449
+ // the lifecycle service (never at replay). Marks `variables._shredded` so the
1450
+ // folded state records the erasure; a decrypting store read of the affected
1451
+ // fields fails loudly with PayloadShreddedError (the erasure is not silent).
1452
+ PAYLOAD_SHREDDED: (state, event) => {
1453
+ const p = event.payload as { subjectId?: string; reason?: string };
1454
+ return {
1455
+ ...state,
1456
+ variables: {
1457
+ ...state.variables,
1458
+ _shredded: {
1459
+ ...(p.subjectId !== undefined && { subjectId: p.subjectId }),
1460
+ ...(p.reason !== undefined && { reason: p.reason }),
1461
+ at: event.timestamp,
1462
+ },
1463
+ },
1464
+ updatedAt: event.timestamp,
1465
+ };
1466
+ },
1467
+
1468
+ // Loom 2.1 (K7) — an egress denial: the kernel-side broker refused an
1469
+ // isolated tool's network target (not in its static allowlist), BEFORE any
1470
+ // connection was opened. Folded into a reserved `_egress` audit list,
1471
+ // mirroring EFFECT_DENIED's `_denials`. Pure audit — whether the step fails
1472
+ // is the tool's error path (an uncaught denial fails via STEP_FAILED).
1473
+ EGRESS_DENIED: (state, event) => {
1474
+ const p = event.payload as {
1475
+ toolId?: string;
1476
+ stepId?: string;
1477
+ target?: string;
1478
+ reason?: string;
1479
+ };
1480
+ return {
1481
+ ...state,
1482
+ variables: {
1483
+ ...state.variables,
1484
+ _egress: appendAudit(state.variables._egress as unknown[] | undefined, {
1485
+ ...(p.toolId !== undefined && { toolId: p.toolId }),
1486
+ ...(p.stepId !== undefined && { stepId: p.stepId }),
1487
+ ...(p.target !== undefined && { target: p.target }),
1488
+ ...(p.reason !== undefined && { reason: p.reason }),
1489
+ }),
1490
+ },
1491
+ updatedAt: event.timestamp,
1492
+ };
1493
+ },
1494
+
1495
+ // Loom 2.1 (T1) — a durable timer fired: the wake that ends a
1496
+ // `ctx.sleep`/`sleepUntil` (or a timer-service-backed `suspend({ timeout })`).
1497
+ // Pure audit into the reserved `_timers` list — the EXECUTION_RESUMED that
1498
+ // accompanies it owns the status change. `wakeAt` is the SCHEDULED time (from
1499
+ // the log); `firedAt` (event timestamp) records actual delivery, so drift is
1500
+ // observable but never enters control flow.
1501
+ TIMER_FIRED: (state, event) => {
1502
+ const p = event.payload as { wakeAt?: number; scheduleId?: string; occurrence?: number };
1503
+ return {
1504
+ ...state,
1505
+ variables: {
1506
+ ...state.variables,
1507
+ _timers: appendAudit(state.variables._timers as unknown[] | undefined, {
1508
+ firedAt: event.timestamp,
1509
+ ...(p.wakeAt !== undefined && { wakeAt: p.wakeAt }),
1510
+ ...(p.scheduleId !== undefined && { scheduleId: p.scheduleId }),
1511
+ ...(p.occurrence !== undefined && { occurrence: p.occurrence }),
1512
+ }),
1513
+ },
1514
+ updatedAt: event.timestamp,
1515
+ };
1516
+ },
1517
+
1518
+ // Loom 2.0 (K3) — accumulate metered resource usage (tokens / cost / wall-time)
1519
+ // into a reserved `_resources` running total. Emitted by accounting middleware
1520
+ // when an effect actually runs, so the total is deterministic on replay.
1521
+ RESOURCE_METERED: (state, event) => {
1522
+ const p = event.payload as {
1523
+ tokens?: number;
1524
+ costUsd?: number;
1525
+ ms?: number;
1526
+ // ADR 0032 (R1) — additional dimensions, folded additively.
1527
+ memBytes?: number;
1528
+ wallMs?: number;
1529
+ netBytes?: number;
1530
+ // 1.23.0 (ADR 0115) — open metering dimensions, folded into `_resources.custom`.
1531
+ custom?: unknown;
1532
+ };
1533
+ const r = (state.variables._resources as
1534
+ | {
1535
+ tokens: number;
1536
+ costUsd: number;
1537
+ ms: number;
1538
+ count: number;
1539
+ memBytes?: number;
1540
+ wallMs?: number;
1541
+ netBytes?: number;
1542
+ custom?: Record<string, number>;
1543
+ exceeded?: boolean;
1544
+ }
1545
+ | undefined) ?? { tokens: 0, costUsd: 0, ms: 0, count: 0 };
1546
+ // The new dimensions are included only when non-zero, so a log that never meters
1547
+ // them folds to exactly the pre-0032 `_resources` shape (byte-identical replay).
1548
+ const memBytes = (r.memBytes ?? 0) + (p.memBytes ?? 0);
1549
+ const wallMs = (r.wallMs ?? 0) + (p.wallMs ?? 0);
1550
+ const netBytes = (r.netBytes ?? 0) + (p.netBytes ?? 0);
1551
+ const custom = addCustomDimensions(r.custom, p.custom);
1552
+ return {
1553
+ ...state,
1554
+ variables: {
1555
+ ...state.variables,
1556
+ _resources: {
1557
+ tokens: r.tokens + (p.tokens ?? 0),
1558
+ costUsd: r.costUsd + (p.costUsd ?? 0),
1559
+ ms: r.ms + (p.ms ?? 0),
1560
+ count: r.count + 1,
1561
+ ...(memBytes > 0 && { memBytes }),
1562
+ ...(wallMs > 0 && { wallMs }),
1563
+ ...(netBytes > 0 && { netBytes }),
1564
+ ...(custom !== undefined && { custom }),
1565
+ ...(r.exceeded === true && { exceeded: true }),
1566
+ },
1567
+ },
1568
+ updatedAt: event.timestamp,
1569
+ };
1570
+ },
1571
+
1572
+ // A quota admission-control denial: the execution exceeded its budget, so the
1573
+ // effect was refused. Marks `_resources.exceeded` (the STEP_FAILED that follows
1574
+ // fails the step). Pure audit.
1575
+ QUOTA_EXCEEDED: (state, event) => {
1576
+ const p = event.payload as { reason?: string };
1577
+ const r = (state.variables._resources as
1578
+ | { tokens: number; costUsd: number; ms: number; count: number; exceeded?: boolean }
1579
+ | undefined) ?? { tokens: 0, costUsd: 0, ms: 0, count: 0 };
1580
+ return {
1581
+ ...state,
1582
+ variables: {
1583
+ ...state.variables,
1584
+ _resources: { ...r, exceeded: true, ...(p.reason !== undefined && { reason: p.reason }) },
1585
+ },
1586
+ updatedAt: event.timestamp,
1587
+ };
1588
+ },
1589
+
1590
+ // The three terminal handlers also end any open park, so they drop FAILED_WHILE_PARKED
1591
+ // (ADR 0105): the mark describes a park, and must not outlive it into a terminal state (where
1592
+ // a 1.20.0 snapshot, repaired, would not carry it — snapshot + tail ≡ full log).
1593
+ EXECUTION_COMPLETED: (state, event) => {
1594
+ const payload = event.payload as { output?: unknown };
1595
+ const variables = withoutFailedWhileParked(state.variables);
1596
+ return {
1597
+ ...state,
1598
+ status: "completed",
1599
+ ...(payload.output !== undefined
1600
+ ? { variables: { ...variables, _output: payload.output } }
1601
+ : variables !== state.variables && { variables }),
1602
+ updatedAt: event.timestamp,
1603
+ };
1604
+ },
1605
+
1606
+ EXECUTION_FAILED: (state, event) => {
1607
+ const payload = event.payload as { error?: unknown };
1608
+ const variables = withoutFailedWhileParked(state.variables);
1609
+ return {
1610
+ ...state,
1611
+ status: "failed",
1612
+ ...(payload.error !== undefined
1613
+ ? { variables: { ...variables, _error: payload.error } }
1614
+ : variables !== state.variables && { variables }),
1615
+ updatedAt: event.timestamp,
1616
+ };
1617
+ },
1618
+
1619
+ EXECUTION_CANCELLED: (state, event) => {
1620
+ const payload = event.payload as { reason?: unknown };
1621
+ const variables = withoutFailedWhileParked(state.variables);
1622
+ return {
1623
+ ...state,
1624
+ status: "cancelled",
1625
+ ...(payload.reason !== undefined
1626
+ ? { variables: { ...variables, _cancelReason: payload.reason } }
1627
+ : variables !== state.variables && { variables }),
1628
+ updatedAt: event.timestamp,
1629
+ };
1630
+ },
1631
+ };
1632
+
1633
+ export function getHandler(eventType: string): Handler | undefined {
1634
+ // `Object.hasOwn`, not plain indexing: `eventType` is caller-controlled all the way from an
1635
+ // ingress adapter (`runtime.resume`, and A2A `provideInput` passes a network-supplied
1636
+ // ResumeEvent straight through), and `handlers["__proto__"]` resolves up the prototype chain
1637
+ // to `Object.prototype` — TRUTHY, so `foldEvents`' `if (handler)` guard admits it and then
1638
+ // calls a non-function. An unknown event type must fold to a no-op, whatever it is named.
1639
+ if (!Object.hasOwn(handlers, eventType)) return undefined;
1640
+ return handlers[eventType];
1641
+ }
1642
+
1643
+ // ── Event-schema ABI governance (Loom 2.0 K5) ────────────────────────────────
1644
+ //
1645
+ // The event log is the kernel's on-disk ABI: state is `fold(events)`, and stored
1646
+ // logs must replay across deploys (this is what `@loomwork/deploy-sim` guards at the
1647
+ // flow level). This names and versions the *set of event types the fold understands*.
1648
+ // Governance: adding an event type is additive (bump MINOR, update the pinned list);
1649
+ // removing or repurposing one changes how historical logs fold and is a MAJOR break
1650
+ // that must be deliberate — the conformance test pins `knownEventTypes()`.
1651
+ // 1.4.0: additive optional `flowHash` payload field on EXECUTION_STARTED (K11.1) —
1652
+ // per the K5 payload-extension rule, folds treat absence as legacy.
1653
+ // 1.5.0: added EXECUTION_MIGRATED (K11.3).
1654
+ // 1.6.0: added TIMER_FIRED (T1 — durable timers).
1655
+ // 1.7.0: added EGRESS_DENIED (K7 — data-plane isolation & egress control).
1656
+ // 1.8.0: added PAYLOAD_SHREDDED (K10 — crypto-shredding).
1657
+ // 1.9.0: added APPROVAL_GRANTED (K12 — verified approvals).
1658
+ // 1.10.0: additive optional DECISION_RECORDED payload fields `alternatives`,
1659
+ // `estimatedCostUsd`, `estimatedLatencyMs`, `confidence` (ADR 0030 D1 — decision
1660
+ // materialization). Descriptive only; `getDecision` still returns just `choice`, so
1661
+ // folds treat absence as legacy and replay is byte-identical.
1662
+ // 1.11.0: additive optional RESOURCE_METERED payload fields `memBytes`, `wallMs`,
1663
+ // `netBytes` (ADR 0032 R1 — resource dimensions). Folded into `_resources` only when
1664
+ // non-zero, so a log that never meters them replays byte-identically.
1665
+ // 1.12.0: added EXECUTION_DENIED (C.1 — execution-lifecycle authz). Pure audit into
1666
+ // `_executionDenials`; a log that never denies replays byte-identically.
1667
+ // 1.13.0: added EXECUTION_ARCHIVED / EXECUTION_DELETED (K10.1 R5 — retention control
1668
+ // facts). Pure audit into `_lifecycle` on a dedicated stream; a log that never prunes
1669
+ // replays byte-identically.
1670
+ // 1.14.0: added ERASURE_PENDING (T3-R4.5 — deferred-erasure control fact). Pure audit into
1671
+ // `_erasure` on a dedicated stream; a log that never defers an erasure replays identically.
1672
+ // 1.15.0: additive optional EXECUTION_MIGRATED payload field `migratedInput` (K11.5,
1673
+ // ADR 0056 — contract-versioned state migration on resumeAs). When present it replaces
1674
+ // `_input` in the fold and stamps `stateMigrated: true` on the `_migrations` audit
1675
+ // entry; a log that never migrates state replays byte-identically.
1676
+ // 1.16.0: EXECUTION_RESUMED additionally folds `_resumePayloads` — the ORDERED list of
1677
+ // every resume payload (ADR 0057). Fixes the multi-gate suspend bug: the single-latest
1678
+ // `_resumePayload` (kept for compat) fed gate 1 the LAST gate's payload on replay.
1679
+ // Additive fold key from existing events; no payload or kind changes.
1680
+ // 1.17.0: additive optional STEP_FAILED payload fields `channel`, `payload` (DE-4,
1681
+ // ADR 0060 — typed failure channels). Folded into frame `errorChannel`/`errorPayload`;
1682
+ // presence makes the execute seam SUBSTITUTE the recorded failure on replay instead of
1683
+ // re-executing the step. A log that never fails typed replays byte-identically.
1684
+ // 1.18.0: additive optional DECISION_RECORDED payload fields `context`, `policy` (T2,
1685
+ // ADR 0079 — the observation a policy saw and the policy that saw it, incl. the
1686
+ // propensity that makes unbiased off-policy evaluation possible). Same descriptive
1687
+ // contract as the 1.10.0 D1 fields: folded verbatim into `_decisions`, NEVER read by
1688
+ // getDecision, so `choice` stays the sole replay input. A log whose decisions carry
1689
+ // neither field replays byte-identically.
1690
+ // 1.19.0: additive fold key `_flowEmits` from an additive envelope stamp (ADR 0102 —
1691
+ // flow-level emits append once per execution). A flow-level `ctx.emit` / `ctx.stream` /
1692
+ // `ctx.ui` envelope may carry `metadata.flowEmit = <16-hex content digest>`; the fold
1693
+ // counts stamped emits per digest for ANY event type (a generic step in `foldEvents`,
1694
+ // not a handler), so a re-run serves the emits the log already holds. No new kind, no
1695
+ // payload change; a log that never stamps replays byte-identically.
1696
+ // 1.20.0: EXECUTION_RESUMED with a `null` payload that answers no suspend point (the execution
1697
+ // was not parked, or was parked in doubt on "EffectInDoubtResolved") re-opens the execution
1698
+ // WITHOUT adding a gate passthrough (ADR 0104). Every other resume folds as before, so a log
1699
+ // that never carries such a resume replays byte-identically. Also additive: a human step's
1700
+ // EXECUTION_SUSPENDED condition carries the kernel-set `stepId` (folded verbatim into
1701
+ // `suspendedOn`, as the condition always was). No new event kind.
1702
+ // 1.21.0: STEP_FAILED no longer folds the EXECUTION status to "failed" (ADR 0105) — the flow may
1703
+ // catch it, and EXECUTION_FAILED is the execution's failure. No new kind or payload field; the
1704
+ // kernel writes the same events. A STEP_FAILED landing while the execution is parked sets the
1705
+ // reserved fold key `_failedWhileParked` (cleared by whatever ends the park), and a `null`
1706
+ // EXECUTION_RESUMED in that position stays status-only, exactly as 1.20.0 folded it — so every
1707
+ // gate passthrough (`_resumePayloads`) is identical under both. What differs from 1.20.0: the
1708
+ // derived `status` after a STEP_FAILED no lifecycle event has followed (`running`/`suspended`,
1709
+ // not `failed`), and the new key while such a park is open. Snapshots saved by an older fold
1710
+ // are read through `normalizeSnapshotState`.
1711
+ // 1.22.0: added EVENTS_REHOMED (ADR 0107 — the operator repair of the pre-0106 blank-stream
1712
+ // events). Pure audit into `_lifecycle` on the lifecycle stream; a log that is never repaired
1713
+ // replays byte-identically. The re-homed events themselves are the kinds they always were.
1714
+ // 1.23.0: Wave 2 (W2-0, ADRs 0109–0116) — every new fold handler and reserved shape, with no
1715
+ // writer yet. New kinds `EXECUTION_CONTINUED` (→ status `continued`, `_continuedTo`) and
1716
+ // `MESSAGE_CONSUMED` (→ `_inbox`, `_waitAnswers`); a payload and handler for the existing
1717
+ // `PARALLEL_COMPLETED` (→ `_scopes`); additive `EXECUTION_STARTED.{principal, parent, chainId,
1718
+ // generation, continuedFrom, codeHash}` (→ `_principal`, `_parent`, `_chain`, `_codeHash`; the
1719
+ // carry seeds `_resources` / `_inbox`), `EXECUTION_SUSPENDED.condition.waits[]` (→ `_waits`),
1720
+ // a wait-addressed `EXECUTION_RESUMED` `{ waitId, payload }` (→ `_waitAnswers`),
1721
+ // `RESOURCE_METERED.custom` (→ `_resources.custom`), `DECISION_RECORDED.estimatedCost` (the
1722
+ // descriptive open spelling of `estimatedCostUsd`, never a replay input),
1723
+ // `OUTCOME_RECORDED.{recordedBy, late}`.
1724
+ // The envelope metadata the fold now reads: `kernel` (the new kinds, `PARALLEL_COMPLETED` and a
1725
+ // wait-addressed resume fold ONLY a kernel-stamped envelope), `forkChain`, `forkedBy`. A log
1726
+ // carrying none of these replays byte-identically.
1727
+ // 1.24.0: ADR 0128 — each folded `_decisions` entry carries `seq`, the envelope sequence of the
1728
+ // FIRST `DECISION_RECORDED` for that id, so decision ORDER survives a checkpoint (key order does
1729
+ // not survive array-index-like ids or a normalising snapshot store such as Postgres `jsonb`).
1730
+ // Descriptive only: `getDecision` still returns just `choice`. A snapshot written by a 1.23 fold
1731
+ // has no `seq` on its absorbed entries; those are LEGACY and read in map order (no migration, the
1732
+ // same documented exception to snapshot+tail == full fold as 1.21.0). Log bytes are unchanged.
1733
+ // 1.25.0: ADR 0130 residuals (D7-D9), RESERVED - shapes frozen, handlers present, NO writer yet, so
1734
+ // a log written before this version replays byte-identically. (1) The envelope key
1735
+ // `metadata.seal` (`{ v: 1, cipher, covers }`, D7: a sealing decorator's marker): the fold
1736
+ // IGNORES it, so state equals the same log without it. (2) New kinds `INBOX_QUARANTINED`
1737
+ // (`{ topic, inboxSeq, key, messageId, heldAt, operator, reason? }` -> cursor advance by `max`,
1738
+ // an entry pushed onto `_quarantined[topic]`, the dedupe window untouched) and `INBOX_RELEASED`
1739
+ // (`{ topic, key, messageId, disposition: "deliver" | "discard", operator? | auto?, reason? }`:
1740
+ // exactly one of `operator` / `auto`, `auto` only with `deliver`; a fact with both or neither is
1741
+ // ignored; acts only on the topic's FIRST entry while `held`), BOTH folded only from a
1742
+ // kernel-stamped envelope (`metadata.kernel === true`) and both `ignorable` for the park fence.
1743
+ // (3) The fold variable `_quarantined` (`{ [topic]: Array<{ key, inboxSeq, heldAt, state }> }`,
1744
+ // queue shape `released? held*`, at most QUARANTINE_FOLD_CAP = 1024 entries in total). (4)
1745
+ // `MESSAGE_CONSUMED` additionally drops the topic's first `released` entry whose `key` equals the
1746
+ // fact's `messageId`, gated on `_quarantined` existing. (5) `EXECUTION_STARTED.continuedFrom.
1747
+ // carried.quarantined`, a validated seed (invalid parts ignored). Byte-identity: a log carrying
1748
+ // none of these shapes folds exactly as under 1.24.0.
1749
+ // 1.26.0: ADR 0138 D8, NO new kind and NO payload change. The fold stops copying `input` into a frame
1750
+ // when it SETTLES (STEP_COMPLETED / STEP_FAILED / STEP_CANCELLED); STEP_STARTED and STEP_RETRYING keep it,
1751
+ // so an in-flight frame still shows what is running. The folded BYTES change (state is linear in turns, not
1752
+ // quadratic); `normalizeSnapshotState` strips `input` from the settled frames of an older snapshot in one
1753
+ // O(frames) pass, so `fold(snapshot, tail) == fold(events)` holds. Nothing reads a settled frame's input
1754
+ // from the fold; the log (or the compaction archive, ADR 0138) holds it. A pre-1.26 node re-adds an input
1755
+ // to a frame it settles (harmless; stripped by the next normalize) - the documented mixed-fleet exception.
1756
+ export const EVENT_SCHEMA_VERSION = "1.26.0";
1757
+
1758
+ export function knownEventTypes(): string[] {
1759
+ return Object.keys(handlers).sort();
1760
+ }