@rulvar/testing 1.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,1964 @@
1
+ import { CURRENT_HASH_VERSION, ConfigError, EMPTY_SCHEMA_HASH, EMPTY_TOOLSET_HASH, InMemoryStore, InMemoryTranscriptStore, JournalMissError, Replayer, agentScope, buildDeriverRegistry, canonicalizeSchema, createCanonicalIdMinter, createEngine, defineWorkflow, deriveContentKey, hashWorkflowBody, makeOrchestratorWorkflow, normalizeEntry, projectToJsonSchema, registryKeyRing } from "@rulvar/core";
2
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createHash } from "node:crypto";
5
+ //#region src/fake-adapter.ts
6
+ /**
7
+ * FakeAdapter (M1-T14): a REAL ProviderAdapter that resolves calls from
8
+ * declared patterns instead of the network, behind the same seam as live
9
+ * adapters, so unit tests run through the full engine: journal, scheduler,
10
+ * budget layers, and event stream (docs/09, section "Tier 1: FakeAdapter
11
+ * and createTestEngine"). Calls cost zero USD.
12
+ */
13
+ /** Scripts a tool-calling turn from a responder. */
14
+ function fakeToolCalls(...calls) {
15
+ return {
16
+ __fake: "tool-calls",
17
+ calls
18
+ };
19
+ }
20
+ /** Scripts a typed wire failure (e.g. a retryable rate limit). */
21
+ function fakeWireError(error) {
22
+ return {
23
+ __fake: "wire-error",
24
+ error
25
+ };
26
+ }
27
+ function isFakeToolCalls(value) {
28
+ return typeof value === "object" && value !== null && value.__fake === "tool-calls";
29
+ }
30
+ function isFakeWireError(value) {
31
+ return typeof value === "object" && value !== null && value.__fake === "wire-error";
32
+ }
33
+ const FAKE_MODEL = "fake-model";
34
+ const FAKE_MODEL_REF = "fake:fake-model";
35
+ const FAKE_CAPS = {
36
+ structuredOutput: "native",
37
+ supportsTemperature: true,
38
+ supportsParallelTools: true,
39
+ reasoningEfforts: [
40
+ "low",
41
+ "medium",
42
+ "high",
43
+ "xhigh",
44
+ "max"
45
+ ],
46
+ contextWindow: 1e6,
47
+ maxOutputTokens: 64e3,
48
+ pricing: {
49
+ inputUsdPerMTok: 0,
50
+ outputUsdPerMTok: 0
51
+ }
52
+ };
53
+ function lastUserText(req) {
54
+ for (let i = req.messages.length - 1; i >= 0; i -= 1) {
55
+ const msg = req.messages[i];
56
+ if (msg?.role !== "user") continue;
57
+ return msg.parts.filter((part) => part.type === "text").map((part) => part.text).join("\n");
58
+ }
59
+ return "";
60
+ }
61
+ var FakeAdapter = class {
62
+ id = "fake";
63
+ agents;
64
+ mintId = createCanonicalIdMinter();
65
+ /** Every request this adapter served, in order. */
66
+ calls = [];
67
+ constructor(options) {
68
+ this.agents = options.agents;
69
+ }
70
+ caps() {
71
+ return FAKE_CAPS;
72
+ }
73
+ match(call) {
74
+ let fallback;
75
+ for (const [pattern, responder] of Object.entries(this.agents)) {
76
+ if (pattern === "*") {
77
+ fallback = responder;
78
+ continue;
79
+ }
80
+ if (call.agentType === pattern || call.label === pattern) return responder;
81
+ try {
82
+ if (new RegExp(pattern).test(call.prompt)) return responder;
83
+ } catch {}
84
+ }
85
+ return fallback;
86
+ }
87
+ async *stream(req) {
88
+ const telemetry = req.providerOptions?.rulvar ?? {};
89
+ const call = {
90
+ prompt: lastUserText(req),
91
+ req,
92
+ ...telemetry.agentType === void 0 || telemetry.agentType === "" ? {} : { agentType: telemetry.agentType },
93
+ ...telemetry.label === void 0 ? {} : { label: telemetry.label }
94
+ };
95
+ this.calls.push(call);
96
+ const responder = this.match(call);
97
+ if (responder === void 0) {
98
+ yield {
99
+ type: "error",
100
+ error: {
101
+ code: "agent",
102
+ message: `FakeAdapter: no pattern matches agentType='${call.agentType ?? ""}' label='${call.label ?? ""}' prompt='${call.prompt.slice(0, 80)}'; add a '*' fallback`,
103
+ retryable: false,
104
+ data: { kind: "terminal" }
105
+ }
106
+ };
107
+ return;
108
+ }
109
+ let value;
110
+ try {
111
+ value = typeof responder === "function" ? await responder(call) : responder;
112
+ } catch (thrown) {
113
+ yield {
114
+ type: "error",
115
+ error: {
116
+ code: "agent",
117
+ message: thrown instanceof Error ? thrown.message : String(thrown),
118
+ retryable: false,
119
+ data: { kind: "terminal" }
120
+ }
121
+ };
122
+ return;
123
+ }
124
+ if (isFakeWireError(value)) {
125
+ yield {
126
+ type: "error",
127
+ error: value.error
128
+ };
129
+ return;
130
+ }
131
+ if (isFakeToolCalls(value)) {
132
+ const usage = {
133
+ inputTokens: Math.max(1, Math.ceil(call.prompt.length / 4)),
134
+ outputTokens: Math.max(1, value.calls.length * 8),
135
+ cacheReadTokens: 0,
136
+ cacheWriteTokens: 0
137
+ };
138
+ for (const toolCall of value.calls) {
139
+ const id = this.mintId();
140
+ yield {
141
+ type: "tool-call-start",
142
+ id,
143
+ name: toolCall.name
144
+ };
145
+ yield {
146
+ type: "tool-call-end",
147
+ id,
148
+ args: toolCall.args
149
+ };
150
+ }
151
+ yield {
152
+ type: "finish",
153
+ finish: { reason: "tool-calls" },
154
+ usage
155
+ };
156
+ return;
157
+ }
158
+ const text = typeof value === "string" ? value : JSON.stringify(value);
159
+ const usage = {
160
+ inputTokens: Math.max(1, Math.ceil(call.prompt.length / 4)),
161
+ outputTokens: Math.max(1, Math.ceil(text.length / 4)),
162
+ cacheReadTokens: 0,
163
+ cacheWriteTokens: 0
164
+ };
165
+ const forcedName = typeof req.toolChoice === "object" ? req.toolChoice.name : void 0;
166
+ if (forcedName !== void 0) {
167
+ let args = value;
168
+ if (typeof value === "string") try {
169
+ args = JSON.parse(value);
170
+ } catch {
171
+ args = { text: value };
172
+ }
173
+ const id = this.mintId();
174
+ yield {
175
+ type: "tool-call-start",
176
+ id,
177
+ name: forcedName
178
+ };
179
+ yield {
180
+ type: "tool-call-end",
181
+ id,
182
+ args
183
+ };
184
+ yield {
185
+ type: "finish",
186
+ finish: { reason: "tool-calls" },
187
+ usage
188
+ };
189
+ return;
190
+ }
191
+ yield {
192
+ type: "text-delta",
193
+ text
194
+ };
195
+ yield {
196
+ type: "finish",
197
+ finish: { reason: "stop" },
198
+ usage
199
+ };
200
+ }
201
+ };
202
+ //#endregion
203
+ //#region src/test-engine.ts
204
+ /**
205
+ * createTestEngine (M1-T14): a full engine over FakeAdapter with zero
206
+ * network. Orchestration logic is exercised, not mocked around: journal,
207
+ * scheduler, budget layers, and event stream are all real (docs/09,
208
+ * section "Tier 1"). Returned handles record their event stream so the
209
+ * matchers can assert over settled runs.
210
+ */
211
+ function createTestEngine(options) {
212
+ const fake = new FakeAdapter({ agents: options.agents });
213
+ const store = new InMemoryStore();
214
+ const profiles = { ...options.profiles };
215
+ for (const key of Object.keys(options.agents)) if (key !== "*" && profiles[key] === void 0) profiles[key] = {};
216
+ const engine = createEngine({
217
+ adapters: [fake],
218
+ stores: { journal: store },
219
+ defaults: {
220
+ routing: {
221
+ loop: FAKE_MODEL_REF,
222
+ extract: FAKE_MODEL_REF,
223
+ orchestrate: FAKE_MODEL_REF,
224
+ plan: FAKE_MODEL_REF,
225
+ summarize: FAKE_MODEL_REF
226
+ },
227
+ profiles
228
+ },
229
+ ...options.budgetDefaults === void 0 ? {} : { budgetDefaults: options.budgetDefaults },
230
+ ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
231
+ });
232
+ return {
233
+ fake,
234
+ store,
235
+ stores: engine.stores,
236
+ resume: (runId, wf, options) => engine.resume(runId, wf, options),
237
+ deleteRun: (runId) => engine.deleteRun(runId),
238
+ pruneRun: (runId) => engine.pruneRun(runId),
239
+ profileCard: (names) => engine.profileCard(names),
240
+ run(wf, args, opts) {
241
+ const handle = engine.run(wf, args, opts);
242
+ const eventsSeen = [];
243
+ (async () => {
244
+ for await (const event of handle.events) eventsSeen.push(event);
245
+ })().catch(() => void 0);
246
+ return {
247
+ ...handle,
248
+ eventsSeen
249
+ };
250
+ }
251
+ };
252
+ }
253
+ //#endregion
254
+ //#region src/replay-strict.ts
255
+ /**
256
+ * Tier 3: replay-strict journal runs (M2-T10). Executes a workflow
257
+ * against an existing journal and throws JournalMissError on ANY live
258
+ * call: zero live calls or loud failure. Any production journal becomes a
259
+ * deterministic integration test; a journal with open suspensions
260
+ * completes with outcome 'suspended' and zero live calls (docs/09,
261
+ * section "Tier 3: replay-strict journal runs").
262
+ */
263
+ const FAKE_ROUTING = {
264
+ loop: FAKE_MODEL_REF,
265
+ extract: FAKE_MODEL_REF,
266
+ orchestrate: FAKE_MODEL_REF,
267
+ plan: FAKE_MODEL_REF,
268
+ finalize: FAKE_MODEL_REF,
269
+ summarize: FAKE_MODEL_REF
270
+ };
271
+ async function replayRun(wf, args, options) {
272
+ let store;
273
+ let runId;
274
+ if (Array.isArray(options.journal)) {
275
+ runId = options.journal[0] !== void 0 ? "replay-run" : "replay-empty";
276
+ const memory = new InMemoryStore();
277
+ for (const entry of options.journal) await memory.append(runId, entry);
278
+ await memory.putMeta({
279
+ runId,
280
+ status: "suspended",
281
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
282
+ workflowName: wf.name,
283
+ workflowHash: hashWorkflowBody(wf)
284
+ });
285
+ store = memory;
286
+ } else {
287
+ store = options.journal.store;
288
+ runId = options.journal.runId;
289
+ }
290
+ const handle = createEngine({
291
+ adapters: options.adapters ?? [new FakeAdapter({ agents: {} })],
292
+ stores: { journal: store },
293
+ defaults: {
294
+ routing: options.routing ?? FAKE_ROUTING,
295
+ ...options.profiles === void 0 ? {} : { profiles: options.profiles }
296
+ },
297
+ ...options.onEscalation === void 0 ? {} : { onEscalation: options.onEscalation }
298
+ }).resume(runId, wf, {
299
+ args,
300
+ dryRun: true
301
+ });
302
+ const outcome = await handle.result;
303
+ if (outcome.error?.code === "journal_miss") throw new JournalMissError(outcome.error.message, { data: outcome.error.data ?? null });
304
+ return {
305
+ outcome,
306
+ preview: await handle.preview
307
+ };
308
+ }
309
+ //#endregion
310
+ //#region src/cassettes/build-fixtures.ts
311
+ /**
312
+ * M2 cassette and frozen-fixture builders (M2-T12). Fixtures are
313
+ * hand-authored journals with REAL content keys (derived through the
314
+ * frozen KeyDeriver profiles), deterministic timestamps, and fixed span
315
+ * ids, so regeneration is byte-stable.
316
+ *
317
+ * The COMMITTED files under repo cassettes/ and
318
+ * packages/testing/fixtures/frozen/ are the contract; these builders
319
+ * exist to regenerate them DELIBERATELY (scripts/record-m2-cassettes.mjs)
320
+ * and to fail loudly in CI when key derivation drifts (docs/11, section
321
+ * "Frozen journal fixtures": regenerating fixtures to make a test pass is
322
+ * forbidden by policy; any diff requires an explicit hashVersion-bump
323
+ * changeset).
324
+ */
325
+ const BASE_MS$1 = Date.parse("2026-02-01T00:00:00.000Z");
326
+ const SPAN$1 = "fixture-span";
327
+ function stampOf$1(seq) {
328
+ return new Date(BASE_MS$1 + seq * 1e3).toISOString();
329
+ }
330
+ function usageOf(inputTokens, outputTokens) {
331
+ return {
332
+ inputTokens,
333
+ outputTokens,
334
+ cacheReadTokens: 0,
335
+ cacheWriteTokens: 0
336
+ };
337
+ }
338
+ /** The identity ctx.agent computes for a plain prompt against the fake model. */
339
+ function fakeAgentIdentity(prompt, effort) {
340
+ return {
341
+ kind: "agent",
342
+ agentType: "",
343
+ modelSpec: effort === void 0 ? {
344
+ kind: "model",
345
+ model: FAKE_MODEL_REF
346
+ } : {
347
+ kind: "model",
348
+ model: FAKE_MODEL_REF,
349
+ effort
350
+ },
351
+ prompt,
352
+ schemaHash: EMPTY_SCHEMA_HASH,
353
+ toolsetHash: EMPTY_TOOLSET_HASH,
354
+ isolation: "none"
355
+ };
356
+ }
357
+ const RING = registryKeyRing(buildDeriverRegistry());
358
+ /**
359
+ * Derives the content key an entry of `hashVersion` carries for this
360
+ * identity. The synthetic hashVersion 0 profile of @rulvar/compat shares
361
+ * the v1 projection by construction (deriverV0Synthetic =
362
+ * { ...deriverV1, hashVersion: 0 }), so v0 fixture keys derive through
363
+ * the v1 profile; synthetic FUTURE versions borrow the v2 derivation (the
364
+ * compatibility scan rejects them before any key is ever compared).
365
+ */
366
+ function keyFor(identity, hashVersion) {
367
+ const effective = hashVersion === 0 ? 1 : hashVersion > 2 ? 2 : hashVersion;
368
+ const derived = RING.keyFor(identity, effective);
369
+ if (derived === "incomparable") throw new Error(`fixture identity is incomparable under hashVersion ${String(hashVersion)}`);
370
+ return derived.key;
371
+ }
372
+ /** Deterministic journal author: seq, stamps, and ordinals are derived. */
373
+ var FixtureJournal = class {
374
+ entries = [];
375
+ seq = 0;
376
+ ordinals = /* @__PURE__ */ new Map();
377
+ mint(partial) {
378
+ const seq = this.seq;
379
+ this.seq += 1;
380
+ const ordinalKey = `${partial.scope} ${partial.hashVersion} ${partial.key}`;
381
+ const ordinal = partial.ref === void 0 && partial.kind !== "resolution" && partial.kind !== "abandon" ? this.ordinals.get(ordinalKey) ?? 0 : 0;
382
+ if (partial.ref === void 0 && partial.kind !== "resolution" && partial.kind !== "abandon") this.ordinals.set(ordinalKey, ordinal + 1);
383
+ const entry = {
384
+ ...partial,
385
+ seq,
386
+ ordinal,
387
+ spanId: SPAN$1,
388
+ startedAt: stampOf$1(seq)
389
+ };
390
+ this.entries.push(entry);
391
+ return entry;
392
+ }
393
+ /** Two-phase agent operation: running plus terminal. Returns the running entry. */
394
+ agentOp(input) {
395
+ const hashVersion = input.hashVersion ?? 2;
396
+ const identity = fakeAgentIdentity(input.prompt, input.effort);
397
+ const running = this.mint({
398
+ hashVersion,
399
+ scope: input.scope ?? "",
400
+ key: keyFor(identity, hashVersion),
401
+ kind: "agent",
402
+ status: "running",
403
+ ...input.memoizeOutcome === void 0 ? {} : { memoizeOutcome: input.memoizeOutcome }
404
+ });
405
+ const terminal = this.mint({
406
+ hashVersion,
407
+ ref: running.seq,
408
+ scope: running.scope,
409
+ key: running.key,
410
+ kind: "agent",
411
+ status: input.status ?? "ok",
412
+ ...input.value === void 0 ? {} : { value: input.value },
413
+ ...input.error === void 0 ? {} : { error: input.error },
414
+ ...input.usage === void 0 ? {} : { usage: input.usage },
415
+ servedBy: FAKE_MODEL_REF,
416
+ endedAt: stampOf$1(this.seq)
417
+ });
418
+ terminal.ordinal = running.ordinal;
419
+ return running;
420
+ }
421
+ /** A hanging two-phase dispatch (crash before the terminal). */
422
+ danglingAgent(input) {
423
+ const hashVersion = input.hashVersion ?? 2;
424
+ const identity = fakeAgentIdentity(input.prompt);
425
+ return this.mint({
426
+ hashVersion,
427
+ scope: input.scope ?? "",
428
+ key: keyFor(identity, hashVersion),
429
+ kind: "agent",
430
+ status: "running"
431
+ });
432
+ }
433
+ stepOp(input) {
434
+ const hashVersion = input.hashVersion ?? 2;
435
+ const identity = {
436
+ kind: "step",
437
+ key: input.label,
438
+ deps: []
439
+ };
440
+ const running = this.mint({
441
+ hashVersion,
442
+ scope: "",
443
+ key: keyFor(identity, hashVersion),
444
+ kind: "step",
445
+ status: "running"
446
+ });
447
+ const terminal = this.mint({
448
+ hashVersion,
449
+ ref: running.seq,
450
+ scope: "",
451
+ key: running.key,
452
+ kind: "step",
453
+ status: "ok",
454
+ value: input.value,
455
+ endedAt: stampOf$1(this.seq)
456
+ });
457
+ terminal.ordinal = running.ordinal;
458
+ return running;
459
+ }
460
+ randNow(value, hashVersion = 2) {
461
+ return this.mint({
462
+ hashVersion,
463
+ scope: "",
464
+ key: keyFor({
465
+ kind: "rand",
466
+ subtype: "now"
467
+ }, hashVersion),
468
+ kind: "rand",
469
+ status: "ok",
470
+ value: {
471
+ subtype: "now",
472
+ value
473
+ },
474
+ endedAt: stampOf$1(this.seq)
475
+ });
476
+ }
477
+ external(input) {
478
+ const hashVersion = input.hashVersion ?? 2;
479
+ const identity = {
480
+ kind: "external",
481
+ key: input.key
482
+ };
483
+ const payload = { key: input.key };
484
+ if (input.prompt !== void 0) payload.prompt = input.prompt;
485
+ if (input.schema !== void 0) payload.schema = canonicalizeSchema(projectToJsonSchema(input.schema));
486
+ return this.mint({
487
+ hashVersion,
488
+ scope: input.scope ?? "",
489
+ key: keyFor(identity, hashVersion),
490
+ kind: "external",
491
+ status: "suspended",
492
+ value: payload
493
+ });
494
+ }
495
+ approvalSuspended(input) {
496
+ const hashVersion = input.hashVersion ?? 2;
497
+ const identity = {
498
+ kind: "approval",
499
+ toolName: input.toolName,
500
+ input: input.toolInput
501
+ };
502
+ return this.mint({
503
+ hashVersion,
504
+ scope: "",
505
+ key: keyFor(identity, hashVersion),
506
+ kind: "approval",
507
+ status: "suspended",
508
+ value: {
509
+ toolName: input.toolName,
510
+ input: input.toolInput
511
+ }
512
+ });
513
+ }
514
+ resolution(input) {
515
+ const target = this.entries.find((entry) => entry.seq === input.target);
516
+ return this.mint({
517
+ hashVersion: 2,
518
+ ref: input.target,
519
+ scope: target?.scope ?? "",
520
+ key: "",
521
+ kind: "resolution",
522
+ status: "ok",
523
+ resolution: {
524
+ target: input.target,
525
+ by: input.by,
526
+ value: input.value,
527
+ ...input.decisionRef === void 0 ? {} : { decisionRef: input.decisionRef }
528
+ }
529
+ });
530
+ }
531
+ abandon(input) {
532
+ const target = this.entries.find((entry) => entry.seq === input.target);
533
+ const payload = {
534
+ target: input.target,
535
+ authorizedBy: input.authorizedBy,
536
+ reason: input.reason,
537
+ retainCheckpoint: true,
538
+ retainWorktree: false
539
+ };
540
+ return this.mint({
541
+ hashVersion: 2,
542
+ ref: input.target,
543
+ scope: target?.scope ?? "",
544
+ key: "",
545
+ kind: "abandon",
546
+ status: "ok",
547
+ abandon: payload
548
+ });
549
+ }
550
+ decision(input) {
551
+ return this.mint({
552
+ hashVersion: 2,
553
+ scope: "",
554
+ key: deriveContentKey({
555
+ kind: "step",
556
+ key: `decision:${input.decisionType}`,
557
+ deps: []
558
+ }),
559
+ kind: "decision",
560
+ status: "ok",
561
+ value: {
562
+ decisionType: input.decisionType,
563
+ ...input.payload
564
+ },
565
+ endedAt: stampOf$1(this.seq)
566
+ });
567
+ }
568
+ };
569
+ /** Shared schemas (canonical projections are pinned inside the fixtures). */
570
+ const APPROVED_SCHEMA = {
571
+ type: "object",
572
+ additionalProperties: false,
573
+ required: ["approved"],
574
+ properties: { approved: { type: "boolean" } }
575
+ };
576
+ const DECISION_SCHEMA = {
577
+ type: "object",
578
+ additionalProperties: false,
579
+ required: ["decision"],
580
+ properties: { decision: { type: "string" } }
581
+ };
582
+ const GO_SCHEMA = {
583
+ type: "object",
584
+ additionalProperties: false,
585
+ required: ["go"],
586
+ properties: { go: { type: "boolean" } }
587
+ };
588
+ /** Prompts shared between fixture entries and cassette workflow bodies. */
589
+ const PROMPTS = {
590
+ branchWork: "branch work",
591
+ childOk: "child ok",
592
+ childEscalated: "child escalated",
593
+ childHanging: "child hanging",
594
+ classify: "classify the document",
595
+ summarize: "summarize the document",
596
+ alpha: "alpha stage",
597
+ beta: "beta stage",
598
+ gamma: "gamma stage",
599
+ analyze: "analyze the incident",
600
+ reviseReport: "revise the report",
601
+ childResearch: "child research",
602
+ childDraft: "child draft",
603
+ revisionEffects: "apply the revision effects",
604
+ branchAlpha: "branch alpha",
605
+ subtreeAlpha: "subtree alpha",
606
+ innerAlpha: "inner alpha work",
607
+ v0Relic: "v0 relic stage",
608
+ futureStage: "future stage",
609
+ draftSummary: "draft the summary",
610
+ polishIntro: "polish the intro",
611
+ sharedStage: "shared stage",
612
+ crossCheck: "cross-check the citations",
613
+ assessTone: "assess the tone"
614
+ };
615
+ function buildM2CassetteFixtures() {
616
+ const fixtures = [];
617
+ {
618
+ const j = new FixtureJournal();
619
+ j.agentOp({
620
+ prompt: PROMPTS.alpha,
621
+ hashVersion: 1,
622
+ value: "alpha out",
623
+ usage: usageOf(100, 10)
624
+ });
625
+ j.agentOp({
626
+ prompt: PROMPTS.beta,
627
+ hashVersion: 1,
628
+ status: "error",
629
+ error: {
630
+ code: "agent",
631
+ message: "upstream disconnected",
632
+ retryable: true,
633
+ data: { kind: "transport" }
634
+ },
635
+ usage: usageOf(10, 0)
636
+ });
637
+ j.agentOp({
638
+ prompt: PROMPTS.gamma,
639
+ hashVersion: 1,
640
+ status: "cancelled",
641
+ usage: usageOf(5, 0)
642
+ });
643
+ fixtures.push({
644
+ id: "v1-journal-on-v2",
645
+ note: "DEF-1: a journal without the new statuses and kinds resumes on the v2 engine; ok replays, error and cancelled rerun, byte-identical to the round-1 table.",
646
+ entries: j.entries
647
+ });
648
+ }
649
+ {
650
+ const j = new FixtureJournal();
651
+ j.agentOp({
652
+ prompt: PROMPTS.analyze,
653
+ value: "analysis: rollback recommended",
654
+ usage: usageOf(120, 30)
655
+ });
656
+ const gate = j.external({
657
+ key: "escalation-report",
658
+ schema: DECISION_SCHEMA,
659
+ prompt: "Escalation decision required"
660
+ });
661
+ j.resolution({
662
+ target: gate.seq,
663
+ by: "external",
664
+ value: { decision: "rollback" }
665
+ });
666
+ j.resolution({
667
+ target: gate.seq,
668
+ by: "timeout",
669
+ value: { decision: "proceed-default" }
670
+ });
671
+ fixtures.push({
672
+ id: "timeout-vs-live-race",
673
+ note: "DEF-4: the live decision wins in journal order; the timeout attempt lands as a journaled noop whose effects are never re-issued.",
674
+ entries: j.entries
675
+ });
676
+ }
677
+ {
678
+ const j = new FixtureJournal();
679
+ const r1 = j.external({ key: "report-1" });
680
+ const r2 = j.external({ key: "report-2" });
681
+ const r3 = j.external({ key: "report-3" });
682
+ j.resolution({
683
+ target: r2.seq,
684
+ by: "operator",
685
+ value: { action: "retry" }
686
+ });
687
+ const dec = j.decision({
688
+ decisionType: "escalation-class",
689
+ payload: {
690
+ action: "retry",
691
+ appliesTo: [
692
+ r1.seq,
693
+ r2.seq,
694
+ r3.seq
695
+ ]
696
+ }
697
+ });
698
+ j.resolution({
699
+ target: r1.seq,
700
+ by: "class_decision",
701
+ decisionRef: dec.seq,
702
+ value: { action: "retry" }
703
+ });
704
+ j.resolution({
705
+ target: r2.seq,
706
+ by: "class_decision",
707
+ decisionRef: dec.seq,
708
+ value: { action: "retry" }
709
+ });
710
+ j.resolution({
711
+ target: r3.seq,
712
+ by: "class_decision",
713
+ decisionRef: dec.seq,
714
+ value: { action: "retry" }
715
+ });
716
+ fixtures.push({
717
+ id: "class-decision-fanout",
718
+ note: "DEF-4: a class-level decision closes three suspended reports, one already closed individually: two applied, one noop with decisionRef preserved. The decision fact itself gains a live consumer in M4.",
719
+ entries: j.entries
720
+ });
721
+ }
722
+ {
723
+ const j = new FixtureJournal();
724
+ const spawn = j.danglingAgent({ prompt: PROMPTS.reviseReport });
725
+ const subtree = agentScope("", spawn.seq);
726
+ j.agentOp({
727
+ prompt: PROMPTS.childResearch,
728
+ scope: subtree,
729
+ value: "notes",
730
+ usage: usageOf(80, 15)
731
+ });
732
+ j.agentOp({
733
+ prompt: PROMPTS.childDraft,
734
+ scope: subtree,
735
+ value: "draft",
736
+ usage: usageOf(90, 25)
737
+ });
738
+ j.abandon({
739
+ target: spawn.seq,
740
+ authorizedBy: spawn.seq,
741
+ reason: "plan revision: cancel_task"
742
+ });
743
+ fixtures.push({
744
+ id: "abandon-then-crash-then-resume",
745
+ note: "DEF-4: crash strictly after the abandon and before any effects; resume derives skipped for the whole subtree (skipped, never orphaned) and re-issues only the revision effects.",
746
+ entries: j.entries
747
+ });
748
+ }
749
+ {
750
+ const j = new FixtureJournal();
751
+ const spawnA = j.danglingAgent({ prompt: PROMPTS.branchAlpha });
752
+ const suspA = j.external({
753
+ key: "alpha-gate",
754
+ scope: agentScope("", spawnA.seq)
755
+ });
756
+ j.abandon({
757
+ target: spawnA.seq,
758
+ authorizedBy: spawnA.seq,
759
+ reason: "cancelled"
760
+ });
761
+ j.resolution({
762
+ target: suspA.seq,
763
+ by: "external",
764
+ value: { go: true }
765
+ });
766
+ const suspB = j.external({
767
+ key: "beta-gate",
768
+ schema: GO_SCHEMA
769
+ });
770
+ j.resolution({
771
+ target: suspB.seq,
772
+ by: "external",
773
+ value: { go: true }
774
+ });
775
+ j.abandon({
776
+ target: suspB.seq,
777
+ authorizedBy: spawnA.seq,
778
+ reason: "late cancel"
779
+ });
780
+ fixtures.push({
781
+ id: "abandon-vs-resolution-race",
782
+ note: "DEF-4: a resolution after a covering abandon is a noop (target_abandoned); the reverse order yields an applied resolution and a noop abandon (already_resolved).",
783
+ entries: j.entries
784
+ });
785
+ }
786
+ {
787
+ const j = new FixtureJournal();
788
+ const gate = j.external({
789
+ key: "deploy-approval",
790
+ schema: APPROVED_SCHEMA,
791
+ prompt: "Approve the deployment?"
792
+ });
793
+ j.resolution({
794
+ target: gate.seq,
795
+ by: "external",
796
+ value: { approved: "yes" }
797
+ });
798
+ j.resolution({
799
+ target: gate.seq,
800
+ by: "operator",
801
+ value: { approved: true }
802
+ });
803
+ fixtures.push({
804
+ id: "offline-invalid-then-valid",
805
+ note: "DEF-4: the schema-invalid offline resolution classifies invalid and never closes; the valid one applies; resume consumes the valid value deterministically.",
806
+ entries: j.entries
807
+ });
808
+ }
809
+ {
810
+ const j = new FixtureJournal();
811
+ const spawn = j.agentOp({
812
+ prompt: PROMPTS.subtreeAlpha,
813
+ value: "alpha done",
814
+ usage: usageOf(150, 30)
815
+ });
816
+ const inner = j.agentOp({
817
+ prompt: PROMPTS.innerAlpha,
818
+ scope: agentScope("", spawn.seq),
819
+ value: "inner out",
820
+ usage: usageOf(70, 10)
821
+ });
822
+ j.abandon({
823
+ target: spawn.seq,
824
+ authorizedBy: spawn.seq,
825
+ reason: "first revision"
826
+ });
827
+ j.abandon({
828
+ target: inner.seq,
829
+ authorizedBy: spawn.seq,
830
+ reason: "second revision overlaps"
831
+ });
832
+ fixtures.push({
833
+ id: "double-abandon-idempotent",
834
+ note: "DEF-4: the second abandon over an already-covered target folds to noop; abandon beats the terminal ok status; live and replayed states identical, no repayment.",
835
+ entries: j.entries
836
+ });
837
+ }
838
+ {
839
+ const j = new FixtureJournal();
840
+ j.agentOp({
841
+ prompt: PROMPTS.v0Relic,
842
+ hashVersion: 0,
843
+ value: "relic out",
844
+ usage: usageOf(40, 5)
845
+ });
846
+ fixtures.push({
847
+ id: "reject-version-too-old",
848
+ note: "DEF-6: hashVersion 0 sits outside the [1,2] window: JournalCompatibilityError HASH_VERSION_TOO_OLD with zero side effects; deriverV0Synthetic from @rulvar/compat reopens the window via extraDerivers.",
849
+ entries: j.entries
850
+ });
851
+ }
852
+ {
853
+ const j = new FixtureJournal();
854
+ j.agentOp({
855
+ prompt: PROMPTS.futureStage,
856
+ hashVersion: 3,
857
+ value: "future out",
858
+ usage: usageOf(40, 5)
859
+ });
860
+ fixtures.push({
861
+ id: "reject-version-from-future",
862
+ note: "DEF-6: a hashVersion 3 entry on the v2 engine: HASH_VERSION_TOO_NEW at load with zero side effects; the lease-acquire repetition of the scan re-records with queue mode in M5.",
863
+ entries: j.entries
864
+ });
865
+ }
866
+ return fixtures;
867
+ }
868
+ /**
869
+ * The frozen v1 journal (docs/11, section "Frozen journal fixtures"): a
870
+ * round-1 JSONL file with kinds agent, step, rand, external, approval and
871
+ * the legacy `v: 1` field (no hashVersion member). Returned as raw
872
+ * JSON-ready objects, one per line.
873
+ */
874
+ function buildFrozenV1JournalRaw() {
875
+ const j = new FixtureJournal();
876
+ j.agentOp({
877
+ prompt: PROMPTS.draftSummary,
878
+ hashVersion: 1,
879
+ value: "summary text, first draft",
880
+ usage: usageOf(220, 80)
881
+ });
882
+ j.stepOp({
883
+ label: "persist-draft",
884
+ value: {
885
+ written: true,
886
+ path: "drafts/summary.md"
887
+ },
888
+ hashVersion: 1
889
+ });
890
+ j.randNow(17067456e5, 1);
891
+ j.external({
892
+ key: "editor-approval",
893
+ schema: APPROVED_SCHEMA,
894
+ prompt: "Approve the draft?",
895
+ hashVersion: 1
896
+ });
897
+ j.approvalSuspended({
898
+ toolName: "publish",
899
+ toolInput: { channel: "blog" },
900
+ hashVersion: 1
901
+ });
902
+ j.agentOp({
903
+ prompt: PROMPTS.polishIntro,
904
+ hashVersion: 1,
905
+ value: "intro pass one",
906
+ usage: usageOf(60, 12)
907
+ });
908
+ j.agentOp({
909
+ prompt: PROMPTS.polishIntro,
910
+ hashVersion: 1,
911
+ value: "intro pass two",
912
+ usage: usageOf(61, 13)
913
+ });
914
+ return j.entries.map((entry) => {
915
+ const { hashVersion: _hashVersion, ...rest } = entry;
916
+ return {
917
+ v: 1,
918
+ ...rest
919
+ };
920
+ });
921
+ }
922
+ /** The docs/03 section 1.5 worked example, frozen as executable data. */
923
+ const WORKED_EXAMPLE_INPUT = {
924
+ kind: "agent",
925
+ agentType: "reviewer",
926
+ modelSpec: {
927
+ kind: "model",
928
+ model: "anthropic:claude-sonnet-4",
929
+ effort: "high"
930
+ },
931
+ prompt: "Review the attached diff for correctness.",
932
+ schemaHash: "f1342f68c9dbb49e8056d0414479659414776dfa4c599b3bebd166c8fdc416ba",
933
+ toolsetHash: "d2c59d7e8cb64de34366877e8764eab84d615942f14167d8715a15d8dbff105c",
934
+ isolation: "none"
935
+ };
936
+ /**
937
+ * v2 golden identity fixtures: worked examples per spawn kind (M2-T12).
938
+ * The keys freeze the hashVersion 2 profile; the v1 members freeze the
939
+ * effort-insensitive projection and the incomparable domain.
940
+ */
941
+ function buildV2GoldenIdentity() {
942
+ return {
943
+ note: "FROZEN v2 identity contract (DEF-6). Any diff requires a hashVersion-bump changeset.",
944
+ workedExampleKey: "66ef15922e576a8f6884b28176c8c21fee9b4d3bb98c76592ed6ca1d3c8f1062",
945
+ emptySchemaHash: EMPTY_SCHEMA_HASH,
946
+ emptyToolsetHash: EMPTY_TOOLSET_HASH,
947
+ perKind: [
948
+ {
949
+ name: "agent (docs/03 1.5 worked example)",
950
+ input: WORKED_EXAMPLE_INPUT
951
+ },
952
+ {
953
+ name: "agent (fake model, no effort)",
954
+ input: fakeAgentIdentity(PROMPTS.draftSummary)
955
+ },
956
+ {
957
+ name: "child",
958
+ input: {
959
+ kind: "child",
960
+ workflow: "sub-flow",
961
+ args: { topic: "rulvar" }
962
+ }
963
+ },
964
+ {
965
+ name: "step",
966
+ input: {
967
+ kind: "step",
968
+ key: "persist-draft",
969
+ deps: []
970
+ }
971
+ },
972
+ {
973
+ name: "step (deps)",
974
+ input: {
975
+ kind: "step",
976
+ key: "fetch",
977
+ deps: [{ page: 2 }]
978
+ }
979
+ },
980
+ {
981
+ name: "external",
982
+ input: {
983
+ kind: "external",
984
+ key: "editor-approval"
985
+ }
986
+ },
987
+ {
988
+ name: "approval",
989
+ input: {
990
+ kind: "approval",
991
+ toolName: "publish",
992
+ input: { channel: "blog" }
993
+ }
994
+ },
995
+ {
996
+ name: "rand now",
997
+ input: {
998
+ kind: "rand",
999
+ subtype: "now"
1000
+ }
1001
+ },
1002
+ {
1003
+ name: "rand keyed",
1004
+ input: {
1005
+ kind: "rand",
1006
+ subtype: "random",
1007
+ key: "jitter"
1008
+ }
1009
+ }
1010
+ ].map((example) => ({
1011
+ name: example.name,
1012
+ input: example.input,
1013
+ key: deriveContentKey(example.input)
1014
+ })),
1015
+ v1: {
1016
+ agentEffortInsensitive: {
1017
+ withEffort: keyFor(fakeAgentIdentity(PROMPTS.draftSummary, "high"), 1),
1018
+ withoutEffort: keyFor(fakeAgentIdentity(PROMPTS.draftSummary), 1)
1019
+ },
1020
+ incomparableKinds: [
1021
+ "decision",
1022
+ "plan.revision",
1023
+ "plan.decision",
1024
+ "ledger.op",
1025
+ "node.link",
1026
+ "termination.init",
1027
+ "termination.denied"
1028
+ ]
1029
+ }
1030
+ };
1031
+ }
1032
+ //#endregion
1033
+ //#region src/cassettes/record-live.ts
1034
+ /**
1035
+ * Live-recorded cassettes (M3-T11): the DEF-1 live set (escalate-replay,
1036
+ * crash-between-report-and-decision, flavor-b-timeout) plus the
1037
+ * re-recorded M2 synthetic DEF-1 subset (abandon-subtree,
1038
+ * memoize-classifier), produced through the REAL runtime: engine runs
1039
+ * over FakeAdapter for everything with a live producer, kernel write
1040
+ * APIs (Replayer) for the orchestrator-subtree shape whose spawning
1041
+ * producer arrives with mode (c) in M6/M7. Timestamps and span ids are
1042
+ * normalized deterministically after recording (matching never reads
1043
+ * them); regeneration is DELIBERATE per the frozen-fixture policy
1044
+ * (docs/11).
1045
+ */
1046
+ const BASE_MS = Date.parse("2026-02-01T00:00:00.000Z");
1047
+ const SPAN = "fixture-span";
1048
+ function stampOf(seq) {
1049
+ return new Date(BASE_MS + seq * 1e3).toISOString();
1050
+ }
1051
+ /**
1052
+ * Wall-clock and span normalization: matching consumes neither, and
1053
+ * frozen bytes must not depend on the recording machine.
1054
+ */
1055
+ function normalizeEntries(entries) {
1056
+ return entries.map((entry) => ({
1057
+ ...entry,
1058
+ spanId: SPAN,
1059
+ startedAt: stampOf(entry.seq),
1060
+ ...entry.endedAt === void 0 ? {} : { endedAt: stampOf(entry.seq) },
1061
+ ...entry.deadlineAt === void 0 ? {} : { deadlineAt: new Date(BASE_MS + entry.seq * 1e3 + 6e4).toISOString() }
1062
+ }));
1063
+ }
1064
+ /** The deterministic escalate request every recording uses. */
1065
+ const RECORDED_ESCALATE_ARGS = {
1066
+ kind: "scope_bigger",
1067
+ scopeDelta: "the billing migration spans nine services, not one",
1068
+ revisedEstimate: {
1069
+ usd: 40,
1070
+ turns: 90
1071
+ },
1072
+ blockers: ["schema ownership is unclear"]
1073
+ };
1074
+ const ESCALATE_REPLAY_PROMPTS = {
1075
+ work: "migrate the billing system",
1076
+ retry: "retry: split the billing migration by service"
1077
+ };
1078
+ /** The workflow of the escalate-replay cassette; tests import it. */
1079
+ function escalateReplayWorkflow() {
1080
+ return defineWorkflow({ name: "escalate-replay" }, async (ctx) => {
1081
+ try {
1082
+ await ctx.agent(ESCALATE_REPLAY_PROMPTS.work, { escalation: {} });
1083
+ return "no escalation";
1084
+ } catch {
1085
+ return await ctx.agent(ESCALATE_REPLAY_PROMPTS.retry);
1086
+ }
1087
+ });
1088
+ }
1089
+ const CRASH_BETWEEN_PROMPT = "stabilize the flaky import pipeline";
1090
+ /** Phase 1 (recorded): the report lands, the process dies before any decision. */
1091
+ function crashBetweenPhase1Workflow() {
1092
+ return defineWorkflow({ name: "crash-between-report-and-decision" }, async (ctx) => {
1093
+ await ctx.agent(CRASH_BETWEEN_PROMPT, {
1094
+ escalation: {},
1095
+ result: "full"
1096
+ });
1097
+ return "reported";
1098
+ });
1099
+ }
1100
+ const FLAVOR_B_PROMPT = "reconcile the ledger discrepancies";
1101
+ const FLAVOR_B_OPTIONS = {
1102
+ flavor: "B",
1103
+ deadlineMs: 25,
1104
+ defaultDecision: {
1105
+ kind: "cancel",
1106
+ reason: "nobody decided before the deadline"
1107
+ }
1108
+ };
1109
+ /** The workflow of the flavor-b-timeout cassette; tests import it. */
1110
+ function flavorBTimeoutWorkflow() {
1111
+ return defineWorkflow({ name: "flavor-b-timeout" }, async (ctx) => {
1112
+ return (await ctx.agent(FLAVOR_B_PROMPT, {
1113
+ escalation: FLAVOR_B_OPTIONS,
1114
+ result: "full"
1115
+ })).status;
1116
+ });
1117
+ }
1118
+ const CLASSIFY_SCHEMA = {
1119
+ type: "object",
1120
+ additionalProperties: false,
1121
+ required: ["label"],
1122
+ properties: { label: { type: "string" } }
1123
+ };
1124
+ /** The workflow of the re-recorded memoize-classifier cassette. */
1125
+ function memoizeClassifierWorkflow() {
1126
+ return defineWorkflow({ name: "memoize-classifier" }, async (ctx) => {
1127
+ await ctx.agent(PROMPTS.classify, {
1128
+ schema: CLASSIFY_SCHEMA,
1129
+ memoizeOutcome: true,
1130
+ result: "full"
1131
+ });
1132
+ await ctx.agent(PROMPTS.summarize, {
1133
+ memoizeOutcome: true,
1134
+ result: "full"
1135
+ });
1136
+ return "classified";
1137
+ });
1138
+ }
1139
+ async function recordEngineRun(options) {
1140
+ const journal = new InMemoryStore();
1141
+ const outcome = await createEngine({
1142
+ adapters: [new FakeAdapter({ agents: options.agents })],
1143
+ stores: { journal },
1144
+ defaults: { routing: {
1145
+ loop: FAKE_MODEL_REF,
1146
+ extract: FAKE_MODEL_REF
1147
+ } },
1148
+ ...options.onEscalation === void 0 ? {} : { onEscalation: options.onEscalation }
1149
+ }).run(options.workflow, void 0, { runId: "record" }).result;
1150
+ if (outcome.status !== "ok") throw new Error(`cassette recording run ended '${outcome.status}': ${outcome.error?.message ?? ""}`);
1151
+ return normalizeEntries(await journal.load("record"));
1152
+ }
1153
+ /** A realistic validated report for the kernel-recorded escalated child. */
1154
+ function subtreeChildReport() {
1155
+ return {
1156
+ kind: "scope_bigger",
1157
+ scopeDelta: "the child branch needs the whole schema registry",
1158
+ revisedEstimate: {
1159
+ usd: 12,
1160
+ turns: 30
1161
+ },
1162
+ blockers: [],
1163
+ proposedDecomposition: [],
1164
+ costToDate: {
1165
+ usd: 0,
1166
+ turns: 2
1167
+ },
1168
+ salvage: {
1169
+ transcriptRef: "record/t-child",
1170
+ artifacts: []
1171
+ }
1172
+ };
1173
+ }
1174
+ /**
1175
+ * The orchestrator-subtree shape: written through the KERNEL write APIs
1176
+ * (the same calls mode (c) spawning uses from M6), since no script-mode
1177
+ * producer can spawn agents under an agent scope yet; re-recorded again
1178
+ * live in M7 with the orchestrator producers (docs/10, cassette plan).
1179
+ */
1180
+ async function recordAbandonSubtree() {
1181
+ let tick = 0;
1182
+ const replayer = new Replayer({
1183
+ runId: "record",
1184
+ store: new InMemoryStore(),
1185
+ now: () => BASE_MS + tick++ * 1e3
1186
+ });
1187
+ const parent = await replayer.appendRunning({
1188
+ scope: "",
1189
+ key: deriveContentKey(fakeAgentIdentity(PROMPTS.branchWork)),
1190
+ kind: "agent",
1191
+ spanId: SPAN
1192
+ });
1193
+ const subtree = agentScope("", parent.seq);
1194
+ const okRunning = await replayer.appendRunning({
1195
+ scope: subtree,
1196
+ key: deriveContentKey(fakeAgentIdentity(PROMPTS.childOk)),
1197
+ kind: "agent",
1198
+ spanId: SPAN
1199
+ });
1200
+ await replayer.appendTerminal(okRunning.seq, {
1201
+ status: "ok",
1202
+ value: "child ok out",
1203
+ usage: usageOf(100, 20),
1204
+ servedBy: FAKE_MODEL_REF
1205
+ });
1206
+ const escalatedRunning = await replayer.appendRunning({
1207
+ scope: subtree,
1208
+ key: deriveContentKey(fakeAgentIdentity(PROMPTS.childEscalated)),
1209
+ kind: "agent",
1210
+ spanId: SPAN
1211
+ });
1212
+ await replayer.appendTerminal(escalatedRunning.seq, {
1213
+ status: "escalated",
1214
+ escalation: subtreeChildReport(),
1215
+ usage: usageOf(200, 40),
1216
+ servedBy: FAKE_MODEL_REF
1217
+ });
1218
+ await replayer.appendRunning({
1219
+ scope: subtree,
1220
+ key: deriveContentKey(fakeAgentIdentity(PROMPTS.childHanging)),
1221
+ kind: "agent",
1222
+ spanId: SPAN
1223
+ });
1224
+ const decision = await replayer.appendSinglePhase({
1225
+ scope: "",
1226
+ key: "",
1227
+ kind: "decision",
1228
+ status: "ok",
1229
+ spanId: SPAN,
1230
+ value: {
1231
+ decisionType: "escalation.decision",
1232
+ targetRef: escalatedRunning.seq,
1233
+ decision: {
1234
+ kind: "cancel",
1235
+ reason: "owner cancelled the branch"
1236
+ },
1237
+ countsAgainstLimit: true
1238
+ }
1239
+ });
1240
+ await replayer.abandonBranch({
1241
+ target: parent.seq,
1242
+ authorizedBy: decision.seq,
1243
+ reason: "cancel_task"
1244
+ });
1245
+ return normalizeEntries(replayer.snapshot());
1246
+ }
1247
+ /**
1248
+ * Records the five live cassettes. Deterministic by construction:
1249
+ * scripted FakeAdapter responders, fixed runId, normalized stamps.
1250
+ */
1251
+ /**
1252
+ * The v1 flow re-run under explicit high effort (DEF-6, M4-T08): the
1253
+ * body matches the frozen v1 journal's call sequence; the assessTone
1254
+ * call is the one genuinely new spawn and records with hashVersion 2
1255
+ * and canonical effort in identity.
1256
+ */
1257
+ function effortShiftWorkflow() {
1258
+ return defineWorkflow({ name: "v1-flow" }, async (ctx) => {
1259
+ return {
1260
+ draft: await ctx.agent(PROMPTS.draftSummary, { effort: "high" }),
1261
+ saved: await ctx.step("persist-draft", () => ({
1262
+ written: true,
1263
+ path: "drafts/summary.md"
1264
+ })),
1265
+ stampMs: ctx.now(),
1266
+ intro1: await ctx.agent(PROMPTS.polishIntro, { effort: "high" }),
1267
+ intro2: await ctx.agent(PROMPTS.polishIntro, { effort: "high" }),
1268
+ tone: await ctx.agent(PROMPTS.assessTone, { effort: "high" }),
1269
+ approved: (await ctx.awaitExternal("editor-approval", {
1270
+ schema: APPROVED_SCHEMA,
1271
+ prompt: "Approve the draft?"
1272
+ })).approved
1273
+ };
1274
+ });
1275
+ }
1276
+ /** Locates the frozen v1 journal from both src (vitest) and dist (script) layouts. */
1277
+ function frozenV1JournalPath() {
1278
+ const candidates = [new URL("../../fixtures/frozen/v1-journal.jsonl", import.meta.url), new URL("../fixtures/frozen/v1-journal.jsonl", import.meta.url)];
1279
+ for (const candidate of candidates) {
1280
+ const path = fileURLToPath(candidate);
1281
+ if (existsSync(path)) return path;
1282
+ }
1283
+ throw new Error("frozen v1-journal.jsonl not found next to the recorder");
1284
+ }
1285
+ /**
1286
+ * effort-defaults-shift (DEF-6; docs/10 M4 gating row): the frozen v1
1287
+ * prefix (recorded without effort) is closed offline the way an
1288
+ * operator would (the external resolves, the approval flow is
1289
+ * abandoned under its authority), then the SAME flow resumes LIVE under
1290
+ * a config requesting high effort with the completed effort semantics:
1291
+ * every v1 entry matches (the v1 predicate strips effort), and the one
1292
+ * new spawn records canonical effort in v2 identity.
1293
+ */
1294
+ async function recordEffortDefaultsShift() {
1295
+ const RUN = "RUNV1";
1296
+ const v1Entries = readFileSync(frozenV1JournalPath(), "utf8").split("\n").filter((line) => line.trim() !== "").map((line) => normalizeEntry(JSON.parse(line)));
1297
+ const journal = new InMemoryStore();
1298
+ for (const entry of v1Entries) await journal.append(RUN, entry);
1299
+ await journal.putMeta({
1300
+ runId: RUN,
1301
+ status: "suspended",
1302
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
1303
+ workflowName: "v1-flow"
1304
+ });
1305
+ const offline = new Replayer({
1306
+ runId: RUN,
1307
+ store: journal,
1308
+ priorEntries: v1Entries
1309
+ });
1310
+ const resolved = await offline.resolveSuspended(5, {
1311
+ by: "external",
1312
+ value: { approved: true }
1313
+ });
1314
+ if (!resolved.applied) throw new Error("offline resolution of the v1 external did not apply");
1315
+ if (!(await offline.abandonBranch({
1316
+ target: 6,
1317
+ authorizedBy: resolved.seq,
1318
+ reason: "approval flow superseded by the operator"
1319
+ })).applied) throw new Error("offline abandon of the v1 approval did not apply");
1320
+ const outcome = await createEngine({
1321
+ adapters: [new FakeAdapter({ agents: { "*": "tone assessed" } })],
1322
+ stores: { journal },
1323
+ defaults: { routing: {
1324
+ loop: FAKE_MODEL_REF,
1325
+ extract: FAKE_MODEL_REF
1326
+ } }
1327
+ }).resume(RUN, effortShiftWorkflow()).result;
1328
+ if (outcome.status !== "ok") throw new Error(`effort-defaults-shift recording ended '${outcome.status}': ${outcome.error?.message ?? ""}`);
1329
+ return normalizeEntries(await journal.load(RUN));
1330
+ }
1331
+ /**
1332
+ * A live engine over one durable store, for multi-leg recordings with
1333
+ * offline kernel writes between the legs (the M8 server/worker shape).
1334
+ */
1335
+ function liveEngine(store, agents) {
1336
+ return createEngine({
1337
+ adapters: [new FakeAdapter({ agents })],
1338
+ stores: { journal: store },
1339
+ defaults: { routing: {
1340
+ loop: FAKE_MODEL_REF,
1341
+ extract: FAKE_MODEL_REF
1342
+ } }
1343
+ });
1344
+ }
1345
+ /** An offline kernel writer over the loaded priors (docs/03, section 8). */
1346
+ async function offlineReplayer(store, runId) {
1347
+ let tick = 500;
1348
+ return new Replayer({
1349
+ runId,
1350
+ store,
1351
+ priorEntries: await store.load(runId),
1352
+ now: () => BASE_MS + tick++ * 1e3
1353
+ });
1354
+ }
1355
+ /** Polls the store until the external suspension is durable. */
1356
+ async function waitForSuspension(store, runId, key) {
1357
+ for (let i = 0; i < 400; i += 1) {
1358
+ if ((await store.load(runId)).some((entry) => entry.status === "suspended" && entry.value?.key === key)) return;
1359
+ await new Promise((resolve) => setTimeout(resolve, 5));
1360
+ }
1361
+ throw new Error(`recording: the '${key}' suspension never landed`);
1362
+ }
1363
+ function suspendedSeqOf(entries, key) {
1364
+ const suspended = entries.find((entry) => entry.status === "suspended" && entry.value?.key === key);
1365
+ if (suspended === void 0) throw new Error(`recording: no suspended entry for external key '${key}'`);
1366
+ return suspended.seq;
1367
+ }
1368
+ /**
1369
+ * DEF-4 timeout-vs-live-race, LIVE form: the run suspends on the
1370
+ * decision, the LIVE resolution wins through RunHandle.resolveExternal,
1371
+ * and the late timer attempt lands through the offline kernel writer as
1372
+ * the journaled noop (first-wins; docs/03, 8.6).
1373
+ */
1374
+ async function recordTimeoutVsLiveRace() {
1375
+ const store = new InMemoryStore();
1376
+ const wf = defineWorkflow({ name: "timeout-race" }, async (ctx) => {
1377
+ return {
1378
+ analysis: await ctx.agent(PROMPTS.analyze),
1379
+ decision: (await ctx.awaitExternal("escalation-report", {
1380
+ schema: DECISION_SCHEMA,
1381
+ prompt: "Escalation decision required"
1382
+ })).decision
1383
+ };
1384
+ });
1385
+ const handle = liveEngine(store, { "*": "analysis: rollback recommended" }).run(wf, void 0, { runId: "record" });
1386
+ await waitForSuspension(store, "record", "escalation-report");
1387
+ const settle = await handle.result;
1388
+ if (settle.status !== "suspended") throw new Error(`timeout-vs-live-race: expected the suspension settle, got '${settle.status}'`);
1389
+ if (!(await handle.resolveExternal("escalation-report", { decision: "rollback" })).applied) throw new Error("timeout-vs-live-race: the live resolution must win");
1390
+ const replayer = await offlineReplayer(store, "record");
1391
+ const target = suspendedSeqOf(await store.load("record"), "escalation-report");
1392
+ if ((await replayer.resolveSuspended(target, {
1393
+ by: "timeout",
1394
+ value: { decision: "abort" }
1395
+ })).applied) throw new Error("timeout-vs-live-race: the timeout attempt must land as a noop");
1396
+ const outcome = await liveEngine(store, { "*": "analysis: rollback recommended" }).resume("record", wf).result;
1397
+ if (outcome.status !== "ok") throw new Error(`timeout-vs-live-race: the resume ended '${outcome.status}'`);
1398
+ return normalizeEntries(await store.load("record"));
1399
+ }
1400
+ /**
1401
+ * DEF-4 class-decision-fanout, LIVE form: three sequential suspensions;
1402
+ * the first closes individually (applied by external); ONE class-level
1403
+ * decision entry closes the remaining two by class_decision with the
1404
+ * decisionRef, and the late class attempt against the first lands noop
1405
+ * (docs/03, 8.6; the plan-side class producer has its own cassette,
1406
+ * class-storm-single-turn).
1407
+ */
1408
+ async function recordClassDecisionFanout() {
1409
+ const store = new InMemoryStore();
1410
+ const wf = defineWorkflow({ name: "fanout" }, async (ctx) => {
1411
+ const one = await ctx.awaitExternal("report-1");
1412
+ const two = await ctx.awaitExternal("report-2");
1413
+ const three = await ctx.awaitExternal("report-3");
1414
+ return [
1415
+ one.action,
1416
+ two.action,
1417
+ three.action
1418
+ ];
1419
+ });
1420
+ const first = liveEngine(store, {}).run(wf, void 0, { runId: "record" });
1421
+ await waitForSuspension(store, "record", "report-1");
1422
+ const settle1 = await first.result;
1423
+ if (settle1.status !== "suspended") throw new Error(`class-decision-fanout: expected the report-1 settle, got '${settle1.status}'`);
1424
+ if (!(await first.resolveExternal("report-1", { action: "retry" })).applied) throw new Error("class-decision-fanout: the individual close must apply");
1425
+ const leg2 = await liveEngine(store, {}).resume("record", wf).result;
1426
+ if (leg2.status !== "suspended") throw new Error(`class-decision-fanout: expected the report-2 settle, got '${leg2.status}'`);
1427
+ const replayer = await offlineReplayer(store, "record");
1428
+ const decision = await replayer.appendSinglePhase({
1429
+ scope: "",
1430
+ key: "",
1431
+ kind: "decision",
1432
+ status: "ok",
1433
+ spanId: SPAN,
1434
+ value: {
1435
+ decisionType: "escalation.class",
1436
+ decision: { kind: "retry" },
1437
+ coverage: "all pending reports of this class"
1438
+ }
1439
+ });
1440
+ const entriesNow = await store.load("record");
1441
+ if (!(await replayer.resolveSuspended(suspendedSeqOf(entriesNow, "report-2"), {
1442
+ by: "class_decision",
1443
+ value: { action: "retry" },
1444
+ decisionRef: decision.seq
1445
+ })).applied) throw new Error("class-decision-fanout: the class close of report-2 must apply");
1446
+ if ((await replayer.resolveSuspended(suspendedSeqOf(entriesNow, "report-1"), {
1447
+ by: "class_decision",
1448
+ value: { action: "retry" },
1449
+ decisionRef: decision.seq
1450
+ })).applied) throw new Error("class-decision-fanout: the class attempt on report-1 must land noop");
1451
+ const secondSettle = await liveEngine(store, {}).resume("record", wf).result;
1452
+ if (secondSettle.status !== "suspended") throw new Error(`class-decision-fanout: expected report-3 to suspend, got '${secondSettle.status}'`);
1453
+ if (!(await (await offlineReplayer(store, "record")).resolveSuspended(suspendedSeqOf(await store.load("record"), "report-3"), {
1454
+ by: "class_decision",
1455
+ value: { action: "retry" },
1456
+ decisionRef: decision.seq
1457
+ })).applied) throw new Error("class-decision-fanout: the class close of report-3 must apply");
1458
+ const final = await liveEngine(store, {}).resume("record", wf).result;
1459
+ if (final.status !== "ok") throw new Error(`class-decision-fanout: the final resume ended '${final.status}'`);
1460
+ return normalizeEntries(await store.load("record"));
1461
+ }
1462
+ /**
1463
+ * DEF-4 abandon-then-crash-then-resume, LIVE form: the paid branch is
1464
+ * abandoned offline (decision plus abandon through the kernel writer),
1465
+ * the crash cut drops the effects, and the resume derives skipped for
1466
+ * the branch while paying the revision effects exactly once.
1467
+ */
1468
+ async function recordAbandonThenCrashThenResume() {
1469
+ const store = new InMemoryStore();
1470
+ const wf = defineWorkflow({ name: "crash-resume" }, async (ctx) => {
1471
+ if ((await ctx.agent(PROMPTS.reviseReport, { result: "full" })).status !== "skipped") return "branch unexpectedly ran";
1472
+ return ctx.agent(PROMPTS.revisionEffects);
1473
+ });
1474
+ const scratch = new InMemoryStore();
1475
+ const outcome1 = await liveEngine(scratch, { "*": "branch out" }).run(wf, void 0, { runId: "record" }).result;
1476
+ if (outcome1.status !== "ok") throw new Error(`abandon-then-crash: life 1 ended '${outcome1.status}'`);
1477
+ const scratchEntries = await scratch.load("record");
1478
+ const branchTerminal = scratchEntries.find((entry) => entry.kind === "agent" && entry.ref !== void 0);
1479
+ if (branchTerminal === void 0) throw new Error("abandon-then-crash: the branch terminal is missing");
1480
+ for (const meta of await scratch.listRuns()) if (meta.runId === "record") await store.putMeta({
1481
+ ...meta,
1482
+ status: "suspended"
1483
+ });
1484
+ for (const entry of scratchEntries) if (entry.seq <= branchTerminal.seq) await store.append("record", entry);
1485
+ const replayer = await offlineReplayer(store, "record");
1486
+ const branchRoot = (await store.load("record")).find((entry) => entry.kind === "agent" && entry.ref === void 0);
1487
+ if (branchRoot === void 0) throw new Error("abandon-then-crash: the branch root is missing");
1488
+ if (!(await replayer.abandonBranch({
1489
+ target: branchRoot.seq,
1490
+ authorizedBy: branchRoot.seq,
1491
+ reason: "plan revision"
1492
+ })).applied) throw new Error("abandon-then-crash: the abandon must apply");
1493
+ const outcome2 = await liveEngine(store, { "*": "fresh live output" }).resume("record", wf).result;
1494
+ if (outcome2.status !== "ok" || outcome2.value !== "fresh live output") throw new Error(`abandon-then-crash: the resume must pay the effects once, got '${outcome2.status}'`);
1495
+ return normalizeEntries(await store.load("record"));
1496
+ }
1497
+ /**
1498
+ * DEF-4 abandon-vs-resolution-race, LIVE form: both orders through the
1499
+ * kernel writer over one journal: an abandon covering a suspension makes
1500
+ * the late resolution a noop (target_abandoned); the reverse order
1501
+ * applies the resolution and the late abandon folds noop.
1502
+ */
1503
+ async function recordAbandonVsResolutionRace() {
1504
+ const store = new InMemoryStore();
1505
+ const wf = defineWorkflow({ name: "race-directions" }, async (ctx) => {
1506
+ const alpha = await ctx.agent(PROMPTS.branchAlpha, { result: "full" });
1507
+ const beta = await ctx.awaitExternal("beta-gate", { schema: GO_SCHEMA });
1508
+ return {
1509
+ alphaStatus: alpha.status,
1510
+ go: beta.go
1511
+ };
1512
+ });
1513
+ const settle1 = await liveEngine(store, { "*": "alpha branch out" }).run(wf, void 0, { runId: "record" }).result;
1514
+ if (settle1.status !== "suspended") throw new Error(`abandon-vs-resolution: expected the beta suspension, got '${settle1.status}'`);
1515
+ const replayer = await offlineReplayer(store, "record");
1516
+ const loaded = await store.load("record");
1517
+ const alphaRoot = loaded.find((entry) => entry.kind === "agent" && entry.ref === void 0);
1518
+ if (alphaRoot === void 0) throw new Error("abandon-vs-resolution: the alpha root is missing");
1519
+ const innerApproval = await replayer.appendSuspended({
1520
+ scope: agentScope("", alphaRoot.seq),
1521
+ key: "",
1522
+ kind: "approval",
1523
+ spanId: SPAN,
1524
+ value: {
1525
+ toolName: "deploy",
1526
+ input: { env: "prod" }
1527
+ }
1528
+ });
1529
+ if (!(await replayer.abandonBranch({
1530
+ target: alphaRoot.seq,
1531
+ authorizedBy: alphaRoot.seq,
1532
+ reason: "abandon first"
1533
+ })).applied) throw new Error("abandon-vs-resolution: the covering abandon must apply");
1534
+ if ((await replayer.resolveSuspended(innerApproval.seq, {
1535
+ by: "external",
1536
+ value: { approved: true }
1537
+ })).applied) throw new Error("abandon-vs-resolution: the covered resolution must land noop");
1538
+ const betaSeq = suspendedSeqOf(loaded, "beta-gate");
1539
+ if (!(await replayer.resolveSuspended(betaSeq, {
1540
+ by: "external",
1541
+ value: { go: true }
1542
+ })).applied) throw new Error("abandon-vs-resolution: the beta resolution must apply");
1543
+ if ((await replayer.abandonBranch({
1544
+ target: betaSeq,
1545
+ authorizedBy: alphaRoot.seq,
1546
+ reason: "abandon second"
1547
+ })).applied) throw new Error("abandon-vs-resolution: the late abandon must fold noop");
1548
+ return normalizeEntries(await store.load("record"));
1549
+ }
1550
+ /**
1551
+ * DEF-4 offline-invalid-then-valid, LIVE form (the M8 server machinery
1552
+ * end to end): the run suspends on a schema-validated external; the
1553
+ * offline writer appends an INVALID then a VALID resolution; the resume
1554
+ * consumes the valid value with zero live calls inside the suspension.
1555
+ */
1556
+ async function recordOfflineInvalidThenValid() {
1557
+ const store = new InMemoryStore();
1558
+ const wf = defineWorkflow({ name: "invalid-then-valid" }, async (ctx) => {
1559
+ return (await ctx.awaitExternal("deploy-approval", {
1560
+ schema: APPROVED_SCHEMA,
1561
+ prompt: "Approve the deployment?"
1562
+ })).approved;
1563
+ });
1564
+ const settle1 = await liveEngine(store, {}).run(wf, void 0, { runId: "record" }).result;
1565
+ if (settle1.status !== "suspended") throw new Error(`offline-invalid-then-valid: expected suspended, got '${settle1.status}'`);
1566
+ const replayer = await offlineReplayer(store, "record");
1567
+ const target = suspendedSeqOf(await store.load("record"), "deploy-approval");
1568
+ if ((await replayer.resolveSuspended(target, {
1569
+ by: "external",
1570
+ value: { approved: "yes-ish" }
1571
+ })).applied) throw new Error("offline-invalid-then-valid: the invalid payload must not close");
1572
+ if (!(await replayer.resolveSuspended(target, {
1573
+ by: "external",
1574
+ value: { approved: true }
1575
+ })).applied) throw new Error("offline-invalid-then-valid: the valid payload must apply");
1576
+ const outcome = await liveEngine(store, {}).resume("record", wf).result;
1577
+ if (outcome.status !== "ok" || outcome.value !== true) throw new Error(`offline-invalid-then-valid: the resume ended '${outcome.status}'`);
1578
+ return normalizeEntries(await store.load("record"));
1579
+ }
1580
+ /**
1581
+ * DEF-4 double-abandon-idempotent, LIVE form: two covering abandons over
1582
+ * overlapping targets through the kernel writer; the second folds noop,
1583
+ * the terminal ok inside the coverage derives skipped, and the ledger
1584
+ * pays nothing on replay.
1585
+ */
1586
+ async function recordDoubleAbandonIdempotent() {
1587
+ const store = new InMemoryStore();
1588
+ const wf = defineWorkflow({ name: "double-abandon" }, async (ctx) => {
1589
+ return (await ctx.agent(PROMPTS.subtreeAlpha, { result: "full" })).status;
1590
+ });
1591
+ const settle1 = await liveEngine(store, { "*": "alpha done" }).run(wf, void 0, { runId: "record" }).result;
1592
+ if (settle1.status !== "ok") throw new Error(`double-abandon: life 1 ended '${settle1.status}'`);
1593
+ const replayer = await offlineReplayer(store, "record");
1594
+ const root = (await store.load("record")).find((entry) => entry.kind === "agent" && entry.ref === void 0);
1595
+ if (root === void 0) throw new Error("double-abandon: the alpha root is missing");
1596
+ if (!(await replayer.abandonBranch({
1597
+ target: root.seq,
1598
+ authorizedBy: root.seq,
1599
+ reason: "first cancel"
1600
+ })).applied) throw new Error("double-abandon: the first abandon must apply");
1601
+ if ((await replayer.abandonBranch({
1602
+ target: root.seq,
1603
+ authorizedBy: root.seq,
1604
+ reason: "second cancel overlaps"
1605
+ })).applied) throw new Error("double-abandon: the second abandon must fold noop");
1606
+ return normalizeEntries(await store.load("record"));
1607
+ }
1608
+ async function recordLiveCassettes() {
1609
+ const fixtures = [];
1610
+ fixtures.push({
1611
+ id: "effort-defaults-shift",
1612
+ note: "DEF-6 (recorded through the live runtime in M4-T08): the frozen v1 prefix recorded without effort, closed offline, then resumed live under explicit high effort with the completed effort semantics; every v1 entry matches (the v1 predicate strips effort) and the one new spawn carries canonical effort in v2 identity.",
1613
+ entries: await recordEffortDefaultsShift()
1614
+ });
1615
+ fixtures.push({
1616
+ id: "abandon-subtree",
1617
+ note: "DEF-1 (re-recorded through the kernel write APIs in M3): abandon over a subtree with ok, escalated, and a hanging running entry, authorized by the owner cancel decision; all derive skipped, zero live calls, zero spend increment.",
1618
+ entries: await recordAbandonSubtree()
1619
+ });
1620
+ fixtures.push({
1621
+ id: "memoize-classifier",
1622
+ note: "DEF-1 (re-recorded through the live runtime in M3): memoizeOutcome pins the task-class failure (schema mismatch) for replay; the transport-class failure (rate limit) reruns and is the expected strict miss.",
1623
+ entries: await recordEngineRun({
1624
+ workflow: memoizeClassifierWorkflow(),
1625
+ agents: {
1626
+ "summarize the document": () => fakeWireError({
1627
+ code: "rate-limit",
1628
+ message: "429 too many requests",
1629
+ retryable: true,
1630
+ data: {
1631
+ kind: "rate-limit",
1632
+ retryAfterMs: 1e3
1633
+ }
1634
+ }),
1635
+ "*": "this is not the requested JSON, just prose"
1636
+ }
1637
+ }).then((entries) => {
1638
+ return entries;
1639
+ })
1640
+ });
1641
+ fixtures.push({
1642
+ id: "escalate-replay",
1643
+ note: "DEF-1 live set: the worker finishes escalated with a report, the parent decides retry (journaled escalation.decision), and the respawn completes; replay-strict resume yields zero live calls, the byte-identical report, and the decision from the entry.",
1644
+ entries: await recordEngineRun({
1645
+ workflow: escalateReplayWorkflow(),
1646
+ agents: {
1647
+ "migrate the billing": () => fakeToolCalls({
1648
+ name: "escalate",
1649
+ args: RECORDED_ESCALATE_ARGS
1650
+ }),
1651
+ "retry: split the billing": "migration retried per service"
1652
+ },
1653
+ onEscalation: () => ({
1654
+ kind: "retry",
1655
+ amendedPrompt: "split by service"
1656
+ })
1657
+ })
1658
+ });
1659
+ fixtures.push({
1660
+ id: "crash-between-report-and-decision",
1661
+ note: "DEF-1 live set: the terminal escalated entry landed, the process died before any decision; the first resume replays escalated and pays for the decision live exactly once; the second resume replays both with zero live calls.",
1662
+ entries: await recordEngineRun({
1663
+ workflow: crashBetweenPhase1Workflow(),
1664
+ agents: { "stabilize the flaky": () => fakeToolCalls({
1665
+ name: "escalate",
1666
+ args: RECORDED_ESCALATE_ARGS
1667
+ }) }
1668
+ })
1669
+ });
1670
+ fixtures.push({
1671
+ id: "timeout-vs-live-race",
1672
+ note: "DEF-4 (re-recorded through the live producers in M9): the live resolution wins through RunHandle.resolveExternal; the late timer attempt lands through the offline kernel writer as the journaled noop whose effects never re-issue (docs/03, 8.6).",
1673
+ entries: await recordTimeoutVsLiveRace()
1674
+ });
1675
+ fixtures.push({
1676
+ id: "class-decision-fanout",
1677
+ note: "DEF-4 (re-recorded through the live producers in M9): report-1 closes individually; ONE class-level decision closes the remaining reports by class_decision with the decisionRef; the late class attempt on report-1 lands noop; the plan-side class producer has its own cassette (class-storm-single-turn).",
1678
+ entries: await recordClassDecisionFanout()
1679
+ });
1680
+ fixtures.push({
1681
+ id: "abandon-then-crash-then-resume",
1682
+ note: "DEF-4 (re-recorded through the live producers in M9): the paid branch is abandoned through the offline kernel writer (decision plus abandon), the crash cut drops the effects, and the resume derives skipped while paying the effects exactly once.",
1683
+ entries: await recordAbandonThenCrashThenResume()
1684
+ });
1685
+ fixtures.push({
1686
+ id: "abandon-vs-resolution-race",
1687
+ note: "DEF-4 (re-recorded through the live producers in M9): both orders over one journal; an abandon covering a suspension makes the late resolution a noop target_abandoned; the reverse order applies the resolution and the late abandon folds noop.",
1688
+ entries: await recordAbandonVsResolutionRace()
1689
+ });
1690
+ fixtures.push({
1691
+ id: "offline-invalid-then-valid",
1692
+ note: "DEF-4 (re-recorded through the live producers in M9, the M8 offline machinery end to end): the offline writer appends an INVALID then a VALID resolution against the schema-validated suspension; the resume consumes the valid value.",
1693
+ entries: await recordOfflineInvalidThenValid()
1694
+ });
1695
+ fixtures.push({
1696
+ id: "double-abandon-idempotent",
1697
+ note: "DEF-4 (re-recorded through the live producers in M9): two covering abandons over one target; the second folds noop, the terminal ok inside the coverage derives skipped, and replay pays nothing.",
1698
+ entries: await recordDoubleAbandonIdempotent()
1699
+ });
1700
+ fixtures.push({
1701
+ id: "flavor-b-timeout",
1702
+ note: "DEF-1 live set: the escalate tool suspends the agent with a journaled deadline; the timer appends the resolution by timeout applying the defaultDecision (first-wins); dispose and the terminal escalated entry follow as effects; resume replays the closing resolution and the terminal entry with no re-suspension.",
1703
+ entries: await recordEngineRun({
1704
+ workflow: flavorBTimeoutWorkflow(),
1705
+ agents: { "reconcile the ledger": () => fakeToolCalls({
1706
+ name: "escalate",
1707
+ args: RECORDED_ESCALATE_ARGS
1708
+ }) }
1709
+ })
1710
+ });
1711
+ return fixtures;
1712
+ }
1713
+ //#endregion
1714
+ //#region src/cassettes/m6-orchestrator.ts
1715
+ const M6_ORCH_RUN_ID = "m6-orchestrator-crash";
1716
+ const M6_ORCH_GOAL = "m6 cassette: gather two facts";
1717
+ const M6_ORCH_PROFILES = { worker: { description: "does one task" } };
1718
+ function agentTypeOf(call) {
1719
+ return (call.req.providerOptions?.rulvar)?.agentType ?? "";
1720
+ }
1721
+ /** Extracts spawn handles from the tool results the model saw. */
1722
+ function handlesInRequest(req) {
1723
+ const handles = [];
1724
+ for (const msg of req.messages) for (const part of msg.parts) if (part.type === "tool-result") {
1725
+ const result = part.result;
1726
+ if (typeof result?.handle === "number") handles.push(result.handle);
1727
+ }
1728
+ return handles;
1729
+ }
1730
+ /** Fixes wall clock and spans; everything else is deterministic already. */
1731
+ function normalizeM6Entries(entries) {
1732
+ return entries.map((entry) => ({
1733
+ ...entry,
1734
+ spanId: "fixture-span",
1735
+ startedAt: "2026-02-01T00:00:00.000Z",
1736
+ ...entry.endedAt === void 0 ? {} : { endedAt: "2026-02-01T00:00:00.000Z" }
1737
+ }));
1738
+ }
1739
+ /**
1740
+ * Phase 1: record the pre-crash journal. The transcripts store carries
1741
+ * the boundary checkpoint the resume restores from; the recorder keeps
1742
+ * it in memory because the cassette pins only journal bytes (checkpoint
1743
+ * blobs are engine-internal at-least-once state, docs/03 section 11).
1744
+ */
1745
+ async function recordOrchestratorCrash() {
1746
+ let orchestratorTurn = 0;
1747
+ const adapter = new FakeAdapter({ agents: { "*": (call) => {
1748
+ if (agentTypeOf(call) === "worker") return `paid: ${call.prompt}`;
1749
+ orchestratorTurn += 1;
1750
+ if (orchestratorTurn === 1) return fakeToolCalls({
1751
+ name: "spawn_agent",
1752
+ args: {
1753
+ agentType: "worker",
1754
+ prompt: "expensive A"
1755
+ }
1756
+ }, {
1757
+ name: "spawn_agent",
1758
+ args: {
1759
+ agentType: "worker",
1760
+ prompt: "expensive B"
1761
+ }
1762
+ });
1763
+ return fakeWireError({
1764
+ code: "agent",
1765
+ message: "simulated crash",
1766
+ retryable: false
1767
+ });
1768
+ } } });
1769
+ const store = new InMemoryStore();
1770
+ const transcripts = new InMemoryTranscriptStore();
1771
+ const engine = createEngine({
1772
+ adapters: [adapter],
1773
+ stores: {
1774
+ journal: store,
1775
+ transcripts
1776
+ },
1777
+ defaults: {
1778
+ routing: {
1779
+ loop: FAKE_MODEL_REF,
1780
+ orchestrate: FAKE_MODEL_REF
1781
+ },
1782
+ profiles: M6_ORCH_PROFILES
1783
+ }
1784
+ });
1785
+ const wf = makeOrchestratorWorkflow(M6_ORCH_GOAL, {});
1786
+ const outcome = await engine.run(wf, void 0, { runId: M6_ORCH_RUN_ID }).result.then((settled) => settled);
1787
+ if (outcome.status !== "error") throw new Error(`the recorder expected a crashed run, got '${outcome.status}'`);
1788
+ const entries = await store.load(M6_ORCH_RUN_ID);
1789
+ const orchestratorTerminal = entries.find((entry) => entry.kind === "agent" && !entry.scope.startsWith("agent:") && entry.status !== "running" && entry.status !== "suspended");
1790
+ if (orchestratorTerminal === void 0) throw new Error("the recorder found no orchestrator terminal to cut");
1791
+ const cut = entries.filter((entry) => entry.seq < orchestratorTerminal.seq);
1792
+ const checkpoints = {};
1793
+ for (const ref of await transcripts.list(M6_ORCH_RUN_ID)) {
1794
+ if (!ref.includes("/ckpt/")) continue;
1795
+ const blob = await transcripts.get(ref);
1796
+ if (blob !== null) checkpoints[ref] = Buffer.from(blob).toString("base64");
1797
+ }
1798
+ return {
1799
+ entries: normalizeM6Entries(cut),
1800
+ checkpoints
1801
+ };
1802
+ }
1803
+ //#endregion
1804
+ //#region src/vcr.ts
1805
+ /**
1806
+ * VCR cassettes at the adapter boundary (M5-T04; docs/09, section 5.2;
1807
+ * docs/11, section 5): `record` wraps live adapters and captures
1808
+ * request/event pairs into a redacted JSONL cassette keyed by a hash of
1809
+ * the canonical wire-contract request; `replay` serves recorded streams
1810
+ * back with `onMiss: 'throw'` (hermetic CI) or `'passthrough'` (mixed
1811
+ * live/recorded development runs). Because the boundary speaks the L0
1812
+ * wire contract, cassettes are vendor-neutral by construction.
1813
+ *
1814
+ * Redaction happens at record time and secrets MUST never reach the
1815
+ * committed cassette bytes: the built-in policy masks authorization
1816
+ * material (bearer tokens, api-key-shaped strings) in every stored
1817
+ * string, and a `redact` hook composes on top. The request HASH is
1818
+ * computed over the raw canonical request (minus the engine-populated
1819
+ * `providerOptions.rulvar` telemetry namespace, which is never
1820
+ * identity), so replay matching is redaction-independent while stored
1821
+ * bytes stay clean. Cassettes record the hashVersion they were produced
1822
+ * under (DEF-6).
1823
+ */
1824
+ /**
1825
+ * Built-in redaction: authorization material never reaches cassette
1826
+ * bytes (docs/11, section 5.2). Deliberately aggressive; compose a
1827
+ * custom hook for payload-specific secrets.
1828
+ */
1829
+ function defaultRedact(value) {
1830
+ return value.replace(/\b(sk|pk|rk)-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]").replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]").replace(/\b(api[-_]?key|authorization|x-api-key)(["':\s=]+)((?!Bearer\b)[^\s"',;]+)/gi, "$1$2[REDACTED]");
1831
+ }
1832
+ function walkStrings(value, fn) {
1833
+ if (typeof value === "string") return fn(value);
1834
+ if (Array.isArray(value)) return value.map((item) => walkStrings(item, fn));
1835
+ if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, walkStrings(item, fn)]));
1836
+ return value;
1837
+ }
1838
+ /** Deterministic canonical JSON: sorted keys, no whitespace. */
1839
+ function canonicalJson(value) {
1840
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
1841
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
1842
+ return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
1843
+ }
1844
+ /**
1845
+ * The cassette key: a hash of the canonical wire-contract request. The
1846
+ * engine-populated telemetry namespace is excluded (docs/04, section
1847
+ * 1.8: never identity); everything else the adapter would send keys the
1848
+ * row.
1849
+ */
1850
+ function requestHash(req) {
1851
+ const { providerOptions, ...rest } = req;
1852
+ const filtered = providerOptions === void 0 ? {} : Object.fromEntries(Object.entries(providerOptions).filter(([namespace]) => namespace !== "rulvar"));
1853
+ const withoutTelemetry = Object.keys(filtered).length === 0 ? rest : {
1854
+ ...rest,
1855
+ providerOptions: filtered
1856
+ };
1857
+ return createHash("sha256").update(canonicalJson(withoutTelemetry), "utf8").digest("hex");
1858
+ }
1859
+ function headerLine() {
1860
+ return JSON.stringify({
1861
+ v: 1,
1862
+ kind: "rulvar-vcr",
1863
+ hashVersion: CURRENT_HASH_VERSION,
1864
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
1865
+ });
1866
+ }
1867
+ /**
1868
+ * Wraps live adapters for recording: every completed stream appends one
1869
+ * redacted row to the cassette JSONL. The wrapped adapters are drop-in:
1870
+ * same ids, providers, caps, and event streams.
1871
+ */
1872
+ function record(options) {
1873
+ const redact = options.redact ? (value) => defaultRedact(options.redact ? options.redact(value) : value) : defaultRedact;
1874
+ if (!existsSync(options.cassette)) writeFileSync(options.cassette, `${headerLine()}\n`, "utf8");
1875
+ return options.adapters.map((adapter) => ({
1876
+ ...adapter,
1877
+ id: adapter.id,
1878
+ ...adapter.provider === void 0 ? {} : { provider: adapter.provider },
1879
+ caps: (model) => adapter.caps(model),
1880
+ async *stream(req, signal) {
1881
+ const events = [];
1882
+ for await (const event of adapter.stream(req, signal)) {
1883
+ events.push(event);
1884
+ yield event;
1885
+ }
1886
+ const row = {
1887
+ adapterId: adapter.id,
1888
+ ...adapter.provider === void 0 ? {} : { provider: adapter.provider },
1889
+ requestHash: requestHash(req),
1890
+ request: walkStrings(JSON.parse(JSON.stringify(req)), redact),
1891
+ events: walkStrings(JSON.parse(JSON.stringify(events)), redact),
1892
+ caps: adapter.caps(req.model),
1893
+ model: req.model
1894
+ };
1895
+ appendFileSync(options.cassette, `${JSON.stringify(row)}\n`, "utf8");
1896
+ }
1897
+ }));
1898
+ }
1899
+ /** Typed hermetic-miss error; onMiss: 'throw' raises it on any unrecorded request. */
1900
+ var VcrMissError = class extends Error {
1901
+ requestHash;
1902
+ constructor(adapterId, hash) {
1903
+ super(`VCR miss: adapter '${adapterId}' received a request with no recorded row (hash ${hash.slice(0, 12)}); onMiss: 'throw' keeps cassette tests hermetic`);
1904
+ this.name = "VcrMissError";
1905
+ this.requestHash = hash;
1906
+ }
1907
+ };
1908
+ /** Parses a cassette file (one header line plus one JSON row per line). */
1909
+ function readCassette(path) {
1910
+ const lines = readFileSync(path, "utf8").split("\n").filter((line) => line.trim() !== "");
1911
+ const header = JSON.parse(lines[0] ?? "{}");
1912
+ if (header.kind !== "rulvar-vcr") throw new ConfigError(`${path} is not a rulvar VCR cassette`);
1913
+ return {
1914
+ header,
1915
+ rows: lines.slice(1).map((line) => JSON.parse(line))
1916
+ };
1917
+ }
1918
+ /**
1919
+ * Builds replay adapters from a cassette. `onMiss: 'throw'` is the
1920
+ * hermetic CI mode; `'passthrough'` forwards unrecorded requests to the
1921
+ * matching live adapter in `adapters` (a development convenience only,
1922
+ * docs/11 section 5.1).
1923
+ */
1924
+ function replay(options) {
1925
+ const { rows } = readCassette(options.cassette);
1926
+ const byAdapter = /* @__PURE__ */ new Map();
1927
+ for (const row of rows) {
1928
+ const forAdapter = byAdapter.get(row.adapterId) ?? /* @__PURE__ */ new Map();
1929
+ forAdapter.set(row.requestHash, row);
1930
+ byAdapter.set(row.adapterId, forAdapter);
1931
+ }
1932
+ const live = new Map((options.adapters ?? []).map((adapter) => [adapter.id, adapter]));
1933
+ return [.../* @__PURE__ */ new Set([...byAdapter.keys(), ...live.keys()])].map((adapterId) => {
1934
+ const recorded = byAdapter.get(adapterId) ?? /* @__PURE__ */ new Map();
1935
+ const passthrough = live.get(adapterId);
1936
+ const someRow = [...recorded.values()][0];
1937
+ const capsByModel = /* @__PURE__ */ new Map();
1938
+ for (const row of recorded.values()) capsByModel.set(row.model, row.caps);
1939
+ return {
1940
+ id: adapterId,
1941
+ ...someRow?.provider === void 0 ? {} : { provider: someRow.provider },
1942
+ caps: (model) => {
1943
+ const snapshot = capsByModel.get(model) ?? passthrough?.caps(model);
1944
+ if (snapshot === void 0) throw new ConfigError(`VCR replay adapter '${adapterId}' has no caps snapshot for model '${model}'`);
1945
+ return snapshot;
1946
+ },
1947
+ async *stream(req, signal) {
1948
+ const hash = requestHash(req);
1949
+ const row = recorded.get(hash);
1950
+ if (row !== void 0) {
1951
+ for (const event of row.events) yield event;
1952
+ return;
1953
+ }
1954
+ if (options.onMiss === "passthrough" && passthrough !== void 0) {
1955
+ yield* passthrough.stream(req, signal);
1956
+ return;
1957
+ }
1958
+ throw new VcrMissError(adapterId, hash);
1959
+ }
1960
+ };
1961
+ });
1962
+ }
1963
+ //#endregion
1964
+ export { FAKE_MODEL, FAKE_MODEL_REF, FakeAdapter, M6_ORCH_GOAL, M6_ORCH_PROFILES, M6_ORCH_RUN_ID, VcrMissError, buildFrozenV1JournalRaw, buildM2CassetteFixtures, buildV2GoldenIdentity, createTestEngine, defaultRedact, fakeToolCalls, fakeWireError, handlesInRequest, normalizeM6Entries, readCassette, record, recordLiveCassettes, recordOrchestratorCrash, replay, replayRun, requestHash };