@effect-agent/storage-cloudflare 0.1.0-beta.6 → 0.1.0-beta.60

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 (72) 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-UrxrvbnF.d.mts +98 -0
  14. package/dist/DoStorageError.d.mts +2 -0
  15. package/dist/DoStorageError.mjs +162 -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 +34 -0
  21. package/dist/DoStorageFailpointTesting.mjs +35 -0
  22. package/dist/DoStorageFailpointTesting.mjs.map +1 -0
  23. package/dist/DoStorageVersion-Pt3jEHdW.d.mts +10 -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 +1826 -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 +432 -0
  35. package/dist/DoThreadStore.mjs.map +1 -0
  36. package/dist/MemoryProtocol.d.mts +19796 -0
  37. package/dist/MemoryProtocol.mjs +182 -0
  38. package/dist/MemoryProtocol.mjs.map +1 -0
  39. package/dist/PortProtocol.d.mts +2951 -0
  40. package/dist/PortProtocol.mjs +176 -0
  41. package/dist/PortProtocol.mjs.map +1 -0
  42. package/dist/PortRouting.d.mts +82 -0
  43. package/dist/PortRouting.mjs +405 -0
  44. package/dist/PortRouting.mjs.map +1 -0
  45. package/dist/do-journal-Brv7xSH5.mjs +869 -0
  46. package/dist/do-journal-Brv7xSH5.mjs.map +1 -0
  47. package/dist/index.d.mts +14 -1505
  48. package/dist/index.mjs +14 -3695
  49. package/dist/migrations-soC86CQA.mjs +267 -0
  50. package/dist/migrations-soC86CQA.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 +59 -0
  54. package/src/DoMessageDeliveryStore.ts +53 -0
  55. package/src/DoScheduleStore.ts +622 -0
  56. package/src/{errors.ts → DoStorageError.ts} +8 -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} +757 -123
  61. package/src/DoSubscriptionStore.ts +329 -0
  62. package/src/DoThreadStore.ts +924 -0
  63. package/src/MemoryProtocol.ts +291 -0
  64. package/src/{port-protocol.ts → PortProtocol.ts} +41 -34
  65. package/src/{routing.ts → PortRouting.ts} +266 -259
  66. package/src/index.ts +13 -32
  67. package/src/{do-journal.ts → internal/do-journal.ts} +495 -214
  68. package/src/internal/message-delivery-schema.ts +24 -0
  69. package/src/{migrations.ts → internal/migrations.ts} +40 -35
  70. package/dist/index.mjs.map +0 -1
  71. package/src/do-conversation-store.ts +0 -850
  72. /package/src/{do-storage-config.ts → DoStorageConfig.ts} +0 -0
@@ -1,18 +1,34 @@
1
+ import { MessageAdmission } from "@effect-agent/core/Messaging";
2
+ import { EMPTY_TAIL_DIGEST } from "@effect-agent/thread/Digest";
3
+ import {
4
+ ApprovalDecision,
5
+ CanonicalSequence,
6
+ DefinitionDigests,
7
+ Digest,
8
+ PersistedJson,
9
+ WorkerAdmission,
10
+ ProducerEpoch,
11
+ RecordEnvelope,
12
+ SettlementOutcome,
13
+ } from "@effect-agent/thread/Records";
1
14
  import {
2
15
  AbortCommand,
3
16
  AbortIntent,
17
+ AbortIntentRequest,
4
18
  AdmissionAdmitted,
5
19
  AdmissionConflict,
20
+ AdmissionFence,
21
+ AdmissionGroup,
22
+ AdmissionPolicyError,
6
23
  AdmissionNotAdmitted,
7
24
  AdmissionRequest,
25
+ SubmissionAdmissionFence,
8
26
  AdmissionResult,
9
27
  ApprovalConflict,
10
- ApprovalDecision,
11
28
  ApprovalDecisionCommand,
12
29
  ApprovalDecisionIntent,
13
30
  AttachChildToReservationRequest,
14
31
  BeginChildBudgetReleaseRequest,
15
- CanonicalSequence,
16
32
  ChildAttachmentSnapshot,
17
33
  ChildBudgetReservationRequest,
18
34
  ChildBudgetReservationSnapshot,
@@ -22,9 +38,6 @@ import {
22
38
  Claim,
23
39
  ClaimJoiningRequest,
24
40
  ClaimRequest,
25
- DefinitionDigests,
26
- Digest,
27
- EMPTY_TAIL_DIGEST,
28
41
  InputAppliedMarker,
29
42
  JoinSnapshot,
30
43
  JoinedToHost,
@@ -39,10 +52,7 @@ import {
39
52
  OwnershipRenewal,
40
53
  OwnershipSnapshot,
41
54
  ParentLinkage,
42
- PersistedJson,
43
- ProducerEpoch,
44
55
  QueueSequence,
45
- RecordEnvelope,
46
56
  RecoverySnapshot,
47
57
  RecoverySnapshotRequest,
48
58
  ReleaseChildBudgetRequest,
@@ -54,7 +64,6 @@ import {
54
64
  Settlement,
55
65
  SettlementConflict,
56
66
  SettlementFinalization,
57
- SettlementOutcome,
58
67
  SettlementReservation,
59
68
  SubmissionLedger,
60
69
  SubmissionLookup,
@@ -62,6 +71,7 @@ import {
62
71
  SubmissionSnapshot,
63
72
  SubmissionState,
64
73
  SettlementReservationSnapshot,
74
+ settlementFailureFromRecord,
65
75
  SuspendRequest,
66
76
  SuspensionReason,
67
77
  SuspensionSnapshot,
@@ -72,28 +82,28 @@ import {
72
82
  submissionAbortRecordId,
73
83
  type ChildSettledOutcome,
74
84
  type SuspensionOutcome,
75
- } from "@effect-agent/session";
85
+ } from "@effect-agent/thread/SubmissionLedger";
76
86
  import { BrowserCrypto } from "@effect/platform-browser";
77
87
  import { SqliteClient } from "@effect/sql-sqlite-do";
78
88
  import { Clock, Context, Crypto, DateTime, Effect, Layer, Option, Schema, Stream } from "effect";
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,17 @@ 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);
515
+
467
516
  if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
468
- const actualEpoch = yield* conversationEpoch(operation, submission.conversation_id);
517
+ const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
518
+
469
519
  const submissionId = yield* Schema.decodeUnknownEffect(
470
520
  SubmissionSnapshot.fields.submissionId,
471
521
  )(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
522
+
472
523
  return yield* OwnershipLost.make({ submissionId, actualEpoch });
473
524
  }
525
+
474
526
  return ownership.value;
475
527
  });
476
528
 
@@ -489,6 +541,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
489
541
  ),
490
542
  ),
491
543
  );
544
+
492
545
  const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
493
546
  Effect.mapError((error) =>
494
547
  corruptionFailure(
@@ -499,6 +552,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
499
552
  ),
500
553
  ),
501
554
  );
555
+
502
556
  if ((row.parent_submission_id === null) !== (row.parent_tool_call_id === null)) {
503
557
  return yield* corruptionFailure(
504
558
  operation,
@@ -507,9 +561,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
507
561
  "A parent linkage must record both the parent Submission and the parent Tool Call.",
508
562
  );
509
563
  }
564
+
510
565
  return yield* decodeSubmissionSnapshotUnknown({
511
566
  submissionId: row.submission_id,
512
- conversationId: row.conversation_id,
567
+ threadId: row.thread_id,
513
568
  queueSequence: row.queue_sequence,
514
569
  principal: row.principal,
515
570
  idempotencyKey: row.idempotency_key,
@@ -521,6 +576,28 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
521
576
  receiptId: row.receipt_id,
522
577
  state: row.state,
523
578
  createdAt: row.created_at,
579
+ ...(row.admission_group === null ? {} : { admissionGroup: row.admission_group }),
580
+ ...(row.worker_admission_json === null
581
+ ? {}
582
+ : {
583
+ workerAdmission: yield* parseStoredJsonText(row.worker_admission_json).pipe(
584
+ Effect.mapError(internalFailure(operation)),
585
+ ),
586
+ }),
587
+ ...(row.message_admission_json === null
588
+ ? {}
589
+ : {
590
+ messageAdmission: yield* parseStoredJsonText(row.message_admission_json).pipe(
591
+ Effect.mapError(internalFailure(operation)),
592
+ ),
593
+ }),
594
+ ...(row.admission_fence_json === null
595
+ ? {}
596
+ : {
597
+ admissionFence: yield* parseStoredJsonText(row.admission_fence_json).pipe(
598
+ Effect.mapError(internalFailure(operation)),
599
+ ),
600
+ }),
524
601
  ...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
525
602
  ...(row.ready_at === null ? {} : { readyAt: row.ready_at }),
526
603
  ...(row.parent_submission_id === null || row.parent_tool_call_id === null
@@ -561,12 +638,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
561
638
  FROM effect_agent_settlement_reservations
562
639
  WHERE submission_id = ${submissionId}
563
640
  `.pipe(Effect.mapError(sqlFailure(operation)));
641
+
564
642
  const decoded = yield* decodeRows(
565
643
  Schema.Array(ReservationRow),
566
644
  "effect_agent_settlement_reservations",
567
645
  submissionId,
568
646
  rows,
569
647
  ).pipe(Effect.mapError(internalFailure(operation)));
648
+
570
649
  if (decoded.length > 1) {
571
650
  return yield* corruptionFailure(
572
651
  operation,
@@ -575,6 +654,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
575
654
  "A settlement reservation primary key returned more than one row.",
576
655
  );
577
656
  }
657
+
578
658
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
579
659
  });
580
660
 
@@ -592,12 +672,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
592
672
  FROM effect_agent_abort_intents
593
673
  WHERE submission_id = ${submissionId}
594
674
  `.pipe(Effect.mapError(sqlFailure(operation)));
675
+
595
676
  const decoded = yield* decodeRows(
596
677
  Schema.Array(AbortIntentRow),
597
678
  "effect_agent_abort_intents",
598
679
  submissionId,
599
680
  rows,
600
681
  ).pipe(Effect.mapError(internalFailure(operation)));
682
+
601
683
  if (decoded.length > 1) {
602
684
  return yield* corruptionFailure(
603
685
  operation,
@@ -606,6 +688,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
606
688
  "An abort intent primary key returned more than one row.",
607
689
  );
608
690
  }
691
+
609
692
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
610
693
  });
611
694
 
@@ -629,6 +712,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
629
712
  WHERE parent_submission_id = ${parentSubmissionId}
630
713
  ORDER BY child_submission_id ASC
631
714
  `.pipe(Effect.mapError(sqlFailure(operation)));
715
+
632
716
  return yield* decodeRows(
633
717
  Schema.Array(ChildSettlementMarkerRow),
634
718
  "effect_agent_child_settlements",
@@ -651,6 +735,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
651
735
  ): Effect.fn.Return<boolean, LedgerError> {
652
736
  if (markerChildren.has(childSubmissionId)) return true;
653
737
  const childRow = yield* readSubmission(operation, childSubmissionId);
738
+
654
739
  return Option.isSome(childRow) && childRow.value.state === "settled";
655
740
  });
656
741
 
@@ -671,7 +756,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
671
756
  FROM effect_agent_child_reservations
672
757
  WHERE reservation_id = ${reservationId}
673
758
  `.pipe(Effect.mapError(sqlFailure(operation)));
759
+
674
760
  const decoded = yield* decodeChildReservationRows(operation, reservationId, rows);
761
+
675
762
  if (decoded.length > 1) {
676
763
  return yield* corruptionFailure(
677
764
  operation,
@@ -680,6 +767,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
680
767
  "A child reservation primary key returned more than one row.",
681
768
  );
682
769
  }
770
+
683
771
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
684
772
  });
685
773
 
@@ -695,11 +783,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
695
783
  WHERE parent_submission_id = ${parentSubmissionId}
696
784
  AND parent_tool_call_id = ${parentToolCallId}
697
785
  `.pipe(Effect.mapError(sqlFailure(operation)));
786
+
698
787
  const decoded = yield* decodeChildReservationRows(
699
788
  operation,
700
789
  `${parentSubmissionId}/${parentToolCallId}`,
701
790
  rows,
702
791
  );
792
+
703
793
  if (decoded.length > 1) {
704
794
  return yield* corruptionFailure(
705
795
  operation,
@@ -708,6 +798,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
708
798
  "A parent Tool Call returned more than one child reservation.",
709
799
  );
710
800
  }
801
+
711
802
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
712
803
  },
713
804
  );
@@ -725,13 +816,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
725
816
  row.reservation_id,
726
817
  error.message,
727
818
  );
819
+
728
820
  const allocation = yield* parseStoredJsonText(row.allocation_json).pipe(
729
821
  Effect.mapError(rowFailure),
730
822
  );
823
+
731
824
  const accounting =
732
825
  row.accounting_json === null
733
826
  ? undefined
734
827
  : yield* parseStoredJsonText(row.accounting_json).pipe(Effect.mapError(rowFailure));
828
+
735
829
  return yield* decodeChildReservationSnapshotUnknown({
736
830
  reservationId: row.reservation_id,
737
831
  parentSubmissionId: row.parent_submission_id,
@@ -763,6 +857,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
763
857
  WHERE submission_id = ${submissionId}
764
858
  ORDER BY tool_call_id ASC
765
859
  `.pipe(Effect.mapError(sqlFailure(operation)));
