@effect-agent/storage-memory 0.1.0-beta.9 → 0.1.0-beta.90

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.
Files changed (34) hide show
  1. package/dist/MemoryMessageDeliveryStore.d.mts +15 -0
  2. package/dist/MemoryMessageDeliveryStore.mjs +147 -0
  3. package/dist/MemoryMessageDeliveryStore.mjs.map +1 -0
  4. package/dist/MemoryScheduleStore.d.mts +10 -0
  5. package/dist/MemoryScheduleStore.mjs +169 -0
  6. package/dist/MemoryScheduleStore.mjs.map +1 -0
  7. package/dist/MemorySemanticIndex.d.mts +21 -0
  8. package/dist/MemorySemanticIndex.mjs +222 -0
  9. package/dist/MemorySemanticIndex.mjs.map +1 -0
  10. package/dist/MemorySubmissionLedger.d.mts +24 -0
  11. package/dist/MemorySubmissionLedger.mjs +1058 -0
  12. package/dist/MemorySubmissionLedger.mjs.map +1 -0
  13. package/dist/MemorySubscriptionStore.d.mts +9 -0
  14. package/dist/MemorySubscriptionStore.mjs +849 -0
  15. package/dist/MemorySubscriptionStore.mjs.map +1 -0
  16. package/dist/MemoryThreadStore.d.mts +17 -0
  17. package/dist/MemoryThreadStore.mjs +377 -0
  18. package/dist/MemoryThreadStore.mjs.map +1 -0
  19. package/dist/index.d.mts +7 -30
  20. package/dist/index.mjs +7 -1298
  21. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  22. package/package.json +1 -45
  23. package/src/MemoryMessageDeliveryStore.ts +293 -0
  24. package/src/MemoryScheduleStore.ts +328 -0
  25. package/src/MemorySemanticIndex.ts +357 -0
  26. package/src/{memory-ledger.ts → MemorySubmissionLedger.ts} +478 -86
  27. package/src/MemorySubscriptionStore.ts +1549 -0
  28. package/src/MemoryThreadStore.ts +766 -0
  29. package/src/index.ts +6 -2
  30. package/dist/index.mjs.map +0 -1
  31. package/dist/testing.d.mts +0 -2
  32. package/dist/testing.mjs +0 -2
  33. package/src/memory-storage.ts +0 -614
  34. package/src/testing.ts +0 -10
