@effect-agent/storage-cloudflare 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 (74) hide show
  1. package/dist/DoMemoryStore.d.mts +29 -0
  2. package/dist/DoMemoryStore.mjs +55 -0
  3. package/dist/DoMemoryStore.mjs.map +1 -0
  4. package/dist/DoMessageDeliveryStore.d.mts +17 -0
  5. package/dist/DoMessageDeliveryStore.mjs +32 -0
  6. package/dist/DoMessageDeliveryStore.mjs.map +1 -0
  7. package/dist/DoScheduleStore.d.mts +36 -0
  8. package/dist/DoScheduleStore.mjs +388 -0
  9. package/dist/DoScheduleStore.mjs.map +1 -0
  10. package/dist/DoStorageConfig.d.mts +45 -0
  11. package/dist/DoStorageConfig.mjs +52 -0
  12. package/dist/DoStorageConfig.mjs.map +1 -0
  13. package/dist/DoStorageError-Cmhvl4TQ.d.mts +98 -0
  14. package/dist/DoStorageError.d.mts +2 -0
  15. package/dist/DoStorageError.mjs +164 -0
  16. package/dist/DoStorageError.mjs.map +1 -0
  17. package/dist/DoStorageFailpoint.d.mts +17 -0
  18. package/dist/DoStorageFailpoint.mjs +14 -0
  19. package/dist/DoStorageFailpoint.mjs.map +1 -0
  20. package/dist/DoStorageFailpointTesting.d.mts +33 -0
  21. package/dist/DoStorageFailpointTesting.mjs +35 -0
  22. package/dist/DoStorageFailpointTesting.mjs.map +1 -0
  23. package/dist/DoStorageVersion-Db6fjP-I.d.mts +12 -0
  24. package/dist/DoStorageVersion.d.mts +2 -0
  25. package/dist/DoStorageVersion.mjs +8 -0
  26. package/dist/DoStorageVersion.mjs.map +1 -0
  27. package/dist/DoSubmissionLedger.d.mts +23 -0
  28. package/dist/DoSubmissionLedger.mjs +1864 -0
  29. package/dist/DoSubmissionLedger.mjs.map +1 -0
  30. package/dist/DoSubscriptionStore.d.mts +25 -0
  31. package/dist/DoSubscriptionStore.mjs +183 -0
  32. package/dist/DoSubscriptionStore.mjs.map +1 -0
  33. package/dist/DoThreadStore.d.mts +59 -0
  34. package/dist/DoThreadStore.mjs +481 -0
  35. package/dist/DoThreadStore.mjs.map +1 -0
  36. package/dist/MemoryProtocol.d.mts +38160 -0
  37. package/dist/MemoryProtocol.mjs +219 -0
  38. package/dist/MemoryProtocol.mjs.map +1 -0
  39. package/dist/PortProtocol.d.mts +4933 -0
  40. package/dist/PortProtocol.mjs +193 -0
  41. package/dist/PortProtocol.mjs.map +1 -0
  42. package/dist/PortRouting.d.mts +86 -0
  43. package/dist/PortRouting.mjs +439 -0
  44. package/dist/PortRouting.mjs.map +1 -0
  45. package/dist/do-journal-THW8-ZeP.mjs +1041 -0
  46. package/dist/do-journal-THW8-ZeP.mjs.map +1 -0
  47. package/dist/index.d.mts +14 -1525
  48. package/dist/index.mjs +14 -3695
  49. package/dist/migrations-DZKTNU88.mjs +291 -0
  50. package/dist/migrations-DZKTNU88.mjs.map +1 -0
  51. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  52. package/package.json +1 -45
  53. package/src/DoMemoryStore.ts +56 -0
  54. package/src/DoMessageDeliveryStore.ts +53 -0
  55. package/src/DoScheduleStore.ts +622 -0
  56. package/src/{errors.ts → DoStorageError.ts} +10 -3
  57. package/src/DoStorageFailpoint.ts +20 -0
  58. package/src/{do-storage-failpoint.ts → DoStorageFailpointTesting.ts} +5 -18
  59. package/src/DoStorageVersion.ts +2 -0
  60. package/src/{do-ledger.ts → DoSubmissionLedger.ts} +960 -237
  61. package/src/DoSubscriptionStore.ts +329 -0
  62. package/src/DoThreadStore.ts +1039 -0
  63. package/src/MemoryProtocol.ts +351 -0
  64. package/src/{port-protocol.ts → PortProtocol.ts} +76 -38
  65. package/src/{routing.ts → PortRouting.ts} +374 -264
  66. package/src/index.ts +13 -32
  67. package/src/internal/do-journal.ts +1656 -0
  68. package/src/internal/message-delivery-schema.ts +24 -0
  69. package/src/{migrations.ts → internal/migrations.ts} +55 -35
  70. package/src/internal/recovery-checkpoint-schema.ts +17 -0
  71. package/dist/index.mjs.map +0 -1
  72. package/src/do-conversation-store.ts +0 -850
  73. package/src/do-journal.ts +0 -1078
  74. /package/src/{do-storage-config.ts → DoStorageConfig.ts} +0 -0
@@ -1,18 +1,37 @@
1
+ import { BrowserCrypto } from "@effect/platform-browser";
2
+ import { SqliteClient } from "@effect/sql-sqlite-do";
3
+ import { Clock, Context, Crypto, DateTime, Effect, Layer, Option, Schema, Stream } from "effect";
4
+ import { EMPTY_TAIL_DIGEST } from "effect-agent/digest";
5
+ import { InputMessage } from "effect-agent/messaging";
6
+ import {
7
+ ApprovalDecision,
8
+ CanonicalSequence,
9
+ DefinitionDigests,
10
+ Digest,
11
+ PersistedJson,
12
+ WorkerAdmission,
13
+ ProducerEpoch,
14
+ RecordEnvelope,
15
+ SettlementOutcome,
16
+ } from "effect-agent/records";
1
17
  import {
2
18
  AbortCommand,
3
19
  AbortIntent,
20
+ AbortIntentRequest,
4
21
  AdmissionAdmitted,
5
22
  AdmissionConflict,
23
+ AdmissionFence,
24
+ AdmissionGroup,
25
+ AdmissionPolicyError,
6
26
  AdmissionNotAdmitted,
7
27
  AdmissionRequest,
28
+ SubmissionAdmissionFence,
8
29
  AdmissionResult,
9
30
  ApprovalConflict,
10
- ApprovalDecision,
11
31
  ApprovalDecisionCommand,
12
32
  ApprovalDecisionIntent,
13
33
  AttachChildToReservationRequest,
14
34
  BeginChildBudgetReleaseRequest,
15
- CanonicalSequence,
16
35
  ChildAttachmentSnapshot,
17
36
  ChildBudgetReservationRequest,
18
37
  ChildBudgetReservationSnapshot,
@@ -22,9 +41,6 @@ import {
22
41
  Claim,
23
42
  ClaimJoiningRequest,
24
43
  ClaimRequest,
25
- DefinitionDigests,
26
- Digest,
27
- EMPTY_TAIL_DIGEST,
28
44
  InputAppliedMarker,
29
45
  JoinSnapshot,
30
46
  JoinedToHost,
@@ -39,10 +55,7 @@ import {
39
55
  OwnershipRenewal,
40
56
  OwnershipSnapshot,
41
57
  ParentLinkage,
42
- PersistedJson,
43
- ProducerEpoch,
44
58
  QueueSequence,
45
- RecordEnvelope,
46
59
  RecoverySnapshot,
47
60
  RecoverySnapshotRequest,
48
61
  ReleaseChildBudgetRequest,
@@ -54,7 +67,6 @@ import {
54
67
  Settlement,
55
68
  SettlementConflict,
56
69
  SettlementFinalization,
57
- SettlementOutcome,
58
70
  SettlementReservation,
59
71
  SubmissionLedger,
60
72
  SubmissionLookup,
@@ -62,6 +74,7 @@ import {
62
74
  SubmissionSnapshot,
63
75
  SubmissionState,
64
76
  SettlementReservationSnapshot,
77
+ settlementFailureFromRecord,
65
78
  SuspendRequest,
66
79
  SuspensionReason,
67
80
  SuspensionSnapshot,
@@ -72,28 +85,25 @@ import {
72
85
  submissionAbortRecordId,
73
86
  type ChildSettledOutcome,
74
87
  type SuspensionOutcome,
75
- } from "@effect-agent/session";
76
- import { BrowserCrypto } from "@effect/platform-browser";
77
- import { SqliteClient } from "@effect/sql-sqlite-do";
78
- import { Clock, Context, Crypto, DateTime, Effect, Layer, Option, Schema, Stream } from "effect";
88
+ } from "effect-agent/submission-ledger";
79
89
  import * as SqlClientService from "effect/unstable/sql/SqlClient";
80
90
  import type { SqlError } from "effect/unstable/sql/SqlError";
81
91
 
82
- import {
83
- storageConfigLayer,
84
- storageFailpointLayer,
85
- type DoStorageInitializationError,
86
- type DoStorageOptions,
87
- } from "./do-conversation-store.ts";
88
- import { decodeRows, initializeDoJournal } from "./do-journal.ts";
89
- import { DoStorageConfig } from "./do-storage-config.ts";
90
- import { DoStorageFailpoint } from "./do-storage-failpoint.ts";
92
+ import { DoStorageConfig } from "./DoStorageConfig.ts";
91
93
  import {
92
94
  DoLedgerError,
93
95
  DoStorageCorruptionError,
94
96
  DoStorageError,
95
97
  type DoStorageFailpointLocation,
96
- } from "./errors.ts";
98
+ } from "./DoStorageError.ts";
99
+ import { DoStorageFailpoint } from "./DoStorageFailpoint.ts";
100
+ import {
101
+ storageConfigLayer,
102
+ storageFailpointLayer,
103
+ type DoStorageInitializationError,
104
+ type DoStorageOptions,
105
+ } from "./DoThreadStore.ts";
106
+ import { decodeRows, initializeDoJournal } from "./internal/do-journal.ts";
97
107
 
98
108
  type SubmissionId = SubmissionSnapshot["submissionId"];
99
109
 
@@ -107,11 +117,16 @@ const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
107
117
 
108
118
  const SCAN_PAGE_SIZE = 256;
109
119
  const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
120
+ const RESUME_IMMEDIATELY: SuspensionOutcome = "resume-immediately";
121
+ const SUSPENDED: SuspensionOutcome = "suspended";
122
+ const NOT_WAITING: ChildSettledOutcome = "not-waiting";
123
+ const STILL_WAITING: ChildSettledOutcome = "still-waiting";
124
+ const WOKEN: ChildSettledOutcome = "woken";
110
125
  const MAX_IDENTIFIER_LENGTH = 1_024;
111
126
 
112
127
  class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
113
128
  submission_id: BoundedIdentifier,
114
- conversation_id: BoundedIdentifier,
129
+ thread_id: BoundedIdentifier,
115
130
  queue_sequence: QueueSequence,
116
131
  principal: BoundedIdentifier,
117
132
  idempotency_key: BoundedIdentifier,
@@ -134,6 +149,10 @@ class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
134
149
  unknown_tool_call_ids_json: Schema.NullOr(BoundedStoredText),
135
150
  parent_submission_id: Schema.NullOr(BoundedIdentifier),
136
151
  parent_tool_call_id: Schema.NullOr(BoundedIdentifier),
152
+ admission_group: Schema.NullOr(AdmissionGroup),
153
+ admission_fence_json: Schema.NullOr(BoundedStoredText),
154
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
155
+ message_admission_json: Schema.NullOr(BoundedStoredText),
137
156
  }) {}
138
157
 
139
158
  class ChildReservationRow extends Schema.Class<ChildReservationRow>("ChildReservationRow")({
@@ -209,13 +228,22 @@ class MaxQueueSequenceRow extends Schema.Class<MaxQueueSequenceRow>("MaxQueueSeq
209
228
  max_queue_sequence: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
210
229
  }) {}
211
230
 
231
+ class AbortIntentLookupRow extends Schema.Class<AbortIntentLookupRow>("AbortIntentLookupRow")({
232
+ submission_id: BoundedIdentifier,
233
+ abort_submission_id: Schema.NullOr(BoundedIdentifier),
234
+ author: Schema.NullOr(BoundedIdentifier),
235
+ reason: Schema.NullOr(BoundedStoredText),
236
+ requested_at: Schema.NullOr(BoundedTimestamp),
237
+ canonical_record_id: Schema.NullOr(BoundedIdentifier),
238
+ }) {}
239
+
212
240
  class CanonicalRecordIdRow extends Schema.Class<CanonicalRecordIdRow>("CanonicalRecordIdRow")({
213
241
  record_id: BoundedIdentifier,
214
242
  }) {}
215
243
 
216
244
  const SUBMISSION_COLUMNS = `
217
245
  submission_id,
218
- conversation_id,
246
+ thread_id,
219
247
  queue_sequence,
220
248
  principal,
221
249
  idempotency_key,
@@ -237,7 +265,11 @@ const SUBMISSION_COLUMNS = `
237
265
  unknown_reason,
238
266
  unknown_tool_call_ids_json,
239
267
  parent_submission_id,
240
- parent_tool_call_id
268
+ parent_tool_call_id,
269
+ admission_group,
270
+ admission_fence_json,
271
+ worker_admission_json,
272
+ message_admission_json
241
273
  `;
242
274
 
243
275
  const CHILD_RESERVATION_COLUMNS = `
@@ -254,7 +286,7 @@ const CHILD_RESERVATION_COLUMNS = `
254
286
  released_at
255
287
  `;
256
288
 
257
- /** The branded ToolCallId schema, reached through the session port so no core import is needed. */
289
+ /** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
258
290
  const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
259
291
  const ToolCallIdList = Schema.Array(ToolCallIdSchema);
260
292
 
@@ -284,10 +316,15 @@ const decodeSuspensionSnapshot = Schema.decodeUnknownEffect(SuspensionSnapshot);
284
316
  const decodeApprovalDecisionIntent = Schema.decodeUnknownEffect(ApprovalDecisionIntent);
285
317
  const decodeUnknownResolutionIntent = Schema.decodeUnknownEffect(UnknownResolutionIntent);
286
318
  const decodeParentLinkage = Schema.decodeUnknownEffect(ParentLinkage);
319
+
287
320
  const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
288
321
  ChildBudgetReservationSnapshot,
289
322
  );
323
+
290
324
  const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
325
+ const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
326
+ const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
327
+ const isDoStorageError = Schema.is(DoStorageError);
291
328
 
292
329
  /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
293
330
  const internalFailure =
@@ -319,6 +356,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
319
356
  const failpoint = yield* DoStorageFailpoint;
320
357
  const sql = yield* SqlClientService.SqlClient;
321
358
  const crypto = yield* Crypto.Crypto;
359
+ const admissionFence = yield* SubmissionAdmissionFence;
322
360
  const journal = yield* initializeDoJournal(sql, failpoint.hit, config.maxStoredValueBytes);
323
361
 
324
362
  const hitFailpoint = (
@@ -336,6 +374,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
336
374
  A,
337
375
  E extends
338
376
  | AdmissionConflict
377
+ | AdmissionPolicyError
339
378
  | ApprovalConflict
340
379
  | ChildReservationConflict
341
380
  | JoinedToHost
@@ -351,7 +390,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
351
390
  .withWriteTransaction(operation)(effect)
352
391
  .pipe(
353
392
  Effect.mapError((error) =>
354
- error instanceof DoStorageError ? internalFailure(operation)(error) : error,
393
+ isDoStorageError(error) ? internalFailure(operation)(error) : error,
355
394
  ),
356
395
  );
357
396
 
@@ -385,7 +424,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
385
424
  FROM effect_agent_submissions
386
425
  WHERE submission_id = ${submissionId}
387
426
  `.pipe(Effect.mapError(sqlFailure(operation)));
427
+
388
428
  const decoded = yield* decodeSubmissionRows(operation, submissionId, rows);
429
+
389
430
  if (decoded.length > 1) {
390
431
  return yield* corruptionFailure(
391
432
  operation,
@@ -394,6 +435,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
394
435
  "A submission primary key returned more than one row.",
395
436
  );
396
437
  }
438
+
397
439
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
398
440
  });
399
441
 
@@ -402,12 +444,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
402
444
  submissionId: string,
403
445
  ): Effect.fn.Return<SubmissionRow, LedgerError> {
404
446
  const submission = yield* readSubmission(operation, submissionId);
447
+
405
448
  if (Option.isNone(submission)) {
406
449
  return yield* LedgerError.make({
407
450
  operation,
408
451
  message: `Unknown submission ${submissionId}.`,
409
452
  });
410
453
  }
454
+
411
455
  return submission.value;
412
456
  });
413
457
 
@@ -426,12 +470,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
426
470
  FROM effect_agent_submission_ownership
427
471
  WHERE submission_id = ${submissionId}
428
472
  `.pipe(Effect.mapError(sqlFailure(operation)));
473
+
429
474
  const decoded = yield* decodeRows(
430
475
  Schema.Array(OwnershipRow),
431
476
  "effect_agent_submission_ownership",
432
477
  submissionId,
433
478
  rows,
434
479
  ).pipe(Effect.mapError(internalFailure(operation)));
480
+
435
481
  if (decoded.length > 1) {
436
482
  return yield* corruptionFailure(
437
483
  operation,
@@ -440,23 +486,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
440
486
  "An ownership primary key returned more than one row.",
441
487
  );
442
488
  }
489
+
443
490
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
444
491
  });
445
492
 
446
- const conversationEpoch = Effect.fn("DoSubmissionLedger.conversationEpoch")(function* (
493
+ const threadEpoch = Effect.fn("DoSubmissionLedger.threadEpoch")(function* (
447
494
  operation: string,
448
- conversationId: string,
495
+ threadId: string,
449
496
  ): Effect.fn.Return<ProducerEpoch, LedgerError> {
450
- const conversations = yield* journal
451
- .getConversation(conversationId)
497
+ const threads = yield* journal
498
+ .getThread(threadId)
452
499
  .pipe(Effect.mapError(internalFailure(operation)));
453
- return conversations.length === 0 ? EPOCH_ZERO : conversations[0].producer_epoch;
500
+
501
+ return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
454
502
  });
455
503
 
456
504
  /**
457
505
  * Verify inside the surrounding write transaction that the presented token still owns the
458
506
  * Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
459
- * Conversation's current producer epoch (DUR-006).
507
+ * Thread's current producer epoch (DUR-006).
460
508
  */
461
509
  const requireOwnership = Effect.fn("DoSubmissionLedger.requireOwnership")(function* (
462
510
  operation: string,
@@ -464,13 +512,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
464
512
  ownershipToken: string,
465
513
  ): Effect.fn.Return<OwnershipRow, OwnershipLost | LedgerError> {
466
514
  const ownership = yield* readOwnership(operation, submission.submission_id);
467
- if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
468
- const actualEpoch = yield* conversationEpoch(operation, submission.conversation_id);
469
- const submissionId = yield* Schema.decodeUnknownEffect(
470
- SubmissionSnapshot.fields.submissionId,
471
- )(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
515
+ const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
516
+
517
+ if (
518
+ Option.isNone(ownership) ||
519
+ ownership.value.ownership_token !== ownershipToken ||
520
+ ownership.value.producer_epoch !== actualEpoch
521
+ ) {
522
+ const submissionId = yield* Schema.decodeEffect(SubmissionSnapshot.fields.submissionId)(
523
+ submission.submission_id,
524
+ ).pipe(Effect.mapError(internalFailure(operation)));
525
+
472
526
  return yield* OwnershipLost.make({ submissionId, actualEpoch });
473
527
  }
528
+
474
529
  return ownership.value;
475
530
  });
476
531
 
@@ -489,6 +544,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
489
544
  ),
490
545
  ),
491
546
  );