860
+
766
861
  return yield* decodeRows(
767
862
  Schema.Array(ApprovalDecisionRow),
768
863
  "effect_agent_approval_decisions",
@@ -810,6 +905,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
810
905
  WHERE submission_id = ${submissionId}
811
906
  ORDER BY tool_call_id ASC
812
907
  `.pipe(Effect.mapError(sqlFailure(operation)));
908
+
813
909
  return yield* decodeRows(
814
910
  Schema.Array(UnknownResolutionRow),
815
911
  "effect_agent_unknown_resolutions",
@@ -834,6 +930,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
834
930
  ),
835
931
  ),
836
932
  );
933
+
837
934
  return yield* decodeUnknownResolutionIntent({
838
935
  submissionId: row.submission_id,
839
936
  toolCallId: row.tool_call_id,
@@ -860,6 +957,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
860
957
  submission: SubmissionRow,
861
958
  ): Effect.fn.Return<ReadonlyArray<typeof ToolCallIdSchema.Type>, LedgerError> {
862
959
  if (submission.unknown_tool_call_ids_json === null) return [];
960
+
863
961
  return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(
864
962
  Effect.mapError((error) =>
865
963
  corruptionFailure(
@@ -880,22 +978,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
880
978
  */
881
979
  const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (
882
980
  operation: string,
883
- conversationId: string,
981
+ threadId: string,
884
982
  submissionId: SubmissionId,
885
983
  ): Effect.fn.Return<string | undefined, LedgerError> {
886
984
  const recordId = submissionAbortRecordId(submissionId);
985
+
887
986
  const rows = yield* sql<Record<string, unknown>>`
888
987
  SELECT record_id
889
988
  FROM effect_agent_canonical_records
890
- WHERE conversation_id = ${conversationId}
989
+ WHERE thread_id = ${threadId}
891
990
  AND record_id = ${recordId}
892
991
  `.pipe(Effect.mapError(sqlFailure(operation)));
992
+
893
993
  const decoded = yield* decodeRows(
894
994
  Schema.Array(CanonicalRecordIdRow),
895
995
  "effect_agent_canonical_records",
896
- `${conversationId}/${recordId}`,
996
+ `${threadId}/${recordId}`,
897
997
  rows,
898
998
  ).pipe(Effect.mapError(internalFailure(operation)));
999
+
899
1000
  return decoded.length === 0 ? undefined : recordId;
900
1001
  });
901
1002
 
@@ -907,9 +1008,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
907
1008
  ): Effect.fn.Return<AbortIntent, LedgerError> {
908
1009
  const canonicalRecordId = yield* canonicalAbortRecordId(
909
1010
  operation,
910
- submission.conversation_id,
1011
+ submission.thread_id,
911
1012
  submissionId,
912
1013
  );
1014
+
913
1015
  return yield* decodeAbortIntent({
914
1016
  submissionId: row.submission_id,
915
1017
  author: row.author,
@@ -938,47 +1040,92 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
938
1040
  const admit: SubmissionLedger["Service"]["admit"] = Effect.fn("DoSubmissionLedger.admit")(
939
1041
  function* (request: AdmissionRequest) {
940
1042
  const operation = "ledger admit";
1043
+
941
1044
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AdmissionRequest))(
942
1045
  request,
943
1046
  ).pipe(Effect.mapError(internalFailure(operation)));
1047
+
944
1048
  const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(
945
1049
  Effect.mapError(internalFailure(operation)),
946
1050
  );
1051
+
947
1052
  // The platform's ~2 MB per-value bound, refused typed BEFORE any durable mutation
948
1053
  // (resource-limits gate; oversized payloads are the designed R2 overflow path).
949
1054
  yield* journal
950
1055
  .checkValueBound(operation, inputJson)
951
1056
  .pipe(Effect.mapError(internalFailure(operation)));
1057
+
1058
+ const workerAdmissionJson =
1059
+ validated.workerAdmission === undefined
1060
+ ? null
1061
+ : yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
1062
+ validated.workerAdmission,
1063
+ ).pipe(Effect.mapError(internalFailure(operation)));
1064
+
1065
+ if (
1066
+ workerAdmissionJson !== null &&
1067
+ new TextEncoder().encode(workerAdmissionJson).byteLength > config.maxStoredValueBytes
1068
+ ) {
1069
+ return yield* LedgerError.make({
1070
+ operation,
1071
+ message: "Worker admission metadata exceeds the stored value bound",
1072
+ });
1073
+ }
1074
+
1075
+ const messageAdmissionJson =
1076
+ validated.messageAdmission === undefined
1077
+ ? null
1078
+ : yield* Schema.encodeEffect(Schema.fromJsonString(MessageAdmission))(
1079
+ validated.messageAdmission,
1080
+ ).pipe(Effect.mapError(internalFailure(operation)));
1081
+
1082
+ if (
1083
+ messageAdmissionJson !== null &&
1084
+ new TextEncoder().encode(messageAdmissionJson).byteLength > config.maxStoredValueBytes
1085
+ ) {
1086
+ return yield* LedgerError.make({
1087
+ operation,
1088
+ message: "Message admission metadata exceeds the stored value bound",
1089
+ });
1090
+ }
1091
+
952
1092
  const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
953
1093
  Effect.mapError(internalFailure(operation)),
954
1094
  );
955
- // Routable Submission identity (D-P6-5): `{uuidv7}:{conversationId}`. The cross-DO
1095
+
1096
+ // Routable Submission identity (D-P6-5): `{uuidv7}:{threadId}`. The cross-DO
956
1097
  // 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
1098
+ // submissionId-only operations to the owning Thread Object; the id stays opaque
958
1099
  // to every other component, exactly like DN's `submission-{uuid}` prefix.
959
- const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.conversationId}`;
1100
+ const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.threadId}`;
1101
+
960
1102
  if (mintedSubmissionId.length > MAX_IDENTIFIER_LENGTH) {
961
1103
  return yield* LedgerError.make({
962
1104
  operation,
963
1105
  message:
964
1106
  `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds ` +
965
- `the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Conversation identity.`,
1107
+ `the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Thread identity.`,
966
1108
  });
967
1109
  }
968
1110
  const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
1111
+
969
1112
  yield* hitFailpoint("ledger:admit:before", operation);
1113
+
970
1114
  const result = yield* inWriteTransaction(
971
1115
  operation,
972
1116
  Effect.gen(function* () {
973
- const keyRowKey = `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`;
1117
+ const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
1118
+
974
1119
  const existingRows = yield* sql<Record<string, unknown>>`
975
1120
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
976
1121
  FROM effect_agent_submissions
977
- WHERE conversation_id = ${validated.conversationId}
1122
+ WHERE thread_id = ${validated.threadId}
978
1123
  AND principal = ${validated.principal}
979
1124
  AND idempotency_key = ${validated.idempotencyKey}
980
1125
  `.pipe(Effect.mapError(sqlFailure(operation)));
1126
+
981
1127
  const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
1128
+
982
1129
  if (existing.length > 1) {
983
1130
  return yield* corruptionFailure(
984
1131
  operation,
@@ -996,15 +1143,61 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
996
1143
  existing[0].parent_tool_call_id === null
997
1144
  : existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId &&
998
1145
  existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
1146
+
999
1147
  if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
1000
1148
  return yield* AdmissionConflict.make({
1001
- conversationId: validated.conversationId,
1149
+ threadId: validated.threadId,
1002
1150
  principal: validated.principal,
1003
1151
  idempotencyKey: validated.idempotencyKey,
1004
1152
  existingInputDigest: existing[0].input_digest,
1005
1153
  attemptedInputDigest: validated.inputDigest,
1006
1154
  });
1007
1155
  }
1156
+
1157
+ const retainedWorkerAdmission =
1158
+ existing[0].worker_admission_json === null
1159
+ ? undefined
1160
+ : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1161
+ existing[0].worker_admission_json,
1162
+ ).pipe(Effect.mapError(internalFailure(operation)));
1163
+
1164
+ const retainedMessageAdmission =
1165
+ existing[0].message_admission_json === null
1166
+ ? undefined
1167
+ : yield* Schema.decodeEffect(Schema.fromJsonString(MessageAdmission))(
1168
+ existing[0].message_admission_json,
1169
+ ).pipe(Effect.mapError(internalFailure(operation)));
1170
+
1171
+ const retainedFence =
1172
+ existing[0].admission_fence_json === null
1173
+ ? undefined
1174
+ : yield* Schema.decodeEffect(Schema.fromJsonString(AdmissionFence))(
1175
+ existing[0].admission_fence_json,
1176
+ ).pipe(Effect.mapError(internalFailure(operation)));
1177
+
1178
+ if (
1179
+ (existing[0].admission_group ?? undefined) !== validated.admissionGroup ||
1180
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission))(
1181
+ retainedWorkerAdmission,
1182
+ validated.workerAdmission,
1183
+ ) ||
1184
+ !Schema.toEquivalence(Schema.optional(MessageAdmission))(
1185
+ retainedMessageAdmission,
1186
+ validated.messageAdmission,
1187
+ ) ||
1188
+ !Schema.toEquivalence(Schema.optional(AdmissionFence))(
1189
+ retainedFence,
1190
+ validated.admissionFence,
1191
+ )
1192
+ )
1193
+ return yield* AdmissionConflict.make({
1194
+ threadId: validated.threadId,
1195
+ principal: validated.principal,
1196
+ idempotencyKey: validated.idempotencyKey,
1197
+ existingInputDigest: existing[0].input_digest,
1198
+ attemptedInputDigest: validated.inputDigest,
1199
+ });
1200
+
1008
1201
  return yield* decodeAdmissionResult({
1009
1202
  submissionId: existing[0].submission_id,
1010
1203
  receiptId: existing[0].receipt_id,
@@ -1014,26 +1207,78 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1014
1207
  }).pipe(Effect.mapError(internalFailure(operation)));
1015
1208
  }
1016
1209
 
1210
+ // The first accepted input fixes ordinary/worker lane identity atomically with admission.
1211
+ // Canonical origin materialization can lag admission; a log scan cannot fence that race.
1212
+ const firstRows = yield* sql<Record<string, unknown>>`
1213
+ SELECT worker_admission_json FROM effect_agent_submissions
1214
+ WHERE thread_id=${validated.threadId} ORDER BY queue_sequence LIMIT 1
1215
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1216
+
1217
+ const first = yield* Schema.decodeUnknownEffect(
1218
+ Schema.Array(
1219
+ Schema.Struct({
1220
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
1221
+ }),
1222
+ ),
1223
+ )(firstRows).pipe(Effect.mapError(internalFailure(operation)));
1224
+
1225
+ if (first[0] !== undefined) {
1226
+ const previous =
1227
+ first[0].worker_admission_json === null
1228
+ ? undefined
1229
+ : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1230
+ first[0].worker_admission_json,
1231
+ ).pipe(Effect.mapError(internalFailure(operation)));
1232
+
1233
+ if (
1234
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
1235
+ previous?.origin,
1236
+ validated.workerAdmission?.origin,
1237
+ )
1238
+ )
1239
+ return yield* AdmissionPolicyError.make({
1240
+ reason: "refused",
1241
+ code: "worker-origin-conflict",
1242
+ });
1243
+ }
1244
+
1245
+ yield* admissionFence.check(validated);
1246
+ if (validated.admissionGroup !== undefined) {
1247
+ const occupied = yield* sql<Record<string, unknown>>`
1248
+ SELECT submission_id FROM effect_agent_submissions
1249
+ WHERE thread_id=${validated.threadId} AND admission_group=${validated.admissionGroup} AND state<>'settled' LIMIT 1
1250
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1251
+
1252
+ if (occupied.length > 0)
1253
+ return yield* AdmissionPolicyError.make({
1254
+ reason: "occupied",
1255
+ code: "admission-group",
1256
+ });
1257
+ }
1258
+
1017
1259
  const maxRows = yield* sql<Record<string, unknown>>`
1018
1260
  SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
1019
1261
  FROM effect_agent_submissions
1020
- WHERE conversation_id = ${validated.conversationId}
1262
+ WHERE thread_id = ${validated.threadId}
1021
1263
  `.pipe(Effect.mapError(sqlFailure(operation)));
1264
+
1022
1265
  const decodedMax = yield* decodeRows(
1023
1266
  Schema.Array(MaxQueueSequenceRow),
1024
1267
  "effect_agent_submissions",
1025
- validated.conversationId,
1268
+ validated.threadId,
1026
1269
  maxRows,
1027
1270
  ).pipe(Effect.mapError(internalFailure(operation)));
1271
+
1028
1272
  const queueSequence = yield* decodeQueueSequence(
1029
1273
  (decodedMax[0]?.max_queue_sequence ?? 0) + 1,
1030
1274
  ).pipe(Effect.mapError(internalFailure(operation)));
1275
+
1031
1276
  const now = yield* currentInstant;
1032
1277
 
1033
1278
  yield* sql`
1034
1279
  INSERT INTO effect_agent_submissions (
1035
1280
  submission_id,
1036
- conversation_id,
1281
+ thread_id,
1037
1282
  queue_sequence,
1038
1283
  principal,
1039
1284
  idempotency_key,
@@ -1046,10 +1291,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1046
1291
  state,
1047
1292
  created_at,
1048
1293
  parent_submission_id,
1049
- parent_tool_call_id
1294
+ parent_tool_call_id,
1295
+ admission_group,
1296
+ admission_fence_json,
1297
+ worker_admission_json,
1298
+ message_admission_json
1050
1299
  ) VALUES (
1051
1300
  ${mintedSubmissionId},
1052
- ${validated.conversationId},
1301
+ ${validated.threadId},
1053
1302
  ${queueSequence},
1054
1303
  ${validated.principal},
1055
1304
  ${validated.idempotencyKey},
@@ -1062,7 +1311,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1062
1311
  'admitted',
1063
1312
  ${now.iso},
1064
1313
  ${validated.parentLinkage?.parentSubmissionId ?? null},
1065
- ${validated.parentLinkage?.parentToolCallId ?? null}
1314
+ ${validated.parentLinkage?.parentToolCallId ?? null},
1315
+ ${validated.admissionGroup ?? null},
1316
+ ${validated.admissionFence === undefined ? null : JSON.stringify(validated.admissionFence)},
1317
+ ${workerAdmissionJson},
1318
+ ${messageAdmissionJson}
1066
1319
  )
1067
1320
  `.pipe(Effect.mapError(sqlFailure(operation)));
1068
1321
 
@@ -1075,7 +1328,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1075
1328
  }).pipe(Effect.mapError(internalFailure(operation)));
1076
1329
  }),
1077
1330
  );
1331
+
1078
1332
  yield* hitFailpoint("ledger:admit:after", operation);
1333
+
1079
1334
  return result;
1080
1335
  },
1081
1336
  );
@@ -1084,16 +1339,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1084
1339
  "DoSubmissionLedger.markReady",
1085
1340
  )(function* (request: MarkReadyRequest) {
1086
1341
  const operation = "ledger mark ready";
1342
+
1087
1343
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkReadyRequest))(
1088
1344
  request,
1089
1345
  ).pipe(Effect.mapError(internalFailure(operation)));
1346
+
1090
1347
  yield* hitFailpoint("ledger:mark-ready:before", operation);
1091
1348
  yield* inWriteTransaction(
1092
1349
  operation,
1093
1350
  Effect.gen(function* () {
1094
1351
  const submission = yield* requireSubmission(operation, validated.submissionId);
1352
+
1095
1353
  if (submission.state !== "admitted") return;
1096
1354
  const now = yield* currentInstant;
1355
+
1097
1356
  yield* sql`
1098
1357
  UPDATE effect_agent_submissions
1099
1358
  SET state = 'ready', ready_at = ${now.iso}
@@ -1107,40 +1366,48 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1107
1366
  const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn("DoSubmissionLedger.lookup")(
1108
1367
  function* (request: SubmissionLookup) {
1109
1368
  const operation = "ledger lookup";
1369
+
1110
1370
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookup))(
1111
1371
  request,
1112
1372
  ).pipe(Effect.mapError(internalFailure(operation)));
1373
+
1113
1374
  if (validated._tag === "SubmissionLookupById") {
1114
1375
  const row = yield* readSubmission(operation, validated.submissionId);
1376
+
1115
1377
  if (Option.isNone(row)) return Option.none();
1378
+
1116
1379
  return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
1117
1380
  }
1381
+
1118
1382
  const rows = yield* sql<Record<string, unknown>>`
1119
1383
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1120
1384
  FROM effect_agent_submissions
1121
- WHERE conversation_id = ${validated.conversationId}
1385
+ WHERE thread_id = ${validated.threadId}
1122
1386
  AND principal = ${validated.principal}
1123
1387
  AND idempotency_key = ${validated.idempotencyKey}
1124
1388
  `.pipe(Effect.mapError(sqlFailure(operation)));
1389
+
1125
1390
  const decoded = yield* decodeSubmissionRows(
1126
1391
  operation,
1127
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1392
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1128
1393
  rows,
1129
1394
  );
1395
+
1130
1396
  if (decoded.length > 1) {
1131
1397
  return yield* corruptionFailure(
1132
1398
  operation,
1133
1399
  "effect_agent_submissions",
1134
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1400
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1135
1401
  "An admission idempotency key returned more than one row.",
1136
1402
  );
1137
1403
  }
1138
1404
  if (decoded.length === 0) return Option.none();
1405
+
1139
1406
  return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
1140
1407
  },
