@effect-agent/storage-memory 0.1.0-beta.11 → 0.1.0-beta.110

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 +167 -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 +457 -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 +327 -0
  24. package/src/MemoryScheduleStore.ts +328 -0
  25. package/src/MemorySemanticIndex.ts +357 -0
  26. package/src/{memory-ledger.ts → MemorySubmissionLedger.ts} +511 -107
  27. package/src/MemorySubscriptionStore.ts +1549 -0
  28. package/src/MemoryThreadStore.ts +916 -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,18 +341,24 @@ 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
- /** The presented token owns the lane only while it matches the live ownership record. */
317
- const ownsLane = (stored: StoredSubmission, ownershipToken: OwnershipToken): boolean =>
318
- stored.ownership !== undefined && stored.ownership.ownershipToken === ownershipToken;
353
+ /** A retained row token cannot outlive a newer owner of the same Thread. */
354
+ const ownsLane = (
355
+ state: LedgerState,
356
+ stored: StoredSubmission,
357
+ ownershipToken: OwnershipToken,
358
+ ): boolean =>
359
+ stored.ownership !== undefined &&
360
+ stored.ownership.ownershipToken === ownershipToken &&
361
+ stored.ownership.producerEpoch === laneEpoch(state, stored.row.threadId);
319
362
 