547
+
492
548
  const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
493
549
  Effect.mapError((error) =>
494
550
  corruptionFailure(
@@ -499,6 +555,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
499
555
  ),
500
556
  ),
501
557
  );
558
+
502
559
  if ((row.parent_submission_id === null) !== (row.parent_tool_call_id === null)) {
503
560
  return yield* corruptionFailure(
504
561
  operation,
@@ -507,9 +564,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
507
564
  "A parent linkage must record both the parent Submission and the parent Tool Call.",
508
565
  );
509
566
  }
567
+
510
568
  return yield* decodeSubmissionSnapshotUnknown({
511
569
  submissionId: row.submission_id,
512
- conversationId: row.conversation_id,
570
+ threadId: row.thread_id,
513
571
  queueSequence: row.queue_sequence,
514
572
  principal: row.principal,
515
573
  idempotencyKey: row.idempotency_key,
@@ -521,6 +579,28 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
521
579
  receiptId: row.receipt_id,
522
580
  state: row.state,
523
581
  createdAt: row.created_at,
582
+ ...(row.admission_group === null ? {} : { admissionGroup: row.admission_group }),
583
+ ...(row.worker_admission_json === null
584
+ ? {}
585
+ : {
586
+ workerAdmission: yield* parseStoredJsonText(row.worker_admission_json).pipe(
587
+ Effect.mapError(internalFailure(operation)),
588
+ ),
589
+ }),
590
+ ...(row.message_admission_json === null
591
+ ? {}
592
+ : {
593
+ messageAdmission: yield* parseStoredJsonText(row.message_admission_json).pipe(
594
+ Effect.mapError(internalFailure(operation)),
595
+ ),
596
+ }),
597
+ ...(row.admission_fence_json === null
598
+ ? {}
599
+ : {
600
+ admissionFence: yield* parseStoredJsonText(row.admission_fence_json).pipe(
601
+ Effect.mapError(internalFailure(operation)),
602
+ ),
603
+ }),
524
604
  ...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
525
605
  ...(row.ready_at === null ? {} : { readyAt: row.ready_at }),
526
606
  ...(row.parent_submission_id === null || row.parent_tool_call_id === null
@@ -561,12 +641,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
561
641
  FROM effect_agent_settlement_reservations
562
642
  WHERE submission_id = ${submissionId}
563
643
  `.pipe(Effect.mapError(sqlFailure(operation)));
644
+
564
645
  const decoded = yield* decodeRows(
565
646
  Schema.Array(ReservationRow),
566
647
  "effect_agent_settlement_reservations",
567
648
  submissionId,
568
649
  rows,
569
650
  ).pipe(Effect.mapError(internalFailure(operation)));
651
+
570
652
  if (decoded.length > 1) {
571
653
  return yield* corruptionFailure(
572
654
  operation,
@@ -575,6 +657,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
575
657
  "A settlement reservation primary key returned more than one row.",
576
658
  );
577
659
  }
660
+
578
661
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
579
662
  });
580
663
 
@@ -592,12 +675,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
592
675
  FROM effect_agent_abort_intents
593
676
  WHERE submission_id = ${submissionId}
594
677
  `.pipe(Effect.mapError(sqlFailure(operation)));
678
+
595
679
  const decoded = yield* decodeRows(
596
680
  Schema.Array(AbortIntentRow),
597
681
  "effect_agent_abort_intents",
598
682
  submissionId,
599
683
  rows,
600
684
  ).pipe(Effect.mapError(internalFailure(operation)));
685
+
601
686
  if (decoded.length > 1) {
602
687
  return yield* corruptionFailure(
603
688
  operation,
@@ -606,6 +691,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
606
691
  "An abort intent primary key returned more than one row.",
607
692
  );
608
693
  }
694
+
609
695
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
610
696
  });
611
697
 
@@ -629,6 +715,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
629
715
  WHERE parent_submission_id = ${parentSubmissionId}
630
716
  ORDER BY child_submission_id ASC
631
717
  `.pipe(Effect.mapError(sqlFailure(operation)));
718
+
632
719
  return yield* decodeRows(
633
720
  Schema.Array(ChildSettlementMarkerRow),
634
721
  "effect_agent_child_settlements",
@@ -651,6 +738,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
651
738
  ): Effect.fn.Return<boolean, LedgerError> {
652
739
  if (markerChildren.has(childSubmissionId)) return true;
653
740
  const childRow = yield* readSubmission(operation, childSubmissionId);
741
+
654
742
  return Option.isSome(childRow) && childRow.value.state === "settled";
655
743
  });
656
744
 
@@ -671,7 +759,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
671
759
  FROM effect_agent_child_reservations
672
760
  WHERE reservation_id = ${reservationId}
673
761
  `.pipe(Effect.mapError(sqlFailure(operation)));
762
+
674
763
  const decoded = yield* decodeChildReservationRows(operation, reservationId, rows);
764
+
675
765
  if (decoded.length > 1) {
676
766
  return yield* corruptionFailure(
677
767
  operation,
@@ -680,6 +770,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
680
770
  "A child reservation primary key returned more than one row.",
681
771
  );
682
772
  }
773
+
683
774
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
684
775
  });
685
776
 
@@ -695,11 +786,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
695
786
  WHERE parent_submission_id = ${parentSubmissionId}
696
787
  AND parent_tool_call_id = ${parentToolCallId}
697
788
  `.pipe(Effect.mapError(sqlFailure(operation)));
789
+
698
790
  const decoded = yield* decodeChildReservationRows(
699
791
  operation,
700
792
  `${parentSubmissionId}/${parentToolCallId}`,
701
793
  rows,
702
794
  );
795
+
703
796
  if (decoded.length > 1) {
704
797
  return yield* corruptionFailure(
705
798
  operation,
@@ -708,6 +801,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
708
801
  "A parent Tool Call returned more than one child reservation.",
709
802
  );
710
803
  }
804
+
711
805
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
712
806
  },
713
807
  );
@@ -725,13 +819,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
725
819
  row.reservation_id,
726
820
  error.message,
727
821
  );
822
+
728
823
  const allocation = yield* parseStoredJsonText(row.allocation_json).pipe(
729
824
  Effect.mapError(rowFailure),
730
825
  );
826
+
731
827
  const accounting =
732
828
  row.accounting_json === null
733
829
  ? undefined
734
830
  : yield* parseStoredJsonText(row.accounting_json).pipe(Effect.mapError(rowFailure));
831
+
735
832
  return yield* decodeChildReservationSnapshotUnknown({
736
833
  reservationId: row.reservation_id,
737
834
  parentSubmissionId: row.parent_submission_id,
@@ -763,6 +860,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
763
860
  WHERE submission_id = ${submissionId}
764
861
  ORDER BY tool_call_id ASC
765
862
  `.pipe(Effect.mapError(sqlFailure(operation)));
863
+
766
864
  return yield* decodeRows(
767
865
  Schema.Array(ApprovalDecisionRow),
768
866
  "effect_agent_approval_decisions",
@@ -810,6 +908,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
810
908
  WHERE submission_id = ${submissionId}
811
909
  ORDER BY tool_call_id ASC
812
910
  `.pipe(Effect.mapError(sqlFailure(operation)));
911
+
813
912
  return yield* decodeRows(
814
913
  Schema.Array(UnknownResolutionRow),
815
914
  "effect_agent_unknown_resolutions",
@@ -834,6 +933,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
834
933
  ),
835
934
  ),
836
935
  );
936
+
837
937
  return yield* decodeUnknownResolutionIntent({
838
938
  submissionId: row.submission_id,
839
939
  toolCallId: row.tool_call_id,
@@ -860,6 +960,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
860
960
  submission: SubmissionRow,
861
961
  ): Effect.fn.Return<ReadonlyArray<typeof ToolCallIdSchema.Type>, LedgerError> {
862
962
  if (submission.unknown_tool_call_ids_json === null) return [];
963
+
863
964
  return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(
864
965
  Effect.mapError((error) =>
865
966
  corruptionFailure(
@@ -880,22 +981,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
880
981
  */
881
982
  const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (
882
983
  operation: string,
883
- conversationId: string,
984
+ threadId: string,
884
985
  submissionId: SubmissionId,
885
986
  ): Effect.fn.Return<string | undefined, LedgerError> {
886
987
  const recordId = submissionAbortRecordId(submissionId);
988
+
887
989
  const rows = yield* sql<Record<string, unknown>>`
888
990
  SELECT record_id
889
991
  FROM effect_agent_canonical_records
890
- WHERE conversation_id = ${conversationId}
992
+ WHERE thread_id = ${threadId}
891
993
  AND record_id = ${recordId}
892
994
  `.pipe(Effect.mapError(sqlFailure(operation)));
995
+
893
996
  const decoded = yield* decodeRows(
894
997
  Schema.Array(CanonicalRecordIdRow),
895
998
  "effect_agent_canonical_records",
896
- `${conversationId}/${recordId}`,
999
+ `${threadId}/${recordId}`,
897
1000
  rows,
898
1001
  ).pipe(Effect.mapError(internalFailure(operation)));
1002
+
899
1003
  return decoded.length === 0 ? undefined : recordId;
900
1004
  });
901
1005
 
@@ -907,9 +1011,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
907
1011
  ): Effect.fn.Return<AbortIntent, LedgerError> {
908
1012
  const canonicalRecordId = yield* canonicalAbortRecordId(
909
1013
  operation,
910
- submission.conversation_id,
1014
+ submission.thread_id,
911
1015
  submissionId,
912
1016
  );
1017
+
913
1018
  return yield* decodeAbortIntent({
914
1019
  submissionId: row.submission_id,
915
1020
  author: row.author,
@@ -938,47 +1043,92 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
938
1043
  const admit: SubmissionLedger["Service"]["admit"] = Effect.fn("DoSubmissionLedger.admit")(
939
1044
  function* (request: AdmissionRequest) {
940
1045
  const operation = "ledger admit";
941
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AdmissionRequest))(
942
- request,
943
- ).pipe(Effect.mapError(internalFailure(operation)));
1046
+
1047
+ const validated = yield* Schema.decodeEffect(Schema.toType(AdmissionRequest))(request).pipe(
1048
+ Effect.mapError(internalFailure(operation)),
1049
+ );
1050
+
944
1051
  const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(
945
1052
  Effect.mapError(internalFailure(operation)),
946
1053
  );
1054
+
947
1055
  // The platform's ~2 MB per-value bound, refused typed BEFORE any durable mutation
948
1056
  // (resource-limits gate; oversized payloads are the designed R2 overflow path).
949
1057
  yield* journal
950
1058
  .checkValueBound(operation, inputJson)
951
1059
  .pipe(Effect.mapError(internalFailure(operation)));
1060
+
1061
+ const workerAdmissionJson =
1062
+ validated.workerAdmission === undefined
1063
+ ? null
1064
+ : yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
1065
+ validated.workerAdmission,
1066
+ ).pipe(Effect.mapError(internalFailure(operation)));
1067
+
1068
+ if (
1069
+ workerAdmissionJson !== null &&
1070
+ new TextEncoder().encode(workerAdmissionJson).byteLength > config.maxStoredValueBytes
1071
+ ) {
1072
+ return yield* LedgerError.make({
1073
+ operation,
1074
+ message: "Worker admission metadata exceeds the stored value bound",
1075
+ });
1076
+ }
1077
+
1078
+ const messageAdmissionJson =
1079
+ validated.messageAdmission === undefined
1080
+ ? null
1081
+ : yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
1082
+ validated.messageAdmission,
1083
+ ).pipe(Effect.mapError(internalFailure(operation)));
1084
+
1085
+ if (
1086
+ messageAdmissionJson !== null &&
1087
+ new TextEncoder().encode(messageAdmissionJson).byteLength > config.maxStoredValueBytes
1088
+ ) {
1089
+ return yield* LedgerError.make({
1090
+ operation,
1091
+ message: "Message admission metadata exceeds the stored value bound",
1092
+ });
1093
+ }
1094
+
952
1095
  const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
953
1096
  Effect.mapError(internalFailure(operation)),
954
1097
  );
955
- // Routable Submission identity (D-P6-5): `{uuidv7}:{conversationId}`. The cross-DO
1098
+
1099
+ // Routable Submission identity (D-P6-5): `{uuidv7}:{threadId}`. The cross-DO
956
1100
  // routing layer parses ITS OWN minted format (split at the first ":") to address
957
- // submissionId-only operations to the owning Conversation Object; the id stays opaque
1101
+ // submissionId-only operations to the owning Thread Object; the id stays opaque
958
1102
  // to every other component, exactly like DN's `submission-{uuid}` prefix.
959
- const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.conversationId}`;
1103
+ const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.threadId}`;
1104
+
960
1105
  if (mintedSubmissionId.length > MAX_IDENTIFIER_LENGTH) {
961
1106
  return yield* LedgerError.make({
962
1107
  operation,
963
1108
  message:
964
1109
  `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds ` +
965
- `the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Conversation identity.`,
1110
+ `the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Thread identity.`,
966
1111
  });
967
1112
  }
968
1113
  const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
1114
+
969
1115
  yield* hitFailpoint("ledger:admit:before", operation);
1116
+
970
1117
  const result = yield* inWriteTransaction(
971
1118
  operation,
972
1119
  Effect.gen(function* () {
973
- const keyRowKey = `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`;
1120
+ const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
1121
+
974
1122
  const existingRows = yield* sql<Record<string, unknown>>`