1141
1408
  );
1142
1409
 
1143
- // This LOCAL facet is the authoritative owner of every Conversation stored in this Durable
1410
+ // This LOCAL facet is the authoritative owner of every Thread stored in this Durable
1144
1411
  // Object, so the key-scoped read IS the admission truth and the tri-state degenerates to
1145
1412
  // NotAdmitted or Admitted (SUB-031). `AdmissionIndeterminate` becomes real one layer out:
1146
1413
  // the WP2 routed decorator answers it when the OWNING Durable Object is unreachable.
@@ -1148,30 +1415,35 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1148
1415
  "DoSubmissionLedger.resolveAdmission",
1149
1416
  )(function* (request: SubmissionLookupByKey) {
1150
1417
  const operation = "ledger resolve admission";
1418
+
1151
1419
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookupByKey))(
1152
1420
  request,
1153
1421
  ).pipe(Effect.mapError(internalFailure(operation)));
1422
+
1154
1423
  const rows = yield* sql<Record<string, unknown>>`
1155
1424
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1156
1425
  FROM effect_agent_submissions
1157
- WHERE conversation_id = ${validated.conversationId}
1426
+ WHERE thread_id = ${validated.threadId}
1158
1427
  AND principal = ${validated.principal}
1159
1428
  AND idempotency_key = ${validated.idempotencyKey}
1160
1429
  `.pipe(Effect.mapError(sqlFailure(operation)));
1430
+
1161
1431
  const decoded = yield* decodeSubmissionRows(
1162
1432
  operation,
1163
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1433
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1164
1434
  rows,
1165
1435
  );
1436
+
1166
1437
  if (decoded.length > 1) {
1167
1438
  return yield* corruptionFailure(
1168
1439
  operation,
1169
1440
  "effect_agent_submissions",
1170
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1441
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1171
1442
  "An admission idempotency key returned more than one row.",
1172
1443
  );
1173
1444
  }
1174
1445
  if (decoded.length === 0) return AdmissionNotAdmitted.make();
1446
+
1175
1447
  return AdmissionAdmitted.make({
1176
1448
  submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
1177
1449
  });
@@ -1180,71 +1452,82 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1180
1452
  const claim: SubmissionLedger["Service"]["claim"] = Effect.fn("DoSubmissionLedger.claim")(
1181
1453
  function* (request: ClaimRequest) {
1182
1454
  const operation = "ledger claim";
1455
+
1183
1456
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimRequest))(
1184
1457
  request,
1185
1458
  ).pipe(Effect.mapError(internalFailure(operation)));
1459
+
1186
1460
  const attemptId = `attempt-${yield* mintUuid(operation)}`;
1187
1461
  const ownershipToken = `owner-${yield* mintUuid(operation)}`;
1462
+
1188
1463
  yield* hitFailpoint("ledger:claim:before", operation);