320
363
  const withSubmission = (state: LedgerState, stored: StoredSubmission): LedgerState => ({
321
364
  ...state,
@@ -330,21 +373,25 @@ const withChildReservation = (
330
373
  childReservations: new Map(state.childReservations).set(reservation.reservationId, reservation),
331
374
  });
332
375
 
333
- const findHead = (
334
- state: LedgerState,
335
- conversationId: ConversationId,
336
- ): StoredSubmission | undefined => {
376
+ const findHead = (state: LedgerState, threadId: ThreadId): StoredSubmission | undefined => {
337
377
  let head: StoredSubmission | undefined;
378
+
338
379
  for (const stored of state.submissions.values()) {
339
- if (stored.row.conversationId !== conversationId || stored.row.state === "settled") continue;
380
+ if (
381
+ stored.row.threadId !== threadId ||
382
+ stored.row.state === "settled" ||
383
+ (stored.row.state === "unknown" && stored.abortIntent === undefined)
384
+ )
385
+ continue;
340
386
  if (head === undefined || stored.row.queueSequence < head.row.queueSequence) head = stored;
341
387
  }
388
+
342
389
  return head;
343
390
  };
344
391
 
345
392
  /**
346
393
  * Reference in-memory SubmissionLedger. It implements the full port contract — atomic idempotent
347
- * admission, FIFO-head claims, producer-epoch fencing, Clock-driven ownership leases, idempotent
394
+ * admission, eligible FIFO claims, producer-epoch fencing, Clock-driven ownership leases, idempotent
348
395
  * settlement reservation/finalization, and durable abort intent — with every transition applied
349
396
  * as one atomic `Ref.modify`, but its state does not survive the process (`non-durable`).
350
397
  *
@@ -354,8 +401,8 @@ const findHead = (
354
401
  * deterministically; no wall clock is consulted.
355
402
  * - The ownership lease is pinned to `DEFAULT_OWNERSHIP_LEASE_DURATION` (D5); durable adapters
356
403
  * own the configuration seam.
357
- * - A live lease blocks claims from other producers only: the same `producerId` may reclaim its
358
- * own live lease (restart recovery), which supersedes and fences the prior Attempt's token.
404
+ * - Any live lease in the Thread blocks every new claim, including the same `producerId`.
405
+ * Unresolved unknown work is skipped only after ownership is released or expires.
359
406
  * - Claiming advances `ready` to `running` and otherwise preserves the recorded state, so
360
407
  * progress markers from an earlier Attempt survive a reclaim.
361
408
  * - `renewOwnership` keeps the token stable (the port allows rotation); a replayed admission
@@ -385,6 +432,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
385
432
  childReservations: new Map(),
386
433
  mintCounter: 0,
387
434
  });
435
+
436
+ const admissionFence = yield* SubmissionAdmissionFence;
388
437
  const leaseMillis = Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION);
389
438
 
390
439
  const capabilities = Effect.succeed(LedgerCapabilities.make({ durability: "non-durable" }));
@@ -393,23 +442,45 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
393
442
  (unvalidated) =>
394
443
  Effect.gen(function* () {
395
444
  const request = yield* validate(AdmissionRequest, "admit", unvalidated);
445
+
446
+ const workerAdmissionJson =
447
+ request.workerAdmission === undefined
448
+ ? undefined
449
+ : yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
450
+ request.workerAdmission,
451
+ ).pipe(
452
+ Effect.mapError(() => ledgerError("admit", "Invalid worker admission metadata")),
453
+ );
454
+
455
+ const messageAdmissionJson =
456
+ request.messageAdmission === undefined
457
+ ? undefined
458
+ : yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
459
+ request.messageAdmission,
460
+ ).pipe(
461
+ Effect.mapError(() => ledgerError("admit", "Invalid message admission metadata")),
462
+ );
463
+
396
464
  const nowMillis = yield* Clock.currentTimeMillis;
465
+ const services = yield* Effect.context<never>();
466
+
397
467
  const decision = yield* Ref.modify(
398
468
  state,
399
469
  (
400
470
  current,
401
471
  ): readonly [
402
- Decision<AdmissionResult, AdmissionConflict | LedgerError>,
472
+ (
473
+ | Decision<AdmissionResult, AdmissionConflict | AdmissionPolicyError | LedgerError>
474
+ | { readonly _tag: "cause"; readonly cause: Cause.Cause<AdmissionPolicyError> }
475
+ ),
403
476
  LedgerState,
404
477
  ] => {
405
- const key = admissionKey(
406
- request.conversationId,
407
- request.principal,
408
- request.idempotencyKey,
409
- );
478
+ const key = admissionKey(request.threadId, request.principal, request.idempotencyKey);
410
479
  const existingId = current.admissionIndex.get(key);
480
+
411
481
  if (existingId !== undefined) {
412
482
  const existing = current.submissions.get(existingId);
483
+
413
484
  if (existing === undefined) {
414
485
  return [
415
486
  failure(
@@ -422,12 +493,33 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
422
493
  // (or its absence): linkage is immutable lineage (spec §12 step 5, SUB-016).
423
494
  if (
424
495
  existing.row.inputDigest !== request.inputDigest ||
425
- !sameParentLinkage(existing.row.parentLinkage, request.parentLinkage)
496
+ !sameParentLinkage(existing.row.parentLinkage, request.parentLinkage) ||
497
+ existing.row.admissionGroup !== request.admissionGroup ||
498
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission))(
499
+ existing.row.workerAdmissionJson === undefined
500
+ ? undefined
501
+ : Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
502
+ existing.row.workerAdmissionJson,
503
+ ),
504
+ request.workerAdmission,
505
+ ) ||
506
+ !Schema.toEquivalence(Schema.optional(InputMessage))(
507
+ existing.row.messageAdmissionJson === undefined
508
+ ? undefined
509
+ : Schema.decodeSync(Schema.fromJsonString(InputMessage))(
510
+ existing.row.messageAdmissionJson,
511
+ ),
512
+ request.messageAdmission,
513
+ ) ||
514
+ !Schema.toEquivalence(Schema.optional(Schema.Json))(
515
+ existing.row.admissionFence,
516
+ request.admissionFence,
517
+ )
426
518
  ) {
427
519
  return [
428
520
  failure(
429
521
  AdmissionConflict.make({
430
- conversationId: request.conversationId,
522
+ threadId: request.threadId,
431
523
  principal: request.principal,
432
524
  idempotencyKey: request.idempotencyKey,
433
525
  existingInputDigest: existing.row.inputDigest,
@@ -437,6 +529,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
437
529
  current,
438
530
  ];
439
531
  }
532
+
440
533
  return [
441
534
  success(
442
535
  AdmissionResult.make({
@@ -450,6 +543,58 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
450
543
  current,
451
544
  ];
452
545
  }
546
+
547
+ // A Thread's first admission fixes its worker origin before canonical materialization.
548
+ const first = [...current.submissions.values()].find(
549
+ ({ row }) => row.threadId === request.threadId,
550
+ );
551
+
552
+ if (first !== undefined) {
553
+ const previous =
554
+ first.row.workerAdmissionJson === undefined
555
+ ? undefined
556
+ : Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
557
+ first.row.workerAdmissionJson,
558
+ );
559
+
560
+ if (
561
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
562
+ previous?.origin,
563
+ request.workerAdmission?.origin,
564
+ )
565
+ )
566
+ return [
567
+ failure(
568
+ AdmissionPolicyError.make({
569
+ reason: "refused",
570
+ code: "worker-origin-conflict",
571
+ }),
572
+ ),
573
+ current,
574
+ ];
575
+ }
576
+ // A memory policy and the ledger mutation share one synchronous critical section.
577
+ // An asynchronous policy cannot fence this Ref and therefore fails closed.
578
+ const checked = Effect.runSyncExitWith(services)(admissionFence.check(request));
579
+
580
+ if (Exit.isFailure(checked)) {
581
+ return [{ _tag: "cause", cause: checked.cause }, current];
582
+ }
583
+ if (
584
+ request.admissionGroup !== undefined &&
585
+ [...current.submissions.values()].some(
586
+ ({ row }) =>
587
+ row.threadId === request.threadId &&
588
+ row.admissionGroup === request.admissionGroup &&
589
+ row.state !== "settled",
590
+ )
591
+ )
592
+ return [
593
+ failure(
594
+ AdmissionPolicyError.make({ reason: "occupied", code: "admission-group" }),
595
+ ),
596
+ current,
597
+ ];
453
598
  if (current.submissions.size >= MAX_SUBMISSIONS) {
454
599
  return [
455
600
  failure(
@@ -458,14 +603,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
458
603
  current,
459
604
  ];
460
605
  }
461
- const lane = current.lanes.get(request.conversationId) ?? {
606
+
607
+ const lane = current.lanes.get(request.threadId) ?? {
462
608
  nextQueueSequence: 1,
463
609
  producerEpoch: 0,
464
610
  };
611
+
465
612
  const mintCounter = current.mintCounter + 1;
613
+
466
614
  const row: SubmissionRow = {
467
615
  submissionId: decodeSubmissionId(`submission-memory-${mintCounter}`),
468
- conversationId: request.conversationId,
616
+ threadId: request.threadId,
469
617
  queueSequence: decodeQueueSequence(lane.nextQueueSequence),
470
618
  principal: request.principal,
471
619
  idempotencyKey: request.idempotencyKey,
@@ -480,7 +628,16 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
480
628
  createdAtMillis: nowMillis,
481
629
  readyAtMillis: undefined,
482
630
  parentLinkage: request.parentLinkage,
631
+ ...(workerAdmissionJson === undefined ? {} : { workerAdmissionJson }),
632
+ ...(messageAdmissionJson === undefined ? {} : { messageAdmissionJson }),
633
+ ...(request.admissionGroup === undefined
634
+ ? {}
635
+ : { admissionGroup: request.admissionGroup }),
636
+ ...(request.admissionFence === undefined
637
+ ? {}
638
+ : { admissionFence: request.admissionFence }),
483
639
  };
640
+
484
641
  const submissions = new Map(current.submissions).set(row.submissionId, {
485
642
  row,
486
643
  ownership: undefined,
@@ -493,11 +650,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
493
650
  approvalDecisions: new Map<ToolCallId, ApprovalDecisionIntent>(),
494
651
  unknownResolutions: new Map<ToolCallId, StoredUnknownResolution>(),
495
652
  });
653
+
496
654
  const admissionIndex = new Map(current.admissionIndex).set(key, row.submissionId);
497
- const lanes = new Map(current.lanes).set(request.conversationId, {
655
+
656
+ const lanes = new Map(current.lanes).set(request.threadId, {
498
657
  nextQueueSequence: lane.nextQueueSequence + 1,
499
658
  producerEpoch: lane.producerEpoch,
500
659
  });
660
+
501
661
  return [
502
662
  success(
503
663
  AdmissionResult.make({
@@ -512,9 +672,26 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
512
672
  ];
513
673
  },
514
674
  );
675
+
515
676
  if (decision._tag === "failure") return yield* decision.error;
677
+ if (decision._tag === "cause") {
678
+ for (const reason of decision.cause.reasons) {
679
+ if (Cause.isDieReason(reason) && Cause.isAsyncFiberError(reason.defect)) {
680
+ yield* Fiber.interrupt(reason.defect.fiber);
681
+
682
+ return yield* AdmissionPolicyError.make({
683
+ reason: "unavailable",
684
+ code: "synchronous-memory-policy-required",
685
+ });
686
+ }
687
+ }
688
+
689
+ return yield* Effect.failCause(decision.cause);
690
+ }
691
+
516
692
  return decision.value;
517
693
  }),
694
+ Effect.uninterruptible,
518
695
  );
519
696
 
520
697
  const markReady: SubmissionLedger["Service"]["markReady"] = Effect.fn(
@@ -523,10 +700,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
523
700
  Effect.gen(function* () {
524
701
  const request = yield* validate(MarkReadyRequest, "markReady", unvalidated);
525
702
  const nowMillis = yield* Clock.currentTimeMillis;
703
+
526
704
  const decision = yield* Ref.modify(
527
705
  state,
528
706
  (current): readonly [Decision<void, LedgerError>, LedgerState] => {
529
707
  const stored = current.submissions.get(request.submissionId);
708
+
530
709
  if (stored === undefined) {
531
710
  return [
532
711
  failure(ledgerError("markReady", `Unknown Submission ${request.submissionId}`)),
@@ -534,6 +713,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
534
713
  ];
535
714
  }
536
715
  if (stored.row.state !== "admitted") return [success(undefined), current];
716
+
537
717
  return [
538
718
  success(undefined),
539
719
  withSubmission(current, {
@@ -543,6 +723,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
543
723
  ];
544
724
  },
545
725
  );
726
+
546
727
  if (decision._tag === "failure") return yield* decision.error;
547
728
  }),
548
729
  );
@@ -553,14 +734,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
553
734
  Effect.gen(function* () {
554
735
  const request = yield* validate(SubmissionLookup, "lookup", unvalidated);
555
736
  const current = yield* Ref.get(state);
737
+
556
738
  const submissionId =
557
739
  request._tag === "SubmissionLookupById"
558
740
  ? request.submissionId
559
741
  : current.admissionIndex.get(
560
- admissionKey(request.conversationId, request.principal, request.idempotencyKey),
742
+ admissionKey(request.threadId, request.principal, request.idempotencyKey),
561
743
  );
744
+
562
745
  const stored =
563
746
  submissionId === undefined ? undefined : current.submissions.get(submissionId);
747
+
564
748
  return stored === undefined ? Option.none() : Option.some(toSnapshot(stored.row));
565
749
  }),
566
750
  );
@@ -570,20 +754,25 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
570
754
  )((unvalidated) =>
571
755
  Effect.gen(function* () {
572
756
  const request = yield* validate(SubmissionLookupByKey, "resolveAdmission", unvalidated);
757
+
573
758
  // Test-only fault seam: lets suites exercise the Indeterminate classification that a
574
759
  // single strongly consistent store never produces on its own (SUB-031, P6 honesty).
575
760
  if (options.resolveAdmissionFault !== undefined) {
576
761
  const fault = yield* options.resolveAdmissionFault;
762
+
577
763
  if (Option.isSome(fault)) {
578
764
  return AdmissionIndeterminate.make({ reason: fault.value });
579
765
  }
580
766
  }
581
767
  const current = yield* Ref.get(state);
768
+
582
769
  const submissionId = current.admissionIndex.get(
583
- admissionKey(request.conversationId, request.principal, request.idempotencyKey),
770
+ admissionKey(request.threadId, request.principal, request.idempotencyKey),
584
771
  );
772
+
585
773
  const stored =
586
774
  submissionId === undefined ? undefined : current.submissions.get(submissionId);
775
+
587
776
  return stored === undefined
588
777
  ? AdmissionNotAdmitted.make()
589
778
  : AdmissionAdmitted.make({ submission: toSnapshot(stored.row) });
@@ -595,30 +784,34 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
595
784
  Effect.gen(function* () {
596
785
  const request = yield* validate(ClaimRequest, "claim", unvalidated);
597
786
  const nowMillis = yield* Clock.currentTimeMillis;
787
+
598
788
  const decision = yield* Ref.modify(
599
789
  state,
600
790
  (current): readonly [Decision<Option.Option<Claim>, LedgerError>, LedgerState] => {
601
- const head = findHead(current, request.conversationId);
791
+ for (const stored of current.submissions.values()) {
792
+ if (
793
+ stored.row.threadId === request.threadId &&
794
+ stored.ownership !== undefined &&
795
+ stored.ownership.leaseExpiresAtMillis > nowMillis
796
+ ) {
797
+ return [success(Option.none()), current];
798
+ }
799
+ }
800
+ const head = findHead(current, request.threadId);
801
+
602
802
  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
803
  if (BLOCKED_HEAD_STATES.has(head.row.state)) return [success(Option.none()), current];
606
- if (
607
- head.ownership !== undefined &&
608
- head.ownership.leaseExpiresAtMillis > nowMillis &&
609
- head.ownership.ownerProducerId !== request.producerId
610
- ) {
611
- return [success(Option.none()), current];
612
- }
613
- const lane = current.lanes.get(request.conversationId);
804
+ const lane = current.lanes.get(request.threadId);
805
+
614
806
  if (lane === undefined) {
615
807
  return [
616
- failure(ledgerError("claim", "Claimable head without a Conversation lane")),
808
+ failure(ledgerError("claim", "Claimable head without a Thread lane")),
617
809
  current,
618
810
  ];
619
811
  }
620
812
  const producerEpoch = decodeProducerEpoch(lane.producerEpoch + 1);
621
813
  const mintCounter = current.mintCounter + 1;
814
+
622
815
  const ownership: StoredOwnership = {
623
816
  attemptId: decodeAttemptId(`attempt-memory-${mintCounter}`),
624
817
  ownershipToken: decodeOwnershipToken(`ownership-memory-${mintCounter}`),
@@ -626,13 +819,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
626
819
  ownerProducerId: request.producerId,
627
820
  leaseExpiresAtMillis: nowMillis + leaseMillis,
628
821
  };
822
+
629
823
  const row: SubmissionRow =
630
824
  head.row.state === "ready" ? { ...head.row, state: "running" } : head.row;
825
+
631
826
  const next = withSubmission(current, { ...head, row, ownership });
632
- const lanes = new Map(next.lanes).set(request.conversationId, {
827
+
828
+ const lanes = new Map(next.lanes).set(request.threadId, {
633
829
  nextQueueSequence: lane.nextQueueSequence,
634
830
  producerEpoch: lane.producerEpoch + 1,
635
831
  });
832
+
636
833
  return [
637
834
  success(
638
835
  Option.some(
@@ -650,7 +847,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
650
847
  ];
651
848
  },
652
849
  );
850
+
653
851
  if (decision._tag === "failure") return yield* decision.error;
852
+
654
853
  return decision.value;
655
854
  }),
656
855
  );
@@ -661,12 +860,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
661
860
  Effect.gen(function* () {
662
861
  const request = yield* validate(RenewOwnershipRequest, "renewOwnership", unvalidated);
663
862
  const nowMillis = yield* Clock.currentTimeMillis;
863
+
664
864
  const decision = yield* Ref.modify(
665
865
  state,
666
866
  (
667
867
  current,
668
868
  ): readonly [Decision<OwnershipRenewal, OwnershipLost | LedgerError>, LedgerState] => {
669
869
  const stored = current.submissions.get(request.submissionId);
870
+
670
871
  if (stored === undefined) {
671
872
  return [
672
873
  failure(
@@ -675,13 +876,18 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
675
876
  current,
676
877
  ];
677
878
  }
678
- if (stored.ownership === undefined || !ownsLane(stored, request.ownershipToken)) {
879
+ if (
880
+ stored.ownership === undefined ||
881
+ !ownsLane(current, stored, request.ownershipToken)
882
+ ) {
679
883
  return [failure(ownershipLost(current, stored)), current];
680
884
  }
885
+
681
886
  const ownership: StoredOwnership = {
682
887
  ...stored.ownership,
683
888
  leaseExpiresAtMillis: nowMillis + leaseMillis,
684
889
  };
890
+
685
891
  return [
686
892
  success(
687
893
  OwnershipRenewal.make({
@@ -693,7 +899,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
693
899
  ];
694
900
  },
695
901
  );
902
+
696
903
  if (decision._tag === "failure") return yield* decision.error;
904
+
697
905
  return decision.value;
698
906
  }),
699
907
  );
@@ -703,10 +911,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
703
911
  )((unvalidated) =>
704
912
  Effect.gen(function* () {
705
913
  const request = yield* validate(ReleaseOwnershipRequest, "releaseOwnership", unvalidated);
914
+
706
915
  const decision = yield* Ref.modify(
707
916
  state,
708
917
  (current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
709
918
  const stored = current.submissions.get(request.submissionId);
919
+
710
920
  if (stored === undefined) {
711
921
  return [
712
922
  failure(
@@ -715,15 +925,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
715
925
  current,
716
926
  ];
717
927
  }
718
- if (!ownsLane(stored, request.ownershipToken)) {
928
+ if (!ownsLane(current, stored, request.ownershipToken)) {
719
929
  return [failure(ownershipLost(current, stored)), current];
720
930
  }
931
+
721
932
  return [
722
933
  success(undefined),
723
934
  withSubmission(current, { ...stored, ownership: undefined }),
724
935
  ];
725
936
  },
726
937
  );
938
+
727
939
  if (decision._tag === "failure") return yield* decision.error;
728
940
  }),
729
941
  );
@@ -733,10 +945,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
733
945
  )((unvalidated) =>
734
946
  Effect.gen(function* () {
735
947
  const request = yield* validate(MarkInputAppliedRequest, "markInputApplied", unvalidated);
948
+
736
949
  const decision = yield* Ref.modify(
737
950
  state,
738
951
  (current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
739
952
  const stored = current.submissions.get(request.submissionId);
953
+
740
954
  if (stored === undefined) {
741
955
  return [
742
956
  failure(
@@ -745,23 +959,46 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
745
959
  current,
746
960
  ];
747
961
  }
748
- if (!ownsLane(stored, request.ownershipToken)) {
962
+ if (!ownsLane(current, stored, request.ownershipToken)) {
749
963
  return [failure(ownershipLost(current, stored)), current];
750
964
  }
965
+
966
+ if (stored.inputApplied !== undefined) {
967
+ if (
968
+ stored.inputApplied.recordId === request.recordId &&
969
+ stored.inputApplied.sequence === request.sequence
970
+ ) {
971
+ return [success(undefined), current];
972
+ }
973
+
974
+ return [
975
+ failure(
976
+ ledgerError(
977
+ "markInputApplied",
978
+ `A different canonical input marker is already recorded for Submission ${request.submissionId}`,
979
+ ),
980
+ ),
981
+ current,
982
+ ];
983
+ }
984
+
751
985
  const marker = InputAppliedMarker.make({
752
986
  recordId: request.recordId,
753
987
  sequence: request.sequence,
754
988
  });
989
+
755
990
  const row: SubmissionRow =
756
991
  STATE_RANK[stored.row.state] < STATE_RANK["input-applied"]
757
992
  ? { ...stored.row, state: "input-applied" }
758
993
  : stored.row;
994
+
759
995
  return [
760
996
  success(undefined),
761
997
  withSubmission(current, { ...stored, row, inputApplied: marker }),
762
998
  ];
763
999
  },
764
1000
  );
1001
+
765
1002
  if (decision._tag === "failure") return yield* decision.error;
766
1003
  }),
767
1004
  );
@@ -771,6 +1008,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
771
1008
  )((unvalidated) =>
772
1009
  Effect.gen(function* () {
773
1010
  const request = yield* validate(SettlementReservation, "reserveSettlement", unvalidated);
1011
+
774
1012
  const decision = yield* Ref.modify(
775
1013
  state,
776
1014
  (
@@ -780,6 +1018,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
780
1018
  LedgerState,
781
1019
  ] => {
782
1020
  const stored = current.submissions.get(request.submissionId);
1021
+
783
1022
  if (stored === undefined) {
784
1023
  return [
785
1024
  failure(
@@ -788,11 +1027,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
788
1027
  current,
789
1028
  ];
790
1029
  }
1030
+
791
1031
  // A `joined` Submission settles WITH its host (plan §2.5) and its lane is never
792
1032
  // worker-claimable, so no ownership token can exist for it: the recorded host
793
1033
  // linkage authorizes the reservation and the presented token is not consulted.
794
1034
  const joinedSettlement =
795
1035
  stored.row.state === "joined" && stored.joinedHostSubmissionId !== undefined;
1036
+
796
1037
  // P7 §7(c): an aborted, never-claimed, still-queued Submission likewise has no
797
1038
  // live ownership to fence against — its durable abort intent authorizes exactly
798
1039
  // its ABORTED settlement (`terminalizing` is the same pass's crash replay). Every
@@ -802,14 +1043,16 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
802
1043
  stored.abortIntent !== undefined &&
803
1044
  stored.ownership === undefined &&
804
1045
  (stored.row.state === "ready" || stored.row.state === "terminalizing");
1046
+
805
1047
  if (
806
1048
  !joinedSettlement &&
807
1049
  !queuedAbortSettlement &&
808
- !ownsLane(stored, request.ownershipToken)
1050
+ !ownsLane(current, stored, request.ownershipToken)
809
1051
  ) {
810
1052
  return [failure(ownershipLost(current, stored)), current];
811
1053
  }
812
1054
  const existing = stored.reservation;
1055
+
813
1056
  if (existing !== undefined) {
814
1057
  if (
815
1058
  existing.settlementId !== request.settlementId ||
@@ -826,6 +1069,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
826
1069
  current,
827
1070
  ];
828
1071
  }
1072
+
829
1073
  return [
830
1074
  success(
831
1075
  ReservedSettlement.make({
@@ -840,6 +1084,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
840
1084
  current,
841
1085
  ];
842
1086
  }
1087
+
843
1088
  const reservation: StoredReservation = {
844
1089
  settlementId: request.settlementId,
845
1090
  outcome: request.outcome,
@@ -847,10 +1092,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
847
1092
  recordDigest: request.recordDigest,
848
1093
  finalizedAtMillis: undefined,
849
1094
  };
1095
+
850
1096
  const row: SubmissionRow =
851
1097
  STATE_RANK[stored.row.state] < STATE_RANK.terminalizing
852
1098
  ? { ...stored.row, state: "terminalizing" }
853
1099
  : stored.row;
1100
+
854
1101
  return [
855
1102
  success(
856
1103
  ReservedSettlement.make({
@@ -866,7 +1113,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
866
1113
  ];
867
1114
  },
868
1115
  );
1116
+
869
1117
  if (decision._tag === "failure") return yield* decision.error;
1118
+
870
1119
  return decision.value;
871
1120
  }),
872
1121
  );
@@ -877,12 +1126,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
877
1126
  Effect.gen(function* () {
878
1127
  const request = yield* validate(SettlementFinalization, "finalizeSettlement", unvalidated);
879
1128
  const nowMillis = yield* Clock.currentTimeMillis;
1129
+
880
1130
  const decision = yield* Ref.modify(
881
1131
  state,
882
1132
  (
883
1133
  current,
884
1134
  ): readonly [Decision<Settlement, SettlementConflict | LedgerError>, LedgerState] => {
885
1135
  const stored = current.submissions.get(request.submissionId);
1136
+
886
1137
  if (stored === undefined) {
887
1138
  return [
888
1139
  failure(
@@ -892,6 +1143,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
892
1143
  ];
893
1144
  }
894
1145
  const reservation = stored.reservation;
1146
+
895
1147
  if (reservation === undefined) {
896
1148
  return [
897
1149
  failure(
@@ -903,6 +1155,19 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
903
1155
  current,
904
1156
  ];
905
1157
  }
1158
+ const settlementFailure = settlementFailureFromRecord(reservation.record);
1159
+
1160
+ if ((reservation.outcome === "failed") !== (settlementFailure !== undefined)) {
1161
+ return [
1162
+ failure(
1163
+ ledgerError(
1164
+ "finalizeSettlement",
1165
+ `Settlement reservation for Submission ${request.submissionId} has contradictory failure evidence`,
1166
+ ),
1167
+ ),
1168
+ current,
1169
+ ];
1170
+ }
906
1171
  if (reservation.settlementId !== request.settlementId) {
907
1172
  return [
908
1173
  failure(
@@ -922,18 +1187,21 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
922
1187
  settlementId: reservation.settlementId,
923
1188
  receiptId: stored.row.receiptId,
924
1189
  outcome: reservation.outcome,
1190
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
925
1191
  settledAt: utc(reservation.finalizedAtMillis),
926
1192
  }),
927
1193
  ),
928
1194
  current,
929
1195
  ];
930
1196
  }
1197
+
931
1198
  const next = withSubmission(current, {
932
1199
  ...stored,
933
1200
  row: { ...stored.row, state: "settled", settledOutcome: reservation.outcome },
934
1201
  ownership: undefined,
935
1202
  reservation: { ...reservation, finalizedAtMillis: nowMillis },
936
1203
  });
1204
+
937
1205
  return [
938
1206
  success(
939
1207
  Settlement.make({
@@ -941,6 +1209,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
941
1209
  settlementId: reservation.settlementId,
942
1210
  receiptId: stored.row.receiptId,
943
1211
  outcome: reservation.outcome,
1212
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
944
1213
  settledAt: utc(nowMillis),
945
1214
  }),
946
1215
  ),
@@ -948,7 +1217,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
948
1217
  ];
949
1218
  },
950
1219
  );
1220
+
951
1221
  if (decision._tag === "failure") return yield* decision.error;
1222
+
952
1223
  return decision.value;
953
1224
  }),
954
1225
  );
@@ -959,6 +1230,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
959
1230
  Effect.gen(function* () {
960
1231
  const request = yield* validate(AbortCommand, "requestAbort", unvalidated);
961
1232
  const nowMillis = yield* Clock.currentTimeMillis;
1233
+
962
1234
  const decision = yield* Ref.modify(
963
1235
  state,
964
1236
  (
@@ -968,6 +1240,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
968
1240
  LedgerState,
969
1241
  ] => {
970
1242
  const stored = current.submissions.get(request.submissionId);
1243
+
971
1244
  if (stored === undefined) {
972
1245
  return [
973
1246
  failure(ledgerError("requestAbort", `Unknown Submission ${request.submissionId}`)),
@@ -989,6 +1262,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
989
1262
  current,
990
1263
  ];
991
1264
  }
1265
+
992
1266
  return [
993
1267
  failure(
994
1268
  JoinedToHost.make({
@@ -1011,6 +1285,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1011
1285
  current,
1012
1286
  ];
1013
1287
  }
1288
+
1014
1289
  return [
1015
1290
  failure(
1016
1291
  SettlementConflict.make({
@@ -1022,16 +1297,20 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1022
1297
  ];
1023
1298
  }
1024
1299
  if (stored.abortIntent !== undefined) return [success(stored.abortIntent), current];
1300
+
1025
1301
  const intent = AbortIntent.make({
1026
1302
  submissionId: request.submissionId,
1027
1303
  author: request.author,
1028
1304
  reason: request.reason,
1029
1305
  requestedAt: utc(nowMillis),
1030
1306
  });
1307
+
1031
1308
  return [success(intent), withSubmission(current, { ...stored, abortIntent: intent })];
1032
1309
  },
1033
1310
  );
1311
+
1034
1312
  if (decision._tag === "failure") return yield* decision.error;
1313
+
1035
1314
  return decision.value;
1036
1315
  }),
1037
1316
  );
@@ -1041,6 +1320,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1041
1320
  )((unvalidated) =>
1042
1321
  Effect.gen(function* () {
1043
1322
  const request = yield* validate(ClaimJoiningRequest, "claimJoining", unvalidated);
1323
+
1044
1324
  const decision = yield* Ref.modify(
1045
1325
  state,
1046
1326
  (
@@ -1050,6 +1330,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1050
1330
  LedgerState,
1051
1331
  ] => {
1052
1332
  const host = current.submissions.get(request.hostSubmissionId);
1333
+
1053
1334
  if (host === undefined) {
1054
1335
  return [
1055
1336
  failure(
@@ -1058,29 +1339,32 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1058
1339
  current,
1059
1340
  ];
1060
1341
  }
1061
- if (host.row.conversationId !== request.conversationId) {
1342
+ if (host.row.threadId !== request.threadId) {
1062
1343
  return [
1063
1344
  failure(
1064
1345
  ledgerError(
1065
1346
  "claimJoining",
1066
- `Host Submission ${request.hostSubmissionId} does not belong to Conversation ${request.conversationId}`,
1347
+ `Host Submission ${request.hostSubmissionId} does not belong to Thread ${request.threadId}`,
1067
1348
  ),
1068
1349
  ),
1069
1350
  current,
1070
1351
  ];
1071
1352
  }
1072
- if (!ownsLane(host, request.ownershipToken)) {
1353
+ if (!ownsLane(current, host, request.ownershipToken)) {
1073
1354
  return [failure(ownershipLost(current, host)), current];
1074
1355
  }
1356
+
1075
1357
  const later = [...current.submissions.values()]
1076
1358
  .filter(
1077
1359
  (stored) =>
1078
- stored.row.conversationId === request.conversationId &&
1360
+ stored.row.threadId === request.threadId &&
1079
1361
  stored.row.queueSequence > host.row.queueSequence,
1080
1362
  )
1081
1363
  .sort((left, right) => left.row.queueSequence - right.row.queueSequence);
1364
+
1082
1365
  const claims: Array<JoiningClaim> = [];
1083
1366
  const submissions = new Map(current.submissions);
1367
+
1084
1368
  for (const stored of later) {
1085
1369
  if (claims.length >= request.maxCount) break;
1086
1370
  // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
@@ -1113,10 +1397,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1113
1397
  }),
1114
1398
  );
1115
1399
  }
1400
+
1116
1401
  return [success(claims), { ...current, submissions }];
1117
1402
  },
1118
1403
  );
1404
+
1119
1405
  if (decision._tag === "failure") return yield* decision.error;
1406
+
1120
1407
  return decision.value;
1121
1408
  }),
1122
1409
  );
@@ -1126,10 +1413,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1126
1413
  )((unvalidated) =>
1127
1414
  Effect.gen(function* () {
1128
1415
  const request = yield* validate(MarkJoinedRequest, "markJoined", unvalidated);
1416
+
1129
1417
  const decision = yield* Ref.modify(
1130
1418
  state,
1131
1419
  (current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
1132
1420
  const stored = current.submissions.get(request.submissionId);
1421
+
1133
1422
  if (stored === undefined) {
1134
1423
  return [
1135
1424
  failure(ledgerError("markJoined", `Unknown Submission ${request.submissionId}`)),
@@ -1148,6 +1437,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1148
1437
  ];
1149
1438
  }
1150
1439
  const host = current.submissions.get(stored.joinedHostSubmissionId);
1440
+
1151
1441
  if (host === undefined) {
1152
1442
  return [
1153
1443
  failure(
@@ -1161,7 +1451,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1161
1451
  }
1162
1452
  // The lane is host-owned: the presented token must own the HOST's ownership period,
1163
1453
  // which also lets a later host Attempt repair a lost marker from history (DUR-016).
1164
- if (!ownsLane(host, request.ownershipToken)) {
1454
+ if (!ownsLane(current, host, request.ownershipToken)) {
1165
1455
  return [failure(ownershipLost(current, host)), current];
1166
1456
  }
1167
1457
  if (stored.inputApplied !== undefined) {
@@ -1171,6 +1461,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1171
1461
  ) {
1172
1462
  return [success(undefined), current];
1173
1463
  }
1464
+
1174
1465
  return [
1175
1466
  failure(
1176
1467
  ledgerError(
@@ -1192,10 +1483,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1192
1483
  current,
1193
1484
  ];
1194
1485
  }
1486
+
1195
1487
  const marker = InputAppliedMarker.make({
1196
1488
  recordId: request.recordId,
1197
1489
  sequence: request.sequence,
1198
1490
  });
1491
+
1199
1492
  return [
1200
1493
  success(undefined),
1201
1494
  withSubmission(current, {
@@ -1206,6 +1499,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1206
1499
  ];
1207
1500
  },
1208
1501
  );
1502
+
1209
1503
  if (decision._tag === "failure") return yield* decision.error;
1210
1504
  }),
1211
1505
  );
@@ -1215,10 +1509,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1215
1509
  )((unvalidated) =>
1216
1510
  Effect.gen(function* () {
1217
1511
  const request = yield* validate(RevertJoiningRequest, "revertJoining", unvalidated);
1512
+
1218
1513
  const decision = yield* Ref.modify(
1219
1514
  state,
1220
1515
  (current): readonly [Decision<void, LedgerError>, LedgerState] => {
1221
1516
  const stored = current.submissions.get(request.submissionId);
1517
+
1222
1518
  if (stored === undefined) {
1223
1519
  return [
1224
1520
  failure(ledgerError("revertJoining", `Unknown Submission ${request.submissionId}`)),
@@ -1228,6 +1524,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1228
1524
  // Idempotent and recovery-only: only a still-`joining` Submission reverts; an
1229
1525
  // already-joined (or already-reverted) Submission is a no-op (DUR-016).
1230
1526
  if (stored.row.state !== "joining") return [success(undefined), current];
1527
+
1231
1528
  return [
1232
1529
  success(undefined),
1233
1530
  withSubmission(current, {
@@ -1238,6 +1535,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1238
1535
  ];
1239
1536
  },
1240
1537
  );
1538
+
1241
1539
  if (decision._tag === "failure") return yield* decision.error;
1242
1540
  }),
1243
1541
  );
@@ -1248,6 +1546,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1248
1546
  Effect.gen(function* () {
1249
1547
  const request = yield* validate(SuspendRequest, "suspend", unvalidated);
1250
1548
  const nowMillis = yield* Clock.currentTimeMillis;
1549
+
1251
1550
  const decision = yield* Ref.modify(
1252
1551
  state,
1253
1552
  (
@@ -1257,6 +1556,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1257
1556
  LedgerState,
1258
1557
  ] => {
1259
1558
  const stored = current.submissions.get(request.submissionId);
1559
+
1260
1560
  if (stored === undefined) {
1261
1561
  return [
1262
1562
  failure(ledgerError("suspend", `Unknown Submission ${request.submissionId}`)),
@@ -1275,6 +1575,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1275
1575
  current,
1276
1576
  ];
1277
1577
  }
1578
+
1278
1579
  return [
1279
1580
  failure(
1280
1581
  SettlementConflict.make({
@@ -1298,9 +1599,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1298
1599
  current,
1299
1600
  ];
1300
1601
  }
1301
- if (!ownsLane(stored, request.ownershipToken)) {
1602
+ if (!ownsLane(current, stored, request.ownershipToken)) {
1302
1603
  return [failure(ownershipLost(current, stored)), current];
1303
1604
  }
1605
+
1304
1606
  // A covering event that raced ahead of the suspend transaction (an approval decision,
1305
1607
  // or a child settlement observed directly from the child's row in this single store)
1306
1608
  // resumes the caller immediately WITHOUT releasing the lane (plan §2.6, spec §12).
@@ -1313,9 +1615,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1313
1615
  (child) =>
1314
1616
  current.submissions.get(child.childSubmissionId)?.row.state === "settled",
1315
1617
  );
1618
+
1316
1619
  if (alreadyCovered) {
1317
1620
  return [success("resume-immediately" as const), current];
1318
1621
  }
1622
+
1319
1623
  return [
1320
1624
  success("suspended" as const),
1321
1625
  withSubmission(current, {
@@ -1327,7 +1631,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1327
1631
  ];
1328
1632
  },
1329
1633
  );
1634
+
1330
1635
  if (decision._tag === "failure") return yield* decision.error;
1636
+
1331
1637
  return decision.value;
1332
1638
  }),
1333
1639
  );
@@ -1341,7 +1647,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1341
1647
  "recordApprovalDecision",
1342
1648
  unvalidated,
1343
1649
  );
1650
+
1344
1651
  const nowMillis = yield* Clock.currentTimeMillis;
1652
+
1345
1653
  const decision = yield* Ref.modify(
1346
1654
  state,
1347
1655
  (
@@ -1351,6 +1659,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1351
1659
  LedgerState,
1352
1660
  ] => {
1353
1661
  const stored = current.submissions.get(command.submissionId);
1662
+
1354
1663
  if (stored === undefined) {
1355
1664
  return [
1356
1665
  failure(
@@ -1374,6 +1683,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1374
1683
  current,
1375
1684
  ];
1376
1685
  }
1686
+
1377
1687
  return [
1378
1688
  failure(
1379
1689
  SettlementConflict.make({
@@ -1385,6 +1695,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1385
1695
  ];
1386
1696
  }
1387
1697
  const existing = stored.approvalDecisions.get(command.toolCallId);
1698
+
1388
1699
  if (existing !== undefined) {
1389
1700
  if (existing.decision !== command.decision) {
1390
1701
  return [
@@ -1398,8 +1709,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1398
1709
  current,
1399
1710
  ];
1400
1711
  }
1712
+
1401
1713
  return [success(existing), current];
1402
1714
  }
1715
+
1403
1716
  const intent = ApprovalDecisionIntent.make({
1404
1717
  submissionId: command.submissionId,
1405
1718
  toolCallId: command.toolCallId,
@@ -1408,10 +1721,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1408
1721
  reason: command.reason,
1409
1722
  decidedAt: utc(nowMillis),
1410
1723
  });
1724
+
1411
1725
  const approvalDecisions = new Map(stored.approvalDecisions).set(
1412
1726
  command.toolCallId,
1413
1727
  intent,
1414
1728
  );
1729
+
1415
1730
  // Once every pending call of an ApprovalPending suspension is decided, the lane
1416
1731
  // wakes: suspended → input-applied (plan §2.6). A WaitingForChild suspension wakes
1417
1732
  // only through recordChildSettled.
@@ -1422,6 +1737,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1422
1737
  stored.suspension.reason.toolCallIds.every((toolCallId) =>
1423
1738
  approvalDecisions.has(toolCallId),
1424
1739
  );
1740
+
1425
1741
  return [
1426
1742
  success(intent),
1427
1743
  withSubmission(current, {
@@ -1433,7 +1749,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1433
1749
  ];
1434
1750
  },
1435
1751
  );
1752
+
1436
1753
  if (decision._tag === "failure") return yield* decision.error;
1754
+
1437
1755
  return decision.value;
1438
1756
  }),
1439
1757
  );
@@ -1443,10 +1761,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1443
1761
  )((unvalidated) =>
1444
1762
  Effect.gen(function* () {
1445
1763
  const request = yield* validate(MarkUnknownRequest, "markUnknown", unvalidated);
1764
+
1446
1765
  const decision = yield* Ref.modify(
1447
1766
  state,
1448
1767
  (current): readonly [Decision<void, SettlementConflict | LedgerError>, LedgerState] => {
1449
1768
  const stored = current.submissions.get(request.submissionId);
1769
+
1450
1770
  if (stored === undefined) {
1451
1771
  return [
1452
1772
  failure(ledgerError("markUnknown", `Unknown Submission ${request.submissionId}`)),
@@ -1465,6 +1785,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1465
1785
  current,
1466
1786
  ];
1467
1787
  }
1788
+
1468
1789
  return [
1469
1790
  failure(
1470
1791
  SettlementConflict.make({
@@ -1492,10 +1813,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1492
1813
  // set while the first recorded reason is kept.
1493
1814
  const existing = stored.unknownMark;
1494
1815
  const known = new Set(existing?.toolCallIds ?? []);
1816
+
1495
1817
  const merged = [
1496
1818
  ...(existing?.toolCallIds ?? []),
1497
1819
  ...request.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
1498
1820
  ];
1821
+
1499
1822
  return [
1500
1823
  success(undefined),
1501
1824
  withSubmission(current, {
@@ -1507,6 +1830,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1507
1830
  ];
1508
1831
  },
1509
1832
  );
1833
+
1510
1834
  if (decision._tag === "failure") return yield* decision.error;
1511
1835
  }),
1512
1836
  );
@@ -1519,15 +1843,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1519
1843
  "recordUnknownResolution",
1520
1844
  unvalidated,
1521
1845
  );
1846
+
1522
1847
  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);
1848
+
1531
1849
  const decision = yield* Ref.modify(
1532
1850
  state,
1533
1851
  (
@@ -1540,6 +1858,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1540
1858
  LedgerState,
1541
1859
  ] => {
1542
1860
  const stored = current.submissions.get(command.submissionId);
1861
+
1543
1862
  if (stored === undefined) {
1544
1863
  return [
1545
1864
  failure(
@@ -1563,6 +1882,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1563
1882
  current,
1564
1883
  ];
1565
1884
  }
1885
+
1566
1886
  return [
1567
1887
  failure(
1568
1888
  SettlementConflict.make({
@@ -1574,7 +1894,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1574
1894
  ];
1575
1895
  }
1576
1896
  const existing = stored.unknownResolutions.get(command.toolCallId);
1577
- if (existing !== undefined && existing.resolutionJson !== resolutionJson) {
1897
+
1898
+ if (
1899
+ existing !== undefined &&
1900
+ !equivalentUnknownResolution(existing.intent.resolution, command.resolution)
1901
+ ) {
1578
1902
  return [
1579
1903
  failure(
1580
1904
  UnknownResolutionConflict.make({
@@ -1585,6 +1909,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1585
1909
  current,
1586
1910
  ];
1587
1911
  }
1912
+
1588
1913
  const intent =
1589
1914
  existing?.intent ??
1590
1915
  UnknownResolutionIntent.make({
@@ -1595,13 +1920,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1595
1920
  resolution: command.resolution,
1596
1921
  resolvedAt: utc(nowMillis),
1597
1922
  });
1923
+
1598
1924
  const unknownResolutions =
1599
1925
  existing !== undefined
1600
1926
  ? stored.unknownResolutions
1601
1927
  : new Map(stored.unknownResolutions).set(command.toolCallId, {
1602
1928
  intent,
1603
- resolutionJson,
1604
1929
  });
1930
+
1605
1931
  // The lane reopens only when EVERY marked open call has a durable resolution
1606
1932
  // intent: unknown → input-applied (DUR-017). Replays re-run the coverage check so
1607
1933
  // a recovering caller can wake the lane idempotently.
@@ -1611,6 +1937,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1611
1937
  stored.unknownMark.toolCallIds.every((toolCallId) =>
1612
1938
  unknownResolutions.has(toolCallId),
1613
1939
  );
1940
+
1614
1941
  return [
1615
1942
  success(intent),
1616
1943
  withSubmission(current, {
@@ -1622,7 +1949,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1622
1949
  ];
1623
1950
  },
1624
1951
  );
1952
+
1625
1953
  if (decision._tag === "failure") return yield* decision.error;
1954
+
1626
1955
  return decision.value;
1627
1956
  }),
1628
1957
  );
@@ -1636,10 +1965,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1636
1965
  "recordChildSettled",
1637
1966
  unvalidated,
1638
1967
  );
1968
+
1639
1969
  const decision = yield* Ref.modify(
1640
1970
  state,
1641
1971
  (current): readonly [Decision<ChildSettledOutcome, LedgerError>, LedgerState] => {
1642
1972
  const parent = current.submissions.get(request.parentSubmissionId);
1973
+
1643
1974
  if (parent === undefined) {
1644
1975
  return [
1645
1976
  failure(
@@ -1651,10 +1982,18 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1651
1982
  current,
1652
1983
  ];
1653
1984
  }
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.
1985
+ // The caller may notify after the canonical Settlement append but before ledger
1986
+ // finalization. In a single store, an exact reservation plus `terminalizing` is the
1987
+ // narrow durable prefix that makes that ordering admissible; earlier states remain
1988
+ // a caller error.
1656
1989
  const child = current.submissions.get(request.childSubmissionId);
1657
- if (child === undefined || child.row.state !== "settled") {
1990
+
1991
+ const announced =
1992
+ child !== undefined &&
1993
+ (child.row.state === "settled" ||
1994
+ (child.row.state === "terminalizing" && child.reservation !== undefined));
1995
+
1996
+ if (!announced) {
1658
1997
  return [
1659
1998
  failure(
1660
1999
  ledgerError(
@@ -1673,16 +2012,24 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1673
2012
  return [success("not-waiting" as const), current];
1674
2013
  }
1675
2014
  const children = parent.suspension.reason.children;
2015
+
1676
2016
  if (!children.some((entry) => entry.childSubmissionId === request.childSubmissionId)) {
1677
2017
  return [success("not-waiting" as const), current];
1678
2018
  }
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
- );
2019
+
2020
+ // Every listed child must be either finalized or canonically announced from the
2021
+ // exact terminalizing reservation. Replays re-run this coverage check idempotently.
2022
+ const allSettled = children.every((entry) => {
2023
+ const listed = current.submissions.get(entry.childSubmissionId);
2024
+
2025
+ return (
2026
+ listed?.row.state === "settled" ||
2027
+ (listed?.row.state === "terminalizing" && listed.reservation !== undefined)
2028
+ );
2029
+ });
2030
+
1685
2031
  if (!allSettled) return [success("still-waiting" as const), current];
2032
+
1686
2033
  return [
1687
2034
  success("woken" as const),
1688
2035
  withSubmission(current, {
@@ -1693,7 +2040,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1693
2040
  ];
1694
2041
  },
1695
2042
  );
2043
+
1696
2044
  if (decision._tag === "failure") return yield* decision.error;
2045
+
1697
2046
  return decision.value;
1698
2047
  }),
1699
2048
  );
@@ -1707,8 +2056,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1707
2056
  "reserveChildBudget",
1708
2057
  unvalidated,
1709
2058
  );
2059
+
1710
2060
  const nowMillis = yield* Clock.currentTimeMillis;
1711
- const allocationJson = JSON.stringify(request.allocation);
2061
+
1712
2062
  const decision = yield* Ref.modify(
1713
2063
  state,
1714
2064
  (
@@ -1718,6 +2068,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1718
2068
  LedgerState,
1719
2069
  ] => {
1720
2070
  const existing = current.childReservations.get(request.reservationId);
2071
+
1721
2072
  if (existing !== undefined) {
1722
2073
  // Identical replays short-circuit before the fence, mirroring reserveSettlement:
1723
2074
  // a replay creates nothing, so a recovering caller resumes rather than duplicates.
@@ -1725,7 +2076,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1725
2076
  existing.parentSubmissionId === request.parentSubmissionId &&
1726
2077
  existing.parentToolCallId === request.parentToolCallId &&
1727
2078
  existing.allocationDigest === request.allocationDigest &&
1728
- existing.allocationJson === allocationJson;
2079
+ equivalentPersistedJson(existing.allocation, request.allocation);
2080
+
1729
2081
  if (!identical) {
1730
2082
  return [
1731
2083
  failure(
@@ -1739,6 +2091,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1739
2091
  current,
1740
2092
  ];
1741
2093
  }
2094
+
1742
2095
  return [
1743
2096
  success(
1744
2097
  ReservedChildBudget.make({
@@ -1767,6 +2120,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1767
2120
  }
1768
2121
  }
1769
2122
  const parent = current.submissions.get(request.parentSubmissionId);
2123
+
1770
2124
  if (parent === undefined) {
1771
2125
  return [
1772
2126
  failure(
@@ -1780,9 +2134,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1780
2134
  }
1781
2135
  // Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
1782
2136
  // parent Attempt can never create new reservation state.
1783
- if (!ownsLane(parent, request.ownershipToken)) {
2137
+ if (!ownsLane(current, parent, request.ownershipToken)) {
1784
2138
  return [failure(ownershipLost(current, parent)), current];
1785
2139
  }
2140
+
1786
2141
  const reservation: StoredChildReservation = {
1787
2142
  reservationId: request.reservationId,
1788
2143
  parentSubmissionId: request.parentSubmissionId,
@@ -1790,14 +2145,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1790
2145
  childSubmissionId: undefined,
1791
2146
  status: "reserved",
1792
2147
  allocation: request.allocation,
1793
- allocationJson,
1794
2148
  allocationDigest: request.allocationDigest,
1795
2149
  accounting: undefined,
1796
- accountingJson: undefined,
1797
2150
  reservedAtMillis: nowMillis,
1798
2151
  releaseBeganAtMillis: undefined,
1799
2152
  releasedAtMillis: undefined,
1800
2153
  };
2154
+
1801
2155
  return [
1802
2156
  success(
1803
2157
  ReservedChildBudget.make({
@@ -1809,7 +2163,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1809
2163
  ];
1810
2164
  },
1811
2165
  );
2166
+
1812
2167
  if (decision._tag === "failure") return yield* decision.error;
2168
+
1813
2169
  return decision.value;
1814
2170
  }),
1815
2171
  );
@@ -1822,6 +2178,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1822
2178
  "attachChildToReservation",
1823
2179
  unvalidated,
1824
2180
  );
2181
+
1825
2182
  const decision = yield* Ref.modify(
1826
2183
  state,
1827
2184
  (
@@ -1834,6 +2191,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1834
2191
  LedgerState,
1835
2192
  ] => {
1836
2193
  const reservation = current.childReservations.get(request.reservationId);
2194
+
1837
2195
  if (reservation === undefined) {
1838
2196
  return [
1839
2197
  failure(
@@ -1850,6 +2208,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1850
2208
  if (reservation.childSubmissionId === request.childSubmissionId) {
1851
2209
  return [success(toReservationSnapshot(reservation)), current];
1852
2210
  }
2211
+
1853
2212
  return [
1854
2213
  failure(
1855
2214
  ChildReservationConflict.make({
@@ -1862,6 +2221,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1862
2221
  ];
1863
2222
  }
1864
2223
  const parent = current.submissions.get(reservation.parentSubmissionId);
2224
+
1865
2225
  if (parent === undefined) {
1866
2226
  return [
1867
2227
  failure(
@@ -1873,7 +2233,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1873
2233
  current,
1874
2234
  ];
1875
2235
  }
1876
- if (!ownsLane(parent, request.ownershipToken)) {
2236
+ if (!ownsLane(current, parent, request.ownershipToken)) {
1877
2237
  return [failure(ownershipLost(current, parent)), current];
1878
2238
  }
1879
2239
  if (reservation.status !== "reserved") {
@@ -1901,17 +2261,21 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1901
2261
  current,
1902
2262
  ];
1903
2263
  }
2264
+
1904
2265
  const attached: StoredChildReservation = {
1905
2266
  ...reservation,
1906
2267
  childSubmissionId: request.childSubmissionId,
1907
2268
  };
2269
+
1908
2270
  return [
1909
2271
  success(toReservationSnapshot(attached)),
1910
2272
  withChildReservation(current, attached),
1911
2273
  ];
1912
2274
  },
1913
2275
  );
2276
+
1914
2277
  if (decision._tag === "failure") return yield* decision.error;
2278
+
1915
2279
  return decision.value;
1916
2280
  }),
1917
2281
  );
@@ -1924,8 +2288,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1924
2288
  "beginChildBudgetRelease",
1925
2289
  unvalidated,
1926
2290
  );
2291
+
1927
2292
  const nowMillis = yield* Clock.currentTimeMillis;
1928
- const accountingJson = JSON.stringify(request.accounting);
2293
+
1929
2294
  const decision = yield* Ref.modify(
1930
2295
  state,
1931
2296
  (
@@ -1935,6 +2300,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1935
2300
  LedgerState,
1936
2301
  ] => {
1937
2302
  const reservation = current.childReservations.get(request.reservationId);
2303
+
1938
2304
  if (reservation === undefined) {
1939
2305
  return [
1940
2306
  failure(
@@ -1949,9 +2315,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1949
2315
  if (reservation.status !== "reserved") {
1950
2316
  // The accounting decision was already frozen exactly once; an identical replay is
1951
2317
  // a no-op and a divergent decision conflicts (spec §12 join step 6).
1952
- if (reservation.accountingJson === accountingJson) {
2318
+ if (
2319
+ reservation.accounting !== undefined &&
2320
+ equivalentPersistedJson(reservation.accounting, request.accounting)
2321
+ ) {
1953
2322
  return [success(toReservationSnapshot(reservation)), current];
1954
2323
  }
2324
+
1955
2325
  return [
1956
2326
  failure(
1957
2327
  ChildReservationConflict.make({
@@ -1964,20 +2334,23 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1964
2334
  current,
1965
2335
  ];
1966
2336
  }
2337
+
1967
2338
  const frozen: StoredChildReservation = {
1968
2339
  ...reservation,
1969
2340
  status: "releasePending",
1970
2341
  accounting: request.accounting,
1971
- accountingJson,
1972
2342
  releaseBeganAtMillis: nowMillis,
1973
2343
  };
2344
+
1974
2345
  return [
1975
2346
  success(toReservationSnapshot(frozen)),
1976
2347
  withChildReservation(current, frozen),
1977
2348
  ];
1978
2349
  },
1979
2350
  );
2351
+
1980
2352
  if (decision._tag === "failure") return yield* decision.error;
2353
+
1981
2354
  return decision.value;
1982
2355
  }),
1983
2356
  );
@@ -1991,7 +2364,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1991
2364
  "releaseChildBudget",
1992
2365
  unvalidated,
1993
2366
  );
2367
+
1994
2368
  const nowMillis = yield* Clock.currentTimeMillis;
2369
+
1995
2370
  const decision = yield* Ref.modify(
1996
2371
  state,
1997
2372
  (
@@ -2001,6 +2376,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2001
2376
  LedgerState,
2002
2377
  ] => {
2003
2378
  const reservation = current.childReservations.get(request.reservationId);
2379
+
2004
2380
  if (reservation === undefined) {
2005
2381
  return [
2006
2382
  failure(
@@ -2030,18 +2406,22 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2030
2406
  current,
2031
2407
  ];
2032
2408
  }
2409
+
2033
2410
  const released: StoredChildReservation = {
2034
2411
  ...reservation,
2035
2412
  status: "released",
2036
2413
  releasedAtMillis: nowMillis,
2037
2414
  };
2415
+
2038
2416
  return [
2039
2417
  success(toReservationSnapshot(released)),
2040
2418
  withChildReservation(current, released),
2041
2419
  ];
2042
2420
  },
2043
2421
  );
2422
+
2044
2423
  if (decision._tag === "failure") return yield* decision.error;
2424
+
2045
2425
  return decision.value;
2046
2426
  }),
2047
2427
  );
@@ -2052,18 +2432,32 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2052
2432
  const snapshots = [...current.submissions.values()]
2053
2433
  .filter((stored) => stored.row.state !== "settled")
2054
2434
  .sort((left, right) =>
2055
- left.row.conversationId < right.row.conversationId
2435
+ left.row.threadId < right.row.threadId
2056
2436
  ? -1
2057
- : left.row.conversationId > right.row.conversationId
2437
+ : left.row.threadId > right.row.threadId
2058
2438
  ? 1
2059
2439
  : left.row.queueSequence - right.row.queueSequence,
2060
2440
  )
2061
2441
  .map((stored) => toSnapshot(stored.row));
2442
+
2062
2443
  return Stream.fromIterable(snapshots);
2063
2444
  }),
2064
2445
  ),
2065
2446
  );
2066
2447
 
2448
+ const readAbortIntent: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
2449
+ "MemorySubmissionLedger.readAbortIntent",
2450
+ )(function* (unvalidated) {
2451
+ const request = yield* validate(AbortIntentRequest, "readAbortIntent", unvalidated);
2452
+ const stored = (yield* Ref.get(state)).submissions.get(request.submissionId);
2453
+
2454
+ if (stored === undefined) {
2455
+ return yield* ledgerError("readAbortIntent", `Unknown Submission ${request.submissionId}`);
2456
+ }
2457
+
2458
+ return stored.abortIntent;
2459
+ });
2460
+
2067
2461
  const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
2068
2462
  "MemorySubmissionLedger.loadRecoverySnapshot",
2069
2463
  )((unvalidated) =>
@@ -2073,14 +2467,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2073
2467
  "loadRecoverySnapshot",
2074
2468
  unvalidated,
2075
2469
  );
2470
+
2076
2471
  const current = yield* Ref.get(state);
2077
2472
  const stored = current.submissions.get(request.submissionId);
2473
+
2078
2474
  if (stored === undefined) {
2079
2475
  return yield* ledgerError(
2080
2476
  "loadRecoverySnapshot",
2081
2477
  `Unknown Submission ${request.submissionId}`,
2082
2478
  );
2083
2479
  }
2480
+
2084
2481
  const joins = [...current.submissions.values()]
2085
2482
  .filter((candidate) => candidate.joinedHostSubmissionId === request.submissionId)
2086
2483
  .sort((left, right) => left.row.queueSequence - right.row.queueSequence)
@@ -2091,11 +2488,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2091
2488
  hostSubmissionId: request.submissionId,
2092
2489
  }),
2093
2490
  );
2491
+
2094
2492
  const byToolCallId = <A extends { readonly toolCallId: ToolCallId }>(
2095
2493
  left: A,
2096
2494
  right: A,
2097
2495
  ): number =>
2098
2496
  left.toolCallId < right.toolCallId ? -1 : left.toolCallId > right.toolCallId ? 1 : 0;
2497
+
2099
2498
  // Parent-side subagent view: this Submission's child budget reservations in parent Tool
2100
2499
  // Call order, plus each attached child's current lane state (a disposable derived view;
2101
2500
  // the canonical records stay the recovery truth, DUR-015).
@@ -2108,10 +2507,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2108
2507
  ? 1
2109
2508
  : 0,
2110
2509
  );
2510
+
2111
2511
  const childAttachments: Array<ChildAttachmentSnapshot> = [];
2512
+
2112
2513
  for (const reservation of childReservations) {
2113
2514
  if (reservation.childSubmissionId === undefined) continue;
2114
2515
  const child = current.submissions.get(reservation.childSubmissionId);
2516
+
2115
2517
  if (child === undefined) continue;
2116
2518
  childAttachments.push(
2117
2519
  ChildAttachmentSnapshot.make({
@@ -2124,6 +2526,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2124
2526
  }),
2125
2527
  );
2126
2528
  }
2529
+
2127
2530
  return RecoverySnapshot.make({
2128
2531
  submission: toSnapshot(stored.row),
2129
2532
  joins,
@@ -2201,6 +2604,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2201
2604
  releaseChildBudget,
2202
2605
  scanNonterminal,
2203
2606
  loadRecoverySnapshot,
2607
+ readAbortIntent,
2204
2608
  });
2205
2609
  });
2206
2610