975
1123
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
976
1124
  FROM effect_agent_submissions
977
- WHERE conversation_id = ${validated.conversationId}
1125
+ WHERE thread_id = ${validated.threadId}
978
1126
  AND principal = ${validated.principal}
979
1127
  AND idempotency_key = ${validated.idempotencyKey}
980
1128
  `.pipe(Effect.mapError(sqlFailure(operation)));
1129
+
981
1130
  const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
1131
+
982
1132
  if (existing.length > 1) {
983
1133
  return yield* corruptionFailure(
984
1134
  operation,
@@ -996,15 +1146,61 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
996
1146
  existing[0].parent_tool_call_id === null
997
1147
  : existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId &&
998
1148
  existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
1149
+
999
1150
  if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
1000
1151
  return yield* AdmissionConflict.make({
1001
- conversationId: validated.conversationId,
1152
+ threadId: validated.threadId,
1002
1153
  principal: validated.principal,
1003
1154
  idempotencyKey: validated.idempotencyKey,
1004
1155
  existingInputDigest: existing[0].input_digest,
1005
1156
  attemptedInputDigest: validated.inputDigest,
1006
1157
  });
1007
1158
  }
1159
+
1160
+ const retainedWorkerAdmission =
1161
+ existing[0].worker_admission_json === null
1162
+ ? undefined
1163
+ : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1164
+ existing[0].worker_admission_json,
1165
+ ).pipe(Effect.mapError(internalFailure(operation)));
1166
+
1167
+ const retainedInputMessage =
1168
+ existing[0].message_admission_json === null
1169
+ ? undefined
1170
+ : yield* Schema.decodeEffect(Schema.fromJsonString(InputMessage))(
1171
+ existing[0].message_admission_json,
1172
+ ).pipe(Effect.mapError(internalFailure(operation)));
1173
+
1174
+ const retainedFence =
1175
+ existing[0].admission_fence_json === null
1176
+ ? undefined
1177
+ : yield* Schema.decodeEffect(Schema.fromJsonString(AdmissionFence))(
1178
+ existing[0].admission_fence_json,
1179
+ ).pipe(Effect.mapError(internalFailure(operation)));
1180
+
1181
+ if (
1182
+ (existing[0].admission_group ?? undefined) !== validated.admissionGroup ||
1183
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission))(
1184
+ retainedWorkerAdmission,
1185
+ validated.workerAdmission,
1186
+ ) ||
1187
+ !Schema.toEquivalence(Schema.optional(InputMessage))(
1188
+ retainedInputMessage,
1189
+ validated.messageAdmission,
1190
+ ) ||
1191
+ !Schema.toEquivalence(Schema.optional(AdmissionFence))(
1192
+ retainedFence,
1193
+ validated.admissionFence,
1194
+ )
1195
+ )
1196
+ return yield* AdmissionConflict.make({
1197
+ threadId: validated.threadId,
1198
+ principal: validated.principal,
1199
+ idempotencyKey: validated.idempotencyKey,
1200
+ existingInputDigest: existing[0].input_digest,
1201
+ attemptedInputDigest: validated.inputDigest,
1202
+ });
1203
+
1008
1204
  return yield* decodeAdmissionResult({
1009
1205
  submissionId: existing[0].submission_id,
1010
1206
  receiptId: existing[0].receipt_id,
@@ -1014,26 +1210,78 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1014
1210
  }).pipe(Effect.mapError(internalFailure(operation)));
1015
1211
  }
1016
1212
 
1213
+ // The first accepted input fixes ordinary/worker lane identity atomically with admission.
1214
+ // Canonical origin materialization can lag admission; a log scan cannot fence that race.
1215
+ const firstRows = yield* sql<Record<string, unknown>>`
1216
+ SELECT worker_admission_json FROM effect_agent_submissions
1217
+ WHERE thread_id=${validated.threadId} ORDER BY queue_sequence LIMIT 1
1218
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1219
+
1220
+ const first = yield* Schema.decodeUnknownEffect(
1221
+ Schema.Array(
1222
+ Schema.Struct({
1223
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
1224
+ }),
1225
+ ),
1226
+ )(firstRows).pipe(Effect.mapError(internalFailure(operation)));
1227
+
1228
+ if (first[0] !== undefined) {
1229
+ const previous =
1230
+ first[0].worker_admission_json === null
1231
+ ? undefined
1232
+ : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1233
+ first[0].worker_admission_json,
1234
+ ).pipe(Effect.mapError(internalFailure(operation)));
1235
+
1236
+ if (
1237
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
1238
+ previous?.origin,
1239
+ validated.workerAdmission?.origin,
1240
+ )
1241
+ )
1242
+ return yield* AdmissionPolicyError.make({
1243
+ reason: "refused",
1244
+ code: "worker-origin-conflict",
1245
+ });
1246
+ }
1247
+
1248
+ yield* admissionFence.check(validated);
1249
+ if (validated.admissionGroup !== undefined) {
1250
+ const occupied = yield* sql<Record<string, unknown>>`
1251
+ SELECT submission_id FROM effect_agent_submissions
1252
+ WHERE thread_id=${validated.threadId} AND admission_group=${validated.admissionGroup} AND state<>'settled' LIMIT 1
1253
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1254
+
1255
+ if (occupied.length > 0)
1256
+ return yield* AdmissionPolicyError.make({
1257
+ reason: "occupied",
1258
+ code: "admission-group",
1259
+ });
1260
+ }
1261
+
1017
1262
  const maxRows = yield* sql<Record<string, unknown>>`
1018
1263
  SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
1019
1264
  FROM effect_agent_submissions
1020
- WHERE conversation_id = ${validated.conversationId}
1265
+ WHERE thread_id = ${validated.threadId}
1021
1266
  `.pipe(Effect.mapError(sqlFailure(operation)));
1267
+
1022
1268
  const decodedMax = yield* decodeRows(
1023
1269
  Schema.Array(MaxQueueSequenceRow),
1024
1270
  "effect_agent_submissions",
1025
- validated.conversationId,
1271
+ validated.threadId,
1026
1272
  maxRows,
1027
1273
  ).pipe(Effect.mapError(internalFailure(operation)));
1274
+
1028
1275
  const queueSequence = yield* decodeQueueSequence(
1029
1276
  (decodedMax[0]?.max_queue_sequence ?? 0) + 1,
1030
1277
  ).pipe(Effect.mapError(internalFailure(operation)));
1278
+
1031
1279
  const now = yield* currentInstant;
1032
1280
 
1033
1281
  yield* sql`
1034
1282
  INSERT INTO effect_agent_submissions (
1035
1283
  submission_id,
1036
- conversation_id,
1284
+ thread_id,
1037
1285
  queue_sequence,
1038
1286
  principal,
1039
1287
  idempotency_key,
@@ -1046,10 +1294,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1046
1294
  state,
1047
1295
  created_at,
1048
1296
  parent_submission_id,
1049
- parent_tool_call_id
1297
+ parent_tool_call_id,
1298
+ admission_group,
1299
+ admission_fence_json,
1300
+ worker_admission_json,
1301
+ message_admission_json
1050
1302
  ) VALUES (
1051
1303
  ${mintedSubmissionId},
1052
- ${validated.conversationId},
1304
+ ${validated.threadId},
1053
1305
  ${queueSequence},
1054
1306
  ${validated.principal},
1055
1307
  ${validated.idempotencyKey},
@@ -1062,7 +1314,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1062
1314
  'admitted',
1063
1315
  ${now.iso},
1064
1316
  ${validated.parentLinkage?.parentSubmissionId ?? null},
1065
- ${validated.parentLinkage?.parentToolCallId ?? null}
1317
+ ${validated.parentLinkage?.parentToolCallId ?? null},
1318
+ ${validated.admissionGroup ?? null},
1319
+ ${validated.admissionFence === undefined ? null : JSON.stringify(validated.admissionFence)},
1320
+ ${workerAdmissionJson},
1321
+ ${messageAdmissionJson}
1066
1322
  )
1067
1323
  `.pipe(Effect.mapError(sqlFailure(operation)));
1068
1324
 
@@ -1075,7 +1331,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1075
1331
  }).pipe(Effect.mapError(internalFailure(operation)));
1076
1332
  }),
1077
1333
  );
1334
+
1078
1335
  yield* hitFailpoint("ledger:admit:after", operation);
1336
+
1079
1337
  return result;
1080
1338
  },
1081
1339
  );
@@ -1084,16 +1342,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1084
1342
  "DoSubmissionLedger.markReady",
1085
1343
  )(function* (request: MarkReadyRequest) {
1086
1344
  const operation = "ledger mark ready";
1087
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkReadyRequest))(
1088
- request,
1089
- ).pipe(Effect.mapError(internalFailure(operation)));
1345
+
1346
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkReadyRequest))(request).pipe(
1347
+ Effect.mapError(internalFailure(operation)),
1348
+ );
1349
+
1090
1350
  yield* hitFailpoint("ledger:mark-ready:before", operation);
1091
1351
  yield* inWriteTransaction(
1092
1352
  operation,
1093
1353
  Effect.gen(function* () {
1094
1354
  const submission = yield* requireSubmission(operation, validated.submissionId);
1355
+
1095
1356
  if (submission.state !== "admitted") return;
1096
1357
  const now = yield* currentInstant;
1358
+
1097
1359
  yield* sql`
1098
1360
  UPDATE effect_agent_submissions
1099
1361
  SET state = 'ready', ready_at = ${now.iso}
@@ -1107,40 +1369,48 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1107
1369
  const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn("DoSubmissionLedger.lookup")(
1108
1370
  function* (request: SubmissionLookup) {
1109
1371
  const operation = "ledger lookup";
1110
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookup))(
1111
- request,
1112
- ).pipe(Effect.mapError(internalFailure(operation)));
1372
+
1373
+ const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookup))(request).pipe(
1374
+ Effect.mapError(internalFailure(operation)),
1375
+ );
1376
+
1113
1377
  if (validated._tag === "SubmissionLookupById") {
1114
1378
  const row = yield* readSubmission(operation, validated.submissionId);
1379
+
1115
1380
  if (Option.isNone(row)) return Option.none();
1381
+
1116
1382
  return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
1117
1383
  }
1384
+
1118
1385
  const rows = yield* sql<Record<string, unknown>>`
1119
1386
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1120
1387
  FROM effect_agent_submissions
1121
- WHERE conversation_id = ${validated.conversationId}
1388
+ WHERE thread_id = ${validated.threadId}
1122
1389
  AND principal = ${validated.principal}
1123
1390
  AND idempotency_key = ${validated.idempotencyKey}
1124
1391
  `.pipe(Effect.mapError(sqlFailure(operation)));
1392
+
1125
1393
  const decoded = yield* decodeSubmissionRows(
1126
1394
  operation,
1127
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1395
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1128
1396
  rows,
1129
1397
  );
1398
+
1130
1399
  if (decoded.length > 1) {
1131
1400
  return yield* corruptionFailure(
1132
1401
  operation,
1133
1402
  "effect_agent_submissions",
1134
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1403
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1135
1404
  "An admission idempotency key returned more than one row.",
1136
1405
  );
1137
1406
  }
1138
1407
  if (decoded.length === 0) return Option.none();
1408
+
1139
1409
  return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
1140
1410
  },
1141
1411
  );
1142
1412
 
1143
- // This LOCAL facet is the authoritative owner of every Conversation stored in this Durable
1413
+ // This LOCAL facet is the authoritative owner of every Thread stored in this Durable
1144
1414
  // Object, so the key-scoped read IS the admission truth and the tri-state degenerates to
1145
1415
  // NotAdmitted or Admitted (SUB-031). `AdmissionIndeterminate` becomes real one layer out:
1146
1416
  // the WP2 routed decorator answers it when the OWNING Durable Object is unreachable.
@@ -1148,30 +1418,35 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1148
1418
  "DoSubmissionLedger.resolveAdmission",
1149
1419
  )(function* (request: SubmissionLookupByKey) {
1150
1420
  const operation = "ledger resolve admission";
1151
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookupByKey))(
1421
+
1422
+ const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookupByKey))(
1152
1423
  request,
1153
1424
  ).pipe(Effect.mapError(internalFailure(operation)));
1425
+
1154
1426
  const rows = yield* sql<Record<string, unknown>>`
1155
1427
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1156
1428
  FROM effect_agent_submissions
1157
- WHERE conversation_id = ${validated.conversationId}
1429
+ WHERE thread_id = ${validated.threadId}
1158
1430
  AND principal = ${validated.principal}
1159
1431
  AND idempotency_key = ${validated.idempotencyKey}
1160
1432
  `.pipe(Effect.mapError(sqlFailure(operation)));
1433
+
1161
1434
  const decoded = yield* decodeSubmissionRows(
1162
1435
  operation,
1163
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1436
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1164
1437
  rows,
1165
1438
  );
1439
+
1166
1440
  if (decoded.length > 1) {
1167
1441
  return yield* corruptionFailure(
1168
1442
  operation,
1169
1443
  "effect_agent_submissions",
1170
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1444
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1171
1445
  "An admission idempotency key returned more than one row.",
1172
1446
  );
1173
1447
  }
1174
1448
  if (decoded.length === 0) return AdmissionNotAdmitted.make();
1449
+
1175
1450
  return AdmissionAdmitted.make({
1176
1451
  submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
1177
1452
  });
@@ -1180,71 +1455,103 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1180
1455
  const claim: SubmissionLedger["Service"]["claim"] = Effect.fn("DoSubmissionLedger.claim")(
1181
1456
  function* (request: ClaimRequest) {
1182
1457
  const operation = "ledger claim";
1183
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimRequest))(
1184
- request,
1185
- ).pipe(Effect.mapError(internalFailure(operation)));
1458
+
1459
+ const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
1460
+ Effect.mapError(internalFailure(operation)),
1461
+ );
1462
+
1186
1463
  const attemptId = `attempt-${yield* mintUuid(operation)}`;
1187
1464
  const ownershipToken = `owner-${yield* mintUuid(operation)}`;
1465
+
1188
1466
  yield* hitFailpoint("ledger:claim:before", operation);