1464
+
1189
1465
  const claimed = yield* inWriteTransaction(
1190
1466
  operation,
1191
1467
  Effect.gen(function* () {
1192
1468
  const headRows = yield* sql<Record<string, unknown>>`
1193
1469
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1194
1470
  FROM effect_agent_submissions
1195
- WHERE conversation_id = ${validated.conversationId}
1471
+ WHERE thread_id = ${validated.threadId}
1196
1472
  AND state <> 'settled'
1197
1473
  ORDER BY queue_sequence ASC
1198
1474
  LIMIT 1
1199
1475
  `.pipe(Effect.mapError(sqlFailure(operation)));
1200
- const heads = yield* decodeSubmissionRows(operation, validated.conversationId, headRows);
1476
+
1477
+ const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
1478
+
1201
1479
  if (heads.length === 0) return Option.none<Claim>();
1202
1480
  const head = heads[0];
1203
1481
 
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).
1482
+ // Unknown work is claimable only for a durably requested abort: the coordinator
1483
+ // cleans up children and settles without replaying ordinary Tools. Read the intent
1484
+ // in this claim transaction; retain uncertainty evidence and all ownership fencing.
1207
1485
  if (
1208
1486
  head.state === "joining" ||
1209
1487
  head.state === "joined" ||
1210
1488
  head.state === "suspended" ||
1211
- head.state === "unknown"
1489
+ (head.state === "unknown" &&
1490
+ Option.isNone(yield* readAbortIntent(operation, head.submission_id)))
1212
1491
  ) {
1213
1492
  return Option.none<Claim>();
1214
1493
  }
1215
1494
 
1216
1495
  const now = yield* currentInstant;
1217
1496
  const ownership = yield* readOwnership(operation, head.submission_id);
1497
+
1218
1498
  if (Option.isSome(ownership)) {
1219
1499
  const expiresAt = yield* timestampMillis(
1220
1500
  operation,
1221
1501
  head.submission_id,
1222
1502
  )(ownership.value.lease_expires_at);
1503
+
1223
1504
  // A live lease blocks every new claim; expiry alone only revokes the liveness
1224
1505
  // assumption — correctness stays with producer-epoch fencing (D5). In DC a live
1225
1506
  // lease under another token can only come from an evicted incarnation.
1226
1507
  if (expiresAt > now.millis) return Option.none<Claim>();
1227
1508
  }
1228
1509
 
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
1510
+ // Bump the Thread's producer epoch atomically with the claim so every stale
1511
+ // Attempt is fenced out of canonical appends (DUR-006). A Thread that was
1231
1512
  // never materialized (eviction between admission and materialization) is created
1232
1513
  // here so recovery can claim first and re-materialize idempotently at this epoch.
1233
- const conversations = yield* journal
1234
- .getConversation(head.conversation_id)
1514
+ const threads = yield* journal
1515
+ .getThread(head.thread_id)
1235
1516
  .pipe(Effect.mapError(internalFailure(operation)));
1517
+
1236
1518
  let producerEpoch: number;
1237
- if (conversations.length === 0) {
1519
+
1520
+ if (threads.length === 0) {
1238
1521
  producerEpoch = 1;
1239
1522
  yield* sql`
1240
- INSERT INTO effect_agent_conversations (
1241
- conversation_id,
1523
+ INSERT INTO effect_agent_threads (
1524
+ thread_id,
1242
1525
  created_at,
1243
1526
  tail_sequence,
1244
1527
  tail_digest,
1245
1528
  producer_epoch
1246
1529
  ) VALUES (
1247
- ${head.conversation_id},
1530
+ ${head.thread_id},
1248
1531
  ${now.iso},
1249
1532
  0,
1250
1533
  ${EMPTY_TAIL_DIGEST},
@@ -1252,15 +1535,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1252
1535
  )
1253
1536
  `.pipe(Effect.mapError(sqlFailure(operation)));
1254
1537
  } else {
1255
- producerEpoch = conversations[0].producer_epoch + 1;
1538
+ producerEpoch = threads[0].producer_epoch + 1;
1256
1539
  yield* sql`
1257
- UPDATE effect_agent_conversations
1540
+ UPDATE effect_agent_threads
1258
1541
  SET producer_epoch = ${producerEpoch}
1259
- WHERE conversation_id = ${head.conversation_id}
1542
+ WHERE thread_id = ${head.thread_id}
1260
1543
  `.pipe(Effect.mapError(sqlFailure(operation)));
1261
1544
  }
1262
1545
 
1263
1546
  const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1547
+
1264
1548
  yield* sql`
1265
1549
  INSERT INTO effect_agent_submission_ownership (
1266
1550
  submission_id,
@@ -1289,14 +1573,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1289
1573
  INSERT INTO effect_agent_attempts (
1290
1574
  attempt_id,
1291
1575
  submission_id,
1292
- conversation_id,
1576
+ thread_id,
1293
1577
  owner_producer_id,
1294
1578
  producer_epoch,
1295
1579
  claimed_at
1296
1580
  ) VALUES (
1297
1581
  ${attemptId},
1298
1582
  ${head.submission_id},
1299
- ${head.conversation_id},
1583
+ ${head.thread_id},
1300
1584
  ${validated.producerId},
1301
1585
  ${producerEpoch},
1302
1586
  ${now.iso}
@@ -1321,6 +1605,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1321
1605
  ),
1322
1606
  ),
1323
1607
  );
1608
+
1324
1609
  return Option.some(
1325
1610
  yield* decodeClaim({
1326
1611
  submissionId: head.submission_id,
@@ -1333,7 +1618,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1333
1618
  );
1334
1619
  }),
1335
1620
  );
1621
+
1336
1622
  yield* hitFailpoint("ledger:claim:after", operation);
1623
+
1337
1624
  return claimed;
1338
1625
  },
1339
1626
  );
@@ -1342,29 +1629,37 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1342
1629
  "DoSubmissionLedger.renewOwnership",
1343
1630
  )(function* (request: RenewOwnershipRequest) {
1344
1631
  const operation = "ledger renew ownership";
1632
+
1345
1633
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RenewOwnershipRequest))(
1346
1634
  request,
1347
1635
  ).pipe(Effect.mapError(internalFailure(operation)));
1636
+
1348
1637
  yield* hitFailpoint("ledger:renew:before", operation);
1638
+
1349
1639
  const renewal = yield* inWriteTransaction(
1350
1640
  operation,
1351
1641
  Effect.gen(function* () {
1352
1642
  const submission = yield* requireSubmission(operation, validated.submissionId);
1643
+
1353
1644
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1354
1645
  const now = yield* currentInstant;
1355
1646
  const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1647
+
1356
1648
  yield* sql`
1357
1649
  UPDATE effect_agent_submission_ownership
1358
1650
  SET lease_expires_at = ${leaseExpiresAt}
1359
1651
  WHERE submission_id = ${validated.submissionId}
1360
1652
  `.pipe(Effect.mapError(sqlFailure(operation)));
1653
+
1361
1654
  return yield* decodeOwnershipRenewal({
1362
1655
  ownershipToken: validated.ownershipToken,
1363
1656
  leaseExpiresAt,
1364
1657
  }).pipe(Effect.mapError(internalFailure(operation)));
1365
1658
  }),
1366
1659
  );
1660
+
1367
1661
  yield* hitFailpoint("ledger:renew:after", operation);
1662
+
1368
1663
  return renewal;
1369
1664
  });
1370
1665
 
@@ -1372,14 +1667,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1372
1667
  "DoSubmissionLedger.releaseOwnership",
1373
1668
  )(function* (request: ReleaseOwnershipRequest) {
1374
1669
  const operation = "ledger release ownership";
1670
+
1375
1671
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseOwnershipRequest))(
1376
1672
  request,
1377
1673
  ).pipe(Effect.mapError(internalFailure(operation)));
1674
+
1378
1675
  yield* hitFailpoint("ledger:release:before", operation);
1379
1676
  yield* inWriteTransaction(
1380
1677
  operation,
1381
1678
  Effect.gen(function* () {
1382
1679
  const submission = yield* requireSubmission(operation, validated.submissionId);
1680
+
1383
1681
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1384
1682
  yield* sql`
1385
1683
  DELETE FROM effect_agent_submission_ownership
@@ -1401,14 +1699,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1401
1699
  "DoSubmissionLedger.markInputApplied",
1402
1700
  )(function* (request: MarkInputAppliedRequest) {
1403
1701
  const operation = "ledger mark input applied";
1702
+
1404
1703
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkInputAppliedRequest))(
1405
1704
  request,
1406
1705
  ).pipe(Effect.mapError(internalFailure(operation)));
1706
+
1407
1707
  yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1408
1708
  yield* inWriteTransaction(
1409
1709
  operation,
1410
1710
  Effect.gen(function* () {
1411
1711
  const submission = yield* requireSubmission(operation, validated.submissionId);
1712
+
1412
1713
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1413
1714
  if (submission.input_applied_record_id !== null) {
1414
1715
  if (
@@ -1417,6 +1718,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1417
1718
  ) {
1418
1719
  return;
1419
1720
  }
1721
+
1420
1722
  return yield* corruptionFailure(
1421
1723
  operation,
1422
1724
  "effect_agent_submissions",
@@ -1444,34 +1746,41 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1444
1746
  "DoSubmissionLedger.reserveSettlement",
1445
1747
  )(function* (request: SettlementReservation) {
1446
1748
  const operation = "ledger reserve settlement";
1749
+
1447
1750
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementReservation))(
1448
1751
  request,
1449
1752
  ).pipe(Effect.mapError(internalFailure(operation)));
1753
+
1450
1754
  const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(
1451
1755
  Effect.mapError(internalFailure(operation)),
1452
1756
  );
1757
+
1453
1758
  // The reserved record is appended canonically later; refuse over-bound payloads typed
1454
1759
  // before the reservation row exists.
1455
1760
  yield* journal
1456
1761
  .checkValueBound(operation, recordJson)
1457
1762
  .pipe(Effect.mapError(internalFailure(operation)));
1458
1763
  yield* hitFailpoint("ledger:reserve-settlement:before", operation);
1764
+
1459
1765
  const reserved = yield* inWriteTransaction(
1460
1766
  operation,
1461
1767
  Effect.gen(function* () {
1462
1768
  const existing = yield* readReservation(operation, validated.submissionId);
1769
+
1463
1770
  if (Option.isSome(existing)) {
1464
1771
  const identical =
1465
1772
  existing.value.settlement_id === validated.settlementId &&
1466
1773
  existing.value.outcome === validated.outcome &&
1467
1774
  existing.value.record_digest === validated.recordDigest &&
1468
1775
  existing.value.record_json === recordJson;
1776
+
1469
1777
  if (!identical) {
1470
1778
  return yield* SettlementConflict.make({
1471
1779
  submissionId: validated.submissionId,
1472
1780
  existingOutcome: existing.value.outcome,
1473
1781
  });
1474
1782
  }
1783
+
1475
1784
  const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(
1476
1785
  Effect.mapError((error) =>
1477
1786
  corruptionFailure(
@@ -1482,6 +1791,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1482
1791
  ),
1483
1792
  ),
1484
1793
  );
1794
+
1485
1795
  return ReservedSettlement.make({
1486
1796
  submissionId: validated.submissionId,
1487
1797
  settlementId: validated.settlementId,
@@ -1493,6 +1803,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1493
1803
  }
1494
1804
 
1495
1805
  const submission = yield* requireSubmission(operation, validated.submissionId);
1806
+
1496
1807
  if (submission.state === "settled") {
1497
1808
  if (submission.settled_outcome === null) {
1498
1809
  return yield* corruptionFailure(
@@ -1502,6 +1813,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1502
1813
  "A settled Submission carries no terminal outcome.",
1503
1814
  );
1504
1815
  }
1816
+
1505
1817
  return yield* SettlementConflict.make({
1506
1818
  submissionId: validated.submissionId,
1507
1819
  existingOutcome: submission.settled_outcome,
@@ -1516,13 +1828,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1516
1828
  // ABORTED settlement (`terminalizing` is the same pass's crash replay). Every other
1517
1829
  // reservation stays fenced by the target lane's live ownership.
1518
1830
  let queuedAbortSettlement = false;
1831
+
1519
1832
  if (
1520
1833
  validated.outcome === "aborted" &&
1521
1834
  (submission.state === "ready" || submission.state === "terminalizing")
1522
1835
  ) {
1523
1836
  const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
1837
+
1524
1838
  if (Option.isSome(abortIntent)) {
1525
1839
  const ownership = yield* readOwnership(operation, validated.submissionId);
1840
+
1526
1841
  queuedAbortSettlement = Option.isNone(ownership);
1527
1842
  }
1528
1843
  }
@@ -1531,6 +1846,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1531
1846
  }
1532
1847
  }
1533
1848
  const now = yield* currentInstant;
1849
+
1534
1850
  yield* sql`
1535
1851
  INSERT INTO effect_agent_settlement_reservations (
1536
1852
  submission_id,
@@ -1555,6 +1871,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1555
1871
  SET state = 'terminalizing'
1556
1872
  WHERE submission_id = ${validated.submissionId}
1557
1873
  `.pipe(Effect.mapError(sqlFailure(operation)));
1874
+
1558
1875
  return ReservedSettlement.make({
1559
1876
  submissionId: validated.submissionId,
1560
1877
  settlementId: validated.settlementId,
@@ -1565,7 +1882,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1565
1882
  });
1566
1883
  }),
1567
1884
  );
1885
+
1568
1886
  yield* hitFailpoint("ledger:reserve-settlement:after", operation);
1887
+
1569
1888
  return reserved;
1570
1889
  });
1571
1890
 
@@ -1573,14 +1892,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1573
1892
  "DoSubmissionLedger.finalizeSettlement",
1574
1893
  )(function* (request: SettlementFinalization) {
1575
1894
  const operation = "ledger finalize settlement";
1895
+
1576
1896
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementFinalization))(
1577
1897
  request,
1578
1898
  ).pipe(Effect.mapError(internalFailure(operation)));
1899
+
1579
1900
  yield* hitFailpoint("ledger:finalize-settlement:before", operation);
1901
+
1580
1902
  const settlement = yield* inWriteTransaction(
1581
1903
  operation,
1582
1904
  Effect.gen(function* () {
1583
1905
  const reservation = yield* readReservation(operation, validated.submissionId);
1906
+
1584
1907
  if (Option.isNone(reservation)) {
1585
1908
  return yield* LedgerError.make({
1586
1909
  operation,
@@ -1593,7 +1916,32 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1593
1916
  existingOutcome: reservation.value.outcome,
1594
1917
  });
1595
1918
  }
1919
+
1920
+ const reservationRecord = yield* decodeRecordEnvelopeText(
1921
+ reservation.value.record_json,
1922
+ ).pipe(
1923
+ Effect.mapError((error) =>
1924
+ corruptionFailure(
1925
+ operation,
1926
+ "effect_agent_settlement_reservations",
1927
+ validated.submissionId,
1928
+ error.message,
1929
+ ),
1930
+ ),
1931
+ );
1932
+
1933
+ const settlementFailure = settlementFailureFromRecord(reservationRecord);
1934
+
1935
+ if ((reservation.value.outcome === "failed") !== (settlementFailure !== undefined)) {
1936
+ return yield* corruptionFailure(
1937
+ operation,
1938
+ "effect_agent_settlement_reservations",
1939
+ validated.submissionId,
1940
+ "The reserved outcome and canonical failure diagnostic disagree.",
1941
+ );
1942
+ }
1596
1943
  const submission = yield* requireSubmission(operation, validated.submissionId);
1944
+
1597
1945
  if (submission.state === "settled") {
1598
1946
  if (reservation.value.finalized_at === null) {
1599
1947
  return yield* corruptionFailure(
@@ -1603,15 +1951,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1603
1951
  "A settled Submission's reservation carries no finalization timestamp.",
1604
1952
  );
1605
1953
  }
1954
+
1606
1955
  return yield* decodeSettlement({
1607
1956
  submissionId: validated.submissionId,
1608
1957
  settlementId: validated.settlementId,
1609
1958
  receiptId: submission.receipt_id,
1610
1959
  outcome: reservation.value.outcome,
1960
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1611
1961
  settledAt: reservation.value.finalized_at,
1612
1962
  }).pipe(Effect.mapError(internalFailure(operation)));
1613
1963
  }
1614
1964
  const now = yield* currentInstant;
1965
+
1615
1966
  yield* sql`
1616
1967
  UPDATE effect_agent_submissions
1617
1968
  SET state = 'settled', settled_outcome = ${reservation.value.outcome}
@@ -1626,16 +1977,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1626
1977
  DELETE FROM effect_agent_submission_ownership
1627
1978
  WHERE submission_id = ${validated.submissionId}
1628
1979
  `.pipe(Effect.mapError(sqlFailure(operation)));
1980
+
1629
1981
  return yield* decodeSettlement({
1630
1982
  submissionId: validated.submissionId,
1631
1983
  settlementId: validated.settlementId,
1632
1984
  receiptId: submission.receipt_id,
1633
1985
  outcome: reservation.value.outcome,
1986
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1634
1987
  settledAt: now.iso,
1635
1988
  }).pipe(Effect.mapError(internalFailure(operation)));
1636
1989
  }),
1637
1990
  );
1991
+
1638
1992
  yield* hitFailpoint("ledger:finalize-settlement:after", operation);
1993
+
1639
1994
  return settlement;
1640
1995
  });
1641
1996
 
@@ -1643,14 +1998,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1643
1998
  "DoSubmissionLedger.requestAbort",
1644
1999
  )(function* (request: AbortCommand) {
1645
2000
  const operation = "ledger request abort";
2001
+
1646
2002
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AbortCommand))(request).pipe(
1647
2003
  Effect.mapError(internalFailure(operation)),
1648
2004
  );
2005
+
1649
2006
  yield* hitFailpoint("ledger:request-abort:before", operation);
2007
+
1650
2008
  const intent = yield* inWriteTransaction(
1651
2009
  operation,
1652
2010
  Effect.gen(function* () {
1653
2011
  const submission = yield* requireSubmission(operation, validated.submissionId);
2012
+
1654
2013
  if (submission.state === "settled") {
1655
2014
  if (submission.settled_outcome === null) {
1656
2015
  return yield* corruptionFailure(
@@ -1660,6 +2019,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1660
2019
  "A settled Submission carries no terminal outcome.",
1661
2020
  );
1662
2021
  }
2022
+
1663
2023
  return yield* SettlementConflict.make({
1664
2024
  submissionId: validated.submissionId,
1665
2025
  existingOutcome: submission.settled_outcome,
@@ -1677,15 +2037,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1677
2037
  "A joined Submission carries no host linkage.",
1678
2038
  );
1679
2039
  }
2040
+
1680
2041
  const hostSubmissionId = yield* decodeSubmissionId(
1681
2042
  submission.joined_host_submission_id,
1682
2043
  ).pipe(Effect.mapError(internalFailure(operation)));
2044
+
1683
2045
  return yield* JoinedToHost.make({
1684
2046
  submissionId: validated.submissionId,
1685
2047
  hostSubmissionId,
1686
2048
  });
1687
2049
  }
1688
2050
  const existing = yield* readAbortIntent(operation, validated.submissionId);
2051
+
1689
2052
  if (Option.isSome(existing)) {
1690
2053
  return yield* abortIntentFromRow(
1691
2054
  operation,
@@ -1695,6 +2058,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1695
2058
  );
1696
2059
  }
1697
2060
  const now = yield* currentInstant;
2061
+
1698
2062
  yield* sql`
1699
2063
  INSERT INTO effect_agent_abort_intents (
1700
2064
  submission_id,
@@ -1708,11 +2072,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1708
2072
  ${now.iso}
1709
2073
  )
1710
2074
  `.pipe(Effect.mapError(sqlFailure(operation)));
2075
+
1711
2076
  const canonicalRecordId = yield* canonicalAbortRecordId(
1712
2077
  operation,
1713
- submission.conversation_id,
2078
+ submission.thread_id,
1714
2079
  validated.submissionId,
1715
2080
  );
2081
+
1716
2082
  return yield* decodeAbortIntent({
1717
2083
  submissionId: validated.submissionId,
1718
2084
  author: validated.author,
@@ -1722,7 +2088,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1722
2088
  }).pipe(Effect.mapError(internalFailure(operation)));
1723
2089
  }),
1724
2090
  );
2091
+
1725
2092
  yield* hitFailpoint("ledger:request-abort:after", operation);
2093
+
1726
2094
  return intent;
1727
2095
  });
1728
2096
 
@@ -1730,31 +2098,38 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1730
2098
  "DoSubmissionLedger.claimJoining",
1731
2099
  )(function* (request: ClaimJoiningRequest) {
1732
2100
  const operation = "ledger claim joining";
2101
+
1733
2102
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimJoiningRequest))(
1734
2103
  request,
1735
2104
  ).pipe(Effect.mapError(internalFailure(operation)));