@@ -1,19 +1,48 @@
1
1
  import {
2
+ Clock,
3
+ Cause,
4
+ Exit,
5
+ Fiber,
6
+ DateTime,
7
+ Duration,
8
+ Effect,
9
+ Layer,
10
+ Option,
11
+ Ref,
12
+ Schema,
13
+ Stream,
14
+ } from "effect";
15
+ import {
16
+ type ToolCallId,
2
17
  AttemptId,
3
18
  ReceiptId,
4
19
  SubmissionId,
5
- ToolCallId,
6
20
  type AgentId,
7
- type ConversationId,
21
+ type ThreadId,
8
22
  type SettlementId,
9
- } from "@effect-agent/core";
23
+ } from "effect-agent/identifiers";
24
+ import { InputMessage } from "effect-agent/messaging";
25
+ import {
26
+ PersistedJson,
27
+ WorkerAdmission,
28
+ ProducerEpoch,
29
+ type DefinitionDigests,
30
+ type DeploymentId,
31
+ type Digest,
32
+ type ProducerId,
33
+ type RecordEnvelope,
34
+ type SettlementOutcome,
35
+ } from "effect-agent/records";
10
36
  import {
37
+ type ParentLinkage,
11
38
  AbortCommand,
12
39
  AdmissionAdmitted,
13
40
  AdmissionConflict,
41
+ AdmissionPolicyError,
14
42
  AdmissionIndeterminate,
15
43
  AdmissionNotAdmitted,
16
44
  AdmissionRequest,
45
+ SubmissionAdmissionFence,
17
46
  AdmissionResult,
18
47
  ApprovalConflict,
19
48
  ApprovalDecisionCommand,
@@ -43,8 +72,6 @@ import {
43
72
  OwnershipRenewal,
44
73
  OwnershipSnapshot,
45
74
  OwnershipToken,
46
- ParentLinkage,
47
- ProducerEpoch,
48
75
  QueueSequence,
49
76
  RecoverySnapshot,
50
77
  RecoverySnapshotRequest,
@@ -59,7 +86,9 @@ import {
59
86
  SettlementFinalization,
60
87
  SettlementReservation,
61
88
  SettlementReservationSnapshot,
89
+ settlementFailureFromRecord,
62
90
  AbortIntent,
91
+ AbortIntentRequest,
63
92
  SubmissionLedger,
64
93
  SubmissionLookup,
65
94
  SubmissionLookupByKey,
@@ -73,20 +102,12 @@ import {
73
102
  type ChildReservationId,
74
103
  type ChildReservationStatus,
75
104
  type ChildSettledOutcome,
76
- type DefinitionDigests,
77
- type DeploymentId,
78
- type Digest,
79
105
  type IdempotencyKey,
80
- type PersistedJson,
81
106
  type Principal,
82
- type ProducerId,
83
- type RecordEnvelope,
84
- type SettlementOutcome,
85
107
  type SubmissionState,
86
108
  type SuspensionOutcome,
87
109
  type SuspensionReason,
88
- } from "@effect-agent/session";
89
- import { Clock, DateTime, Duration, Effect, Layer, Option, Ref, Schema, Stream } from "effect";
110
+ } from "effect-agent/submission-ledger";
90
111
 
91
112
  const MAX_SUBMISSIONS = 65_536;
92
113
 
@@ -110,7 +131,7 @@ const STATE_RANK: Record<SubmissionState, number> = {
110
131
 
111
132
  interface SubmissionRow {
112
133
  readonly submissionId: SubmissionId;
113
- readonly conversationId: ConversationId;
134
+ readonly threadId: ThreadId;
114
135
  readonly queueSequence: QueueSequence;
115
136
  readonly principal: Principal;
116
137
  readonly idempotencyKey: IdempotencyKey;
@@ -126,6 +147,10 @@ interface SubmissionRow {
126
147
  readonly readyAtMillis: number | undefined;
127
148
  /** Immutable child-side lineage recorded at admission (spec §12 step 5). */
128
149
  readonly parentLinkage: ParentLinkage | undefined;
150
+ readonly admissionGroup?: string;
151
+ readonly admissionFence?: AdmissionRequest["admissionFence"];
152
+ readonly workerAdmissionJson?: string;
153
+ readonly messageAdmissionJson?: string;
129
154
  }
130
155
 
131
156
  interface StoredOwnership {
@@ -156,8 +181,6 @@ interface StoredUnknownMark {
156
181
 
157
182
  interface StoredUnknownResolution {
158
183
  readonly intent: UnknownResolutionIntent;
159
- /** Canonical JSON of the Schema-encoded resolution, for divergent re-resolution detection. */
160
- readonly resolutionJson: string;
161
184
  }
162
185
 
163
186
  interface StoredSubmission {
@@ -176,13 +199,13 @@ interface StoredSubmission {
176
199
 
177
200
  /**
178
201
  * States in which `claim` never grants the head: the lane is host-owned (`joining`/`joined`)
179
- * or durably blocked (`suspended`/`unknown`) rather than worker-claimable.
202
+ * or durably suspended rather than worker-claimable. Unknown heads are checked against abort
203
+ * intent separately: abort authorizes cleanup and settlement, never ordinary Tool replay.
180
204
  */
181
205
  const BLOCKED_HEAD_STATES: ReadonlySet<SubmissionState> = new Set([
182
206
  "joining",
183
207
  "joined",
184
208
  "suspended",
185
- "unknown",
186
209
  ]);
187
210
 
188
211
  interface LaneState {
@@ -198,12 +221,8 @@ interface StoredChildReservation {
198
221
  readonly childSubmissionId: SubmissionId | undefined;
199
222
  readonly status: ChildReservationStatus;
200
223
  readonly allocation: PersistedJson;
201
- /** Canonical JSON of the allocation, for divergent-replay detection. */
202
- readonly allocationJson: string;
203
224
  readonly allocationDigest: Digest;
204
225
  readonly accounting: PersistedJson | undefined;
205
- /** Canonical JSON of the frozen accounting decision, for divergent-freeze detection. */
206
- readonly accountingJson: string | undefined;
207
226
  readonly reservedAtMillis: number;
208
227
  readonly releaseBeganAtMillis: number | undefined;
209
228
  readonly releasedAtMillis: number | undefined;
@@ -212,7 +231,7 @@ interface StoredChildReservation {
212
231
  interface LedgerState {
213
232
  readonly submissions: ReadonlyMap<SubmissionId, StoredSubmission>;
214
233
  readonly admissionIndex: ReadonlyMap<string, SubmissionId>;
215
- readonly lanes: ReadonlyMap<ConversationId, LaneState>;
234
+ readonly lanes: ReadonlyMap<ThreadId, LaneState>;
216
235
  readonly childReservations: ReadonlyMap<ChildReservationId, StoredChildReservation>;
217
236
  readonly mintCounter: number;
218
237
  }
@@ -247,19 +266,21 @@ const decodeAttemptId = Schema.decodeSync(AttemptId);
247
266
  const decodeOwnershipToken = Schema.decodeSync(OwnershipToken);
248
267
  const decodeQueueSequence = Schema.decodeSync(QueueSequence);
249
268
  const decodeProducerEpoch = Schema.decodeSync(ProducerEpoch);
269
+ const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
270
+ const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
250
271
 
251
272
  const utc = (millis: number): DateTime.Utc => DateTime.toUtc(DateTime.makeUnsafe(millis));
252
273
 
253
274
  const admissionKey = (
254
- conversationId: ConversationId,
275
+ threadId: ThreadId,
255
276
  principal: Principal,
256
277
  idempotencyKey: IdempotencyKey,
257
- ): string => `${conversationId}\u001f${principal}\u001f${idempotencyKey}`;
278
+ ): string => JSON.stringify([threadId, principal, idempotencyKey]);
258
279
 
259
280
  const toSnapshot = (row: SubmissionRow): SubmissionSnapshot =>
260
281
  SubmissionSnapshot.make({
261
282
  submissionId: row.submissionId,
262
- conversationId: row.conversationId,
283
+ threadId: row.threadId,
263
284
  queueSequence: row.queueSequence,
264
285
  principal: row.principal,
265
286
  idempotencyKey: row.idempotencyKey,
@@ -271,6 +292,22 @@ const toSnapshot = (row: SubmissionRow): SubmissionSnapshot =>
271
292
  receiptId: row.receiptId,
272
293
  state: row.state,
273
294
  createdAt: utc(row.createdAtMillis),
295
+ ...(row.admissionGroup === undefined ? {} : { admissionGroup: row.admissionGroup }),
296
+ ...(row.admissionFence === undefined ? {} : { admissionFence: row.admissionFence }),
297
+ ...(row.workerAdmissionJson === undefined
298
+ ? {}
299
+ : {
300
+ workerAdmission: Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
301
+ row.workerAdmissionJson,
302
+ ),
303
+ }),
304
+ ...(row.messageAdmissionJson === undefined
305
+ ? {}
306
+ : {
307
+ messageAdmission: Schema.decodeSync(Schema.fromJsonString(InputMessage))(
308
+ row.messageAdmissionJson,
309
+ ),
310
+ }),
274
311
  ...(row.settledOutcome === undefined ? {} : { settledOutcome: row.settledOutcome }),
275
312
  ...(row.readyAtMillis === undefined ? {} : { readyAt: utc(row.readyAtMillis) }),
276
313
  ...(row.parentLinkage === undefined ? {} : { parentLinkage: row.parentLinkage }),
@@ -304,13 +341,13 @@ const sameParentLinkage = (
304
341
  left.parentSubmissionId === right.parentSubmissionId &&
305
342
  left.parentToolCallId === right.parentToolCallId;
306
343
 
307
- const laneEpoch = (state: LedgerState, conversationId: ConversationId): number =>
308
- state.lanes.get(conversationId)?.producerEpoch ?? 0;
344
+ const laneEpoch = (state: LedgerState, threadId: ThreadId): number =>
345
+ state.lanes.get(threadId)?.producerEpoch ?? 0;
309
346
 
310
347
  const ownershipLost = (state: LedgerState, stored: StoredSubmission): OwnershipLost =>
311
348
  OwnershipLost.make({
312
349
  submissionId: stored.row.submissionId,
313
- actualEpoch: decodeProducerEpoch(laneEpoch(state, stored.row.conversationId)),
350
+ actualEpoch: decodeProducerEpoch(laneEpoch(state, stored.row.threadId)),
314
351
  });
315
352
 
316
353
  /** The presented token owns the lane only while it matches the live ownership record. */
@@ -330,15 +367,14 @@ const withChildReservation = (
330
367
  childReservations: new Map(state.childReservations).set(reservation.reservationId, reservation),
331
368
  });
332
369
 
333
- const findHead = (
334
- state: LedgerState,
335
- conversationId: ConversationId,
336
- ): StoredSubmission | undefined => {
370
+ const findHead = (state: LedgerState, threadId: ThreadId): StoredSubmission | undefined => {
337
371
  let head: StoredSubmission | undefined;
372
+
338
373
  for (const stored of state.submissions.values()) {
339
- if (stored.row.conversationId !== conversationId || stored.row.state === "settled") continue;
374
+ if (stored.row.threadId !== threadId || stored.row.state === "settled") continue;
340
375
  if (head === undefined || stored.row.queueSequence < head.row.queueSequence) head = stored;
341
376
  }
377
+
342
378
  return head;
343
379
  };
344
380
 
@@ -385,6 +421,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
385
421
  childReservations: new Map(),
386
422
  mintCounter: 0,
387
423
  });
424
+
425
+ const admissionFence = yield* SubmissionAdmissionFence;
388
426
  const leaseMillis = Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION);
389
427
 
390
428
  const capabilities = Effect.succeed(LedgerCapabilities.make({ durability: "non-durable" }));
@@ -393,23 +431,45 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
393
431
  (unvalidated) =>
394
432
  Effect.gen(function* () {
395
433
  const request = yield* validate(AdmissionRequest, "admit", unvalidated);
434
+
435
+ const workerAdmissionJson =
436
+ request.workerAdmission === undefined
437
+ ? undefined
438
+ : yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
439
+ request.workerAdmission,
440
+ ).pipe(
441
+ Effect.mapError(() => ledgerError("admit", "Invalid worker admission metadata")),
442
+ );
443
+
444
+ const messageAdmissionJson =
445
+ request.messageAdmission === undefined
446
+ ? undefined
447
+ : yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
448
+ request.messageAdmission,
449
+ ).pipe(
450
+ Effect.mapError(() => ledgerError("admit", "Invalid message admission metadata")),
451
+ );
452
+
396
453
  const nowMillis = yield* Clock.currentTimeMillis;
454
+ const services = yield* Effect.context<never>();
455
+
397
456
  const decision = yield* Ref.modify(
398
457
  state,
399
458
  (
400
459
  current,
401
460
  ): readonly [
402
- Decision<AdmissionResult, AdmissionConflict | LedgerError>,
461
+ (
462
+ | Decision<AdmissionResult, AdmissionConflict | AdmissionPolicyError | LedgerError>
463
+ | { readonly _tag: "cause"; readonly cause: Cause.Cause<AdmissionPolicyError> }
464
+ ),
403
465
  LedgerState,
404
466
  ] => {
405
- const key = admissionKey(
406
- request.conversationId,
407
- request.principal,
408
- request.idempotencyKey,
409
- );
467
+ const key = admissionKey(request.threadId, request.principal, request.idempotencyKey);
410
468
  const existingId = current.admissionIndex.get(key);
469
+
411
470
  if (existingId !== undefined) {
412
471
  const existing = current.submissions.get(existingId);
472
+
413
473
  if (existing === undefined) {
414
474
  return [
415
475
  failure(
@@ -422,12 +482,33 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
422
482
  // (or its absence): linkage is immutable lineage (spec §12 step 5, SUB-016).
423
483
  if (
424
484
  existing.row.inputDigest !== request.inputDigest ||
425
- !sameParentLinkage(existing.row.parentLinkage, request.parentLinkage)
485
+ !sameParentLinkage(existing.row.parentLinkage, request.parentLinkage) ||
486
+ existing.row.admissionGroup !== request.admissionGroup ||
487
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission))(
488
+ existing.row.workerAdmissionJson === undefined
489
+ ? undefined
490
+ : Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
491
+ existing.row.workerAdmissionJson,
492
+ ),
493
+ request.workerAdmission,
494
+ ) ||
495
+ !Schema.toEquivalence(Schema.optional(InputMessage))(
496
+ existing.row.messageAdmissionJson === undefined
497
+ ? undefined
498
+ : Schema.decodeSync(Schema.fromJsonString(InputMessage))(
499
+ existing.row.messageAdmissionJson,
500
+ ),
501
+ request.messageAdmission,
502
+ ) ||
503
+ !Schema.toEquivalence(Schema.optional(Schema.Json))(
504
+ existing.row.admissionFence,
505
+ request.admissionFence,
506
+ )
426
507
  ) {
427
508
  return [
428
509
  failure(
429
510
  AdmissionConflict.make({
430
- conversationId: request.conversationId,
511
+ threadId: request.threadId,
431
512
  principal: request.principal,
432
513
  idempotencyKey: request.idempotencyKey,
433
514
  existingInputDigest: existing.row.inputDigest,
@@ -437,6 +518,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
437
518
  current,
438
519
  ];
439
520
  }
521
+
440
522
  return [
441
523
  success(
442
524
  AdmissionResult.make({
@@ -450,6 +532,58 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
450
532
  current,
451
533
  ];
452
534
  }
535
+
536
+ // A Thread's first admission fixes its worker origin before canonical materialization.
537
+ const first = [...current.submissions.values()].find(
538
+ ({ row }) => row.threadId === request.threadId,
539
+ );
540
+
541
+ if (first !== undefined) {
542
+ const previous =
543
+ first.row.workerAdmissionJson === undefined
544
+ ? undefined
545
+ : Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
546
+ first.row.workerAdmissionJson,
547
+ );
548
+
549
+ if (
550
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
551
+ previous?.origin,
552
+ request.workerAdmission?.origin,
553
+ )
554
+ )
555
+ return [
556
+ failure(
557
+ AdmissionPolicyError.make({
558
+ reason: "refused",
559
+ code: "worker-origin-conflict",
560
+ }),
561
+ ),
562
+ current,
563
+ ];
564
+ }
565
+ // A memory policy and the ledger mutation share one synchronous critical section.
566
+ // An asynchronous policy cannot fence this Ref and therefore fails closed.
567
+ const checked = Effect.runSyncExitWith(services)(admissionFence.check(request));
568
+
569
+ if (Exit.isFailure(checked)) {
570
+ return [{ _tag: "cause", cause: checked.cause }, current];
571
+ }
572
+ if (
573
+ request.admissionGroup !== undefined &&
574
+ [...current.submissions.values()].some(
575
+ ({ row }) =>
576
+ row.threadId === request.threadId &&
577
+ row.admissionGroup === request.admissionGroup &&
578
+ row.state !== "settled",
579
+ )
580
+ )
581
+ return [
582
+ failure(
583
+ AdmissionPolicyError.make({ reason: "occupied", code: "admission-group" }),
584
+ ),
585
+ current,
586
+ ];
453
587
  if (current.submissions.size >= MAX_SUBMISSIONS) {
454
588
  return [
455
589
  failure(
@@ -458,14 +592,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
458
592
  current,
459
593
  ];
460
594
  }
461
- const lane = current.lanes.get(request.conversationId) ?? {
595
+
596
+ const lane = current.lanes.get(request.threadId) ?? {
462
597
  nextQueueSequence: 1,
463
598
  producerEpoch: 0,
464
599
  };
600
+
465
601
  const mintCounter = current.mintCounter + 1;
602
+
466
603
  const row: SubmissionRow = {
467
604
  submissionId: decodeSubmissionId(`submission-memory-${mintCounter}`),
468
- conversationId: request.conversationId,
605
+ threadId: request.threadId,
469
606
  queueSequence: decodeQueueSequence(lane.nextQueueSequence),
470
607
  principal: request.principal,
471
608
  idempotencyKey: request.idempotencyKey,
@@ -480,7 +617,16 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
480
617
  createdAtMillis: nowMillis,
481
618
  readyAtMillis: undefined,
482
619
  parentLinkage: request.parentLinkage,
620
+ ...(workerAdmissionJson === undefined ? {} : { workerAdmissionJson }),
621
+ ...(messageAdmissionJson === undefined ? {} : { messageAdmissionJson }),
622
+ ...(request.admissionGroup === undefined
623
+ ? {}
624
+ : { admissionGroup: request.admissionGroup }),
625
+ ...(request.admissionFence === undefined
626
+ ? {}
627
+ : { admissionFence: request.admissionFence }),
483
628
  };
629
+
484
630
  const submissions = new Map(current.submissions).set(row.submissionId, {
485
631
  row,
486
632
  ownership: undefined,
@@ -493,11 +639,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
493
639
  approvalDecisions: new Map<ToolCallId, ApprovalDecisionIntent>(),
494
640
  unknownResolutions: new Map<ToolCallId, StoredUnknownResolution>(),
495
641
  });
642
+
496
643
  const admissionIndex = new Map(current.admissionIndex).set(key, row.submissionId);
497
- const lanes = new Map(current.lanes).set(request.conversationId, {
644
+
645
+ const lanes = new Map(current.lanes).set(request.threadId, {
498
646
  nextQueueSequence: lane.nextQueueSequence + 1,
499
647
  producerEpoch: lane.producerEpoch,
500
648
  });
649
+
501
650
  return [
502
651
  success(
503
652
  AdmissionResult.make({
@@ -512,9 +661,26 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
512
661
  ];
513
662
  },
514
663
  );
664
+
515
665
  if (decision._tag === "failure") return yield* decision.error;
666
+ if (decision._tag === "cause") {
667
+ for (const reason of decision.cause.reasons) {
668
+ if (Cause.isDieReason(reason) && Cause.isAsyncFiberError(reason.defect)) {
669
+ yield* Fiber.interrupt(reason.defect.fiber);
670
+
671
+ return yield* AdmissionPolicyError.make({
672
+ reason: "unavailable",
673
+ code: "synchronous-memory-policy-required",
674
+ });
675
+ }
676
+ }
677
+
678
+ return yield* Effect.failCause(decision.cause);
679
+ }
680
+
516
681
  return decision.value;
517
682
  }),
683
+ Effect.uninterruptible,
518
684
  );
519
685
 
520
686
  const markReady: SubmissionLedger["Service"]["markReady"] = Effect.fn(
@@ -523,10 +689,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
523
689
  Effect.gen(function* () {
524
690
  const request = yield* validate(MarkReadyRequest, "markReady", unvalidated);
525
691
  const nowMillis = yield* Clock.currentTimeMillis;
692
+
526
693
  const decision = yield* Ref.modify(
527
694
  state,
528
695
  (current): readonly [Decision<void, LedgerError>, LedgerState] => {
529
696
  const stored = current.submissions.get(request.submissionId);
697
+
530
698
  if (stored === undefined) {
531
699
  return [
532
700
  failure(ledgerError("markReady", `Unknown Submission ${request.submissionId}`)),
@@ -534,6 +702,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
534
702
  ];
535
703
  }
536
704
  if (stored.row.state !== "admitted") return [success(undefined), current];
705
+
537
706
  return [
538
707
  success(undefined),
539
708
  withSubmission(current, {
@@ -543,6 +712,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
543
712
  ];
544
713
  },
545
714
  );
715
+
546
716
  if (decision._tag === "failure") return yield* decision.error;
547
717
  }),
548
718
  );
@@ -553,14 +723,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
553
723
  Effect.gen(function* () {
554
724
  const request = yield* validate(SubmissionLookup, "lookup", unvalidated);
555
725
  const current = yield* Ref.get(state);
726
+
556
727
  const submissionId =
557
728
  request._tag === "SubmissionLookupById"
558
729
  ? request.submissionId
559
730
  : current.admissionIndex.get(
560
- admissionKey(request.conversationId, request.principal, request.idempotencyKey),
731
+ admissionKey(request.threadId, request.principal, request.idempotencyKey),
561
732
  );
733
+
562
734
  const stored =
563
735
  submissionId === undefined ? undefined : current.submissions.get(submissionId);
736
+
564
737
  return stored === undefined ? Option.none() : Option.some(toSnapshot(stored.row));
565
738
  }),
566
739
  );
@@ -570,20 +743,25 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
570
743
  )((unvalidated) =>
571
744
  Effect.gen(function* () {
572
745
  const request = yield* validate(SubmissionLookupByKey, "resolveAdmission", unvalidated);
746
+
573
747
  // Test-only fault seam: lets suites exercise the Indeterminate classification that a
574
748
  // single strongly consistent store never produces on its own (SUB-031, P6 honesty).
575
749
  if (options.resolveAdmissionFault !== undefined) {
576
750
  const fault = yield* options.resolveAdmissionFault;
751
+
577
752
  if (Option.isSome(fault)) {
578
753
  return AdmissionIndeterminate.make({ reason: fault.value });
579
754
  }
580
755
  }
581
756
  const current = yield* Ref.get(state);
757
+
582
758
  const submissionId = current.admissionIndex.get(
583
- admissionKey(request.conversationId, request.principal, request.idempotencyKey),
759
+ admissionKey(request.threadId, request.principal, request.idempotencyKey),
584
760
  );
761
+
585
762
  const stored =
586
763
  submissionId === undefined ? undefined : current.submissions.get(submissionId);
764
+
587
765
  return stored === undefined
588
766
  ? AdmissionNotAdmitted.make()
589
767
  : AdmissionAdmitted.make({ submission: toSnapshot(stored.row) });
@@ -595,14 +773,18 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
595
773
  Effect.gen(function* () {
596
774
  const request = yield* validate(ClaimRequest, "claim", unvalidated);
597
775
  const nowMillis = yield* Clock.currentTimeMillis;
776
+
598
777
  const decision = yield* Ref.modify(
599
778
  state,
600
779
  (current): readonly [Decision<Option.Option<Claim>, LedgerError>, LedgerState] => {
601
- const head = findHead(current, request.conversationId);
780
+ const head = findHead(current, request.threadId);
781
+
602
782
  if (head === undefined) return [success(Option.none()), current];
603
- // A joining/joined head is host-owned and a suspended/unknown head is durably
604
- // blocked; the lane produces no claim and later ready work is never skipped.
605
- if (BLOCKED_HEAD_STATES.has(head.row.state)) return [success(Option.none()), current];
783
+ if (
784
+ BLOCKED_HEAD_STATES.has(head.row.state) ||
785
+ (head.row.state === "unknown" && head.abortIntent === undefined)
786
+ )
787
+ return [success(Option.none()), current];
606
788
  if (
607
789
  head.ownership !== undefined &&
608
790
  head.ownership.leaseExpiresAtMillis > nowMillis &&
@@ -610,15 +792,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
610
792
  ) {
611
793
  return [success(Option.none()), current];
612
794
  }
613
- const lane = current.lanes.get(request.conversationId);
795
+ const lane = current.lanes.get(request.threadId);
796
+
614
797
  if (lane === undefined) {
615
798
  return [
616
- failure(ledgerError("claim", "Claimable head without a Conversation lane")),
799
+ failure(ledgerError("claim", "Claimable head without a Thread lane")),
617
800
  current,
618
801
  ];
619
802
  }
620
803
  const producerEpoch = decodeProducerEpoch(lane.producerEpoch + 1);
621
804
  const mintCounter = current.mintCounter + 1;
805
+
622
806
  const ownership: StoredOwnership = {
623
807
  attemptId: decodeAttemptId(`attempt-memory-${mintCounter}`),
624
808
  ownershipToken: decodeOwnershipToken(`ownership-memory-${mintCounter}`),
@@ -626,13 +810,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
626
810
  ownerProducerId: request.producerId,
627
811
  leaseExpiresAtMillis: nowMillis + leaseMillis,
628
812
  };
813
+
629
814
  const row: SubmissionRow =
630
815
  head.row.state === "ready" ? { ...head.row, state: "running" } : head.row;
816
+
631
817
  const next = withSubmission(current, { ...head, row, ownership });
632
- const lanes = new Map(next.lanes).set(request.conversationId, {
818
+
819
+ const lanes = new Map(next.lanes).set(request.threadId, {
633
820
  nextQueueSequence: lane.nextQueueSequence,
634
821
  producerEpoch: lane.producerEpoch + 1,
635
822
  });
823
+
636
824
  return [
637
825
  success(
638
826
  Option.some(
@@ -650,7 +838,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
650
838
  ];
651
839
  },
652
840
  );
841
+
653
842
  if (decision._tag === "failure") return yield* decision.error;
843
+
654
844
  return decision.value;
655
845
  }),
656
846
  );
@@ -661,12 +851,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
661
851
  Effect.gen(function* () {
662
852
  const request = yield* validate(RenewOwnershipRequest, "renewOwnership", unvalidated);
663
853
  const nowMillis = yield* Clock.currentTimeMillis;
854
+
664
855
  const decision = yield* Ref.modify(
665
856
  state,
666
857
  (
667
858
  current,
668
859
  ): readonly [Decision<OwnershipRenewal, OwnershipLost | LedgerError>, LedgerState] => {
669
860
  const stored = current.submissions.get(request.submissionId);
861
+
670
862
  if (stored === undefined) {
671
863
  return [
672
864
  failure(
@@ -678,10 +870,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
678
870
  if (stored.ownership === undefined || !ownsLane(stored, request.ownershipToken)) {
679
871
  return [failure(ownershipLost(current, stored)), current];
680
872
  }
873
+
681
874
  const ownership: StoredOwnership = {
682
875
  ...stored.ownership,
683
876
  leaseExpiresAtMillis: nowMillis + leaseMillis,
684
877
  };
878
+
685
879
  return [
686
880
  success(
687
881
  OwnershipRenewal.make({
@@ -693,7 +887,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
693
887
  ];
694
888
  },
695
889
  );
890
+
696
891
  if (decision._tag === "failure") return yield* decision.error;
892
+
697
893
  return decision.value;
698
894
  }),
699
895
  );
@@ -703,10 +899,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
703
899
  )((unvalidated) =>
704
900
  Effect.gen(function* () {
705
901
  const request = yield* validate(ReleaseOwnershipRequest, "releaseOwnership", unvalidated);
902
+
706
903
  const decision = yield* Ref.modify(
707
904
  state,
708
905
  (current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
709
906
  const stored = current.submissions.get(request.submissionId);
907
+
710
908
  if (stored === undefined) {
711
909
  return [
712
910
  failure(
@@ -718,12 +916,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
718
916
  if (!ownsLane(stored, request.ownershipToken)) {
719
917
  return [failure(ownershipLost(current, stored)), current];
720
918
  }
919
+
721
920
  return [
722
921
  success(undefined),
723
922
  withSubmission(current, { ...stored, ownership: undefined }),
724
923
  ];
725
924
  },
726
925
  );
926
+
727
927
  if (decision._tag === "failure") return yield* decision.error;
728
928
  }),
729
929
  );
@@ -733,10 +933,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
733
933
  )((unvalidated) =>
734
934
  Effect.gen(function* () {
735
935
  const request = yield* validate(MarkInputAppliedRequest, "markInputApplied", unvalidated);
936
+
736
937
  const decision = yield* Ref.modify(
737
938
  state,
738
939
  (current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
739
940
  const stored = current.submissions.get(request.submissionId);
941
+
740
942
  if (stored === undefined) {
741
943
  return [
742
944
  failure(
@@ -748,20 +950,43 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
748
950
  if (!ownsLane(stored, request.ownershipToken)) {
749
951
  return [failure(ownershipLost(current, stored)), current];
750
952
  }
953
+
954
+ if (stored.inputApplied !== undefined) {
955
+ if (
956
+ stored.inputApplied.recordId === request.recordId &&
957
+ stored.inputApplied.sequence === request.sequence
958
+ ) {
959
+ return [success(undefined), current];
960
+ }
961
+
962
+ return [
963
+ failure(
964
+ ledgerError(
965
+ "markInputApplied",
966
+ `A different canonical input marker is already recorded for Submission ${request.submissionId}`,
967
+ ),
968
+ ),
969
+ current,
970
+ ];
971
+ }
972
+
751
973
  const marker = InputAppliedMarker.make({
752
974
  recordId: request.recordId,
753
975
  sequence: request.sequence,
754
976
  });
977
+
755
978
  const row: SubmissionRow =
756
979
  STATE_RANK[stored.row.state] < STATE_RANK["input-applied"]
757
980
  ? { ...stored.row, state: "input-applied" }
758
981
  : stored.row;
982
+
759
983
  return [
760
984
  success(undefined),
761
985
  withSubmission(current, { ...stored, row, inputApplied: marker }),
762
986
  ];
763
987
  },
764
988
  );
989
+
765
990
  if (decision._tag === "failure") return yield* decision.error;
766
991
  }),
767
992
  );
@@ -771,6 +996,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
771
996
  )((unvalidated) =>
772
997
  Effect.gen(function* () {
773
998
  const request = yield* validate(SettlementReservation, "reserveSettlement", unvalidated);
999
+
774
1000
  const decision = yield* Ref.modify(
775
1001
  state,
776
1002
  (
@@ -780,6 +1006,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
780
1006
  LedgerState,
781
1007
  ] => {
782
1008
  const stored = current.submissions.get(request.submissionId);
1009
+
783
1010
  if (stored === undefined) {
784
1011
  return [
785
1012
  failure(
@@ -788,11 +1015,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
788
1015
  current,
789
1016
  ];
790
1017
  }
1018
+
791
1019
  // A `joined` Submission settles WITH its host (plan §2.5) and its lane is never
792
1020
  // worker-claimable, so no ownership token can exist for it: the recorded host
793
1021
  // linkage authorizes the reservation and the presented token is not consulted.
794
1022
  const joinedSettlement =
795
1023
  stored.row.state === "joined" && stored.joinedHostSubmissionId !== undefined;
1024
+
796
1025
  // P7 §7(c): an aborted, never-claimed, still-queued Submission likewise has no
797
1026
  // live ownership to fence against — its durable abort intent authorizes exactly
798
1027
  // its ABORTED settlement (`terminalizing` is the same pass's crash replay). Every
@@ -802,6 +1031,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
802
1031
  stored.abortIntent !== undefined &&
803
1032
  stored.ownership === undefined &&
804
1033
  (stored.row.state === "ready" || stored.row.state === "terminalizing");
1034
+
805
1035
  if (
806
1036
  !joinedSettlement &&
807
1037
  !queuedAbortSettlement &&
@@ -810,6 +1040,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
810
1040
  return [failure(ownershipLost(current, stored)), current];
811
1041
  }
812
1042
  const existing = stored.reservation;
1043
+
813
1044
  if (existing !== undefined) {
814
1045
  if (
815
1046
  existing.settlementId !== request.settlementId ||
@@ -826,6 +1057,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
826
1057
  current,
827
1058
  ];
828
1059
  }
1060
+
829
1061
  return [
830
1062
  success(
831
1063
  ReservedSettlement.make({
@@ -840,6 +1072,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
840
1072
  current,
841
1073
  ];
842
1074
  }
1075
+
843
1076
  const reservation: StoredReservation = {
844
1077
  settlementId: request.settlementId,
845
1078
  outcome: request.outcome,
@@ -847,10 +1080,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
847
1080
  recordDigest: request.recordDigest,
848
1081
  finalizedAtMillis: undefined,
849
1082
  };
1083
+
850
1084
  const row: SubmissionRow =
851
1085
  STATE_RANK[stored.row.state] < STATE_RANK.terminalizing
852
1086
  ? { ...stored.row, state: "terminalizing" }
853
1087
  : stored.row;
1088
+
854
1089
  return [
855
1090
  success(
856
1091
  ReservedSettlement.make({
@@ -866,7 +1101,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
866
1101
  ];
867
1102
  },
868
1103
  );
1104
+
869
1105
  if (decision._tag === "failure") return yield* decision.error;
1106
+
870
1107
  return decision.value;
871
1108
  }),
872
1109
  );
@@ -877,12 +1114,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
877
1114
  Effect.gen(function* () {
878
1115
  const request = yield* validate(SettlementFinalization, "finalizeSettlement", unvalidated);
879
1116
  const nowMillis = yield* Clock.currentTimeMillis;
1117
+
880
1118
  const decision = yield* Ref.modify(
881
1119
  state,
882
1120
  (
883
1121
  current,
884
1122
  ): readonly [Decision<Settlement, SettlementConflict | LedgerError>, LedgerState] => {
885
1123
  const stored = current.submissions.get(request.submissionId);
1124
+
886
1125
  if (stored === undefined) {
887
1126
  return [
888
1127
  failure(
@@ -892,6 +1131,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
892
1131
  ];
893
1132
  }
894
1133
  const reservation = stored.reservation;
1134
+
895
1135
  if (reservation === undefined) {
896
1136
  return [
897
1137
  failure(
@@ -903,6 +1143,19 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
903
1143
  current,
904
1144
  ];
905
1145
  }
1146
+ const settlementFailure = settlementFailureFromRecord(reservation.record);
1147
+
1148
+ if ((reservation.outcome === "failed") !== (settlementFailure !== undefined)) {
1149
+ return [
1150
+ failure(
1151
+ ledgerError(
1152
+ "finalizeSettlement",
1153
+ `Settlement reservation for Submission ${request.submissionId} has contradictory failure evidence`,
1154
+ ),
1155
+ ),
1156
+ current,
1157
+ ];
1158
+ }
906
1159
  if (reservation.settlementId !== request.settlementId) {
907
1160
  return [
908
1161
  failure(
@@ -922,18 +1175,21 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
922
1175
  settlementId: reservation.settlementId,
923
1176
  receiptId: stored.row.receiptId,
924
1177
  outcome: reservation.outcome,
1178
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
925
1179
  settledAt: utc(reservation.finalizedAtMillis),
926
1180
  }),
927
1181
  ),
928
1182
  current,
929
1183
  ];
930
1184
  }
1185
+
931
1186
  const next = withSubmission(current, {
932
1187
  ...stored,
933
1188
  row: { ...stored.row, state: "settled", settledOutcome: reservation.outcome },
934
1189
  ownership: undefined,
935
1190
  reservation: { ...reservation, finalizedAtMillis: nowMillis },
936
1191
  });
1192
+
937
1193
  return [
938
1194
  success(
939
1195
  Settlement.make({
@@ -941,6 +1197,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
941
1197
  settlementId: reservation.settlementId,
942
1198
  receiptId: stored.row.receiptId,
943
1199
  outcome: reservation.outcome,
1200
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
944
1201
  settledAt: utc(nowMillis),
945
1202
  }),
946
1203
  ),
@@ -948,7 +1205,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
948
1205
  ];
949
1206
  },
950
1207
  );
1208
+
951
1209
  if (decision._tag === "failure") return yield* decision.error;
1210
+
952
1211
  return decision.value;
953
1212
  }),
954
1213
  );
@@ -959,6 +1218,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
959
1218
  Effect.gen(function* () {
960
1219
  const request = yield* validate(AbortCommand, "requestAbort", unvalidated);
961
1220
  const nowMillis = yield* Clock.currentTimeMillis;
1221
+
962
1222
  const decision = yield* Ref.modify(
963
1223
  state,
964
1224
  (
@@ -968,6 +1228,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
968
1228
  LedgerState,
969
1229
  ] => {
970
1230
  const stored = current.submissions.get(request.submissionId);
1231
+
971
1232
  if (stored === undefined) {
972
1233
  return [
973
1234
  failure(ledgerError("requestAbort", `Unknown Submission ${request.submissionId}`)),
@@ -989,6 +1250,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
989
1250
  current,
990
1251
  ];
991
1252
  }
1253
+
992
1254
  return [
993
1255
  failure(
994
1256
  JoinedToHost.make({
@@ -1011,6 +1273,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1011
1273
  current,
1012
1274
  ];
1013
1275
  }
1276
+
1014
1277
  return [
1015
1278
  failure(
1016
1279
  SettlementConflict.make({
@@ -1022,16 +1285,20 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1022
1285
  ];
1023
1286
  }
1024
1287
  if (stored.abortIntent !== undefined) return [success(stored.abortIntent), current];
1288
+
1025
1289
  const intent = AbortIntent.make({
1026
1290
  submissionId: request.submissionId,
1027
1291
  author: request.author,
1028
1292
  reason: request.reason,
1029
1293
  requestedAt: utc(nowMillis),
1030
1294
  });
1295
+
1031
1296
  return [success(intent), withSubmission(current, { ...stored, abortIntent: intent })];
1032
1297
  },
1033
1298
  );
1299
+
1034
1300
  if (decision._tag === "failure") return yield* decision.error;
1301
+
1035
1302
  return decision.value;
1036
1303
  }),
1037
1304
  );
@@ -1041,6 +1308,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1041
1308
  )((unvalidated) =>
1042
1309
  Effect.gen(function* () {
1043
1310
  const request = yield* validate(ClaimJoiningRequest, "claimJoining", unvalidated);
1311
+
1044
1312
  const decision = yield* Ref.modify(
1045
1313
  state,
1046
1314
  (
@@ -1050,6 +1318,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1050
1318
  LedgerState,
1051
1319
  ] => {
1052
1320
  const host = current.submissions.get(request.hostSubmissionId);
1321
+
1053
1322
  if (host === undefined) {
1054
1323
  return [
1055
1324
  failure(
@@ -1058,12 +1327,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1058
1327
  current,
1059
1328
  ];
1060
1329
  }
1061
- if (host.row.conversationId !== request.conversationId) {
1330
+ if (host.row.threadId !== request.threadId) {
1062
1331
  return [
1063
1332
  failure(
1064
1333
  ledgerError(
1065
1334
  "claimJoining",
1066
- `Host Submission ${request.hostSubmissionId} does not belong to Conversation ${request.conversationId}`,
1335
+ `Host Submission ${request.hostSubmissionId} does not belong to Thread ${request.threadId}`,
1067
1336
  ),
1068
1337
  ),
1069
1338
  current,
@@ -1072,15 +1341,18 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1072
1341
  if (!ownsLane(host, request.ownershipToken)) {
1073
1342
  return [failure(ownershipLost(current, host)), current];
1074
1343
  }
1344
+
1075
1345
  const later = [...current.submissions.values()]
1076
1346
  .filter(
1077
1347
  (stored) =>
1078
- stored.row.conversationId === request.conversationId &&
1348
+ stored.row.threadId === request.threadId &&
1079
1349
  stored.row.queueSequence > host.row.queueSequence,
1080
1350
  )
1081
1351
  .sort((left, right) => left.row.queueSequence - right.row.queueSequence);
1352
+
1082
1353
  const claims: Array<JoiningClaim> = [];
1083
1354
  const submissions = new Map(current.submissions);
1355
+
1084
1356
  for (const stored of later) {
1085
1357
  if (claims.length >= request.maxCount) break;
1086
1358
  // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
@@ -1113,10 +1385,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1113
1385
  }),
1114
1386
  );
1115
1387
  }
1388
+
1116
1389
  return [success(claims), { ...current, submissions }];
1117
1390
  },
1118
1391
  );
1392
+
1119
1393
  if (decision._tag === "failure") return yield* decision.error;
1394
+
1120
1395
  return decision.value;
1121
1396
  }),
1122
1397
  );
@@ -1126,10 +1401,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1126
1401
  )((unvalidated) =>
1127
1402
  Effect.gen(function* () {
1128
1403
  const request = yield* validate(MarkJoinedRequest, "markJoined", unvalidated);
1404
+
1129
1405
  const decision = yield* Ref.modify(
1130
1406
  state,
1131
1407
  (current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
1132
1408
  const stored = current.submissions.get(request.submissionId);
1409
+
1133
1410
  if (stored === undefined) {
1134
1411
  return [
1135
1412
  failure(ledgerError("markJoined", `Unknown Submission ${request.submissionId}`)),
@@ -1148,6 +1425,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1148
1425
  ];
1149
1426
  }
1150
1427
  const host = current.submissions.get(stored.joinedHostSubmissionId);
1428
+
1151
1429
  if (host === undefined) {
1152
1430
  return [
1153
1431
  failure(
@@ -1171,6 +1449,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1171
1449
  ) {
1172
1450
  return [success(undefined), current];
1173
1451
  }
1452
+
1174
1453
  return [
1175
1454
  failure(
1176
1455
  ledgerError(
@@ -1192,10 +1471,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1192
1471
  current,
1193
1472
  ];
1194
1473
  }
1474
+
1195
1475
  const marker = InputAppliedMarker.make({
1196
1476
  recordId: request.recordId,
1197
1477
  sequence: request.sequence,
1198
1478
  });
1479
+
1199
1480
  return [
1200
1481
  success(undefined),
1201
1482
  withSubmission(current, {
@@ -1206,6 +1487,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1206
1487
  ];
1207
1488
  },
1208
1489
  );
1490
+
1209
1491
  if (decision._tag === "failure") return yield* decision.error;
1210
1492
  }),
1211
1493
  );
@@ -1215,10 +1497,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1215
1497
  )((unvalidated) =>
1216
1498
  Effect.gen(function* () {
1217
1499
  const request = yield* validate(RevertJoiningRequest, "revertJoining", unvalidated);
1500
+
1218
1501
  const decision = yield* Ref.modify(
1219
1502
  state,
1220
1503
  (current): readonly [Decision<void, LedgerError>, LedgerState] => {
1221
1504
  const stored = current.submissions.get(request.submissionId);
1505
+
1222
1506
  if (stored === undefined) {
1223
1507
  return [
1224
1508
  failure(ledgerError("revertJoining", `Unknown Submission ${request.submissionId}`)),
@@ -1228,6 +1512,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1228
1512
  // Idempotent and recovery-only: only a still-`joining` Submission reverts; an
1229
1513
  // already-joined (or already-reverted) Submission is a no-op (DUR-016).
1230
1514
  if (stored.row.state !== "joining") return [success(undefined), current];
1515
+
1231
1516
  return [
1232
1517
  success(undefined),
1233
1518
  withSubmission(current, {
@@ -1238,6 +1523,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1238
1523
  ];
1239
1524
  },
1240
1525
  );
1526
+
1241
1527
  if (decision._tag === "failure") return yield* decision.error;
1242
1528
  }),
1243
1529
  );
@@ -1248,6 +1534,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1248
1534
  Effect.gen(function* () {
1249
1535
  const request = yield* validate(SuspendRequest, "suspend", unvalidated);
1250
1536
  const nowMillis = yield* Clock.currentTimeMillis;
1537
+
1251
1538
  const decision = yield* Ref.modify(
1252
1539
  state,
1253
1540
  (
@@ -1257,6 +1544,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1257
1544
  LedgerState,
1258
1545
  ] => {
1259
1546
  const stored = current.submissions.get(request.submissionId);
1547
+
1260
1548
  if (stored === undefined) {
1261
1549
  return [
1262
1550
  failure(ledgerError("suspend", `Unknown Submission ${request.submissionId}`)),
@@ -1275,6 +1563,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1275
1563
  current,
1276
1564
  ];
1277
1565
  }
1566
+
1278
1567
  return [
1279
1568
  failure(
1280
1569
  SettlementConflict.make({
@@ -1301,6 +1590,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1301
1590
  if (!ownsLane(stored, request.ownershipToken)) {
1302
1591
  return [failure(ownershipLost(current, stored)), current];
1303
1592
  }
1593
+
1304
1594
  // A covering event that raced ahead of the suspend transaction (an approval decision,
1305
1595
  // or a child settlement observed directly from the child's row in this single store)
1306
1596
  // resumes the caller immediately WITHOUT releasing the lane (plan §2.6, spec §12).
@@ -1313,9 +1603,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1313
1603
  (child) =>
1314
1604
  current.submissions.get(child.childSubmissionId)?.row.state === "settled",
1315
1605
  );
1606
+
1316
1607
  if (alreadyCovered) {
1317
1608
  return [success("resume-immediately" as const), current];
1318
1609
  }
1610
+
1319
1611
  return [
1320
1612
  success("suspended" as const),
1321
1613
  withSubmission(current, {
@@ -1327,7 +1619,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1327
1619
  ];
1328
1620
  },
1329
1621
  );
1622
+
1330
1623
  if (decision._tag === "failure") return yield* decision.error;
1624
+
1331
1625
  return decision.value;
1332
1626
  }),
1333
1627
  );
@@ -1341,7 +1635,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1341
1635
  "recordApprovalDecision",
1342
1636
  unvalidated,
1343
1637
  );
1638
+
1344
1639
  const nowMillis = yield* Clock.currentTimeMillis;
1640
+
1345
1641
  const decision = yield* Ref.modify(
1346
1642
  state,
1347
1643
  (
@@ -1351,6 +1647,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1351
1647
  LedgerState,
1352
1648
  ] => {
1353
1649
  const stored = current.submissions.get(command.submissionId);
1650
+
1354
1651
  if (stored === undefined) {
1355
1652
  return [
1356
1653
  failure(
@@ -1374,6 +1671,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1374
1671
  current,
1375
1672
  ];
1376
1673
  }
1674
+
1377
1675
  return [
1378
1676
  failure(
1379
1677
  SettlementConflict.make({
@@ -1385,6 +1683,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1385
1683
  ];
1386
1684
  }
1387
1685
  const existing = stored.approvalDecisions.get(command.toolCallId);
1686
+
1388
1687
  if (existing !== undefined) {
1389
1688
  if (existing.decision !== command.decision) {
1390
1689
  return [
@@ -1398,8 +1697,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1398
1697
  current,
1399
1698
  ];
1400
1699
  }
1700
+
1401
1701
  return [success(existing), current];
1402
1702
  }
1703
+
1403
1704
  const intent = ApprovalDecisionIntent.make({
1404
1705
  submissionId: command.submissionId,
1405
1706
  toolCallId: command.toolCallId,
@@ -1408,10 +1709,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1408
1709
  reason: command.reason,
1409
1710
  decidedAt: utc(nowMillis),
1410
1711
  });
1712
+
1411
1713
  const approvalDecisions = new Map(stored.approvalDecisions).set(
1412
1714
  command.toolCallId,
1413
1715
  intent,
1414
1716
  );
1717
+
1415
1718
  // Once every pending call of an ApprovalPending suspension is decided, the lane
1416
1719
  // wakes: suspended → input-applied (plan §2.6). A WaitingForChild suspension wakes
1417
1720
  // only through recordChildSettled.
@@ -1422,6 +1725,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1422
1725
  stored.suspension.reason.toolCallIds.every((toolCallId) =>
1423
1726
  approvalDecisions.has(toolCallId),
1424
1727
  );
1728
+
1425
1729
  return [
1426
1730
  success(intent),
1427
1731
  withSubmission(current, {
@@ -1433,7 +1737,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1433
1737
  ];
1434
1738
  },
1435
1739
  );
1740
+
1436
1741
  if (decision._tag === "failure") return yield* decision.error;
1742
+
1437
1743
  return decision.value;
1438
1744
  }),
1439
1745
  );
@@ -1443,10 +1749,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1443
1749
  )((unvalidated) =>
1444
1750
  Effect.gen(function* () {
1445
1751
  const request = yield* validate(MarkUnknownRequest, "markUnknown", unvalidated);
1752
+
1446
1753
  const decision = yield* Ref.modify(
1447
1754
  state,
1448
1755
  (current): readonly [Decision<void, SettlementConflict | LedgerError>, LedgerState] => {
1449
1756
  const stored = current.submissions.get(request.submissionId);
1757
+
1450
1758
  if (stored === undefined) {
1451
1759
  return [
1452
1760
  failure(ledgerError("markUnknown", `Unknown Submission ${request.submissionId}`)),
@@ -1465,6 +1773,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1465
1773
  current,
1466
1774
  ];
1467
1775
  }
1776
+
1468
1777
  return [
1469
1778
  failure(
1470
1779
  SettlementConflict.make({
@@ -1492,10 +1801,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1492
1801
  // set while the first recorded reason is kept.
1493
1802
  const existing = stored.unknownMark;
1494
1803
  const known = new Set(existing?.toolCallIds ?? []);
1804
+
1495
1805
  const merged = [
1496
1806
  ...(existing?.toolCallIds ?? []),
1497
1807
  ...request.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
1498
1808
  ];
1809
+
1499
1810
  return [
1500
1811
  success(undefined),
1501
1812
  withSubmission(current, {
@@ -1507,6 +1818,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1507
1818
  ];
1508
1819
  },
1509
1820
  );
1821
+
1510
1822
  if (decision._tag === "failure") return yield* decision.error;
1511
1823
  }),
1512
1824
  );
@@ -1519,15 +1831,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1519
1831
  "recordUnknownResolution",
1520
1832
  unvalidated,
1521
1833
  );
1834
+
1522
1835
  const nowMillis = yield* Clock.currentTimeMillis;
1523
- const encodedResolution = yield* Schema.encodeUnknownEffect(UnknownResolution)(
1524
- command.resolution,
1525
- ).pipe(
1526
- Effect.mapError((error) =>
1527
- ledgerError("recordUnknownResolution", "Invalid unknown-outcome resolution", error),
1528
- ),
1529
- );
1530
- const resolutionJson = JSON.stringify(encodedResolution);
1836
+
1531
1837
  const decision = yield* Ref.modify(
1532
1838
  state,
1533
1839
  (
@@ -1540,6 +1846,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1540
1846
  LedgerState,
1541
1847
  ] => {
1542
1848
  const stored = current.submissions.get(command.submissionId);
1849
+
1543
1850
  if (stored === undefined) {
1544
1851
  return [
1545
1852
  failure(
@@ -1563,6 +1870,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1563
1870
  current,
1564
1871
  ];
1565
1872
  }
1873
+
1566
1874
  return [
1567
1875
  failure(
1568
1876
  SettlementConflict.make({
@@ -1574,7 +1882,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1574
1882
  ];
1575
1883
  }
1576
1884
  const existing = stored.unknownResolutions.get(command.toolCallId);
1577
- if (existing !== undefined && existing.resolutionJson !== resolutionJson) {
1885
+
1886
+ if (
1887
+ existing !== undefined &&
1888
+ !equivalentUnknownResolution(existing.intent.resolution, command.resolution)
1889
+ ) {
1578
1890
  return [
1579
1891
  failure(
1580
1892
  UnknownResolutionConflict.make({
@@ -1585,6 +1897,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1585
1897
  current,
1586
1898
  ];
1587
1899
  }
1900
+
1588
1901
  const intent =
1589
1902
  existing?.intent ??
1590
1903
  UnknownResolutionIntent.make({
@@ -1595,13 +1908,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1595
1908
  resolution: command.resolution,
1596
1909
  resolvedAt: utc(nowMillis),
1597
1910
  });
1911
+
1598
1912
  const unknownResolutions =
1599
1913
  existing !== undefined
1600
1914
  ? stored.unknownResolutions
1601
1915
  : new Map(stored.unknownResolutions).set(command.toolCallId, {
1602
1916
  intent,
1603
- resolutionJson,
1604
1917
  });
1918
+
1605
1919
  // The lane reopens only when EVERY marked open call has a durable resolution
1606
1920
  // intent: unknown → input-applied (DUR-017). Replays re-run the coverage check so
1607
1921
  // a recovering caller can wake the lane idempotently.
@@ -1611,6 +1925,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1611
1925
  stored.unknownMark.toolCallIds.every((toolCallId) =>
1612
1926
  unknownResolutions.has(toolCallId),
1613
1927
  );
1928
+
1614
1929
  return [
1615
1930
  success(intent),
1616
1931
  withSubmission(current, {
@@ -1622,7 +1937,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1622
1937
  ];
1623
1938
  },
1624
1939
  );
1940
+
1625
1941
  if (decision._tag === "failure") return yield* decision.error;
1942
+
1626
1943
  return decision.value;
1627
1944
  }),
1628
1945
  );
@@ -1636,10 +1953,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1636
1953
  "recordChildSettled",
1637
1954
  unvalidated,
1638
1955
  );
1956
+
1639
1957
  const decision = yield* Ref.modify(
1640
1958
  state,
1641
1959
  (current): readonly [Decision<ChildSettledOutcome, LedgerError>, LedgerState] => {
1642
1960
  const parent = current.submissions.get(request.parentSubmissionId);
1961
+
1643
1962
  if (parent === undefined) {
1644
1963
  return [
1645
1964
  failure(
@@ -1651,10 +1970,18 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1651
1970
  current,
1652
1971
  ];
1653
1972
  }
1654
- // The child's canonical Settlement is the authority for this wake; a notification
1655
- // for an unsettled (or unknown) child is a caller error in a single-store adapter.
1973
+ // The caller may notify after the canonical Settlement append but before ledger
1974
+ // finalization. In a single store, an exact reservation plus `terminalizing` is the
1975
+ // narrow durable prefix that makes that ordering admissible; earlier states remain
1976
+ // a caller error.
1656
1977
  const child = current.submissions.get(request.childSubmissionId);
1657
- if (child === undefined || child.row.state !== "settled") {
1978
+
1979
+ const announced =
1980
+ child !== undefined &&
1981
+ (child.row.state === "settled" ||
1982
+ (child.row.state === "terminalizing" && child.reservation !== undefined));
1983
+
1984
+ if (!announced) {
1658
1985
  return [
1659
1986
  failure(
1660
1987
  ledgerError(
@@ -1673,16 +2000,24 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1673
2000
  return [success("not-waiting" as const), current];
1674
2001
  }
1675
2002
  const children = parent.suspension.reason.children;
2003
+
1676
2004
  if (!children.some((entry) => entry.childSubmissionId === request.childSubmissionId)) {
1677
2005
  return [success("not-waiting" as const), current];
1678
2006
  }
1679
- // The parent wakes exactly when EVERY listed child is settled (spec §12 step 10);
1680
- // replays re-run the coverage check so a recovering caller wakes the lane
1681
- // idempotently.
1682
- const allSettled = children.every(
1683
- (entry) => current.submissions.get(entry.childSubmissionId)?.row.state === "settled",
1684
- );
2007
+
2008
+ // Every listed child must be either finalized or canonically announced from the
2009
+ // exact terminalizing reservation. Replays re-run this coverage check idempotently.
2010
+ const allSettled = children.every((entry) => {
2011
+ const listed = current.submissions.get(entry.childSubmissionId);
2012
+
2013
+ return (
2014
+ listed?.row.state === "settled" ||
2015
+ (listed?.row.state === "terminalizing" && listed.reservation !== undefined)
2016
+ );
2017
+ });
2018
+
1685
2019
  if (!allSettled) return [success("still-waiting" as const), current];
2020
+
1686
2021
  return [
1687
2022
  success("woken" as const),
1688
2023
  withSubmission(current, {
@@ -1693,7 +2028,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1693
2028
  ];
1694
2029
  },
1695
2030
  );
2031
+
1696
2032
  if (decision._tag === "failure") return yield* decision.error;
2033
+
1697
2034
  return decision.value;
1698
2035
  }),
1699
2036
  );
@@ -1707,8 +2044,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1707
2044
  "reserveChildBudget",
1708
2045
  unvalidated,
1709
2046
  );
2047
+
1710
2048
  const nowMillis = yield* Clock.currentTimeMillis;
1711
- const allocationJson = JSON.stringify(request.allocation);
2049
+
1712
2050
  const decision = yield* Ref.modify(
1713
2051
  state,
1714
2052
  (
@@ -1718,6 +2056,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1718
2056
  LedgerState,
1719
2057
  ] => {
1720
2058
  const existing = current.childReservations.get(request.reservationId);
2059
+
1721
2060
  if (existing !== undefined) {
1722
2061
  // Identical replays short-circuit before the fence, mirroring reserveSettlement:
1723
2062
  // a replay creates nothing, so a recovering caller resumes rather than duplicates.
@@ -1725,7 +2064,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1725
2064
  existing.parentSubmissionId === request.parentSubmissionId &&
1726
2065
  existing.parentToolCallId === request.parentToolCallId &&
1727
2066
  existing.allocationDigest === request.allocationDigest &&
1728
- existing.allocationJson === allocationJson;
2067
+ equivalentPersistedJson(existing.allocation, request.allocation);
2068
+
1729
2069
  if (!identical) {
1730
2070
  return [
1731
2071
  failure(
@@ -1739,6 +2079,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1739
2079
  current,
1740
2080
  ];
1741
2081
  }
2082
+
1742
2083
  return [
1743
2084
  success(
1744
2085
  ReservedChildBudget.make({
@@ -1767,6 +2108,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1767
2108
  }
1768
2109
  }
1769
2110
  const parent = current.submissions.get(request.parentSubmissionId);
2111
+
1770
2112
  if (parent === undefined) {
1771
2113
  return [
1772
2114
  failure(
@@ -1783,6 +2125,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1783
2125
  if (!ownsLane(parent, request.ownershipToken)) {
1784
2126
  return [failure(ownershipLost(current, parent)), current];
1785
2127
  }
2128
+
1786
2129
  const reservation: StoredChildReservation = {
1787
2130
  reservationId: request.reservationId,
1788
2131
  parentSubmissionId: request.parentSubmissionId,
@@ -1790,14 +2133,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1790
2133
  childSubmissionId: undefined,
1791
2134
  status: "reserved",
1792
2135
  allocation: request.allocation,
1793
- allocationJson,
1794
2136
  allocationDigest: request.allocationDigest,
1795
2137
  accounting: undefined,
1796
- accountingJson: undefined,
1797
2138
  reservedAtMillis: nowMillis,
1798
2139
  releaseBeganAtMillis: undefined,
1799
2140
  releasedAtMillis: undefined,
1800
2141
  };
2142
+
1801
2143
  return [
1802
2144
  success(
1803
2145
  ReservedChildBudget.make({
@@ -1809,7 +2151,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1809
2151
  ];
1810
2152
  },
1811
2153
  );
2154
+
1812
2155
  if (decision._tag === "failure") return yield* decision.error;
2156
+
1813
2157
  return decision.value;
1814
2158
  }),
1815
2159
  );
@@ -1822,6 +2166,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1822
2166
  "attachChildToReservation",
1823
2167
  unvalidated,
1824
2168
  );
2169
+
1825
2170
  const decision = yield* Ref.modify(
1826
2171
  state,
1827
2172
  (
@@ -1834,6 +2179,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1834
2179
  LedgerState,
1835
2180
  ] => {
1836
2181
  const reservation = current.childReservations.get(request.reservationId);
2182
+
1837
2183
  if (reservation === undefined) {
1838
2184
  return [
1839
2185
  failure(
@@ -1850,6 +2196,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1850
2196
  if (reservation.childSubmissionId === request.childSubmissionId) {
1851
2197
  return [success(toReservationSnapshot(reservation)), current];
1852
2198
  }
2199
+
1853
2200
  return [
1854
2201
  failure(
1855
2202
  ChildReservationConflict.make({
@@ -1862,6 +2209,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1862
2209
  ];
1863
2210
  }
1864
2211
  const parent = current.submissions.get(reservation.parentSubmissionId);
2212
+
1865
2213
  if (parent === undefined) {
1866
2214
  return [
1867
2215
  failure(
@@ -1901,17 +2249,21 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1901
2249
  current,
1902
2250
  ];
1903
2251
  }
2252
+
1904
2253
  const attached: StoredChildReservation = {
1905
2254
  ...reservation,
1906
2255
  childSubmissionId: request.childSubmissionId,
1907
2256
  };
2257
+
1908
2258
  return [
1909
2259
  success(toReservationSnapshot(attached)),
1910
2260
  withChildReservation(current, attached),
1911
2261
  ];
1912
2262
  },
1913
2263
  );
2264
+
1914
2265
  if (decision._tag === "failure") return yield* decision.error;
2266
+
1915
2267
  return decision.value;
1916
2268
  }),
1917
2269
  );
@@ -1924,8 +2276,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1924
2276
  "beginChildBudgetRelease",
1925
2277
  unvalidated,
1926
2278
  );
2279
+
1927
2280
  const nowMillis = yield* Clock.currentTimeMillis;
1928
- const accountingJson = JSON.stringify(request.accounting);
2281
+
1929
2282
  const decision = yield* Ref.modify(
1930
2283
  state,
1931
2284
  (
@@ -1935,6 +2288,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1935
2288
  LedgerState,
1936
2289
  ] => {
1937
2290
  const reservation = current.childReservations.get(request.reservationId);
2291
+
1938
2292
  if (reservation === undefined) {
1939
2293
  return [
1940
2294
  failure(
@@ -1949,9 +2303,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1949
2303
  if (reservation.status !== "reserved") {
1950
2304
  // The accounting decision was already frozen exactly once; an identical replay is
1951
2305
  // a no-op and a divergent decision conflicts (spec §12 join step 6).
1952
- if (reservation.accountingJson === accountingJson) {
2306
+ if (
2307
+ reservation.accounting !== undefined &&
2308
+ equivalentPersistedJson(reservation.accounting, request.accounting)
2309
+ ) {
1953
2310
  return [success(toReservationSnapshot(reservation)), current];
1954
2311
  }
2312
+
1955
2313
  return [
1956
2314
  failure(
1957
2315
  ChildReservationConflict.make({
@@ -1964,20 +2322,23 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1964
2322
  current,
1965
2323
  ];
1966
2324
  }
2325
+
1967
2326
  const frozen: StoredChildReservation = {
1968
2327
  ...reservation,
1969
2328
  status: "releasePending",
1970
2329
  accounting: request.accounting,
1971
- accountingJson,
1972
2330
  releaseBeganAtMillis: nowMillis,
1973
2331
  };
2332
+
1974
2333
  return [
1975
2334
  success(toReservationSnapshot(frozen)),
1976
2335
  withChildReservation(current, frozen),
1977
2336
  ];
1978
2337
  },
1979
2338
  );
2339
+
1980
2340
  if (decision._tag === "failure") return yield* decision.error;
2341
+
1981
2342
  return decision.value;
1982
2343
  }),
1983
2344
  );
@@ -1991,7 +2352,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1991
2352
  "releaseChildBudget",
1992
2353
  unvalidated,
1993
2354
  );
2355
+
1994
2356
  const nowMillis = yield* Clock.currentTimeMillis;
2357
+
1995
2358
  const decision = yield* Ref.modify(
1996
2359
  state,
1997
2360
  (
@@ -2001,6 +2364,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2001
2364
  LedgerState,
2002
2365
  ] => {
2003
2366
  const reservation = current.childReservations.get(request.reservationId);
2367
+
2004
2368
  if (reservation === undefined) {
2005
2369
  return [
2006
2370
  failure(
@@ -2030,18 +2394,22 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2030
2394
  current,
2031
2395
  ];
2032
2396
  }
2397
+
2033
2398
  const released: StoredChildReservation = {
2034
2399
  ...reservation,
2035
2400
  status: "released",
2036
2401
  releasedAtMillis: nowMillis,
2037
2402
  };
2403
+
2038
2404
  return [
2039
2405
  success(toReservationSnapshot(released)),
2040
2406
  withChildReservation(current, released),
2041
2407
  ];
2042
2408
  },
2043
2409
  );
2410
+
2044
2411
  if (decision._tag === "failure") return yield* decision.error;
2412
+
2045
2413
  return decision.value;
2046
2414
  }),
2047
2415
  );
@@ -2052,18 +2420,32 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2052
2420
  const snapshots = [...current.submissions.values()]
2053
2421
  .filter((stored) => stored.row.state !== "settled")
2054
2422
  .sort((left, right) =>
2055
- left.row.conversationId < right.row.conversationId
2423
+ left.row.threadId < right.row.threadId
2056
2424
  ? -1
2057
- : left.row.conversationId > right.row.conversationId
2425
+ : left.row.threadId > right.row.threadId
2058
2426
  ? 1
2059
2427
  : left.row.queueSequence - right.row.queueSequence,
2060
2428
  )
2061
2429
  .map((stored) => toSnapshot(stored.row));
2430
+
2062
2431
  return Stream.fromIterable(snapshots);
2063
2432
  }),
2064
2433
  ),
2065
2434
  );
2066
2435
 
2436
+ const readAbortIntent: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
2437
+ "MemorySubmissionLedger.readAbortIntent",
2438
+ )(function* (unvalidated) {
2439
+ const request = yield* validate(AbortIntentRequest, "readAbortIntent", unvalidated);
2440
+ const stored = (yield* Ref.get(state)).submissions.get(request.submissionId);
2441
+
2442
+ if (stored === undefined) {
2443
+ return yield* ledgerError("readAbortIntent", `Unknown Submission ${request.submissionId}`);
2444
+ }
2445
+
2446
+ return stored.abortIntent;
2447
+ });
2448
+
2067
2449
  const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
2068
2450
  "MemorySubmissionLedger.loadRecoverySnapshot",
2069
2451
  )((unvalidated) =>
@@ -2073,14 +2455,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2073
2455
  "loadRecoverySnapshot",
2074
2456
  unvalidated,
2075
2457
  );
2458
+
2076
2459
  const current = yield* Ref.get(state);
2077
2460
  const stored = current.submissions.get(request.submissionId);
2461
+
2078
2462
  if (stored === undefined) {
2079
2463
  return yield* ledgerError(
2080
2464
  "loadRecoverySnapshot",
2081
2465
  `Unknown Submission ${request.submissionId}`,
2082
2466
  );
2083
2467
  }
2468
+
2084
2469
  const joins = [...current.submissions.values()]
2085
2470
  .filter((candidate) => candidate.joinedHostSubmissionId === request.submissionId)
2086
2471
  .sort((left, right) => left.row.queueSequence - right.row.queueSequence)
@@ -2091,11 +2476,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2091
2476
  hostSubmissionId: request.submissionId,
2092
2477
  }),
2093
2478
  );
2479
+
2094
2480
  const byToolCallId = <A extends { readonly toolCallId: ToolCallId }>(
2095
2481
  left: A,
2096
2482
  right: A,
2097
2483
  ): number =>
2098
2484
  left.toolCallId < right.toolCallId ? -1 : left.toolCallId > right.toolCallId ? 1 : 0;
2485
+
2099
2486
  // Parent-side subagent view: this Submission's child budget reservations in parent Tool
2100
2487
  // Call order, plus each attached child's current lane state (a disposable derived view;
2101
2488
  // the canonical records stay the recovery truth, DUR-015).
@@ -2108,10 +2495,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2108
2495
  ? 1
2109
2496
  : 0,
2110
2497
  );
2498
+
2111
2499
  const childAttachments: Array<ChildAttachmentSnapshot> = [];
2500
+
2112
2501
  for (const reservation of childReservations) {
2113
2502
  if (reservation.childSubmissionId === undefined) continue;
2114
2503
  const child = current.submissions.get(reservation.childSubmissionId);
2504
+
2115
2505
  if (child === undefined) continue;
2116
2506
  childAttachments.push(
2117
2507
  ChildAttachmentSnapshot.make({
@@ -2124,6 +2514,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2124
2514
  }),
2125
2515
  );
2126
2516
  }
2517
+
2127
2518
  return RecoverySnapshot.make({
2128
2519
  submission: toSnapshot(stored.row),
2129
2520
  joins,
@@ -2201,6 +2592,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2201
2592
  releaseChildBudget,
2202
2593
  scanNonterminal,
2203
2594
  loadRecoverySnapshot,
2595
+ readAbortIntent,
2204
2596
  });
2205
2597
  });
2206
2598