1467
+
1189
1468
  const claimed = yield* inWriteTransaction(
1190
1469
  operation,
1191
1470
  Effect.gen(function* () {
1471
+ const now = yield* currentInstant;
1472
+
1473
+ // Ownership belongs to the whole Thread, including unknown work skipped below.
1474
+ // Stored lease instants are normalized UTC strings, so the latest expiry covers
1475
+ // every live lease. This check and the epoch grant share one write transaction.
1476
+ const ownershipRows = yield* sql<Record<string, unknown>>`
1477
+ SELECT ownership.*
1478
+ FROM effect_agent_submission_ownership AS ownership
1479
+ JOIN effect_agent_submissions AS submission
1480
+ ON submission.submission_id = ownership.submission_id
1481
+ WHERE submission.thread_id = ${validated.threadId}
1482
+ ORDER BY ownership.lease_expires_at DESC
1483
+ LIMIT 1
1484
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1485
+
1486
+ const ownership = yield* decodeRows(
1487
+ Schema.Array(OwnershipRow),
1488
+ "effect_agent_submission_ownership",
1489
+ validated.threadId,
1490
+ ownershipRows,
1491
+ ).pipe(Effect.mapError(internalFailure(operation)));
1492
+
1493
+ if (ownership.length > 0) {
1494
+ const expiresAt = yield* timestampMillis(
1495
+ operation,
1496
+ ownership[0].submission_id,
1497
+ )(ownership[0].lease_expires_at);
1498
+
1499
+ if (expiresAt > now.millis) return Option.none<Claim>();
1500
+ }
1501
+
1192
1502
  const headRows = yield* sql<Record<string, unknown>>`
1193
1503
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1194
1504
  FROM effect_agent_submissions
1195
- WHERE conversation_id = ${validated.conversationId}
1505
+ WHERE thread_id = ${validated.threadId}
1196
1506
  AND state <> 'settled'
1507
+ AND (
1508
+ state <> 'unknown' OR EXISTS (
1509
+ SELECT 1 FROM effect_agent_abort_intents
1510
+ WHERE submission_id = effect_agent_submissions.submission_id
1511
+ )
1512
+ )
1197
1513
  ORDER BY queue_sequence ASC
1198
1514
  LIMIT 1
1199
1515
  `.pipe(Effect.mapError(sqlFailure(operation)));
1200
- const heads = yield* decodeSubmissionRows(operation, validated.conversationId, headRows);
1516
+
1517
+ const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
1518
+
1201
1519
  if (heads.length === 0) return Option.none<Claim>();
1202
1520
  const head = heads[0];
1203
1521
 
1204
- // A joining/joined head is host-owned and a suspended/unknown head is durably
1205
- // blocked (DUR-017); the lane produces no claim and later ready work is never
1206
- // skipped past the blocked head (DUR-004).
1522
+ // Approval/delegation suspension and joined work retain their queue barrier.
1523
+ // Unknown work with an abort intent is selected for cleanup without Tool replay.
1207
1524
  if (
1208
1525
  head.state === "joining" ||
1209
1526
  head.state === "joined" ||
1210
1527
  head.state === "suspended" ||
1211
- head.state === "unknown"
1528
+ (head.state === "unknown" &&
1529
+ Option.isNone(yield* readAbortIntent(operation, head.submission_id)))
1212
1530
  ) {
1213
1531
  return Option.none<Claim>();
1214
1532
  }
1215
1533
 
1216
- const now = yield* currentInstant;
1217
- const ownership = yield* readOwnership(operation, head.submission_id);
1218
- if (Option.isSome(ownership)) {
1219
- const expiresAt = yield* timestampMillis(
1220
- operation,
1221
- head.submission_id,
1222
- )(ownership.value.lease_expires_at);
1223
- // A live lease blocks every new claim; expiry alone only revokes the liveness
1224
- // assumption — correctness stays with producer-epoch fencing (D5). In DC a live
1225
- // lease under another token can only come from an evicted incarnation.
1226
- if (expiresAt > now.millis) return Option.none<Claim>();
1227
- }
1228
-
1229
- // Bump the Conversation's producer epoch atomically with the claim so every stale
1230
- // Attempt is fenced out of canonical appends (DUR-006). A Conversation that was
1534
+ // Bump the Thread's producer epoch atomically with the claim so every stale
1535
+ // Attempt is fenced out of canonical appends (DUR-006). A Thread that was
1231
1536
  // never materialized (eviction between admission and materialization) is created
1232
1537
  // here so recovery can claim first and re-materialize idempotently at this epoch.
1233
- const conversations = yield* journal
1234
- .getConversation(head.conversation_id)
1538
+ const threads = yield* journal
1539
+ .getThread(head.thread_id)
1235
1540
  .pipe(Effect.mapError(internalFailure(operation)));
1541
+
1236
1542
  let producerEpoch: number;
1237
- if (conversations.length === 0) {
1543
+
1544
+ if (threads.length === 0) {
1238
1545
  producerEpoch = 1;
1239
1546
  yield* sql`
1240
- INSERT INTO effect_agent_conversations (
1241
- conversation_id,
1547
+ INSERT INTO effect_agent_threads (
1548
+ thread_id,
1242
1549
  created_at,
1243
1550
  tail_sequence,
1244
1551
  tail_digest,
1245
1552
  producer_epoch
1246
1553
  ) VALUES (
1247
- ${head.conversation_id},
1554
+ ${head.thread_id},
1248
1555
  ${now.iso},
1249
1556
  0,
1250
1557
  ${EMPTY_TAIL_DIGEST},
@@ -1252,15 +1559,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1252
1559
  )
1253
1560
  `.pipe(Effect.mapError(sqlFailure(operation)));
1254
1561
  } else {
1255
- producerEpoch = conversations[0].producer_epoch + 1;
1562
+ producerEpoch = threads[0].producer_epoch + 1;
1256
1563
  yield* sql`
1257
- UPDATE effect_agent_conversations
1564
+ UPDATE effect_agent_threads
1258
1565
  SET producer_epoch = ${producerEpoch}
1259
- WHERE conversation_id = ${head.conversation_id}
1566
+ WHERE thread_id = ${head.thread_id}
1260
1567
  `.pipe(Effect.mapError(sqlFailure(operation)));
1261
1568
  }
1262
1569
 
1263
1570
  const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1571
+
1264
1572
  yield* sql`
1265
1573
  INSERT INTO effect_agent_submission_ownership (
1266
1574
  submission_id,
@@ -1289,14 +1597,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1289
1597
  INSERT INTO effect_agent_attempts (
1290
1598
  attempt_id,
1291
1599
  submission_id,
1292
- conversation_id,
1600
+ thread_id,
1293
1601
  owner_producer_id,
1294
1602
  producer_epoch,
1295
1603
  claimed_at
1296
1604
  ) VALUES (
1297
1605
  ${attemptId},
1298
1606
  ${head.submission_id},
1299
- ${head.conversation_id},
1607
+ ${head.thread_id},
1300
1608
  ${validated.producerId},
1301
1609
  ${producerEpoch},
1302
1610
  ${now.iso}
@@ -1321,6 +1629,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1321
1629
  ),
1322
1630
  ),
1323
1631
  );
1632
+
1324
1633
  return Option.some(
1325
1634
  yield* decodeClaim({
1326
1635
  submissionId: head.submission_id,
@@ -1333,7 +1642,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1333
1642
  );
1334
1643
  }),
1335
1644
  );
1645
+
1336
1646
  yield* hitFailpoint("ledger:claim:after", operation);
1647
+
1337
1648
  return claimed;
1338
1649
  },
1339
1650
  );
@@ -1342,29 +1653,37 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1342
1653
  "DoSubmissionLedger.renewOwnership",
1343
1654
  )(function* (request: RenewOwnershipRequest) {
1344
1655
  const operation = "ledger renew ownership";
1345
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RenewOwnershipRequest))(
1656
+
1657
+ const validated = yield* Schema.decodeEffect(Schema.toType(RenewOwnershipRequest))(
1346
1658
  request,
1347
1659
  ).pipe(Effect.mapError(internalFailure(operation)));
1660
+
1348
1661
  yield* hitFailpoint("ledger:renew:before", operation);
1662
+
1349
1663
  const renewal = yield* inWriteTransaction(
1350
1664
  operation,
1351
1665
  Effect.gen(function* () {
1352
1666
  const submission = yield* requireSubmission(operation, validated.submissionId);
1667
+
1353
1668
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1354
1669
  const now = yield* currentInstant;
1355
1670
  const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1671
+
1356
1672
  yield* sql`
1357
1673
  UPDATE effect_agent_submission_ownership
1358
1674
  SET lease_expires_at = ${leaseExpiresAt}
1359
1675
  WHERE submission_id = ${validated.submissionId}
1360
1676
  `.pipe(Effect.mapError(sqlFailure(operation)));
1677
+
1361
1678
  return yield* decodeOwnershipRenewal({
1362
1679
  ownershipToken: validated.ownershipToken,
1363
1680
  leaseExpiresAt,
1364
1681
  }).pipe(Effect.mapError(internalFailure(operation)));
1365
1682
  }),
1366
1683
  );
1684
+
1367
1685
  yield* hitFailpoint("ledger:renew:after", operation);
1686
+
1368
1687
  return renewal;
1369
1688
  });
1370
1689
 
@@ -1372,14 +1691,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1372
1691
  "DoSubmissionLedger.releaseOwnership",
1373
1692
  )(function* (request: ReleaseOwnershipRequest) {
1374
1693
  const operation = "ledger release ownership";
1375
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseOwnershipRequest))(
1694
+
1695
+ const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseOwnershipRequest))(
1376
1696
  request,
1377
1697
  ).pipe(Effect.mapError(internalFailure(operation)));
1698
+
1378
1699
  yield* hitFailpoint("ledger:release:before", operation);