2105
+
1736
2106
  yield* hitFailpoint("ledger:claim-joining:before", operation);
2107
+
1737
2108
  const claims = yield* inWriteTransaction(
1738
2109
  operation,
1739
2110
  Effect.gen(function* () {
1740
2111
  const host = yield* requireSubmission(operation, validated.hostSubmissionId);
1741
- if (host.conversation_id !== validated.conversationId) {
2112
+
2113
+ if (host.thread_id !== validated.threadId) {
1742
2114
  return yield* LedgerError.make({
1743
2115
  operation,
1744
- message: `Host submission ${validated.hostSubmissionId} does not belong to conversation ${validated.conversationId}.`,
2116
+ message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
1745
2117
  });
1746
2118
  }
1747
2119
  // The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
1748
2120
  yield* requireOwnership(operation, host, validated.ownershipToken);
2121
+
1749
2122
  const laterRows = yield* sql<Record<string, unknown>>`
1750
2123
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1751
2124
  FROM effect_agent_submissions
1752
- WHERE conversation_id = ${validated.conversationId}
2125
+ WHERE thread_id = ${validated.threadId}
1753
2126
  AND queue_sequence > ${host.queue_sequence}
1754
2127
  ORDER BY queue_sequence ASC
1755
2128
  `.pipe(Effect.mapError(sqlFailure(operation)));
1756
- const later = yield* decodeSubmissionRows(operation, validated.conversationId, laterRows);
2129
+
2130
+ const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
1757
2131
  const claimed: Array<JoiningClaim> = [];
2132
+
1758
2133
  for (const row of later) {
1759
2134
  if (claimed.length >= validated.maxCount) break;
1760
2135
  // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
@@ -1777,6 +2152,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1777
2152
  SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
1778
2153
  WHERE submission_id = ${row.submission_id}
1779
2154
  `.pipe(Effect.mapError(sqlFailure(operation)));
2155
+
1780
2156
  const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
1781
2157
  Effect.mapError((error) =>
1782
2158
  corruptionFailure(
@@ -1787,6 +2163,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1787
2163
  ),
1788
2164
  ),
1789
2165
  );
2166
+
1790
2167
  claimed.push(
1791
2168
  yield* decodeJoiningClaim({
1792
2169
  submissionId: row.submission_id,
@@ -1795,10 +2172,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1795
2172
  }).pipe(Effect.mapError(internalFailure(operation))),
1796
2173
  );
1797
2174
  }
1798
- return claimed as ReadonlyArray<JoiningClaim>;
2175
+
2176
+ return claimed;
1799
2177
  }),
1800
2178
  );
2179
+
1801
2180
  yield* hitFailpoint("ledger:claim-joining:after", operation);
2181
+
1802
2182
  return claims;
1803
2183
  });
1804
2184
 
@@ -1806,14 +2186,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1806
2186
  "DoSubmissionLedger.markJoined",
1807
2187
  )(function* (request: MarkJoinedRequest) {
1808
2188
  const operation = "ledger mark joined";
2189
+
1809
2190
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkJoinedRequest))(
1810
2191
  request,
1811
2192
  ).pipe(Effect.mapError(internalFailure(operation)));
2193
+
1812
2194
  yield* hitFailpoint("ledger:mark-joined:before", operation);
1813
2195
  yield* inWriteTransaction(
1814
2196
  operation,
1815
2197
  Effect.gen(function* () {
1816
2198
  const submission = yield* requireSubmission(operation, validated.submissionId);
2199
+
1817
2200
  if (submission.joined_host_submission_id === null) {
1818
2201
  return yield* LedgerError.make({
1819
2202
  operation,
@@ -1821,6 +2204,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1821
2204
  });
1822
2205
  }
1823
2206
  const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
2207
+
1824
2208
  // The lane is host-owned: the presented token must own the HOST's ownership period,
1825
2209
  // which also lets a later host Attempt repair a lost marker from history (DUR-016).
1826
2210
  yield* requireOwnership(operation, host, validated.ownershipToken);
@@ -1831,6 +2215,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1831
2215
  ) {
1832
2216
  return;
1833
2217
  }
2218
+
1834
2219
  return yield* corruptionFailure(
1835
2220
  operation,
1836
2221
  "effect_agent_submissions",
@@ -1861,14 +2246,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1861
2246
  "DoSubmissionLedger.revertJoining",
1862
2247
  )(function* (request: RevertJoiningRequest) {
1863
2248
  const operation = "ledger revert joining";
2249
+
1864
2250
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RevertJoiningRequest))(
1865
2251
  request,
1866
2252
  ).pipe(Effect.mapError(internalFailure(operation)));
2253
+
1867
2254
  yield* hitFailpoint("ledger:revert-joining:before", operation);
1868
2255
  yield* inWriteTransaction(
1869
2256
  operation,
1870
2257
  Effect.gen(function* () {
1871
2258
  const submission = yield* requireSubmission(operation, validated.submissionId);
2259
+
1872
2260
  // Idempotent and recovery-only: only a still-`joining` Submission reverts; an
1873
2261
  // already-joined (or already-reverted) Submission is a no-op (DUR-016).
1874
2262
  if (submission.state !== "joining") return;
@@ -1885,17 +2273,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1885
2273
  const suspend: SubmissionLedger["Service"]["suspend"] = Effect.fn("DoSubmissionLedger.suspend")(
1886
2274
  function* (request: SuspendRequest) {
1887
2275
  const operation = "ledger suspend";
2276
+
1888
2277
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SuspendRequest))(
1889
2278
  request,
1890
2279
  ).pipe(Effect.mapError(internalFailure(operation)));
2280
+
1891
2281
  const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
1892
2282
  Effect.mapError(internalFailure(operation)),
1893
2283
  );
2284
+
1894
2285
  yield* hitFailpoint("ledger:suspend:before", operation);
2286
+
1895
2287
  const outcome = yield* inWriteTransaction(
1896
2288
  operation,
1897
2289
  Effect.gen(function* () {
1898
2290
  const submission = yield* requireSubmission(operation, validated.submissionId);
2291
+
1899
2292
  if (submission.state === "settled") {
1900
2293
  if (submission.settled_outcome === null) {
1901
2294
  return yield* corruptionFailure(
@@ -1905,6 +2298,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1905
2298
  "A settled Submission carries no terminal outcome.",
1906
2299
  );
1907
2300
  }
2301
+
1908
2302
  return yield* SettlementConflict.make({
1909
2303
  submissionId: validated.submissionId,
1910
2304
  existingOutcome: submission.settled_outcome,
@@ -1913,6 +2307,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1913
2307
  // An exact terminal outcome is already reserved (DUR-011); suspension would
1914
2308
  // contradict it, so the reservation wins.
1915
2309
  const reservation = yield* readReservation(operation, validated.submissionId);
2310
+
1916
2311
  if (Option.isSome(reservation)) {
1917
2312
  return yield* SettlementConflict.make({
1918
2313
  submissionId: validated.submissionId,
@@ -1923,35 +2318,39 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1923
2318
  // A covering event that raced ahead of the suspend transaction resumes the caller
1924
2319
  // immediately WITHOUT releasing the lane (plan §2.6, §12). For WaitingForChild the
1925
2320
  // 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
2321
+ // notification marker: parent and child Threads live in different Durable
1927
2322
  // Objects, and the port contract requires that a child settlement reported (via
1928
2323
  // `recordChildSettled` → marker) before this suspend commits is observed here.
1929
2324
  if (validated.reason._tag === "ApprovalPending") {
1930
2325
  const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
1931
2326
  const decided = new Set(decisions.map((row) => row.tool_call_id));
2327
+
1932
2328
  if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
1933
- return "resume-immediately" as SuspensionOutcome;
2329
+ return RESUME_IMMEDIATELY;
1934
2330
  }
1935
2331
  } else {
1936
2332
  const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
1937
2333
  const markerChildren = new Set(markers.map((row) => row.child_submission_id));
1938
2334
  let allSettled = true;
2335
+
1939
2336
  for (const child of validated.reason.children) {
1940
2337
  const settled = yield* childProvablySettled(
1941
2338
  operation,
1942
2339
  markerChildren,
1943
2340
  child.childSubmissionId,
1944
2341
  );
2342
+
1945
2343
  if (!settled) {
1946
2344
  allSettled = false;
1947
2345
  break;
1948
2346
  }
1949
2347
  }
1950
2348
  if (allSettled) {
1951
- return "resume-immediately" as SuspensionOutcome;
2349
+ return RESUME_IMMEDIATELY;
1952
2350
  }
1953
2351
  }
1954
2352
  const now = yield* currentInstant;
2353
+
1955
2354
  yield* sql`
1956
2355
  UPDATE effect_agent_submissions
1957
2356
  SET
@@ -1966,10 +2365,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1966
2365
  DELETE FROM effect_agent_submission_ownership
1967
2366
  WHERE submission_id = ${validated.submissionId}
1968
2367
  `.pipe(Effect.mapError(sqlFailure(operation)));
1969
- return "suspended" as SuspensionOutcome;
2368
+
2369
+ return SUSPENDED;
1970
2370
  }),
1971
2371
  );
2372
+
1972
2373
  yield* hitFailpoint("ledger:suspend:after", operation);
2374
+
1973
2375
  return outcome;
1974
2376
  },
1975
2377
  );
@@ -1985,6 +2387,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1985
2387
  submission: SubmissionRow,
1986
2388
  ): Effect.fn.Return<void, LedgerError> {
1987
2389
  if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
2390
+
1988
2391
  const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
1989
2392
  submission.suspended_reason_json,
1990
2393
  ).pipe(
@@ -1997,9 +2400,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1997
2400
  ),
1998
2401
  ),
1999
2402
  );
2403
+
2000
2404
  if (reason._tag !== "ApprovalPending") return;
2001
2405
  const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
2002
2406
  const decided = new Set(decisions.map((row) => row.tool_call_id));
2407
+
2003
2408
  if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
2004
2409
  yield* sql`
2005
2410
  UPDATE effect_agent_submissions
@@ -2015,14 +2420,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2015
2420
  "DoSubmissionLedger.recordApprovalDecision",
2016
2421
  )(function* (command: ApprovalDecisionCommand) {
2017
2422
  const operation = "ledger record approval decision";
2423
+
2018
2424
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ApprovalDecisionCommand))(
2019
2425
  command,
2020
2426
  ).pipe(Effect.mapError(internalFailure(operation)));
2427
+
2021
2428
  yield* hitFailpoint("ledger:approval-decision:before", operation);
2429
+
2022
2430
  const intent = yield* inWriteTransaction(
2023
2431
  operation,
2024
2432
  Effect.gen(function* () {
2025
2433
  const submission = yield* requireSubmission(operation, validated.submissionId);
2434
+
2026
2435
  if (submission.state === "settled") {
2027
2436
  if (submission.settled_outcome === null) {
2028
2437
  return yield* corruptionFailure(
@@ -2032,6 +2441,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2032
2441
  "A settled Submission carries no terminal outcome.",
2033
2442
  );
2034
2443
  }
2444
+
2035
2445
  return yield* SettlementConflict.make({
2036
2446
  submissionId: validated.submissionId,
2037
2447
  existingOutcome: submission.settled_outcome,
@@ -2039,6 +2449,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2039
2449
  }
2040
2450
  const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
2041
2451
  const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
2452
+
2042
2453
  if (existing !== undefined) {
2043
2454
  // Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
2044
2455
  // the recorded intent unchanged; a divergent re-decision conflicts.
@@ -2049,9 +2460,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2049
2460
  existingDecision: existing.decision,
2050
2461
  });
2051
2462
  }
2463
+
2052
2464
  return yield* approvalIntentFromRow(operation, existing);
2053
2465
  }
2054
2466
  const now = yield* currentInstant;
2467
+
2055
2468
  yield* sql`
2056
2469
  INSERT INTO effect_agent_approval_decisions (
2057
2470
  submission_id,
@@ -2070,6 +2483,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2070
2483
  )
2071
2484
  `.pipe(Effect.mapError(sqlFailure(operation)));
2072
2485
  yield* wakeSuspendedIfCovered(operation, submission);
2486
+
2073
2487
  return yield* decodeApprovalDecisionIntent({
2074
2488
  submissionId: validated.submissionId,
2075
2489
  toolCallId: validated.toolCallId,
@@ -2080,7 +2494,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2080
2494
  }).pipe(Effect.mapError(internalFailure(operation)));
2081
2495
  }),
2082
2496
  );
2497
+
2083
2498
  yield* hitFailpoint("ledger:approval-decision:after", operation);
2499
+
2084
2500
  return intent;
2085
2501
  });
2086
2502
 
@@ -2088,14 +2504,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2088
2504
  "DoSubmissionLedger.markUnknown",
2089
2505
  )(function* (request: MarkUnknownRequest) {
2090
2506
  const operation = "ledger mark unknown";
2507
+
2091
2508
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkUnknownRequest))(
2092
2509
  request,
2093
2510
  ).pipe(Effect.mapError(internalFailure(operation)));