1379
1700
  yield* inWriteTransaction(
1380
1701
  operation,
1381
1702
  Effect.gen(function* () {
1382
1703
  const submission = yield* requireSubmission(operation, validated.submissionId);
1704
+
1383
1705
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1384
1706
  yield* sql`
1385
1707
  DELETE FROM effect_agent_submission_ownership
@@ -1401,14 +1723,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1401
1723
  "DoSubmissionLedger.markInputApplied",
1402
1724
  )(function* (request: MarkInputAppliedRequest) {
1403
1725
  const operation = "ledger mark input applied";
1404
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkInputAppliedRequest))(
1726
+
1727
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
1405
1728
  request,
1406
1729
  ).pipe(Effect.mapError(internalFailure(operation)));
1730
+
1407
1731
  yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1408
1732
  yield* inWriteTransaction(
1409
1733
  operation,
1410
1734
  Effect.gen(function* () {
1411
1735
  const submission = yield* requireSubmission(operation, validated.submissionId);
1736
+
1412
1737
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1413
1738
  if (submission.input_applied_record_id !== null) {
1414
1739
  if (
@@ -1417,6 +1742,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1417
1742
  ) {
1418
1743
  return;
1419
1744
  }
1745
+
1420
1746
  return yield* corruptionFailure(
1421
1747
  operation,
1422
1748
  "effect_agent_submissions",
@@ -1444,34 +1770,41 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1444
1770
  "DoSubmissionLedger.reserveSettlement",
1445
1771
  )(function* (request: SettlementReservation) {
1446
1772
  const operation = "ledger reserve settlement";
1447
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementReservation))(
1773
+
1774
+ const validated = yield* Schema.decodeEffect(Schema.toType(SettlementReservation))(
1448
1775
  request,
1449
1776
  ).pipe(Effect.mapError(internalFailure(operation)));
1777
+
1450
1778
  const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(
1451
1779
  Effect.mapError(internalFailure(operation)),
1452
1780
  );
1781
+
1453
1782
  // The reserved record is appended canonically later; refuse over-bound payloads typed
1454
1783
  // before the reservation row exists.
1455
1784
  yield* journal
1456
1785
  .checkValueBound(operation, recordJson)
1457
1786
  .pipe(Effect.mapError(internalFailure(operation)));
1458
1787
  yield* hitFailpoint("ledger:reserve-settlement:before", operation);
1788
+
1459
1789
  const reserved = yield* inWriteTransaction(
1460
1790
  operation,
1461
1791
  Effect.gen(function* () {
1462
1792
  const existing = yield* readReservation(operation, validated.submissionId);
1793
+
1463
1794
  if (Option.isSome(existing)) {
1464
1795
  const identical =
1465
1796
  existing.value.settlement_id === validated.settlementId &&
1466
1797
  existing.value.outcome === validated.outcome &&
1467
1798
  existing.value.record_digest === validated.recordDigest &&
1468
1799
  existing.value.record_json === recordJson;
1800
+
1469
1801
  if (!identical) {
1470
1802
  return yield* SettlementConflict.make({
1471
1803
  submissionId: validated.submissionId,
1472
1804
  existingOutcome: existing.value.outcome,
1473
1805
  });
1474
1806
  }
1807
+
1475
1808
  const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(
1476
1809
  Effect.mapError((error) =>
1477
1810
  corruptionFailure(
@@ -1482,6 +1815,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1482
1815
  ),
1483
1816
  ),
1484
1817
  );
1818
+
1485
1819
  return ReservedSettlement.make({
1486
1820
  submissionId: validated.submissionId,
1487
1821
  settlementId: validated.settlementId,
@@ -1493,6 +1827,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1493
1827
  }
1494
1828
 
1495
1829
  const submission = yield* requireSubmission(operation, validated.submissionId);
1830
+
1496
1831
  if (submission.state === "settled") {
1497
1832
  if (submission.settled_outcome === null) {
1498
1833
  return yield* corruptionFailure(
@@ -1502,6 +1837,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1502
1837
  "A settled Submission carries no terminal outcome.",
1503
1838
  );
1504
1839
  }
1840
+
1505
1841
  return yield* SettlementConflict.make({
1506
1842
  submissionId: validated.submissionId,
1507
1843
  existingOutcome: submission.settled_outcome,
@@ -1516,13 +1852,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1516
1852
  // ABORTED settlement (`terminalizing` is the same pass's crash replay). Every other
1517
1853
  // reservation stays fenced by the target lane's live ownership.
1518
1854
  let queuedAbortSettlement = false;
1855
+
1519
1856
  if (
1520
1857
  validated.outcome === "aborted" &&
1521
1858
  (submission.state === "ready" || submission.state === "terminalizing")
1522
1859
  ) {
1523
1860
  const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
1861
+
1524
1862
  if (Option.isSome(abortIntent)) {
1525
1863
  const ownership = yield* readOwnership(operation, validated.submissionId);
1864
+
1526
1865
  queuedAbortSettlement = Option.isNone(ownership);
1527
1866
  }
1528
1867
  }
@@ -1531,6 +1870,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1531
1870
  }
1532
1871
  }
1533
1872
  const now = yield* currentInstant;
1873
+
1534
1874
  yield* sql`
1535
1875
  INSERT INTO effect_agent_settlement_reservations (
1536
1876
  submission_id,
@@ -1555,6 +1895,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1555
1895
  SET state = 'terminalizing'
1556
1896
  WHERE submission_id = ${validated.submissionId}
1557
1897
  `.pipe(Effect.mapError(sqlFailure(operation)));
1898
+
1558
1899
  return ReservedSettlement.make({
1559
1900
  submissionId: validated.submissionId,
1560
1901
  settlementId: validated.settlementId,
@@ -1565,56 +1906,161 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1565
1906
  });
1566
1907
  }),
1567
1908
  );
1909
+
1568
1910
  yield* hitFailpoint("ledger:reserve-settlement:after", operation);
1911
+
1569
1912
  return reserved;
1570
1913
  });
1571
1914
 
1572
- const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
1573
- "DoSubmissionLedger.finalizeSettlement",
1574
- )(function* (request: SettlementFinalization) {
1915
+ const validateFinalization = Effect.fn("DoSubmissionLedger.validateFinalization")(function* (
1916
+ validated: SettlementFinalization,
1917
+ reservation: Option.Option<ReservationRow>,
1918
+ ) {
1575
1919
  const operation = "ledger finalize settlement";
1576
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementFinalization))(
1577
- request,
1578
- ).pipe(Effect.mapError(internalFailure(operation)));
1579
- yield* hitFailpoint("ledger:finalize-settlement:before", operation);
1580
- const settlement = yield* inWriteTransaction(
1581
- operation,
1582
- Effect.gen(function* () {
1583
- const reservation = yield* readReservation(operation, validated.submissionId);
1584
- if (Option.isNone(reservation)) {
1585
- return yield* LedgerError.make({
1920
+
1921
+ if (Option.isNone(reservation)) {
1922
+ return yield* LedgerError.make({
1923
+ operation,
1924
+ message: `No settlement reservation exists for submission ${validated.submissionId}.`,
1925
+ });
1926
+ }
1927
+ if (reservation.value.settlement_id !== validated.settlementId) {
1928
+ return yield* SettlementConflict.make({
1929
+ submissionId: validated.submissionId,
1930
+ existingOutcome: reservation.value.outcome,
1931
+ });
1932
+ }
1933
+
1934
+ const reservationRecord = yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(
1935
+ Effect.mapError((error) =>
1936
+ corruptionFailure(
1937
+ operation,
1938
+ "effect_agent_settlement_reservations",
1939
+ validated.submissionId,
1940
+ error.message,
1941
+ ),
1942
+ ),
1943
+ );
1944
+
1945
+ const settlementFailure = settlementFailureFromRecord(reservationRecord);
1946
+
1947
+ if ((reservation.value.outcome === "failed") !== (settlementFailure !== undefined)) {
1948
+ return yield* corruptionFailure(
1949
+ operation,
1950
+ "effect_agent_settlement_reservations",
1951
+ validated.submissionId,
1952
+ "The reserved outcome and canonical failure diagnostic disagree.",
1953
+ );
1954
+ }
1955
+
1956
+ return { reservation: reservation.value, settlementFailure };
1957
+ });
1958
+
1959
+ const replayFinalization = Effect.fn("DoSubmissionLedger.replayFinalization")(function* (
1960
+ validated: SettlementFinalization,
1961
+ submission: SubmissionRow,
1962
+ { reservation, settlementFailure }: Effect.Success<ReturnType<typeof validateFinalization>>,
1963
+ ) {
1964
+ const operation = "ledger finalize settlement";
1965
+
1966
+ if (reservation.finalized_at === null) {
1967
+ return yield* corruptionFailure(
1968
+ operation,
1969
+ "effect_agent_settlement_reservations",
1970
+ validated.submissionId,
1971
+ "A settled Submission's reservation carries no finalization timestamp.",
1972
+ );
1973
+ }
1974
+
1975
+ return yield* decodeSettlement({
1976
+ submissionId: validated.submissionId,
1977
+ settlementId: validated.settlementId,
1978
+ receiptId: submission.receipt_id,
1979
+ outcome: reservation.outcome,
1980
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1981
+ settledAt: reservation.finalized_at,
1982
+ }).pipe(Effect.mapError(internalFailure(operation)));
1983
+ });
1984
+
1985
+ const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
1986
+ "DoSubmissionLedger.finalizeSettlement",
1987
+ )(function* (request: SettlementFinalization) {
1988
+ const operation = "ledger finalize settlement";
1989
+
1990
+ const validated = yield* Schema.decodeEffect(Schema.toType(SettlementFinalization))(
1991
+ request,
1992
+ ).pipe(Effect.mapError(internalFailure(operation)));
1993
+
1994
+ yield* hitFailpoint("ledger:finalize-settlement:before", operation);
1995
+
1996
+ // A single statement captures the settled row and its immutable reservation together.
1997
+ // A miss does not authorize finalization: the write path re-reads under its transaction.
1998
+ const replayRows = yield* sql<Record<string, unknown>>`
1999
+ SELECT submission.*, reservation.settlement_id, reservation.outcome,
2000
+ reservation.record_id, reservation.record_json, reservation.record_digest,
2001
+ reservation.reserved_at, reservation.finalized_at
2002
+ FROM effect_agent_submissions AS submission
2003
+ INNER JOIN effect_agent_settlement_reservations AS reservation
2004
+ ON reservation.submission_id = submission.submission_id
2005
+ WHERE submission.submission_id = ${validated.submissionId} AND submission.state = 'settled'
2006
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2007
+
2008
+ if (replayRows.length > 1) {
2009
+ return yield* corruptionFailure(
2010
+ operation,
2011
+ "effect_agent_submissions",
2012
+ validated.submissionId,
2013
+ "A submission primary key returned more than one row.",
2014
+ );
2015
+ }
2016
+ const replayRow = replayRows[0];
2017
+
2018
+ if (replayRow !== undefined) {
2019
+ const reservations = yield* decodeRows(
2020
+ Schema.Array(ReservationRow),
2021
+ "effect_agent_settlement_reservations",
2022
+ validated.submissionId,
2023
+ replayRows,
2024
+ ).pipe(Effect.mapError(internalFailure(operation)));
2025
+
2026
+ const state = yield* validateFinalization(validated, Option.fromUndefinedOr(reservations[0]));
2027
+
2028
+ const submission = yield* Schema.decodeUnknownEffect(SubmissionRow)(replayRow).pipe(
2029
+ Effect.mapError((error) =>
2030
+ corruptionFailure(
1586
2031
  operation,
1587
- message: `No settlement reservation exists for submission ${validated.submissionId}.`,
1588
- });
1589
- }
1590
- if (reservation.value.settlement_id !== validated.settlementId) {
1591
- return yield* SettlementConflict.make({
1592
- submissionId: validated.submissionId,
1593
- existingOutcome: reservation.value.outcome,
1594
- });
1595
- }
2032
+ "effect_agent_submissions",
2033
+ validated.submissionId,
2034
+ error.message,
2035
+ ),
2036
+ ),
2037
+ );
2038
+
2039
+ const settlement = yield* replayFinalization(validated, submission, state);
2040
+
2041
+ yield* hitFailpoint("ledger:finalize-settlement:after", operation);
2042
+
2043
+ return settlement;
2044
+ }
2045
+
2046
+ const settlement = yield* inWriteTransaction(
2047
+ operation,
2048
+ Effect.gen(function* () {
2049
+ const state = yield* validateFinalization(
2050
+ validated,
2051
+ yield* readReservation(operation, validated.submissionId),
2052
+ );
2053
+
2054
+ const { reservation, settlementFailure } = state;
1596
2055
  const submission = yield* requireSubmission(operation, validated.submissionId);
1597
- if (submission.state === "settled") {
1598
- if (reservation.value.finalized_at === null) {
1599
- return yield* corruptionFailure(
1600
- operation,
1601
- "effect_agent_settlement_reservations",
1602
- validated.submissionId,
1603
- "A settled Submission's reservation carries no finalization timestamp.",
1604
- );
1605
- }
1606
- return yield* decodeSettlement({
1607
- submissionId: validated.submissionId,
1608
- settlementId: validated.settlementId,
1609
- receiptId: submission.receipt_id,
1610
- outcome: reservation.value.outcome,
1611
- settledAt: reservation.value.finalized_at,
1612
- }).pipe(Effect.mapError(internalFailure(operation)));
1613
- }
2056
+
2057
+ if (submission.state === "settled")
2058
+ return yield* replayFinalization(validated, submission, state);
1614
2059
  const now = yield* currentInstant;
2060
+
1615
2061
  yield* sql`
1616
2062
  UPDATE effect_agent_submissions
1617
- SET state = 'settled', settled_outcome = ${reservation.value.outcome}
2063
+ SET state = 'settled', settled_outcome = ${reservation.outcome}
1618
2064
  WHERE submission_id = ${validated.submissionId}
1619
2065
  `.pipe(Effect.mapError(sqlFailure(operation)));
1620
2066
  yield* sql`
@@ -1626,16 +2072,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1626
2072
  DELETE FROM effect_agent_submission_ownership
1627
2073
  WHERE submission_id = ${validated.submissionId}
1628
2074
  `.pipe(Effect.mapError(sqlFailure(operation)));
2075
+
1629
2076
  return yield* decodeSettlement({
1630
2077
  submissionId: validated.submissionId,
1631
2078
  settlementId: validated.settlementId,
1632
2079
  receiptId: submission.receipt_id,
1633
- outcome: reservation.value.outcome,
2080
+ outcome: reservation.outcome,
2081
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1634
2082
  settledAt: now.iso,
1635
2083
  }).pipe(Effect.mapError(internalFailure(operation)));
1636
2084
  }),
1637
2085
  );
2086
+
1638
2087
  yield* hitFailpoint("ledger:finalize-settlement:after", operation);
2088
+
1639
2089
  return settlement;
1640
2090
  });
1641
2091
 
@@ -1643,14 +2093,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1643
2093
  "DoSubmissionLedger.requestAbort",
1644
2094
  )(function* (request: AbortCommand) {
1645
2095
  const operation = "ledger request abort";
1646
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AbortCommand))(request).pipe(
2096
+
2097
+ const validated = yield* Schema.decodeEffect(Schema.toType(AbortCommand))(request).pipe(
1647
2098
  Effect.mapError(internalFailure(operation)),
1648
2099
  );
2100
+
1649
2101
  yield* hitFailpoint("ledger:request-abort:before", operation);
2102
+
1650
2103
  const intent = yield* inWriteTransaction(
1651
2104
  operation,
1652
2105
  Effect.gen(function* () {
1653
2106
  const submission = yield* requireSubmission(operation, validated.submissionId);
2107
+
1654
2108
  if (submission.state === "settled") {
1655
2109
  if (submission.settled_outcome === null) {
1656
2110
  return yield* corruptionFailure(
@@ -1660,6 +2114,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1660
2114
  "A settled Submission carries no terminal outcome.",
1661
2115
  );
1662
2116
  }
2117
+
1663
2118
  return yield* SettlementConflict.make({
1664
2119
  submissionId: validated.submissionId,
1665
2120
  existingOutcome: submission.settled_outcome,
@@ -1677,15 +2132,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1677
2132
  "A joined Submission carries no host linkage.",
1678
2133
  );
1679
2134
  }
2135
+
1680
2136
  const hostSubmissionId = yield* decodeSubmissionId(
1681
2137
  submission.joined_host_submission_id,
1682
2138
  ).pipe(Effect.mapError(internalFailure(operation)));
2139
+
1683
2140
  return yield* JoinedToHost.make({
1684
2141
  submissionId: validated.submissionId,
1685
2142
  hostSubmissionId,
1686
2143
  });
1687
2144
  }
1688
2145
  const existing = yield* readAbortIntent(operation, validated.submissionId);
2146
+
1689
2147
  if (Option.isSome(existing)) {
1690
2148
  return yield* abortIntentFromRow(
1691
2149
  operation,
@@ -1695,6 +2153,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1695
2153
  );
1696
2154
  }
1697
2155
  const now = yield* currentInstant;
2156
+
1698
2157
  yield* sql`
1699
2158
  INSERT INTO effect_agent_abort_intents (
1700
2159
  submission_id,
@@ -1708,11 +2167,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1708
2167
  ${now.iso}
1709
2168
  )
1710
2169
  `.pipe(Effect.mapError(sqlFailure(operation)));
2170
+
1711
2171
  const canonicalRecordId = yield* canonicalAbortRecordId(
1712
2172
  operation,
1713
- submission.conversation_id,
2173
+ submission.thread_id,
1714
2174
  validated.submissionId,
1715
2175
  );
2176
+
1716
2177
  return yield* decodeAbortIntent({
1717
2178
  submissionId: validated.submissionId,
1718
2179
  author: validated.author,
@@ -1722,7 +2183,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1722
2183
  }).pipe(Effect.mapError(internalFailure(operation)));
1723
2184
  }),
1724
2185
  );
2186
+
1725
2187
  yield* hitFailpoint("ledger:request-abort:after", operation);
2188
+
1726
2189
  return intent;
1727
2190
  });
1728
2191
 
@@ -1730,31 +2193,38 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1730
2193
  "DoSubmissionLedger.claimJoining",
1731
2194
  )(function* (request: ClaimJoiningRequest) {
1732
2195
  const operation = "ledger claim joining";
1733
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimJoiningRequest))(
1734
- request,
1735
- ).pipe(Effect.mapError(internalFailure(operation)));
2196
+
2197
+ const validated = yield* Schema.decodeEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(
2198
+ Effect.mapError(internalFailure(operation)),
2199
+ );
2200
+
1736
2201
  yield* hitFailpoint("ledger:claim-joining:before", operation);
2202
+
1737
2203
  const claims = yield* inWriteTransaction(
1738
2204
  operation,
1739
2205
  Effect.gen(function* () {
1740
2206
  const host = yield* requireSubmission(operation, validated.hostSubmissionId);
1741
- if (host.conversation_id !== validated.conversationId) {
2207
+
2208
+ if (host.thread_id !== validated.threadId) {
1742
2209
  return yield* LedgerError.make({
1743
2210
  operation,
1744
- message: `Host submission ${validated.hostSubmissionId} does not belong to conversation ${validated.conversationId}.`,
2211
+ message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
1745
2212
  });
1746
2213
  }
1747
2214
  // The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
1748
2215
  yield* requireOwnership(operation, host, validated.ownershipToken);
2216
+
1749
2217
  const laterRows = yield* sql<Record<string, unknown>>`
1750
2218
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1751
2219
  FROM effect_agent_submissions
1752
- WHERE conversation_id = ${validated.conversationId}
2220
+ WHERE thread_id = ${validated.threadId}
1753
2221
  AND queue_sequence > ${host.queue_sequence}
1754
2222
  ORDER BY queue_sequence ASC
1755
2223
  `.pipe(Effect.mapError(sqlFailure(operation)));
1756
- const later = yield* decodeSubmissionRows(operation, validated.conversationId, laterRows);
2224
+
2225
+ const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
1757
2226
  const claimed: Array<JoiningClaim> = [];
2227
+
1758
2228
  for (const row of later) {
1759
2229
  if (claimed.length >= validated.maxCount) break;
1760
2230
  // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
@@ -1777,6 +2247,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1777
2247
  SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
1778
2248
  WHERE submission_id = ${row.submission_id}
1779
2249
  `.pipe(Effect.mapError(sqlFailure(operation)));
2250
+
1780
2251
  const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
1781
2252
  Effect.mapError((error) =>
1782
2253
  corruptionFailure(
@@ -1787,6 +2258,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1787
2258
  ),
1788
2259
  ),
1789
2260
  );
2261
+
1790
2262
  claimed.push(
1791
2263
  yield* decodeJoiningClaim({
1792
2264
  submissionId: row.submission_id,
@@ -1795,10 +2267,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1795
2267
  }).pipe(Effect.mapError(internalFailure(operation))),
1796
2268
  );
1797
2269
  }
1798
- return claimed as ReadonlyArray<JoiningClaim>;
2270
+
2271
+ return claimed;
1799
2272
  }),
1800
2273
  );
2274
+
1801
2275
  yield* hitFailpoint("ledger:claim-joining:after", operation);
2276
+
1802
2277
  return claims;
1803
2278
  });
1804
2279
 
@@ -1806,14 +2281,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1806
2281
  "DoSubmissionLedger.markJoined",
1807
2282
  )(function* (request: MarkJoinedRequest) {
1808
2283
  const operation = "ledger mark joined";
1809
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkJoinedRequest))(
1810
- request,
1811
- ).pipe(Effect.mapError(internalFailure(operation)));
2284
+
2285
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkJoinedRequest))(request).pipe(
2286
+ Effect.mapError(internalFailure(operation)),
2287
+ );
2288
+
1812
2289
  yield* hitFailpoint("ledger:mark-joined:before", operation);
1813
2290
  yield* inWriteTransaction(
1814
2291
  operation,
1815
2292
  Effect.gen(function* () {
1816
2293
  const submission = yield* requireSubmission(operation, validated.submissionId);
2294
+
1817
2295
  if (submission.joined_host_submission_id === null) {
1818
2296
  return yield* LedgerError.make({
1819
2297
  operation,
@@ -1821,6 +2299,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1821
2299
  });
1822
2300
  }
1823
2301
  const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
2302
+
1824
2303
  // The lane is host-owned: the presented token must own the HOST's ownership period,
1825
2304
  // which also lets a later host Attempt repair a lost marker from history (DUR-016).
1826
2305
  yield* requireOwnership(operation, host, validated.ownershipToken);
@@ -1831,6 +2310,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1831
2310
  ) {
1832
2311
  return;
1833
2312
  }
2313
+
1834
2314
  return yield* corruptionFailure(
1835
2315
  operation,
1836
2316
  "effect_agent_submissions",
@@ -1861,14 +2341,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1861
2341
  "DoSubmissionLedger.revertJoining",
1862
2342
  )(function* (request: RevertJoiningRequest) {
1863
2343
  const operation = "ledger revert joining";
1864
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RevertJoiningRequest))(
1865
- request,
1866
- ).pipe(Effect.mapError(internalFailure(operation)));
2344
+
2345
+ const validated = yield* Schema.decodeEffect(Schema.toType(RevertJoiningRequest))(request).pipe(
2346
+ Effect.mapError(internalFailure(operation)),
2347
+ );
2348
+
1867
2349
  yield* hitFailpoint("ledger:revert-joining:before", operation);
1868
2350
  yield* inWriteTransaction(
1869
2351
  operation,
1870
2352
  Effect.gen(function* () {
1871
2353
  const submission = yield* requireSubmission(operation, validated.submissionId);
2354
+
1872
2355
  // Idempotent and recovery-only: only a still-`joining` Submission reverts; an
1873
2356
  // already-joined (or already-reverted) Submission is a no-op (DUR-016).
1874
2357
  if (submission.state !== "joining") return;
@@ -1885,17 +2368,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1885
2368
  const suspend: SubmissionLedger["Service"]["suspend"] = Effect.fn("DoSubmissionLedger.suspend")(
1886
2369
  function* (request: SuspendRequest) {
1887
2370
  const operation = "ledger suspend";
1888
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SuspendRequest))(
1889
- request,
1890
- ).pipe(Effect.mapError(internalFailure(operation)));
2371
+
2372
+ const validated = yield* Schema.decodeEffect(Schema.toType(SuspendRequest))(request).pipe(
2373
+ Effect.mapError(internalFailure(operation)),
2374
+ );
2375
+
1891
2376
  const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
1892
2377
  Effect.mapError(internalFailure(operation)),
1893
2378
  );
2379
+
1894
2380
  yield* hitFailpoint("ledger:suspend:before", operation);
2381
+
1895
2382
  const outcome = yield* inWriteTransaction(
1896
2383
  operation,
1897
2384
  Effect.gen(function* () {
1898
2385
  const submission = yield* requireSubmission(operation, validated.submissionId);
2386
+
1899
2387
  if (submission.state === "settled") {
1900
2388
  if (submission.settled_outcome === null) {
1901
2389
  return yield* corruptionFailure(
@@ -1905,6 +2393,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1905
2393
  "A settled Submission carries no terminal outcome.",
1906
2394
  );
1907
2395
  }
2396
+
1908
2397
  return yield* SettlementConflict.make({
1909
2398
  submissionId: validated.submissionId,
1910
2399
  existingOutcome: submission.settled_outcome,
@@ -1913,6 +2402,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1913
2402
  // An exact terminal outcome is already reserved (DUR-011); suspension would
1914
2403
  // contradict it, so the reservation wins.
1915
2404
  const reservation = yield* readReservation(operation, validated.submissionId);
2405
+
1916
2406
  if (Option.isSome(reservation)) {
1917
2407
  return yield* SettlementConflict.make({
1918
2408
  submissionId: validated.submissionId,
@@ -1923,35 +2413,39 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1923
2413
  // A covering event that raced ahead of the suspend transaction resumes the caller
1924
2414
  // immediately WITHOUT releasing the lane (plan §2.6, §12). For WaitingForChild the
1925
2415
  // covering evidence is EITHER a locally settled child row OR a durable cross-store
1926
- // notification marker: parent and child Conversations live in different Durable
2416
+ // notification marker: parent and child Threads live in different Durable
1927
2417
  // Objects, and the port contract requires that a child settlement reported (via
1928
2418
  // `recordChildSettled` → marker) before this suspend commits is observed here.
1929
2419
  if (validated.reason._tag === "ApprovalPending") {
1930
2420
  const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
1931
2421
  const decided = new Set(decisions.map((row) => row.tool_call_id));
2422
+
1932
2423
  if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
1933
- return "resume-immediately" as SuspensionOutcome;
2424
+ return RESUME_IMMEDIATELY;
1934
2425
  }
1935
2426
  } else {
1936
2427
  const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
1937
2428
  const markerChildren = new Set(markers.map((row) => row.child_submission_id));
1938
2429
  let allSettled = true;
2430
+
1939
2431
  for (const child of validated.reason.children) {
1940
2432
  const settled = yield* childProvablySettled(
1941
2433
  operation,
1942
2434
  markerChildren,
1943
2435
  child.childSubmissionId,
1944
2436
  );
2437
+
1945
2438
  if (!settled) {
1946
2439
  allSettled = false;
1947
2440
  break;
1948
2441
  }
1949
2442
  }
1950
2443
  if (allSettled) {
1951
- return "resume-immediately" as SuspensionOutcome;
2444
+ return RESUME_IMMEDIATELY;
1952
2445
  }
1953
2446
  }
1954
2447
  const now = yield* currentInstant;
2448
+
1955
2449
  yield* sql`
1956
2450
  UPDATE effect_agent_submissions
1957
2451
  SET
@@ -1966,10 +2460,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1966
2460
  DELETE FROM effect_agent_submission_ownership
1967
2461
  WHERE submission_id = ${validated.submissionId}
1968
2462
  `.pipe(Effect.mapError(sqlFailure(operation)));
1969
- return "suspended" as SuspensionOutcome;
2463
+
2464
+ return SUSPENDED;
1970
2465
  }),
1971
2466
  );
2467
+
1972
2468
  yield* hitFailpoint("ledger:suspend:after", operation);
2469
+
1973
2470
  return outcome;
1974
2471
  },
1975
2472
  );
@@ -1985,6 +2482,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1985
2482
  submission: SubmissionRow,
1986
2483
  ): Effect.fn.Return<void, LedgerError> {
1987
2484
  if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
2485
+
1988
2486
  const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
1989
2487
  submission.suspended_reason_json,
1990
2488
  ).pipe(
@@ -1997,9 +2495,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1997
2495
  ),
1998
2496
  ),
1999
2497
  );
2498
+
2000
2499
  if (reason._tag !== "ApprovalPending") return;
2001
2500
  const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
2002
2501
  const decided = new Set(decisions.map((row) => row.tool_call_id));
2502
+
2003
2503
  if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
2004
2504
  yield* sql`
2005
2505
  UPDATE effect_agent_submissions
@@ -2015,14 +2515,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2015
2515
  "DoSubmissionLedger.recordApprovalDecision",
2016
2516
  )(function* (command: ApprovalDecisionCommand) {
2017
2517
  const operation = "ledger record approval decision";
2018
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ApprovalDecisionCommand))(
2518
+
2519
+ const validated = yield* Schema.decodeEffect(Schema.toType(ApprovalDecisionCommand))(
2019
2520
  command,
2020
2521
  ).pipe(Effect.mapError(internalFailure(operation)));
2522
+
2021
2523
  yield* hitFailpoint("ledger:approval-decision:before", operation);
2524
+
2022
2525
  const intent = yield* inWriteTransaction(
2023
2526
  operation,
2024
2527
  Effect.gen(function* () {
2025
2528
  const submission = yield* requireSubmission(operation, validated.submissionId);
2529
+
2026
2530
  if (submission.state === "settled") {
2027
2531
  if (submission.settled_outcome === null) {
2028
2532
  return yield* corruptionFailure(
@@ -2032,6 +2536,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2032
2536
  "A settled Submission carries no terminal outcome.",
2033
2537
  );
2034
2538
  }
2539
+
2035
2540
  return yield* SettlementConflict.make({
2036
2541
  submissionId: validated.submissionId,
2037
2542
  existingOutcome: submission.settled_outcome,
@@ -2039,6 +2544,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2039
2544
  }
2040
2545
  const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
2041
2546
  const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
2547
+
2042
2548
  if (existing !== undefined) {
2043
2549
  // Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
2044
2550
  // the recorded intent unchanged; a divergent re-decision conflicts.
@@ -2049,9 +2555,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2049
2555
  existingDecision: existing.decision,
2050
2556
  });
2051
2557
  }
2558
+
2052
2559
  return yield* approvalIntentFromRow(operation, existing);
2053
2560
  }
2054
2561
  const now = yield* currentInstant;
2562
+
2055
2563
  yield* sql`
2056
2564
  INSERT INTO effect_agent_approval_decisions (
2057
2565
  submission_id,
@@ -2070,6 +2578,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2070
2578
  )
2071
2579
  `.pipe(Effect.mapError(sqlFailure(operation)));
2072
2580
  yield* wakeSuspendedIfCovered(operation, submission);
2581
+
2073
2582
  return yield* decodeApprovalDecisionIntent({
2074
2583
  submissionId: validated.submissionId,
2075
2584
  toolCallId: validated.toolCallId,
@@ -2080,7 +2589,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2080
2589
  }).pipe(Effect.mapError(internalFailure(operation)));
2081
2590
  }),
2082
2591
  );
2592
+
2083
2593
  yield* hitFailpoint("ledger:approval-decision:after", operation);
2594
+
2084
2595
  return intent;
2085
2596
  });
2086
2597
 
@@ -2088,14 +2599,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2088
2599
  "DoSubmissionLedger.markUnknown",
2089
2600
  )(function* (request: MarkUnknownRequest) {
2090
2601
  const operation = "ledger mark unknown";
2091
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkUnknownRequest))(
2092
- request,
2093
- ).pipe(Effect.mapError(internalFailure(operation)));
2602
+
2603
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkUnknownRequest))(request).pipe(
2604
+ Effect.mapError(internalFailure(operation)),
2605
+ );
2606
+
2094
2607
  yield* hitFailpoint("ledger:mark-unknown:before", operation);