2511
+
2094
2512
  yield* hitFailpoint("ledger:mark-unknown:before", operation);
2095
2513
  yield* inWriteTransaction(
2096
2514
  operation,
2097
2515
  Effect.gen(function* () {
2098
2516
  const submission = yield* requireSubmission(operation, validated.submissionId);
2517
+
2099
2518
  if (submission.state === "settled") {
2100
2519
  if (submission.settled_outcome === null) {
2101
2520
  return yield* corruptionFailure(
@@ -2105,6 +2524,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2105
2524
  "A settled Submission carries no terminal outcome.",
2106
2525
  );
2107
2526
  }
2527
+
2108
2528
  return yield* SettlementConflict.make({
2109
2529
  submissionId: validated.submissionId,
2110
2530
  existingOutcome: submission.settled_outcome,
@@ -2113,6 +2533,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2113
2533
  // A reserved exact outcome wins over a late Unknown marking (DUR-011); the recovery
2114
2534
  // classifier orders reservation ahead of MarkUnknown for the same reason.
2115
2535
  const reservation = yield* readReservation(operation, validated.submissionId);
2536
+
2116
2537
  if (Option.isSome(reservation)) {
2117
2538
  return yield* SettlementConflict.make({
2118
2539
  submissionId: validated.submissionId,
@@ -2123,13 +2544,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2123
2544
  // set while the first recorded reason is kept.
2124
2545
  const existingIds = yield* storedUnknownToolCallIds(operation, submission);
2125
2546
  const known = new Set(existingIds);
2547
+
2126
2548
  const merged = [
2127
2549
  ...existingIds,
2128
2550
  ...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
2129
2551
  ];
2552
+
2130
2553
  const idsJson = yield* encodeToolCallIdsText(merged).pipe(
2131
2554
  Effect.mapError(internalFailure(operation)),
2132
2555
  );
2556
+
2133
2557
  yield* sql`
2134
2558
  UPDATE effect_agent_submissions
2135
2559
  SET
@@ -2147,17 +2571,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2147
2571
  "DoSubmissionLedger.recordUnknownResolution",
2148
2572
  )(function* (command: UnknownResolutionCommand) {
2149
2573
  const operation = "ledger record unknown resolution";
2574
+
2150
2575
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(UnknownResolutionCommand))(
2151
2576
  command,
2152
2577
  ).pipe(Effect.mapError(internalFailure(operation)));
2578
+
2153
2579
  const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
2154
2580
  Effect.mapError(internalFailure(operation)),
2155
2581
  );
2582
+
2156
2583
  yield* hitFailpoint("ledger:unknown-resolution:before", operation);
2584
+
2157
2585
  const intent = yield* inWriteTransaction(
2158
2586
  operation,
2159
2587
  Effect.gen(function* () {
2160
2588
  const submission = yield* requireSubmission(operation, validated.submissionId);
2589
+
2161
2590
  if (submission.state === "settled") {
2162
2591
  if (submission.settled_outcome === null) {
2163
2592
  return yield* corruptionFailure(
@@ -2167,6 +2596,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2167
2596
  "A settled Submission carries no terminal outcome.",
2168
2597
  );
2169
2598
  }
2599
+
2170
2600
  return yield* SettlementConflict.make({
2171
2601
  submissionId: validated.submissionId,
2172
2602
  existingOutcome: submission.settled_outcome,
@@ -2174,19 +2604,30 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2174
2604
  }
2175
2605
  const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
2176
2606
  const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
2177
- if (existing !== undefined && existing.resolution_json !== resolutionJson) {
2607
+
2608
+ const existingIntent =
2609
+ existing === undefined
2610
+ ? undefined
2611
+ : yield* unknownResolutionIntentFromRow(operation, existing);
2612
+
2613
+ if (
2614
+ existingIntent !== undefined &&
2615
+ !equivalentUnknownResolution(existingIntent.resolution, validated.resolution)
2616
+ ) {
2178
2617
  return yield* UnknownResolutionConflict.make({
2179
2618
  submissionId: validated.submissionId,
2180
2619
  toolCallId: validated.toolCallId,
2181
2620
  });
2182
2621
  }
2183
2622
  let resolved: UnknownResolutionIntent;
2184
- if (existing !== undefined) {
2623
+
2624
+ if (existingIntent !== undefined) {
2185
2625
  // Idempotent replay of the recorded intent (author/reason may differ; the stored
2186
2626
  // audit fields win, exactly like requestAbort).
2187
- resolved = yield* unknownResolutionIntentFromRow(operation, existing);
2627
+ resolved = existingIntent;
2188
2628
  } else {
2189
2629
  const now = yield* currentInstant;
2630
+
2190
2631
  yield* sql`
2191
2632
  INSERT INTO effect_agent_unknown_resolutions (
2192
2633
  submission_id,
@@ -2204,9 +2645,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2204
2645
  ${now.iso}
2205
2646
  )
2206
2647
  `.pipe(Effect.mapError(sqlFailure(operation)));
2648
+
2207
2649
  const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
2208
2650
  Effect.mapError(internalFailure(operation)),
2209
2651
  );
2652
+
2210
2653
  resolved = yield* decodeUnknownResolutionIntent({
2211
2654
  submissionId: validated.submissionId,
2212
2655
  toolCallId: validated.toolCallId,
@@ -2223,6 +2666,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2223
2666
  const markedIds = yield* storedUnknownToolCallIds(operation, submission);
2224
2667
  const covering = yield* readUnknownResolutions(operation, validated.submissionId);
2225
2668
  const coveredIds = new Set(covering.map((row) => row.tool_call_id));
2669
+
2226
2670
  if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) {
2227
2671
  yield* sql`
2228
2672
  UPDATE effect_agent_submissions
@@ -2234,10 +2678,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2234
2678
  `.pipe(Effect.mapError(sqlFailure(operation)));
2235
2679
  }
2236
2680
  }
2681
+
2237
2682
  return resolved;
2238
2683
  }),
2239
2684
  );
2685
+
2240
2686
  yield* hitFailpoint("ledger:unknown-resolution:after", operation);
2687
+
2241
2688
  return intent;
2242
2689
  });
2243
2690
 
@@ -2245,21 +2692,31 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2245
2692
  "DoSubmissionLedger.recordChildSettled",
2246
2693
  )(function* (request: ChildSettledNotification) {
2247
2694
  const operation = "ledger record child settled";
2695
+
2248
2696
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ChildSettledNotification))(
2249
2697
  request,
2250
2698
  ).pipe(Effect.mapError(internalFailure(operation)));
2699
+
2251
2700
  yield* hitFailpoint("ledger:child-settled:before", operation);
2701
+
2252
2702
  const outcome = yield* inWriteTransaction(
2253
2703
  operation,
2254
2704
  Effect.gen(function* () {
2255
2705
  const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2256
2706
  // 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.
2707
+ // row lives in THIS store (single-store latitude, and every conformance lane), either a
2708
+ // finalized row or an exact terminalizing reservation admits the notification: the
2709
+ // runtime calls only after the canonical append and before ledger finalization. When the
2710
+ // row does not live here — the normal cross-DO case the routed notification from the
2711
+ // child's owning Durable Object is the settlement evidence this store records durably.
2261
2712
  const child = yield* readSubmission(operation, validated.childSubmissionId);
2262
- if (Option.isSome(child) && child.value.state !== "settled") {
2713
+ const childReservation = yield* readReservation(operation, validated.childSubmissionId);
2714
+
2715
+ if (
2716
+ Option.isSome(child) &&
2717
+ child.value.state !== "settled" &&
2718
+ !(child.value.state === "terminalizing" && Option.isSome(childReservation))
2719
+ ) {
2263
2720
  return yield* LedgerError.make({
2264
2721
  operation,
2265
2722
  message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`,
@@ -2270,6 +2727,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2270
2727
  // before the parent's suspend transaction is observed by that suspend's covering
2271
2728
  // check, even when the parent is not (or not yet) suspended.
2272
2729
  const now = yield* currentInstant;
2730
+
2273
2731
  yield* sql`
2274
2732
  INSERT INTO effect_agent_child_settlements (
2275
2733
  parent_submission_id,
@@ -2279,15 +2737,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2279
2737
  ) VALUES (
2280
2738
  ${validated.parentSubmissionId},
2281
2739
  ${validated.childSubmissionId},
2282
- ${Option.isSome(child) ? child.value.settled_outcome : null},
2740
+ ${
2741
+ Option.isSome(child) && child.value.state === "settled"
2742
+ ? child.value.settled_outcome
2743
+ : Option.isSome(childReservation)
2744
+ ? childReservation.value.outcome
2745
+ : null
2746
+ },
2283
2747
  ${now.iso}
2284
2748
  )
2285
2749
  ON CONFLICT (parent_submission_id, child_submission_id) DO NOTHING
2286
2750
  `.pipe(Effect.mapError(sqlFailure(operation)));
2287
2751
 
2288
2752
  if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
2289
- return "not-waiting" as ChildSettledOutcome;
2753
+ return NOT_WAITING;
2290
2754
  }
2755
+
2291
2756
  const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
2292
2757
  parent.suspended_reason_json,
2293
2758
  ).pipe(
@@ -2300,27 +2765,30 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2300
2765
  ),
2301
2766
  ),
2302
2767
  );
2768
+
2303
2769
  if (reason._tag !== "WaitingForChild") {
2304
- return "not-waiting" as ChildSettledOutcome;
2770
+ return NOT_WAITING;
2305
2771
  }
2306
2772
  if (
2307
2773
  !reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
2308
2774
  ) {
2309
- return "not-waiting" as ChildSettledOutcome;
2775
+ return NOT_WAITING;
2310
2776
  }
2311
2777
  // The parent wakes exactly when EVERY listed child is provably settled — from its
2312
2778
  // local row or a recorded marker (spec §12 step 10); replays re-run the coverage
2313
2779
  // check so a recovering caller wakes the lane idempotently.
2314
2780
  const markers = yield* readChildSettlementMarkers(operation, validated.parentSubmissionId);
2315
2781
  const markerChildren = new Set(markers.map((row) => row.child_submission_id));
2782
+
2316
2783
  for (const entry of reason.children) {
2317
2784
  const settled = yield* childProvablySettled(
2318
2785
  operation,
2319
2786
  markerChildren,
2320
2787
  entry.childSubmissionId,
2321
2788
  );
2789
+
2322
2790
  if (!settled) {
2323
- return "still-waiting" as ChildSettledOutcome;
2791
+ return STILL_WAITING;
2324
2792
  }
2325
2793
  }
2326
2794
  yield* sql`
@@ -2331,10 +2799,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2331
2799
  suspended_at = NULL
2332
2800
  WHERE submission_id = ${validated.parentSubmissionId}
2333
2801
  `.pipe(Effect.mapError(sqlFailure(operation)));
2334
- return "woken" as ChildSettledOutcome;
2802
+
2803
+ return WOKEN;
2335
2804
  }),
2336
2805
  );
2806
+
2337
2807
  yield* hitFailpoint("ledger:child-settled:after", operation);
2808
+
2338
2809
  return outcome;
2339
2810
  });