2095
2608
  yield* inWriteTransaction(
2096
2609
  operation,
2097
2610
  Effect.gen(function* () {
2098
2611
  const submission = yield* requireSubmission(operation, validated.submissionId);
2612
+
2099
2613
  if (submission.state === "settled") {
2100
2614
  if (submission.settled_outcome === null) {
2101
2615
  return yield* corruptionFailure(
@@ -2105,6 +2619,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2105
2619
  "A settled Submission carries no terminal outcome.",
2106
2620
  );
2107
2621
  }
2622
+
2108
2623
  return yield* SettlementConflict.make({
2109
2624
  submissionId: validated.submissionId,
2110
2625
  existingOutcome: submission.settled_outcome,
@@ -2113,6 +2628,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2113
2628
  // A reserved exact outcome wins over a late Unknown marking (DUR-011); the recovery
2114
2629
  // classifier orders reservation ahead of MarkUnknown for the same reason.
2115
2630
  const reservation = yield* readReservation(operation, validated.submissionId);
2631
+
2116
2632
  if (Option.isSome(reservation)) {
2117
2633
  return yield* SettlementConflict.make({
2118
2634
  submissionId: validated.submissionId,
@@ -2123,13 +2639,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2123
2639
  // set while the first recorded reason is kept.
2124
2640
  const existingIds = yield* storedUnknownToolCallIds(operation, submission);
2125
2641
  const known = new Set(existingIds);
2642
+
2126
2643
  const merged = [
2127
2644
  ...existingIds,
2128
2645
  ...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
2129
2646
  ];
2647
+
2130
2648
  const idsJson = yield* encodeToolCallIdsText(merged).pipe(
2131
2649
  Effect.mapError(internalFailure(operation)),
2132
2650
  );
2651
+
2133
2652
  yield* sql`
2134
2653
  UPDATE effect_agent_submissions
2135
2654
  SET
@@ -2147,17 +2666,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2147
2666
  "DoSubmissionLedger.recordUnknownResolution",
2148
2667
  )(function* (command: UnknownResolutionCommand) {
2149
2668
  const operation = "ledger record unknown resolution";
2150
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(UnknownResolutionCommand))(
2669
+
2670
+ const validated = yield* Schema.decodeEffect(Schema.toType(UnknownResolutionCommand))(
2151
2671
  command,
2152
2672
  ).pipe(Effect.mapError(internalFailure(operation)));
2673
+
2153
2674
  const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
2154
2675
  Effect.mapError(internalFailure(operation)),
2155
2676
  );
2677
+
2156
2678
  yield* hitFailpoint("ledger:unknown-resolution:before", operation);
2679
+
2157
2680
  const intent = yield* inWriteTransaction(
2158
2681
  operation,
2159
2682
  Effect.gen(function* () {
2160
2683
  const submission = yield* requireSubmission(operation, validated.submissionId);
2684
+
2161
2685
  if (submission.state === "settled") {
2162
2686
  if (submission.settled_outcome === null) {
2163
2687
  return yield* corruptionFailure(
@@ -2167,6 +2691,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2167
2691
  "A settled Submission carries no terminal outcome.",
2168
2692
  );
2169
2693
  }
2694
+
2170
2695
  return yield* SettlementConflict.make({
2171
2696
  submissionId: validated.submissionId,
2172
2697
  existingOutcome: submission.settled_outcome,
@@ -2174,19 +2699,30 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2174
2699
  }
2175
2700
  const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
2176
2701
  const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
2177
- if (existing !== undefined && existing.resolution_json !== resolutionJson) {
2702
+
2703
+ const existingIntent =
2704
+ existing === undefined
2705
+ ? undefined
2706
+ : yield* unknownResolutionIntentFromRow(operation, existing);
2707
+
2708
+ if (
2709
+ existingIntent !== undefined &&
2710
+ !equivalentUnknownResolution(existingIntent.resolution, validated.resolution)
2711
+ ) {
2178
2712
  return yield* UnknownResolutionConflict.make({
2179
2713
  submissionId: validated.submissionId,
2180
2714
  toolCallId: validated.toolCallId,
2181
2715
  });
2182
2716
  }
2183
2717
  let resolved: UnknownResolutionIntent;
2184
- if (existing !== undefined) {
2718
+
2719
+ if (existingIntent !== undefined) {
2185
2720
  // Idempotent replay of the recorded intent (author/reason may differ; the stored
2186
2721
  // audit fields win, exactly like requestAbort).
2187
- resolved = yield* unknownResolutionIntentFromRow(operation, existing);
2722
+ resolved = existingIntent;
2188
2723
  } else {
2189
2724
  const now = yield* currentInstant;
2725
+
2190
2726
  yield* sql`
2191
2727
  INSERT INTO effect_agent_unknown_resolutions (
2192
2728
  submission_id,
@@ -2204,9 +2740,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2204
2740
  ${now.iso}
2205
2741
  )
2206
2742
  `.pipe(Effect.mapError(sqlFailure(operation)));
2743
+
2207
2744
  const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
2208
2745
  Effect.mapError(internalFailure(operation)),
2209
2746
  );
2747
+
2210
2748
  resolved = yield* decodeUnknownResolutionIntent({
2211
2749
  submissionId: validated.submissionId,
2212
2750
  toolCallId: validated.toolCallId,
@@ -2223,6 +2761,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2223
2761
  const markedIds = yield* storedUnknownToolCallIds(operation, submission);
2224
2762
  const covering = yield* readUnknownResolutions(operation, validated.submissionId);
2225
2763
  const coveredIds = new Set(covering.map((row) => row.tool_call_id));
2764
+
2226
2765
  if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) {
2227
2766
  yield* sql`
2228
2767
  UPDATE effect_agent_submissions
@@ -2234,10 +2773,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2234
2773
  `.pipe(Effect.mapError(sqlFailure(operation)));
2235
2774
  }
2236
2775
  }
2776
+
2237
2777
  return resolved;
2238
2778
  }),
2239
2779
  );
2780
+
2240
2781
  yield* hitFailpoint("ledger:unknown-resolution:after", operation);
2782
+
2241
2783
  return intent;
2242
2784
  });
2243
2785
 
@@ -2245,21 +2787,31 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2245
2787
  "DoSubmissionLedger.recordChildSettled",
2246
2788
  )(function* (request: ChildSettledNotification) {
2247
2789
  const operation = "ledger record child settled";
2248
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ChildSettledNotification))(
2790
+
2791
+ const validated = yield* Schema.decodeEffect(Schema.toType(ChildSettledNotification))(
2249
2792
  request,
2250
2793
  ).pipe(Effect.mapError(internalFailure(operation)));
2794
+
2251
2795
  yield* hitFailpoint("ledger:child-settled:before", operation);
2796
+
2252
2797
  const outcome = yield* inWriteTransaction(
2253
2798
  operation,
2254
2799
  Effect.gen(function* () {
2255
2800
  const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2256
2801
  // The child's canonical Settlement is the authority for this wake. When the child's
2257
- // row lives in THIS store (single-store latitude, and every conformance lane), an
2258
- // unsettled child makes the notification a caller error exactly as on Node. When it
2259
- // does not the normal cross-DO case the routed notification from the child's
2260
- // owning Durable Object is the settlement evidence this store records durably.
2802
+ // row lives in THIS store (single-store latitude, and every conformance lane), either a
2803
+ // finalized row or an exact terminalizing reservation admits the notification: the
2804
+ // runtime calls only after the canonical append and before ledger finalization. When the
2805
+ // row does not live here — the normal cross-DO case the routed notification from the
2806
+ // child's owning Durable Object is the settlement evidence this store records durably.
2261
2807
  const child = yield* readSubmission(operation, validated.childSubmissionId);
2262
- if (Option.isSome(child) && child.value.state !== "settled") {
2808
+ const childReservation = yield* readReservation(operation, validated.childSubmissionId);
2809
+
2810
+ if (
2811
+ Option.isSome(child) &&
2812
+ child.value.state !== "settled" &&
2813
+ !(child.value.state === "terminalizing" && Option.isSome(childReservation))
2814
+ ) {
2263
2815
  return yield* LedgerError.make({
2264
2816
  operation,
2265
2817
  message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`,
@@ -2270,6 +2822,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2270
2822
  // before the parent's suspend transaction is observed by that suspend's covering
2271
2823
  // check, even when the parent is not (or not yet) suspended.
2272
2824
  const now = yield* currentInstant;
2825
+
2273
2826
  yield* sql`
2274
2827
  INSERT INTO effect_agent_child_settlements (
2275
2828
  parent_submission_id,
@@ -2279,15 +2832,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2279
2832
  ) VALUES (
2280
2833
  ${validated.parentSubmissionId},
2281
2834
  ${validated.childSubmissionId},
2282
- ${Option.isSome(child) ? child.value.settled_outcome : null},
2835
+ ${
2836
+ Option.isSome(child) && child.value.state === "settled"
2837
+ ? child.value.settled_outcome
2838
+ : Option.isSome(childReservation)
2839
+ ? childReservation.value.outcome
2840
+ : null
2841
+ },
2283
2842
  ${now.iso}
2284
2843
  )
2285
2844
  ON CONFLICT (parent_submission_id, child_submission_id) DO NOTHING
2286
2845
  `.pipe(Effect.mapError(sqlFailure(operation)));
2287
2846
 
2288
2847
  if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
2289
- return "not-waiting" as ChildSettledOutcome;
2848
+ return NOT_WAITING;
2290
2849
  }
2850
+
2291
2851
  const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
2292
2852
  parent.suspended_reason_json,
2293
2853
  ).pipe(
@@ -2300,27 +2860,30 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2300
2860
  ),
2301
2861
  ),
2302
2862
  );
2863
+
2303
2864
  if (reason._tag !== "WaitingForChild") {
2304
- return "not-waiting" as ChildSettledOutcome;
2865
+ return NOT_WAITING;
2305
2866
  }
2306
2867
  if (
2307
2868
  !reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
2308
2869
  ) {
2309
- return "not-waiting" as ChildSettledOutcome;
2870
+ return NOT_WAITING;
2310
2871
  }
2311
2872
  // The parent wakes exactly when EVERY listed child is provably settled — from its
2312
2873
  // local row or a recorded marker (spec §12 step 10); replays re-run the coverage
2313
2874
  // check so a recovering caller wakes the lane idempotently.
2314
2875
  const markers = yield* readChildSettlementMarkers(operation, validated.parentSubmissionId);
2315
2876
  const markerChildren = new Set(markers.map((row) => row.child_submission_id));
2877
+
2316
2878
  for (const entry of reason.children) {
2317
2879
  const settled = yield* childProvablySettled(
2318
2880
  operation,
2319
2881
  markerChildren,
2320
2882
  entry.childSubmissionId,
2321
2883
  );
2884
+
2322
2885
  if (!settled) {
2323
- return "still-waiting" as ChildSettledOutcome;
2886
+ return STILL_WAITING;
2324
2887
  }
2325
2888
  }
2326
2889
  yield* sql`
@@ -2331,10 +2894,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2331
2894
  suspended_at = NULL
2332
2895
  WHERE submission_id = ${validated.parentSubmissionId}
2333
2896
  `.pipe(Effect.mapError(sqlFailure(operation)));
2334
- return "woken" as ChildSettledOutcome;
2897
+
2898
+ return WOKEN;
2335
2899
  }),
2336
2900
  );
2901
+
2337
2902
  yield* hitFailpoint("ledger:child-settled:after", operation);
2903
+
2338
2904
  return outcome;
2339
2905
  });
2340
2906
 
@@ -2342,25 +2908,36 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2342
2908
  "DoSubmissionLedger.reserveChildBudget",
2343
2909
  )(function* (request: ChildBudgetReservationRequest) {
2344
2910
  const operation = "ledger reserve child budget";
2345
- const validated = yield* Schema.decodeUnknownEffect(
2346
- Schema.toType(ChildBudgetReservationRequest),
2347
- )(request).pipe(Effect.mapError(internalFailure(operation)));
2911
+
2912
+ const validated = yield* Schema.decodeEffect(Schema.toType(ChildBudgetReservationRequest))(
2913
+ request,
2914
+ ).pipe(Effect.mapError(internalFailure(operation)));
2915
+
2348
2916
  const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
2349
2917
  Effect.mapError(internalFailure(operation)),
2350
2918
  );
2919
+
2351
2920
  yield* hitFailpoint("ledger:child-reservation:before", operation);
2921
+
2352
2922
  const reserved = yield* inWriteTransaction(
2353
2923
  operation,
2354
2924
  Effect.gen(function* () {
2355
2925
  const existing = yield* readChildReservation(operation, validated.reservationId);
2926
+
2356
2927
  if (Option.isSome(existing)) {
2928
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
2929
+ operation,
2930
+ existing.value,
2931
+ );
2932
+
2357
2933
  // Identical replays short-circuit before the fence, mirroring reserveSettlement: a
2358
2934
  // replay creates nothing, so a recovering caller resumes rather than duplicates.
2359
2935
  const identical =
2360
2936
  existing.value.parent_submission_id === validated.parentSubmissionId &&
2361
2937
  existing.value.parent_tool_call_id === validated.parentToolCallId &&
2362
2938
  existing.value.allocation_digest === validated.allocationDigest &&
2363
- existing.value.allocation_json === allocationJson;
2939
+ equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
2940
+
2364
2941
  if (!identical) {
2365
2942
  return yield* ChildReservationConflict.make({
2366
2943
  reservationId: validated.reservationId,
@@ -2369,16 +2946,19 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2369
2946
  "A reservation with this identity exists with a different parent Tool Call or allocation.",
2370
2947
  });
2371
2948
  }
2949
+
2372
2950
  return ReservedChildBudget.make({
2373
- reservation: yield* childReservationSnapshotFromRow(operation, existing.value),
2951
+ reservation: existingSnapshot,
2374
2952
  replayed: true,
2375
2953
  });
2376
2954
  }
2955
+
2377
2956
  const collision = yield* readChildReservationForCall(
2378
2957
  operation,
2379
2958
  validated.parentSubmissionId,
2380
2959
  validated.parentToolCallId,
2381
2960
  );
2961
+
2382
2962
  if (Option.isSome(collision)) {
2383
2963
  return yield* ChildReservationConflict.make({
2384
2964
  reservationId: validated.reservationId,
@@ -2387,10 +2967,12 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2387
2967
  });
2388
2968
  }
2389
2969
  const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2970
+
2390
2971
  // Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
2391
2972
  // parent Attempt can never create new reservation state.
2392
2973
  yield* requireOwnership(operation, parent, validated.ownershipToken);
2393
2974
  const now = yield* currentInstant;
2975
+
2394
2976
  yield* sql`
2395
2977
  INSERT INTO effect_agent_child_reservations (
2396
2978
  reservation_id,
@@ -2411,6 +2993,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2411
2993
  )
2412
2994
  `.pipe(Effect.mapError(sqlFailure(operation)));
2413
2995
  const inserted = yield* readChildReservation(operation, validated.reservationId);
2996
+
2414
2997
  if (Option.isNone(inserted)) {
2415
2998
  return yield* corruptionFailure(
2416
2999
  operation,
@@ -2419,13 +3002,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2419
3002
  "An inserted child reservation row is missing inside its own transaction.",
2420
3003
  );
2421
3004
  }
3005
+
2422
3006
  return ReservedChildBudget.make({
2423
3007
  reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
2424
3008
  replayed: false,
2425
3009
  });
2426
3010
  }),
2427
3011
  );
3012
+
2428
3013
  yield* hitFailpoint("ledger:child-reservation:after", operation);
3014
+
2429
3015
  return reserved;
2430
3016
  });
2431
3017
 
@@ -2434,14 +3020,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2434
3020
  request: AttachChildToReservationRequest,
2435
3021
  ) {
2436
3022
  const operation = "ledger attach child to reservation";
2437
- const validated = yield* Schema.decodeUnknownEffect(
2438
- Schema.toType(AttachChildToReservationRequest),
2439
- )(request).pipe(Effect.mapError(internalFailure(operation)));
3023
+
3024
+ const validated = yield* Schema.decodeEffect(Schema.toType(AttachChildToReservationRequest))(
3025
+ request,
3026
+ ).pipe(Effect.mapError(internalFailure(operation)));
3027
+
2440
3028
  yield* hitFailpoint("ledger:child-attach:before", operation);
3029
+
2441
3030
  const attached = yield* inWriteTransaction(
2442
3031
  operation,
2443
3032
  Effect.gen(function* () {
2444
3033
  const existing = yield* readChildReservation(operation, validated.reservationId);
3034
+
2445
3035
  if (Option.isNone(existing)) {
2446
3036
  return yield* LedgerError.make({
2447
3037
  operation,
@@ -2453,6 +3043,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2453
3043
  if (existing.value.child_submission_id === validated.childSubmissionId) {
2454
3044
  return yield* childReservationSnapshotFromRow(operation, existing.value);
2455
3045
  }
3046
+
2456
3047
  return yield* ChildReservationConflict.make({
2457
3048
  reservationId: validated.reservationId,
2458
3049
  status: existing.value.status,
@@ -2460,6 +3051,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2460
3051
  });
2461
3052
  }
2462
3053
  const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
3054
+
2463
3055
  yield* requireOwnership(operation, parent, validated.ownershipToken);
2464
3056
  if (existing.value.status !== "reserved") {
2465
3057
  return yield* ChildReservationConflict.make({
@@ -2478,6 +3070,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2478
3070
  WHERE reservation_id = ${validated.reservationId}
2479
3071
  `.pipe(Effect.mapError(sqlFailure(operation)));
2480
3072
  const updated = yield* readChildReservation(operation, validated.reservationId);
3073
+
2481
3074
  if (Option.isNone(updated)) {
2482
3075
  return yield* corruptionFailure(
2483
3076
  operation,
@@ -2486,10 +3079,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2486
3079
  "An updated child reservation row is missing inside its own transaction.",
2487
3080
  );
2488
3081
  }
3082
+
2489
3083
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2490
3084
  }),
2491
3085
  );
3086
+
2492
3087
  yield* hitFailpoint("ledger:child-attach:after", operation);
3088
+
2493
3089
  return attached;
2494
3090
  });
2495
3091
 
@@ -2497,17 +3093,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2497
3093
  "DoSubmissionLedger.beginChildBudgetRelease",
2498
3094
  )(function* (request: BeginChildBudgetReleaseRequest) {
2499
3095
  const operation = "ledger begin child budget release";
2500
- const validated = yield* Schema.decodeUnknownEffect(
2501
- Schema.toType(BeginChildBudgetReleaseRequest),
2502
- )(request).pipe(Effect.mapError(internalFailure(operation)));
3096
+
3097
+ const validated = yield* Schema.decodeEffect(Schema.toType(BeginChildBudgetReleaseRequest))(
3098
+ request,
3099
+ ).pipe(Effect.mapError(internalFailure(operation)));
3100
+
2503
3101
  const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
2504
3102
  Effect.mapError(internalFailure(operation)),
2505
3103
  );
3104
+
2506
3105
  yield* hitFailpoint("ledger:child-release-pending:before", operation);
3106
+
2507
3107
  const frozen = yield* inWriteTransaction(
2508
3108
  operation,
2509
3109
  Effect.gen(function* () {
2510
3110
  const existing = yield* readChildReservation(operation, validated.reservationId);
3111
+
2511
3112
  if (Option.isNone(existing)) {
2512
3113
  return yield* LedgerError.make({
2513
3114
  operation,
@@ -2515,11 +3116,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2515
3116
  });
2516
3117
  }
2517
3118
  if (existing.value.status !== "reserved") {
3119
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
3120
+ operation,
3121
+ existing.value,
3122
+ );
3123
+
2518
3124
  // The accounting decision was already frozen exactly once; an identical replay is a
2519
3125
  // no-op and a divergent decision conflicts (spec §12 join step 6).
2520
- if (existing.value.accounting_json === accountingJson) {
2521
- return yield* childReservationSnapshotFromRow(operation, existing.value);
3126
+ if (
3127
+ existingSnapshot.accounting !== undefined &&
3128
+ equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
3129
+ ) {
3130
+ return existingSnapshot;
2522
3131
  }
3132
+
2523
3133
  return yield* ChildReservationConflict.make({
2524
3134
  reservationId: validated.reservationId,
2525
3135
  status: existing.value.status,
@@ -2527,6 +3137,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2527
3137
  });
2528
3138
  }
2529
3139
  const now = yield* currentInstant;
3140
+
2530
3141
  yield* sql`
2531
3142
  UPDATE effect_agent_child_reservations
2532
3143
  SET
@@ -2536,6 +3147,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2536
3147
  WHERE reservation_id = ${validated.reservationId}
2537
3148
  `.pipe(Effect.mapError(sqlFailure(operation)));
2538
3149
  const updated = yield* readChildReservation(operation, validated.reservationId);
3150
+
2539
3151
  if (Option.isNone(updated)) {
2540
3152
  return yield* corruptionFailure(
2541
3153
  operation,
@@ -2544,10 +3156,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2544
3156
  "An updated child reservation row is missing inside its own transaction.",
2545
3157
  );
2546
3158
  }
3159
+
2547
3160
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2548
3161
  }),
2549
3162
  );
3163
+
2550
3164
  yield* hitFailpoint("ledger:child-release-pending:after", operation);
3165
+
2551
3166
  return frozen;
2552
3167
  });
2553
3168
 
@@ -2555,14 +3170,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2555
3170
  "DoSubmissionLedger.releaseChildBudget",
2556
3171
  )(function* (request: ReleaseChildBudgetRequest) {
2557
3172
  const operation = "ledger release child budget";
2558
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseChildBudgetRequest))(
3173
+
3174
+ const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseChildBudgetRequest))(
2559
3175
  request,
2560
3176
  ).pipe(Effect.mapError(internalFailure(operation)));
3177
+
2561
3178
  yield* hitFailpoint("ledger:child-release:before", operation);
3179
+
2562
3180
  const released = yield* inWriteTransaction(
2563
3181
  operation,
2564
3182
  Effect.gen(function* () {
2565
3183
  const existing = yield* readChildReservation(operation, validated.reservationId);
3184
+
2566
3185
  if (Option.isNone(existing)) {
2567
3186
  return yield* LedgerError.make({
2568
3187
  operation,
@@ -2582,12 +3201,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2582
3201
  });
2583
3202
  }
2584
3203
  const now = yield* currentInstant;
3204
+
2585
3205
  yield* sql`
2586
3206
  UPDATE effect_agent_child_reservations
2587
3207
  SET status = 'released', released_at = ${now.iso}
2588
3208
  WHERE reservation_id = ${validated.reservationId}
2589
3209
  `.pipe(Effect.mapError(sqlFailure(operation)));
2590
3210
  const updated = yield* readChildReservation(operation, validated.reservationId);
3211
+
2591
3212
  if (Option.isNone(updated)) {
2592
3213
  return yield* corruptionFailure(
2593
3214
  operation,
@@ -2596,15 +3217,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2596
3217
  "An updated child reservation row is missing inside its own transaction.",
2597
3218
  );
2598
3219
  }
3220
+
2599
3221
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2600
3222
  }),
2601
3223
  );
3224
+
2602
3225
  yield* hitFailpoint("ledger:child-release:after", operation);
3226
+
2603
3227
  return released;
2604
3228
  });
2605
3229
 
2606
3230
  interface ScanCursor {
2607
- readonly conversationId: string;
3231
+ readonly threadId: string;
2608
3232
  readonly queueSequence: number;
2609
3233
  }
2610
3234
 
@@ -2615,57 +3239,131 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2615
3239
  LedgerError
2616
3240
  > {
2617
3241
  const operation = "ledger scan nonterminal";
3242
+
2618
3243
  const rows = yield* (
2619
3244
  cursor === undefined
2620
3245
  ? sql<Record<string, unknown>>`
2621
3246
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2622
3247
  FROM effect_agent_submissions
2623
3248
  WHERE state <> 'settled'
2624
- ORDER BY conversation_id ASC, queue_sequence ASC
3249
+ ORDER BY thread_id ASC, queue_sequence ASC
2625
3250
  LIMIT ${SCAN_PAGE_SIZE}
2626
3251
  `
2627
3252
  : sql<Record<string, unknown>>`
2628
3253
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2629
3254
  FROM effect_agent_submissions
2630
3255
  WHERE state <> 'settled'
2631
- AND (
2632
- conversation_id > ${cursor.conversationId}
2633
- OR (
2634
- conversation_id = ${cursor.conversationId}
2635
- AND queue_sequence > ${cursor.queueSequence}
2636
- )
2637
- )
2638
- ORDER BY conversation_id ASC, queue_sequence ASC
3256
+ AND (thread_id, queue_sequence) > (${cursor.threadId}, ${cursor.queueSequence})
3257
+ ORDER BY thread_id ASC, queue_sequence ASC
2639
3258
  LIMIT ${SCAN_PAGE_SIZE}
2640
3259
  `
2641
3260
  ).pipe(Effect.mapError(sqlFailure(operation)));
3261
+
2642
3262
  const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
3263
+
2643
3264
  const snapshots = yield* Effect.forEach(decoded, (row) =>
2644
3265
  decodeSubmissionSnapshot(operation, row),
2645
3266
  );
3267
+
2646
3268
  const last = decoded[decoded.length - 1];
3269
+
2647
3270
  const next: Option.Option<ScanCursor | undefined> =
2648
3271
  last === undefined || decoded.length < SCAN_PAGE_SIZE
2649
3272
  ? Option.none()
2650
3273
  : Option.some({
2651
- conversationId: last.conversation_id,
3274
+ threadId: last.thread_id,
2652
3275
  queueSequence: last.queue_sequence,
2653
3276
  });
3277
+
2654
3278
  return [snapshots, next] as const;
2655
3279
  });
2656
3280
 
2657
- const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate(
2658
- undefined as ScanCursor | undefined,
2659
- scanPage,
2660
- );
3281
+ const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate<
3282
+ ScanCursor | undefined,
3283
+ SubmissionSnapshot,
3284
+ LedgerError
3285
+ >(undefined, scanPage);
3286
+
3287
+ const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
3288
+ "DoSubmissionLedger.readAbortIntentForSubmission",
3289
+ )(function* (request) {
3290
+ const operation = "ledger read abort intent";
3291
+
3292
+ const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
3293
+ Effect.mapError(internalFailure(operation)),
3294
+ );
3295
+
3296
+ const recordId = submissionAbortRecordId(validated.submissionId);
3297
+
3298
+ const rows = yield* sql<Record<string, unknown>>`
3299
+ SELECT
3300
+ submission.submission_id,
3301
+ abort.submission_id AS abort_submission_id,
3302
+ abort.author,
3303
+ abort.reason,
3304
+ abort.requested_at,
3305
+ canonical.record_id AS canonical_record_id
3306
+ FROM effect_agent_submissions AS submission
3307
+ LEFT JOIN effect_agent_abort_intents AS abort
3308
+ ON abort.submission_id = submission.submission_id
3309
+ LEFT JOIN effect_agent_canonical_records AS canonical
3310
+ ON canonical.thread_id = submission.thread_id
3311
+ AND canonical.record_id = ${recordId}
3312
+ WHERE submission.submission_id = ${validated.submissionId}
3313
+ `.pipe(Effect.mapError(sqlFailure(operation)));
3314
+
3315
+ const decoded = yield* decodeRows(
3316
+ Schema.Array(AbortIntentLookupRow),
3317
+ "effect_agent_abort_intents",
3318
+ validated.submissionId,
3319
+ rows,
3320
+ ).pipe(Effect.mapError(internalFailure(operation)));
3321
+
3322
+ if (decoded.length === 0) {
3323
+ return yield* LedgerError.make({
3324
+ operation,
3325
+ message: `Unknown submission ${validated.submissionId}.`,
3326
+ });
3327
+ }
3328
+ if (decoded.length !== 1) {
3329
+ return yield* corruptionFailure(
3330
+ operation,
3331
+ "effect_agent_abort_intents",
3332
+ validated.submissionId,
3333
+ "An abort intent lookup returned more than one row.",
3334
+ );
3335
+ }
3336
+ const row = decoded[0];
3337
+
3338
+ if (row.abort_submission_id === null) return undefined;
3339
+
3340
+ return yield* decodeAbortIntent({
3341
+ submissionId: row.abort_submission_id,
3342
+ author: row.author,
3343
+ reason: row.reason,
3344
+ requestedAt: row.requested_at,
3345
+ ...(row.canonical_record_id === null ? {} : { canonicalRecordId: row.canonical_record_id }),
3346
+ }).pipe(
3347
+ Effect.mapError((error) =>
3348
+ corruptionFailure(
3349
+ operation,
3350
+ "effect_agent_abort_intents",
3351
+ validated.submissionId,
3352
+ error.message,
3353
+ ),
3354
+ ),
3355
+ );
3356
+ });
2661
3357
 
2662
3358
  const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
2663
3359
  "DoSubmissionLedger.loadRecoverySnapshot",
2664
3360
  )(function* (request: RecoverySnapshotRequest) {
2665
3361
  const operation = "ledger load recovery snapshot";
2666
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RecoverySnapshotRequest))(
3362
+
3363
+ const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
2667
3364
  request,
2668
3365
  ).pipe(Effect.mapError(internalFailure(operation)));
3366
+
2669
3367
  return yield* sql
2670
3368
  .withTransaction(
2671
3369
  Effect.gen(function* () {
@@ -2674,6 +3372,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2674
3372
 
2675
3373
  let ownership: OwnershipSnapshot | undefined;
2676
3374
  const ownershipRow = yield* readOwnership(operation, validated.submissionId);
3375
+
2677
3376
  if (Option.isSome(ownershipRow)) {
2678
3377
  ownership = yield* decodeOwnershipSnapshot({
2679
3378
  attemptId: ownershipRow.value.attempt_id,
@@ -2684,6 +3383,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2684
3383
  }
2685
3384
 
2686
3385
  let inputApplied: InputAppliedMarker | undefined;
3386
+
2687
3387
  if (
2688
3388
  submissionRow.input_applied_record_id !== null &&
2689
3389
  submissionRow.input_applied_sequence !== null
@@ -2696,6 +3396,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2696
3396
 
2697
3397
  let reservation: SettlementReservationSnapshot | undefined;
2698
3398
  const reservationRow = yield* readReservation(operation, validated.submissionId);
3399
+
2699
3400
  if (Option.isSome(reservationRow)) {
2700
3401
  const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(
2701
3402
  Effect.mapError((error) =>
@@ -2707,9 +3408,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2707
3408
  ),
2708
3409
  ),
2709
3410
  );
2710
- const settlementId = yield* Schema.decodeUnknownEffect(
3411
+
3412
+ const settlementId = yield* Schema.decodeEffect(
2711
3413
  SettlementReservationSnapshot.fields.settlementId,
2712
3414
  )(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
3415
+
2713
3416
  reservation = SettlementReservationSnapshot.make({
2714
3417
  settlementId,
2715
3418
  outcome: reservationRow.value.outcome,
@@ -2721,6 +3424,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2721
3424
 
2722
3425
  let abortIntent: AbortIntent | undefined;
2723
3426
  const abortRow = yield* readAbortIntent(operation, validated.submissionId);
3427
+
2724
3428
  if (Option.isSome(abortRow)) {
2725
3429
  abortIntent = yield* abortIntentFromRow(
2726
3430
  operation,
@@ -2738,11 +3442,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2738
3442
  WHERE joined_host_submission_id = ${validated.submissionId}
2739
3443
  ORDER BY queue_sequence ASC
2740
3444
  `.pipe(Effect.mapError(sqlFailure(operation)));
3445
+
2741
3446
  const joinSubmissions = yield* decodeSubmissionRows(
2742
3447
  operation,
2743
3448
  validated.submissionId,
2744
3449
  joinRows,
2745
3450
  );
3451
+
2746
3452
  const joins = yield* Effect.forEach(joinSubmissions, (row) =>
2747
3453
  decodeJoinSnapshot({
2748
3454
  submissionId: row.submission_id,
@@ -2752,6 +3458,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2752
3458
  );
2753
3459
 
2754
3460
  let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
3461
+
2755
3462
  if (submissionRow.joined_host_submission_id !== null) {
2756
3463
  hostSubmissionId = yield* decodeSubmissionId(
2757
3464
  submissionRow.joined_host_submission_id,
@@ -2759,6 +3466,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2759
3466
  }
2760
3467
 
2761
3468
  let suspension: SuspensionSnapshot | undefined;
3469
+
2762
3470
  if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
2763
3471
  const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(
2764
3472
  Effect.mapError((error) =>
@@ -2770,6 +3478,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2770
3478
  ),
2771
3479
  ),
2772
3480
  );
3481
+
2773
3482
  suspension = yield* decodeSuspensionSnapshot({
2774
3483
  reason,
2775
3484
  suspendedAt: submissionRow.suspended_at,
@@ -2786,11 +3495,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2786
3495
  }
2787
3496
 
2788
3497
  const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
3498
+
2789
3499
  const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
2790
3500
  approvalIntentFromRow(operation, row),
2791
3501
  );
2792
3502
 
2793
3503
  const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
3504
+
2794
3505
  const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
2795
3506
  unknownResolutionIntentFromRow(operation, row),
2796
3507
  );
@@ -2807,20 +3518,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2807
3518
  WHERE parent_submission_id = ${validated.submissionId}
2808
3519
  ORDER BY parent_tool_call_id ASC
2809
3520
  `.pipe(Effect.mapError(sqlFailure(operation)));
3521
+
2810
3522
  const decodedChildReservations = yield* decodeChildReservationRows(
2811
3523
  operation,
2812
3524
  validated.submissionId,
2813
3525
  childReservationRows,
2814
3526
  );
3527
+
2815
3528
  const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
2816
3529
  childReservationSnapshotFromRow(operation, row),
2817
3530
  );
3531
+
2818
3532
  const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
2819
3533
  const markersByChild = new Map(markers.map((row) => [row.child_submission_id, row]));
2820
3534
  const childAttachments: Array<ChildAttachmentSnapshot> = [];
3535
+
2821
3536
  for (const row of decodedChildReservations) {
2822
3537
  if (row.child_submission_id === null) continue;
2823
3538
  const child = yield* readSubmission(operation, row.child_submission_id);
3539
+
2824
3540
  if (Option.isSome(child)) {
2825
3541
  childAttachments.push(
2826
3542
  yield* decodeChildAttachmentSnapshot({
@@ -2835,6 +3551,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2835
3551
  continue;
2836
3552
  }
2837
3553
  const marker = markersByChild.get(row.child_submission_id);
3554
+
2838
3555
  if (marker === undefined) continue;
2839
3556
  childAttachments.push(
2840
3557
  yield* decodeChildAttachmentSnapshot({
@@ -2847,6 +3564,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2847
3564
  }
2848
3565
 
2849
3566
  let parentLinkage: ParentLinkage | undefined;
3567
+
2850
3568
  if (
2851
3569
  submissionRow.parent_submission_id !== null &&
2852
3570
  submissionRow.parent_tool_call_id !== null
@@ -2906,6 +3624,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2906
3624
  releaseChildBudget,
2907
3625
  scanNonterminal,
2908
3626
  loadRecoverySnapshot,
3627
+ readAbortIntent: readAbortIntentForSubmission,
2909
3628
  }),
2910
3629
  );
2911
3630
  });
@@ -2923,18 +3642,22 @@ export const submissionLedgerLayer: Layer.Layer<
2923
3642
 
2924
3643
  /**
2925
3644
  * A composition-root convenience Layer for the durable Submission Ledger. Point it at the
2926
- * same `ctx.storage` as the ConversationStore so claims fence the same producer epochs.
3645
+ * same `ctx.storage` as the ThreadStore so claims fence the same producer epochs.
2927
3646
  */
2928
3647
  export const ledgerLayer = (
2929
3648
  options: DoStorageOptions,
2930
3649
  ): Layer.Layer<SubmissionLedger, DoStorageInitializationError> =>
2931
- submissionLedgerLayer.pipe(
2932
- Layer.provide(
2933
- Layer.mergeAll(
2934
- storageConfigLayer(options),
2935
- storageFailpointLayer(options),
2936
- SqliteClient.layer({ storage: options.storage }),
2937
- BrowserCrypto.layer,
3650
+ Layer.unwrap(
3651
+ Effect.map(DoStorageConfig, (config) =>
3652
+ submissionLedgerLayer.pipe(
3653
+ Layer.provide(
3654
+ Layer.mergeAll(
3655
+ Layer.succeed(DoStorageConfig)(config),
3656
+ storageFailpointLayer(options),
3657
+ SqliteClient.layer({ storage: options.storage }),
3658
+ BrowserCrypto.layer,
3659
+ ),
3660
+ ),
2938
3661
  ),
2939
3662
  ),
2940
- );
3663
+ ).pipe(Layer.provide(storageConfigLayer(options)));