2340
2811
 
@@ -2342,25 +2813,36 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2342
2813
  "DoSubmissionLedger.reserveChildBudget",
2343
2814
  )(function* (request: ChildBudgetReservationRequest) {
2344
2815
  const operation = "ledger reserve child budget";
2816
+
2345
2817
  const validated = yield* Schema.decodeUnknownEffect(
2346
2818
  Schema.toType(ChildBudgetReservationRequest),
2347
2819
  )(request).pipe(Effect.mapError(internalFailure(operation)));
2820
+
2348
2821
  const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
2349
2822
  Effect.mapError(internalFailure(operation)),
2350
2823
  );
2824
+
2351
2825
  yield* hitFailpoint("ledger:child-reservation:before", operation);
2826
+
2352
2827
  const reserved = yield* inWriteTransaction(
2353
2828
  operation,
2354
2829
  Effect.gen(function* () {
2355
2830
  const existing = yield* readChildReservation(operation, validated.reservationId);
2831
+
2356
2832
  if (Option.isSome(existing)) {
2833
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
2834
+ operation,
2835
+ existing.value,
2836
+ );
2837
+
2357
2838
  // Identical replays short-circuit before the fence, mirroring reserveSettlement: a
2358
2839
  // replay creates nothing, so a recovering caller resumes rather than duplicates.
2359
2840
  const identical =
2360
2841
  existing.value.parent_submission_id === validated.parentSubmissionId &&
2361
2842
  existing.value.parent_tool_call_id === validated.parentToolCallId &&
2362
2843
  existing.value.allocation_digest === validated.allocationDigest &&
2363
- existing.value.allocation_json === allocationJson;
2844
+ equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
2845
+
2364
2846
  if (!identical) {
2365
2847
  return yield* ChildReservationConflict.make({
2366
2848
  reservationId: validated.reservationId,
@@ -2369,16 +2851,19 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2369
2851
  "A reservation with this identity exists with a different parent Tool Call or allocation.",
2370
2852
  });
2371
2853
  }
2854
+
2372
2855
  return ReservedChildBudget.make({
2373
- reservation: yield* childReservationSnapshotFromRow(operation, existing.value),
2856
+ reservation: existingSnapshot,
2374
2857
  replayed: true,
2375
2858
  });
2376
2859
  }
2860
+
2377
2861
  const collision = yield* readChildReservationForCall(
2378
2862
  operation,
2379
2863
  validated.parentSubmissionId,
2380
2864
  validated.parentToolCallId,
2381
2865
  );
2866
+
2382
2867
  if (Option.isSome(collision)) {
2383
2868
  return yield* ChildReservationConflict.make({
2384
2869
  reservationId: validated.reservationId,
@@ -2387,10 +2872,12 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2387
2872
  });
2388
2873
  }
2389
2874
  const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2875
+
2390
2876
  // Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
2391
2877
  // parent Attempt can never create new reservation state.
2392
2878
  yield* requireOwnership(operation, parent, validated.ownershipToken);
2393
2879
  const now = yield* currentInstant;
2880
+
2394
2881
  yield* sql`
2395
2882
  INSERT INTO effect_agent_child_reservations (
2396
2883
  reservation_id,
@@ -2411,6 +2898,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2411
2898
  )
2412
2899
  `.pipe(Effect.mapError(sqlFailure(operation)));
2413
2900
  const inserted = yield* readChildReservation(operation, validated.reservationId);
2901
+
2414
2902
  if (Option.isNone(inserted)) {
2415
2903
  return yield* corruptionFailure(
2416
2904
  operation,
@@ -2419,13 +2907,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2419
2907
  "An inserted child reservation row is missing inside its own transaction.",
2420
2908
  );
2421
2909
  }
2910
+
2422
2911
  return ReservedChildBudget.make({
2423
2912
  reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
2424
2913
  replayed: false,
2425
2914
  });
2426
2915
  }),
2427
2916
  );
2917
+
2428
2918
  yield* hitFailpoint("ledger:child-reservation:after", operation);
2919
+
2429
2920
  return reserved;
2430
2921
  });
2431
2922
 
@@ -2434,14 +2925,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2434
2925
  request: AttachChildToReservationRequest,
2435
2926
  ) {
2436
2927
  const operation = "ledger attach child to reservation";
2928
+
2437
2929
  const validated = yield* Schema.decodeUnknownEffect(
2438
2930
  Schema.toType(AttachChildToReservationRequest),
2439
2931
  )(request).pipe(Effect.mapError(internalFailure(operation)));
2932
+
2440
2933
  yield* hitFailpoint("ledger:child-attach:before", operation);
2934
+
2441
2935
  const attached = yield* inWriteTransaction(
2442
2936
  operation,
2443
2937
  Effect.gen(function* () {
2444
2938
  const existing = yield* readChildReservation(operation, validated.reservationId);
2939
+
2445
2940
  if (Option.isNone(existing)) {
2446
2941
  return yield* LedgerError.make({
2447
2942
  operation,
@@ -2453,6 +2948,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2453
2948
  if (existing.value.child_submission_id === validated.childSubmissionId) {
2454
2949
  return yield* childReservationSnapshotFromRow(operation, existing.value);
2455
2950
  }
2951
+
2456
2952
  return yield* ChildReservationConflict.make({
2457
2953
  reservationId: validated.reservationId,
2458
2954
  status: existing.value.status,
@@ -2460,6 +2956,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2460
2956
  });
2461
2957
  }
2462
2958
  const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
2959
+
2463
2960
  yield* requireOwnership(operation, parent, validated.ownershipToken);
2464
2961
  if (existing.value.status !== "reserved") {
2465
2962
  return yield* ChildReservationConflict.make({
@@ -2478,6 +2975,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2478
2975
  WHERE reservation_id = ${validated.reservationId}
2479
2976
  `.pipe(Effect.mapError(sqlFailure(operation)));
2480
2977
  const updated = yield* readChildReservation(operation, validated.reservationId);
2978
+
2481
2979
  if (Option.isNone(updated)) {
2482
2980
  return yield* corruptionFailure(
2483
2981
  operation,
@@ -2486,10 +2984,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2486
2984
  "An updated child reservation row is missing inside its own transaction.",
2487
2985
  );
2488
2986
  }
2987
+
2489
2988
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2490
2989
  }),
2491
2990
  );
2991
+
2492
2992
  yield* hitFailpoint("ledger:child-attach:after", operation);
2993
+
2493
2994
  return attached;
2494
2995
  });
2495
2996
 
@@ -2497,17 +2998,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2497
2998
  "DoSubmissionLedger.beginChildBudgetRelease",
2498
2999
  )(function* (request: BeginChildBudgetReleaseRequest) {
2499
3000
  const operation = "ledger begin child budget release";
3001
+
2500
3002
  const validated = yield* Schema.decodeUnknownEffect(
2501
3003
  Schema.toType(BeginChildBudgetReleaseRequest),
2502
3004
  )(request).pipe(Effect.mapError(internalFailure(operation)));
3005
+
2503
3006
  const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
2504
3007
  Effect.mapError(internalFailure(operation)),
2505
3008
  );
3009
+
2506
3010
  yield* hitFailpoint("ledger:child-release-pending:before", operation);
3011
+
2507
3012
  const frozen = yield* inWriteTransaction(
2508
3013
  operation,
2509
3014
  Effect.gen(function* () {
2510
3015
  const existing = yield* readChildReservation(operation, validated.reservationId);
3016
+
2511
3017
  if (Option.isNone(existing)) {
2512
3018
  return yield* LedgerError.make({
2513
3019
  operation,
@@ -2515,11 +3021,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2515
3021
  });
2516
3022
  }
2517
3023
  if (existing.value.status !== "reserved") {
3024
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
3025
+ operation,
3026
+ existing.value,
3027
+ );
3028
+
2518
3029
  // The accounting decision was already frozen exactly once; an identical replay is a
2519
3030
  // 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);
3031
+ if (
3032
+ existingSnapshot.accounting !== undefined &&
3033
+ equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
3034
+ ) {
3035
+ return existingSnapshot;
2522
3036
  }
3037
+
2523
3038
  return yield* ChildReservationConflict.make({
2524
3039
  reservationId: validated.reservationId,
2525
3040
  status: existing.value.status,
@@ -2527,6 +3042,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2527
3042
  });
2528
3043
  }
2529
3044
  const now = yield* currentInstant;
3045
+
2530
3046
  yield* sql`
2531
3047
  UPDATE effect_agent_child_reservations
2532
3048
  SET
@@ -2536,6 +3052,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2536
3052
  WHERE reservation_id = ${validated.reservationId}
2537
3053
  `.pipe(Effect.mapError(sqlFailure(operation)));
2538
3054
  const updated = yield* readChildReservation(operation, validated.reservationId);
3055
+
2539
3056
  if (Option.isNone(updated)) {
2540
3057
  return yield* corruptionFailure(
2541
3058
  operation,
@@ -2544,10 +3061,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2544
3061
  "An updated child reservation row is missing inside its own transaction.",
2545
3062
  );
2546
3063
  }
3064
+
2547
3065
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2548
3066
  }),
2549
3067
  );
3068
+
2550
3069
  yield* hitFailpoint("ledger:child-release-pending:after", operation);
3070
+
2551
3071
  return frozen;
2552
3072
  });
2553
3073
 
@@ -2555,14 +3075,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2555
3075
  "DoSubmissionLedger.releaseChildBudget",
2556
3076
  )(function* (request: ReleaseChildBudgetRequest) {
2557
3077
  const operation = "ledger release child budget";
3078
+
2558
3079
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseChildBudgetRequest))(
2559
3080
  request,
2560
3081
  ).pipe(Effect.mapError(internalFailure(operation)));
3082
+
2561
3083
  yield* hitFailpoint("ledger:child-release:before", operation);
3084
+
2562
3085
  const released = yield* inWriteTransaction(
2563
3086
  operation,
2564
3087
  Effect.gen(function* () {
2565
3088
  const existing = yield* readChildReservation(operation, validated.reservationId);
3089
+
2566
3090
  if (Option.isNone(existing)) {
2567
3091
  return yield* LedgerError.make({
2568
3092
  operation,
@@ -2582,12 +3106,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2582
3106
  });
2583
3107
  }
2584
3108
  const now = yield* currentInstant;
3109
+
2585
3110
  yield* sql`
2586
3111
  UPDATE effect_agent_child_reservations
2587
3112
  SET status = 'released', released_at = ${now.iso}
2588
3113
  WHERE reservation_id = ${validated.reservationId}
2589
3114
  `.pipe(Effect.mapError(sqlFailure(operation)));
2590
3115
  const updated = yield* readChildReservation(operation, validated.reservationId);
3116
+
2591
3117
  if (Option.isNone(updated)) {
2592
3118
  return yield* corruptionFailure(
2593
3119
  operation,
@@ -2596,15 +3122,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2596
3122
  "An updated child reservation row is missing inside its own transaction.",
2597
3123
  );
2598
3124
  }
3125
+
2599
3126
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2600
3127
  }),
2601
3128
  );
3129
+
2602
3130
  yield* hitFailpoint("ledger:child-release:after", operation);
3131
+
2603
3132
  return released;
2604
3133
  });
2605
3134
 
2606
3135
  interface ScanCursor {
2607
- readonly conversationId: string;
3136
+ readonly threadId: string;
2608
3137
  readonly queueSequence: number;
2609
3138
  }
2610
3139
 
@@ -2615,13 +3144,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2615
3144
  LedgerError
2616
3145
  > {
2617
3146
  const operation = "ledger scan nonterminal";
3147
+
2618
3148
  const rows = yield* (
2619
3149
  cursor === undefined
2620
3150
  ? sql<Record<string, unknown>>`
2621
3151
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2622
3152
  FROM effect_agent_submissions
2623
3153
  WHERE state <> 'settled'
2624
- ORDER BY conversation_id ASC, queue_sequence ASC
3154
+ ORDER BY thread_id ASC, queue_sequence ASC
2625
3155
  LIMIT ${SCAN_PAGE_SIZE}
2626
3156
  `
2627
3157
  : sql<Record<string, unknown>>`
@@ -2629,43 +3159,122 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2629
3159
  FROM effect_agent_submissions
2630
3160
  WHERE state <> 'settled'
2631
3161
  AND (
2632
- conversation_id > ${cursor.conversationId}
3162
+ thread_id > ${cursor.threadId}
2633
3163
  OR (
2634
- conversation_id = ${cursor.conversationId}
3164
+ thread_id = ${cursor.threadId}
2635
3165
  AND queue_sequence > ${cursor.queueSequence}
2636
3166
  )
2637
3167
  )
2638
- ORDER BY conversation_id ASC, queue_sequence ASC
3168
+ ORDER BY thread_id ASC, queue_sequence ASC
2639
3169
  LIMIT ${SCAN_PAGE_SIZE}
2640
3170
  `
2641
3171
  ).pipe(Effect.mapError(sqlFailure(operation)));
3172
+
2642
3173
  const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
3174
+
2643
3175
  const snapshots = yield* Effect.forEach(decoded, (row) =>
2644
3176
  decodeSubmissionSnapshot(operation, row),
2645
3177
  );
3178
+
2646
3179
  const last = decoded[decoded.length - 1];
3180
+
2647
3181
  const next: Option.Option<ScanCursor | undefined> =
2648
3182
  last === undefined || decoded.length < SCAN_PAGE_SIZE
2649
3183
  ? Option.none()
2650
3184
  : Option.some({
2651
- conversationId: last.conversation_id,
3185
+ threadId: last.thread_id,
2652
3186
  queueSequence: last.queue_sequence,
2653
3187
  });
3188
+
2654
3189
  return [snapshots, next] as const;
2655
3190
  });
2656
3191
 
2657
- const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate(
2658
- undefined as ScanCursor | undefined,
2659
- scanPage,
2660
- );
3192
+ const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate<
3193
+ ScanCursor | undefined,
3194
+ SubmissionSnapshot,
3195
+ LedgerError
3196
+ >(undefined, scanPage);
3197
+
3198
+ const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
3199
+ "DoSubmissionLedger.readAbortIntentForSubmission",
3200
+ )(function* (request) {
3201
+ const operation = "ledger read abort intent";
3202
+
3203
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AbortIntentRequest))(
3204
+ request,
3205
+ ).pipe(Effect.mapError(internalFailure(operation)));
3206
+
3207
+ const recordId = submissionAbortRecordId(validated.submissionId);
3208
+
3209
+ const rows = yield* sql<Record<string, unknown>>`
3210
+ SELECT
3211
+ submission.submission_id,
3212
+ abort.submission_id AS abort_submission_id,
3213
+ abort.author,
3214
+ abort.reason,
3215
+ abort.requested_at,
3216
+ canonical.record_id AS canonical_record_id
3217
+ FROM effect_agent_submissions AS submission
3218
+ LEFT JOIN effect_agent_abort_intents AS abort
3219
+ ON abort.submission_id = submission.submission_id
3220
+ LEFT JOIN effect_agent_canonical_records AS canonical
3221
+ ON canonical.thread_id = submission.thread_id
3222
+ AND canonical.record_id = ${recordId}
3223
+ WHERE submission.submission_id = ${validated.submissionId}
3224
+ `.pipe(Effect.mapError(sqlFailure(operation)));
3225
+
3226
+ const decoded = yield* decodeRows(
3227
+ Schema.Array(AbortIntentLookupRow),
3228
+ "effect_agent_abort_intents",
3229
+ validated.submissionId,
3230
+ rows,
3231
+ ).pipe(Effect.mapError(internalFailure(operation)));
3232
+
3233
+ if (decoded.length === 0) {
3234
+ return yield* LedgerError.make({
3235
+ operation,
3236
+ message: `Unknown submission ${validated.submissionId}.`,
3237
+ });
3238
+ }
3239
+ if (decoded.length !== 1) {
3240
+ return yield* corruptionFailure(
3241
+ operation,
3242
+ "effect_agent_abort_intents",
3243
+ validated.submissionId,
3244
+ "An abort intent lookup returned more than one row.",
3245
+ );
3246
+ }
3247
+ const row = decoded[0];
3248
+
3249
+ if (row.abort_submission_id === null) return undefined;
3250
+
3251
+ return yield* decodeAbortIntent({
3252
+ submissionId: row.abort_submission_id,
3253
+ author: row.author,
3254
+ reason: row.reason,
3255
+ requestedAt: row.requested_at,
3256
+ ...(row.canonical_record_id === null ? {} : { canonicalRecordId: row.canonical_record_id }),
3257
+ }).pipe(
3258
+ Effect.mapError((error) =>
3259
+ corruptionFailure(
3260
+ operation,
3261
+ "effect_agent_abort_intents",
3262
+ validated.submissionId,
3263
+ error.message,
3264
+ ),
3265
+ ),
3266
+ );
3267
+ });
2661
3268
 
2662
3269
  const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
2663
3270
  "DoSubmissionLedger.loadRecoverySnapshot",
2664
3271
  )(function* (request: RecoverySnapshotRequest) {
2665
3272
  const operation = "ledger load recovery snapshot";
3273
+
2666
3274
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RecoverySnapshotRequest))(
2667
3275
  request,
2668
3276
  ).pipe(Effect.mapError(internalFailure(operation)));
3277
+
2669
3278
  return yield* sql
2670
3279
  .withTransaction(
2671
3280
  Effect.gen(function* () {
@@ -2674,6 +3283,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2674
3283
 
2675
3284
  let ownership: OwnershipSnapshot | undefined;
2676
3285
  const ownershipRow = yield* readOwnership(operation, validated.submissionId);
3286
+
2677
3287
  if (Option.isSome(ownershipRow)) {
2678
3288
  ownership = yield* decodeOwnershipSnapshot({
2679
3289
  attemptId: ownershipRow.value.attempt_id,
@@ -2684,6 +3294,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2684
3294
  }
2685
3295
 
2686
3296
  let inputApplied: InputAppliedMarker | undefined;
3297
+
2687
3298
  if (
2688
3299
  submissionRow.input_applied_record_id !== null &&
2689
3300
  submissionRow.input_applied_sequence !== null
@@ -2696,6 +3307,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2696
3307
 
2697
3308
  let reservation: SettlementReservationSnapshot | undefined;
2698
3309
  const reservationRow = yield* readReservation(operation, validated.submissionId);
3310
+
2699
3311
  if (Option.isSome(reservationRow)) {
2700
3312
  const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(
2701
3313
  Effect.mapError((error) =>
@@ -2707,9 +3319,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2707
3319
  ),
2708
3320
  ),
2709
3321
  );
3322
+
2710
3323
  const settlementId = yield* Schema.decodeUnknownEffect(
2711
3324
  SettlementReservationSnapshot.fields.settlementId,
2712
3325
  )(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
3326
+
2713
3327
  reservation = SettlementReservationSnapshot.make({
2714
3328
  settlementId,
2715
3329
  outcome: reservationRow.value.outcome,
@@ -2721,6 +3335,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2721
3335
 
2722
3336
  let abortIntent: AbortIntent | undefined;
2723
3337
  const abortRow = yield* readAbortIntent(operation, validated.submissionId);
3338
+
2724
3339
  if (Option.isSome(abortRow)) {
2725
3340
  abortIntent = yield* abortIntentFromRow(
2726
3341
  operation,
@@ -2738,11 +3353,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2738
3353
  WHERE joined_host_submission_id = ${validated.submissionId}
2739
3354
  ORDER BY queue_sequence ASC
2740
3355
  `.pipe(Effect.mapError(sqlFailure(operation)));
3356
+
2741
3357
  const joinSubmissions = yield* decodeSubmissionRows(
2742
3358
  operation,
2743
3359
  validated.submissionId,
2744
3360
  joinRows,
2745
3361
  );
3362
+
2746
3363
  const joins = yield* Effect.forEach(joinSubmissions, (row) =>
2747
3364
  decodeJoinSnapshot({
2748
3365
  submissionId: row.submission_id,
@@ -2752,6 +3369,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2752
3369
  );
2753
3370
 
2754
3371
  let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
3372
+
2755
3373
  if (submissionRow.joined_host_submission_id !== null) {
2756
3374
  hostSubmissionId = yield* decodeSubmissionId(
2757
3375
  submissionRow.joined_host_submission_id,
@@ -2759,6 +3377,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2759
3377
  }
2760
3378
 
2761
3379
  let suspension: SuspensionSnapshot | undefined;
3380
+
2762
3381
  if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
2763
3382
  const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(
2764
3383
  Effect.mapError((error) =>
@@ -2770,6 +3389,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2770
3389
  ),
2771
3390
  ),
2772
3391
  );
3392
+
2773
3393
  suspension = yield* decodeSuspensionSnapshot({
2774
3394
  reason,
2775
3395
  suspendedAt: submissionRow.suspended_at,
@@ -2786,11 +3406,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2786
3406
  }
2787
3407
 
2788
3408
  const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
3409
+
2789
3410
  const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
2790
3411
  approvalIntentFromRow(operation, row),
2791
3412
  );
2792
3413
 
2793
3414
  const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
3415
+
2794
3416
  const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
2795
3417
  unknownResolutionIntentFromRow(operation, row),
2796
3418
  );
@@ -2807,20 +3429,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2807
3429
  WHERE parent_submission_id = ${validated.submissionId}
2808
3430
  ORDER BY parent_tool_call_id ASC
2809
3431
  `.pipe(Effect.mapError(sqlFailure(operation)));
3432
+
2810
3433
  const decodedChildReservations = yield* decodeChildReservationRows(
2811
3434
  operation,
2812
3435
  validated.submissionId,
2813
3436
  childReservationRows,
2814
3437
  );
3438
+
2815
3439
  const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
2816
3440
  childReservationSnapshotFromRow(operation, row),
2817
3441
  );
3442
+
2818
3443
  const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
2819
3444
  const markersByChild = new Map(markers.map((row) => [row.child_submission_id, row]));
2820
3445
  const childAttachments: Array<ChildAttachmentSnapshot> = [];
3446
+
2821
3447
  for (const row of decodedChildReservations) {
2822
3448
  if (row.child_submission_id === null) continue;
2823
3449
  const child = yield* readSubmission(operation, row.child_submission_id);
3450
+
2824
3451
  if (Option.isSome(child)) {
2825
3452
  childAttachments.push(
2826
3453
  yield* decodeChildAttachmentSnapshot({
@@ -2835,6 +3462,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2835
3462
  continue;
2836
3463
  }
2837
3464
  const marker = markersByChild.get(row.child_submission_id);
3465
+
2838
3466
  if (marker === undefined) continue;
2839
3467
  childAttachments.push(
2840
3468
  yield* decodeChildAttachmentSnapshot({
@@ -2847,6 +3475,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2847
3475
  }
2848
3476
 
2849
3477
  let parentLinkage: ParentLinkage | undefined;
3478
+
2850
3479
  if (
2851
3480
  submissionRow.parent_submission_id !== null &&
2852
3481
  submissionRow.parent_tool_call_id !== null
@@ -2906,6 +3535,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2906
3535
  releaseChildBudget,
2907
3536
  scanNonterminal,
2908
3537
  loadRecoverySnapshot,
3538
+ readAbortIntent: readAbortIntentForSubmission,
2909
3539
  }),
2910
3540
  );
2911
3541
  });
@@ -2923,18 +3553,22 @@ export const submissionLedgerLayer: Layer.Layer<
2923
3553
 
2924
3554
  /**
2925
3555
  * 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.
3556
+ * same `ctx.storage` as the ThreadStore so claims fence the same producer epochs.
2927
3557
  */
2928
3558
  export const ledgerLayer = (
2929
3559
  options: DoStorageOptions,
2930
3560
  ): 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,
3561
+ Layer.unwrap(
3562
+ Effect.map(DoStorageConfig, (config) =>
3563
+ submissionLedgerLayer.pipe(
3564
+ Layer.provide(
3565
+ Layer.mergeAll(
3566
+ Layer.succeed(DoStorageConfig)(config),
3567
+ storageFailpointLayer(options),
3568
+ SqliteClient.layer({ storage: options.storage }),
3569
+ BrowserCrypto.layer,
3570
+ ),
3571
+ ),
2938
3572
  ),
2939
3573
  ),
2940
- );
3574
+ ).pipe(Layer.provide(storageConfigLayer(options)));