@effect-agent/storage-sqlite 0.1.0-beta.6 → 0.1.0-beta.61

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 (59) hide show
  1. package/dist/SqliteActivityStore.d.mts +14 -0
  2. package/dist/SqliteActivityStore.mjs +310 -0
  3. package/dist/SqliteActivityStore.mjs.map +1 -0
  4. package/dist/SqliteMessageDeliveryStore.d.mts +14 -0
  5. package/dist/SqliteMessageDeliveryStore.mjs +24 -0
  6. package/dist/SqliteMessageDeliveryStore.mjs.map +1 -0
  7. package/dist/SqliteScheduleStore.d.mts +14 -0
  8. package/dist/SqliteScheduleStore.mjs +255 -0
  9. package/dist/SqliteScheduleStore.mjs.map +1 -0
  10. package/dist/SqliteStorageConfig.d.mts +34 -0
  11. package/dist/SqliteStorageConfig.mjs +39 -0
  12. package/dist/SqliteStorageConfig.mjs.map +1 -0
  13. package/dist/SqliteStorageError-CzKmwCLl.d.mts +86 -0
  14. package/dist/SqliteStorageError.d.mts +2 -0
  15. package/dist/SqliteStorageError.mjs +148 -0
  16. package/dist/SqliteStorageError.mjs.map +1 -0
  17. package/dist/SqliteStorageFailpoint.d.mts +17 -0
  18. package/dist/SqliteStorageFailpoint.mjs +14 -0
  19. package/dist/SqliteStorageFailpoint.mjs.map +1 -0
  20. package/dist/SqliteStorageFailpointTesting.d.mts +15 -0
  21. package/dist/SqliteStorageFailpointTesting.mjs +19 -0
  22. package/dist/SqliteStorageFailpointTesting.mjs.map +1 -0
  23. package/dist/SqliteStorageVersion-gA7_96xM.d.mts +9 -0
  24. package/dist/SqliteStorageVersion.d.mts +2 -0
  25. package/dist/SqliteStorageVersion.mjs +8 -0
  26. package/dist/SqliteStorageVersion.mjs.map +1 -0
  27. package/dist/SqliteSubmissionLedger.d.mts +23 -0
  28. package/dist/SqliteSubmissionLedger.mjs +1765 -0
  29. package/dist/SqliteSubmissionLedger.mjs.map +1 -0
  30. package/dist/SqliteSubscriptionStore.d.mts +13 -0
  31. package/dist/SqliteSubscriptionStore.mjs +28 -0
  32. package/dist/SqliteSubscriptionStore.mjs.map +1 -0
  33. package/dist/SqliteThreadStore.d.mts +49 -0
  34. package/dist/SqliteThreadStore.mjs +427 -0
  35. package/dist/SqliteThreadStore.mjs.map +1 -0
  36. package/dist/index.d.mts +11 -193
  37. package/dist/index.mjs +11 -3082
  38. package/dist/migrations-dnTR0RKq.mjs +326 -0
  39. package/dist/migrations-dnTR0RKq.mjs.map +1 -0
  40. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  41. package/dist/sqlite-journal-lDduEakl.mjs +887 -0
  42. package/dist/sqlite-journal-lDduEakl.mjs.map +1 -0
  43. package/package.json +1 -41
  44. package/src/SqliteActivityStore.ts +556 -0
  45. package/src/SqliteMessageDeliveryStore.ts +42 -0
  46. package/src/SqliteScheduleStore.ts +404 -0
  47. package/src/{sqlite-storage-config.ts → SqliteStorageConfig.ts} +1 -1
  48. package/src/{errors.ts → SqliteStorageError.ts} +8 -3
  49. package/src/SqliteStorageFailpoint.ts +23 -0
  50. package/src/{sqlite-storage-failpoint.ts → SqliteStorageFailpointTesting.ts} +7 -18
  51. package/src/SqliteStorageVersion.ts +2 -0
  52. package/src/{sqlite-ledger.ts → SqliteSubmissionLedger.ts} +753 -118
  53. package/src/SqliteSubscriptionStore.ts +59 -0
  54. package/src/{sqlite-conversation-store.ts → SqliteThreadStore.ts} +372 -302
  55. package/src/index.ts +10 -6
  56. package/src/internal/message-delivery-schema.ts +24 -0
  57. package/src/{migrations.ts → internal/migrations.ts} +145 -79
  58. package/src/{sqlite-journal.ts → internal/sqlite-journal.ts} +534 -222
  59. package/dist/index.mjs.map +0 -1
@@ -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,29 +82,29 @@ 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 { NodeCrypto } from "@effect/platform-node";
77
87
  import { SqliteClient } from "@effect/sql-sqlite-node";
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
 
92
+ import { decodeRows, initializeSqliteJournal } from "./internal/sqlite-journal.ts";
93
+ import { SqliteStorageConfig } from "./SqliteStorageConfig.ts";
82
94
  import {
83
95
  SqliteLedgerError,
84
96
  SqliteStorageCorruptionError,
85
97
  SqliteStorageError,
86
98
  SqliteWriteContention,
87
99
  type SqliteStorageFailpointLocation,
88
- } from "./errors.ts";
100
+ } from "./SqliteStorageError.ts";
101
+ import { SqliteStorageFailpoint } from "./SqliteStorageFailpoint.ts";
89
102
  import {
90
103
  storageConfigLayer,
91
104
  storageFailpointLayer,
92
105
  type SqliteStorageInitializationError,
93
106
  type SqliteStorageOptions,
94
- } from "./sqlite-conversation-store.ts";
95
- import { decodeRows, initializeSqliteJournal } from "./sqlite-journal.ts";
96
- import { SqliteStorageConfig } from "./sqlite-storage-config.ts";
97
- import { SqliteStorageFailpoint } from "./sqlite-storage-failpoint.ts";
107
+ } from "./SqliteThreadStore.ts";
98
108
 
99
109
  type SubmissionId = SubmissionSnapshot["submissionId"];
100
110
 
@@ -104,10 +114,15 @@ const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
104
114
 
105
115
  const SCAN_PAGE_SIZE = 256;
106
116
  const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
117
+ const RESUME_IMMEDIATELY: SuspensionOutcome = "resume-immediately";
118
+ const SUSPENDED: SuspensionOutcome = "suspended";
119
+ const NOT_WAITING: ChildSettledOutcome = "not-waiting";
120
+ const STILL_WAITING: ChildSettledOutcome = "still-waiting";
121
+ const WOKEN: ChildSettledOutcome = "woken";
107
122
 
108
123
  class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
109
124
  submission_id: BoundedIdentifier,
110
- conversation_id: BoundedIdentifier,
125
+ thread_id: BoundedIdentifier,
111
126
  queue_sequence: QueueSequence,
112
127
  principal: BoundedIdentifier,
113
128
  idempotency_key: BoundedIdentifier,
@@ -130,6 +145,10 @@ class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
130
145
  unknown_tool_call_ids_json: Schema.NullOr(BoundedStoredText),
131
146
  parent_submission_id: Schema.NullOr(BoundedIdentifier),
132
147
  parent_tool_call_id: Schema.NullOr(BoundedIdentifier),
148
+ admission_group: Schema.NullOr(AdmissionGroup),
149
+ admission_fence_json: Schema.NullOr(BoundedStoredText),
150
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
151
+ message_admission_json: Schema.NullOr(BoundedStoredText),
133
152
  }) {}
134
153
 
135
154
  class ChildReservationRow extends Schema.Class<ChildReservationRow>("ChildReservationRow")({
@@ -196,13 +215,22 @@ class MaxQueueSequenceRow extends Schema.Class<MaxQueueSequenceRow>("MaxQueueSeq
196
215
  max_queue_sequence: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
197
216
  }) {}
198
217
 
218
+ class AbortIntentLookupRow extends Schema.Class<AbortIntentLookupRow>("AbortIntentLookupRow")({
219
+ submission_id: BoundedIdentifier,
220
+ abort_submission_id: Schema.NullOr(BoundedIdentifier),
221
+ author: Schema.NullOr(BoundedIdentifier),
222
+ reason: Schema.NullOr(BoundedStoredText),
223
+ requested_at: Schema.NullOr(BoundedTimestamp),
224
+ canonical_record_id: Schema.NullOr(BoundedIdentifier),
225
+ }) {}
226
+
199
227
  class CanonicalRecordIdRow extends Schema.Class<CanonicalRecordIdRow>("CanonicalRecordIdRow")({
200
228
  record_id: BoundedIdentifier,
201
229
  }) {}
202
230
 
203
231
  const SUBMISSION_COLUMNS = `
204
232
  submission_id,
205
- conversation_id,
233
+ thread_id,
206
234
  queue_sequence,
207
235
  principal,
208
236
  idempotency_key,
@@ -224,7 +252,11 @@ const SUBMISSION_COLUMNS = `
224
252
  unknown_reason,
225
253
  unknown_tool_call_ids_json,
226
254
  parent_submission_id,
227
- parent_tool_call_id
255
+ parent_tool_call_id,
256
+ admission_group,
257
+ admission_fence_json,
258
+ worker_admission_json,
259
+ message_admission_json
228
260
  `;
229
261
 
230
262
  const CHILD_RESERVATION_COLUMNS = `
@@ -241,7 +273,7 @@ const CHILD_RESERVATION_COLUMNS = `
241
273
  released_at
242
274
  `;
243
275
 
244
- /** The branded ToolCallId schema, reached through the session port so no core import is needed. */
276
+ /** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
245
277
  const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
246
278
  const ToolCallIdList = Schema.Array(ToolCallIdSchema);
247
279
 
@@ -271,10 +303,18 @@ const decodeSuspensionSnapshot = Schema.decodeUnknownEffect(SuspensionSnapshot);
271
303
  const decodeApprovalDecisionIntent = Schema.decodeUnknownEffect(ApprovalDecisionIntent);
272
304
  const decodeUnknownResolutionIntent = Schema.decodeUnknownEffect(UnknownResolutionIntent);
273
305
  const decodeParentLinkage = Schema.decodeUnknownEffect(ParentLinkage);
306
+
274
307
  const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
275
308
  ChildBudgetReservationSnapshot,
276
309
  );
310
+
277
311
  const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
312
+ const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
313
+ const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
314
+
315
+ const isSqliteTransactionFailure = Schema.is(
316
+ Schema.Union([SqliteStorageError, SqliteWriteContention]),
317
+ );
278
318
 
279
319
  /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
280
320
  const internalFailure =
@@ -298,6 +338,7 @@ const sqlFailure =
298
338
  operation,
299
339
  message: error.message,
300
340
  });
341
+
301
342
  return internalFailure(operation)(internal);
302
343
  };
303
344
 
@@ -309,7 +350,8 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
309
350
  const failpoint = yield* SqliteStorageFailpoint;
310
351
  const sql = yield* SqlClientService.SqlClient;
311
352
  const crypto = yield* Crypto.Crypto;
312
- const journal = yield* initializeSqliteJournal(sql, failpoint.hit, config.busyTimeout);
353
+ const admissionFence = yield* SubmissionAdmissionFence;
354
+ const journal = yield* initializeSqliteJournal();
313
355
 
314
356
  const hitFailpoint = (
315
357
  location: SqliteStorageFailpointLocation,
@@ -327,6 +369,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
327
369
  A,
328
370
  E extends
329
371
  | AdmissionConflict
372
+ | AdmissionPolicyError
330
373
  | ApprovalConflict
331
374
  | ChildReservationConflict
332
375
  | JoinedToHost
@@ -342,9 +385,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
342
385
  .withWriteTransaction(operation)(effect)
343
386
  .pipe(
344
387
  Effect.mapError((error) =>
345
- error instanceof SqliteStorageError || error instanceof SqliteWriteContention
346
- ? internalFailure(operation)(error)
347
- : error,
388
+ isSqliteTransactionFailure(error) ? internalFailure(operation)(error) : error,
348
389
  ),
349
390
  );
350
391
 
@@ -381,7 +422,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
381
422
  FROM effect_agent_submissions
382
423
  WHERE submission_id = ${submissionId}
383
424
  `.pipe(Effect.mapError(sqlFailure(operation)));
425
+
384
426
  const decoded = yield* decodeSubmissionRows(operation, submissionId, rows);
427
+
385
428
  if (decoded.length > 1) {
386
429
  return yield* corruptionFailure(
387
430
  operation,
@@ -390,6 +433,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
390
433
  "A submission primary key returned more than one row.",
391
434
  );
392
435
  }
436
+
393
437
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
394
438
  });
395
439
 
@@ -398,12 +442,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
398
442
  submissionId: string,
399
443
  ): Effect.fn.Return<SubmissionRow, LedgerError> {
400
444
  const submission = yield* readSubmission(operation, submissionId);
445
+
401
446
  if (Option.isNone(submission)) {
402
447
  return yield* LedgerError.make({
403
448
  operation,
404
449
  message: `Unknown submission ${submissionId}.`,
405
450
  });
406
451
  }
452
+
407
453
  return submission.value;
408
454
  });
409
455
 
@@ -422,12 +468,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
422
468
  FROM effect_agent_submission_ownership
423
469
  WHERE submission_id = ${submissionId}
424
470
  `.pipe(Effect.mapError(sqlFailure(operation)));
471
+
425
472
  const decoded = yield* decodeRows(
426
473
  Schema.Array(OwnershipRow),
427
474
  "effect_agent_submission_ownership",
428
475
  submissionId,
429
476
  rows,
430
477
  ).pipe(Effect.mapError(internalFailure(operation)));
478
+
431
479
  if (decoded.length > 1) {
432
480
  return yield* corruptionFailure(
433
481
  operation,
@@ -436,23 +484,25 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
436
484
  "An ownership primary key returned more than one row.",
437
485
  );
438
486
  }
487
+
439
488
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
440
489
  });
441
490
 
442
- const conversationEpoch = Effect.fn("SqliteSubmissionLedger.conversationEpoch")(function* (
491
+ const threadEpoch = Effect.fn("SqliteSubmissionLedger.threadEpoch")(function* (
443
492
  operation: string,
444
- conversationId: string,
493
+ threadId: string,
445
494
  ): Effect.fn.Return<ProducerEpoch, LedgerError> {
446
- const conversations = yield* journal
447
- .getConversation(conversationId)
495
+ const threads = yield* journal
496
+ .getThread(threadId)
448
497
  .pipe(Effect.mapError(internalFailure(operation)));
449
- return conversations.length === 0 ? EPOCH_ZERO : conversations[0].producer_epoch;
498
+
499
+ return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
450
500
  });
451
501
 
452
502
  /**
453
503
  * Verify inside the surrounding write transaction that the presented token still owns the
454
504
  * Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
455
- * Conversation's current producer epoch (DUR-006).
505
+ * Thread's current producer epoch (DUR-006).
456
506
  */
457
507
  const requireOwnership = Effect.fn("SqliteSubmissionLedger.requireOwnership")(function* (
458
508
  operation: string,
@@ -460,13 +510,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
460
510
  ownershipToken: string,
461
511
  ): Effect.fn.Return<OwnershipRow, OwnershipLost | LedgerError> {
462
512
  const ownership = yield* readOwnership(operation, submission.submission_id);
513
+
463
514
  if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
464
- const actualEpoch = yield* conversationEpoch(operation, submission.conversation_id);
515
+ const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
516
+
465
517
  const submissionId = yield* Schema.decodeUnknownEffect(
466
518
  SubmissionSnapshot.fields.submissionId,
467
519
  )(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
520
+
468
521
  return yield* OwnershipLost.make({ submissionId, actualEpoch });
469
522
  }
523
+
470
524
  return ownership.value;
471
525
  });
472
526
 
@@ -485,6 +539,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
485
539
  ),
486
540
  ),
487
541
  );
542
+
488
543
  const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
489
544
  Effect.mapError((error) =>
490
545
  corruptionFailure(
@@ -495,6 +550,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
495
550
  ),
496
551
  ),
497
552
  );
553
+
498
554
  if ((row.parent_submission_id === null) !== (row.parent_tool_call_id === null)) {
499
555
  return yield* corruptionFailure(
500
556
  operation,
@@ -503,9 +559,10 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
503
559
  "A parent linkage must record both the parent Submission and the parent Tool Call.",
504
560
  );
505
561
  }
562
+
506
563
  return yield* decodeSubmissionSnapshotUnknown({
507
564
  submissionId: row.submission_id,
508
- conversationId: row.conversation_id,
565
+ threadId: row.thread_id,
509
566
  queueSequence: row.queue_sequence,
510
567
  principal: row.principal,
511
568
  idempotencyKey: row.idempotency_key,
@@ -517,6 +574,28 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
517
574
  receiptId: row.receipt_id,
518
575
  state: row.state,
519
576
  createdAt: row.created_at,
577
+ ...(row.admission_group === null ? {} : { admissionGroup: row.admission_group }),
578
+ ...(row.worker_admission_json === null
579
+ ? {}
580
+ : {
581
+ workerAdmission: yield* parseStoredJsonText(row.worker_admission_json).pipe(
582
+ Effect.mapError(internalFailure(operation)),
583
+ ),
584
+ }),
585
+ ...(row.message_admission_json === null
586
+ ? {}
587
+ : {
588
+ messageAdmission: yield* parseStoredJsonText(row.message_admission_json).pipe(
589
+ Effect.mapError(internalFailure(operation)),
590
+ ),
591
+ }),
592
+ ...(row.admission_fence_json === null
593
+ ? {}
594
+ : {
595
+ admissionFence: yield* parseStoredJsonText(row.admission_fence_json).pipe(
596
+ Effect.mapError(internalFailure(operation)),
597
+ ),
598
+ }),
520
599
  ...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
521
600
  ...(row.ready_at === null ? {} : { readyAt: row.ready_at }),
522
601
  ...(row.parent_submission_id === null || row.parent_tool_call_id === null
@@ -557,12 +636,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
557
636
  FROM effect_agent_settlement_reservations
558
637
  WHERE submission_id = ${submissionId}
559
638
  `.pipe(Effect.mapError(sqlFailure(operation)));
639
+
560
640
  const decoded = yield* decodeRows(
561
641
  Schema.Array(ReservationRow),
562
642
  "effect_agent_settlement_reservations",
563
643
  submissionId,
564
644
  rows,
565
645
  ).pipe(Effect.mapError(internalFailure(operation)));
646
+
566
647
  if (decoded.length > 1) {
567
648
  return yield* corruptionFailure(
568
649
  operation,
@@ -571,6 +652,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
571
652
  "A settlement reservation primary key returned more than one row.",
572
653
  );
573
654
  }
655
+
574
656
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
575
657
  });
576
658
 
@@ -588,12 +670,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
588
670
  FROM effect_agent_abort_intents
589
671
  WHERE submission_id = ${submissionId}
590
672
  `.pipe(Effect.mapError(sqlFailure(operation)));
673
+
591
674
  const decoded = yield* decodeRows(
592
675
  Schema.Array(AbortIntentRow),
593
676
  "effect_agent_abort_intents",
594
677
  submissionId,
595
678
  rows,
596
679
  ).pipe(Effect.mapError(internalFailure(operation)));
680
+
597
681
  if (decoded.length > 1) {
598
682
  return yield* corruptionFailure(
599
683
  operation,
@@ -602,6 +686,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
602
686
  "An abort intent primary key returned more than one row.",
603
687
  );
604
688
  }
689
+
605
690
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
606
691
  });
607
692
 
@@ -622,7 +707,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
622
707
  FROM effect_agent_child_reservations
623
708
  WHERE reservation_id = ${reservationId}
624
709
  `.pipe(Effect.mapError(sqlFailure(operation)));
710
+
625
711
  const decoded = yield* decodeChildReservationRows(operation, reservationId, rows);
712
+
626
713
  if (decoded.length > 1) {
627
714
  return yield* corruptionFailure(
628
715
  operation,
@@ -631,6 +718,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
631
718
  "A child reservation primary key returned more than one row.",
632
719
  );
633
720
  }
721
+
634
722
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
635
723
  });
636
724
 
@@ -647,11 +735,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
647
735
  WHERE parent_submission_id = ${parentSubmissionId}
648
736
  AND parent_tool_call_id = ${parentToolCallId}
649
737
  `.pipe(Effect.mapError(sqlFailure(operation)));
738
+
650
739
  const decoded = yield* decodeChildReservationRows(
651
740
  operation,
652
741
  `${parentSubmissionId}/${parentToolCallId}`,
653
742
  rows,
654
743
  );
744
+
655
745
  if (decoded.length > 1) {
656
746
  return yield* corruptionFailure(
657
747
  operation,
@@ -660,6 +750,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
660
750
  "A parent Tool Call returned more than one child reservation.",
661
751
  );
662
752
  }
753
+
663
754
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
664
755
  });
665
756
 
@@ -676,13 +767,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
676
767
  row.reservation_id,
677
768
  error.message,
678
769
  );
770
+
679
771
  const allocation = yield* parseStoredJsonText(row.allocation_json).pipe(
680
772
  Effect.mapError(rowFailure),
681
773
  );
774
+
682
775
  const accounting =
683
776
  row.accounting_json === null
684
777
  ? undefined
685
778
  : yield* parseStoredJsonText(row.accounting_json).pipe(Effect.mapError(rowFailure));
779
+
686
780
  return yield* decodeChildReservationSnapshotUnknown({
687
781
  reservationId: row.reservation_id,
688
782
  parentSubmissionId: row.parent_submission_id,
@@ -715,6 +809,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
715
809
  WHERE submission_id = ${submissionId}
716
810
  ORDER BY tool_call_id ASC
717
811
  `.pipe(Effect.mapError(sqlFailure(operation)));
812
+
718
813
  return yield* decodeRows(
719
814
  Schema.Array(ApprovalDecisionRow),
720
815
  "effect_agent_approval_decisions",
@@ -766,6 +861,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
766
861
  WHERE submission_id = ${submissionId}
767
862
  ORDER BY tool_call_id ASC
768
863
  `.pipe(Effect.mapError(sqlFailure(operation)));
864
+
769
865
  return yield* decodeRows(
770
866
  Schema.Array(UnknownResolutionRow),
771
867
  "effect_agent_unknown_resolutions",
@@ -791,6 +887,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
791
887
  ),
792
888
  ),
793
889
  );
890
+
794
891
  return yield* decodeUnknownResolutionIntent({
795
892
  submissionId: row.submission_id,
796
893
  toolCallId: row.tool_call_id,
@@ -817,6 +914,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
817
914
  submission: SubmissionRow,
818
915
  ): Effect.fn.Return<ReadonlyArray<typeof ToolCallIdSchema.Type>, LedgerError> {
819
916
  if (submission.unknown_tool_call_ids_json === null) return [];
917
+
820
918
  return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(
821
919
  Effect.mapError((error) =>
822
920
  corruptionFailure(
@@ -838,22 +936,25 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
838
936
  const canonicalAbortRecordId = Effect.fn("SqliteSubmissionLedger.canonicalAbortRecordId")(
839
937
  function* (
840
938
  operation: string,
841
- conversationId: string,
939
+ threadId: string,
842
940
  submissionId: SubmissionId,
843
941
  ): Effect.fn.Return<string | undefined, LedgerError> {
844
942
  const recordId = submissionAbortRecordId(submissionId);
943
+
845
944
  const rows = yield* sql<Record<string, unknown>>`
846
945
  SELECT record_id
847
946
  FROM effect_agent_canonical_records
848
- WHERE conversation_id = ${conversationId}
947
+ WHERE thread_id = ${threadId}
849
948
  AND record_id = ${recordId}
850
949
  `.pipe(Effect.mapError(sqlFailure(operation)));
950
+
851
951
  const decoded = yield* decodeRows(
852
952
  Schema.Array(CanonicalRecordIdRow),
853
953
  "effect_agent_canonical_records",
854
- `${conversationId}/${recordId}`,
954
+ `${threadId}/${recordId}`,
855
955
  rows,
856
956
  ).pipe(Effect.mapError(internalFailure(operation)));
957
+
857
958
  return decoded.length === 0 ? undefined : recordId;
858
959
  },
859
960
  );
@@ -866,9 +967,10 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
866
967
  ): Effect.fn.Return<AbortIntent, LedgerError> {
867
968
  const canonicalRecordId = yield* canonicalAbortRecordId(
868
969
  operation,
869
- submission.conversation_id,
970
+ submission.thread_id,
870
971
  submissionId,
871
972
  );
973
+
872
974
  return yield* decodeAbortIntent({
873
975
  submissionId: row.submission_id,
874
976
  author: row.author,
@@ -892,30 +994,73 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
892
994
  const admit: SubmissionLedger["Service"]["admit"] = Effect.fn("SqliteSubmissionLedger.admit")(
893
995
  function* (request: AdmissionRequest) {
894
996
  const operation = "ledger admit";
997
+
895
998
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AdmissionRequest))(
896
999
  request,
897
1000
  ).pipe(Effect.mapError(internalFailure(operation)));
1001
+
898
1002
  const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(
899
1003
  Effect.mapError(internalFailure(operation)),
900
1004
  );
1005
+
1006
+ const workerAdmissionJson =
1007
+ validated.workerAdmission === undefined
1008
+ ? null
1009
+ : yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
1010
+ validated.workerAdmission,
1011
+ ).pipe(Effect.mapError(internalFailure(operation)));
1012
+
1013
+ if (
1014
+ workerAdmissionJson !== null &&
1015
+ new TextEncoder().encode(workerAdmissionJson).byteLength > 16 * 1024 * 1024
1016
+ ) {
1017
+ return yield* LedgerError.make({
1018
+ operation,
1019
+ message: "Worker admission metadata exceeds the stored value bound",
1020
+ });
1021
+ }
1022
+
1023
+ const messageAdmissionJson =
1024
+ validated.messageAdmission === undefined
1025
+ ? null
1026
+ : yield* Schema.encodeEffect(Schema.fromJsonString(MessageAdmission))(
1027
+ validated.messageAdmission,
1028
+ ).pipe(Effect.mapError(internalFailure(operation)));
1029
+
1030
+ if (
1031
+ messageAdmissionJson !== null &&
1032
+ new TextEncoder().encode(messageAdmissionJson).byteLength > 16 * 1024 * 1024
1033
+ ) {
1034
+ return yield* LedgerError.make({
1035
+ operation,
1036
+ message: "Message admission metadata exceeds the stored value bound",
1037
+ });
1038
+ }
1039
+
901
1040
  const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
902
1041
  Effect.mapError(internalFailure(operation)),
903
1042
  );
1043
+
904
1044
  const mintedSubmissionId = yield* mintIdentifier("submission", operation);
905
1045
  const mintedReceiptId = yield* mintIdentifier("receipt", operation);
1046
+
906
1047
  yield* hitFailpoint("ledger:admit:before", operation);
1048
+
907
1049
  const result = yield* inWriteTransaction(
908
1050
  operation,
909
1051
  Effect.gen(function* () {
910
- const keyRowKey = `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`;
1052
+ const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
1053
+
911
1054
  const existingRows = yield* sql<Record<string, unknown>>`
912
1055
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
913
1056
  FROM effect_agent_submissions
914
- WHERE conversation_id = ${validated.conversationId}
1057
+ WHERE thread_id = ${validated.threadId}
915
1058
  AND principal = ${validated.principal}
916
1059
  AND idempotency_key = ${validated.idempotencyKey}
917
1060
  `.pipe(Effect.mapError(sqlFailure(operation)));
1061
+
918
1062
  const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
1063
+
919
1064
  if (existing.length > 1) {
920
1065
  return yield* corruptionFailure(
921
1066
  operation,
@@ -933,15 +1078,61 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
933
1078
  existing[0].parent_tool_call_id === null
934
1079
  : existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId &&
935
1080
  existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
1081
+
936
1082
  if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
937
1083
  return yield* AdmissionConflict.make({
938
- conversationId: validated.conversationId,
1084
+ threadId: validated.threadId,
939
1085
  principal: validated.principal,
940
1086
  idempotencyKey: validated.idempotencyKey,
941
1087
  existingInputDigest: existing[0].input_digest,
942
1088
  attemptedInputDigest: validated.inputDigest,
943
1089
  });
944
1090
  }
1091
+
1092
+ const retainedWorkerAdmission =
1093
+ existing[0].worker_admission_json === null
1094
+ ? undefined
1095
+ : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1096
+ existing[0].worker_admission_json,
1097
+ ).pipe(Effect.mapError(internalFailure(operation)));
1098
+
1099
+ const retainedMessageAdmission =
1100
+ existing[0].message_admission_json === null
1101
+ ? undefined
1102
+ : yield* Schema.decodeEffect(Schema.fromJsonString(MessageAdmission))(
1103
+ existing[0].message_admission_json,
1104
+ ).pipe(Effect.mapError(internalFailure(operation)));
1105
+
1106
+ const retainedFence =
1107
+ existing[0].admission_fence_json === null
1108
+ ? undefined
1109
+ : yield* Schema.decodeEffect(Schema.fromJsonString(AdmissionFence))(
1110
+ existing[0].admission_fence_json,
1111
+ ).pipe(Effect.mapError(internalFailure(operation)));
1112
+
1113
+ if (
1114
+ (existing[0].admission_group ?? undefined) !== validated.admissionGroup ||
1115
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission))(
1116
+ retainedWorkerAdmission,
1117
+ validated.workerAdmission,
1118
+ ) ||
1119
+ !Schema.toEquivalence(Schema.optional(MessageAdmission))(
1120
+ retainedMessageAdmission,
1121
+ validated.messageAdmission,
1122
+ ) ||
1123
+ !Schema.toEquivalence(Schema.optional(AdmissionFence))(
1124
+ retainedFence,
1125
+ validated.admissionFence,
1126
+ )
1127
+ )
1128
+ return yield* AdmissionConflict.make({
1129
+ threadId: validated.threadId,
1130
+ principal: validated.principal,
1131
+ idempotencyKey: validated.idempotencyKey,
1132
+ existingInputDigest: existing[0].input_digest,
1133
+ attemptedInputDigest: validated.inputDigest,
1134
+ });
1135
+
945
1136
  return yield* decodeAdmissionResult({
946
1137
  submissionId: existing[0].submission_id,
947
1138
  receiptId: existing[0].receipt_id,
@@ -951,26 +1142,78 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
951
1142
  }).pipe(Effect.mapError(internalFailure(operation)));
952
1143
  }
953
1144
 
1145
+ // The first accepted input fixes ordinary/worker lane identity atomically with admission.
1146
+ // Canonical origin materialization can lag admission; a log scan cannot fence that race.
1147
+ const firstRows = yield* sql<Record<string, unknown>>`
1148
+ SELECT worker_admission_json FROM effect_agent_submissions
1149
+ WHERE thread_id=${validated.threadId} ORDER BY queue_sequence LIMIT 1
1150
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1151
+
1152
+ const first = yield* Schema.decodeUnknownEffect(
1153
+ Schema.Array(
1154
+ Schema.Struct({
1155
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
1156
+ }),
1157
+ ),
1158
+ )(firstRows).pipe(Effect.mapError(internalFailure(operation)));
1159
+
1160
+ if (first[0] !== undefined) {
1161
+ const previous =
1162
+ first[0].worker_admission_json === null
1163
+ ? undefined
1164
+ : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1165
+ first[0].worker_admission_json,
1166
+ ).pipe(Effect.mapError(internalFailure(operation)));
1167
+
1168
+ if (
1169
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
1170
+ previous?.origin,
1171
+ validated.workerAdmission?.origin,
1172
+ )
1173
+ )
1174
+ return yield* AdmissionPolicyError.make({
1175
+ reason: "refused",
1176
+ code: "worker-origin-conflict",
1177
+ });
1178
+ }
1179
+
1180
+ yield* admissionFence.check(validated);
1181
+ if (validated.admissionGroup !== undefined) {
1182
+ const occupied = yield* sql<Record<string, unknown>>`
1183
+ SELECT submission_id FROM effect_agent_submissions
1184
+ WHERE thread_id=${validated.threadId} AND admission_group=${validated.admissionGroup} AND state<>'settled' LIMIT 1
1185
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1186
+
1187
+ if (occupied.length > 0)
1188
+ return yield* AdmissionPolicyError.make({
1189
+ reason: "occupied",
1190
+ code: "admission-group",
1191
+ });
1192
+ }
1193
+
954
1194
  const maxRows = yield* sql<Record<string, unknown>>`
955
1195
  SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
956
1196
  FROM effect_agent_submissions
957
- WHERE conversation_id = ${validated.conversationId}
1197
+ WHERE thread_id = ${validated.threadId}
958
1198
  `.pipe(Effect.mapError(sqlFailure(operation)));
1199
+
959
1200
  const decodedMax = yield* decodeRows(
960
1201
  Schema.Array(MaxQueueSequenceRow),
961
1202
  "effect_agent_submissions",
962
- validated.conversationId,
1203
+ validated.threadId,
963
1204
  maxRows,
964
1205
  ).pipe(Effect.mapError(internalFailure(operation)));
1206
+
965
1207
  const queueSequence = yield* decodeQueueSequence(
966
1208
  (decodedMax[0]?.max_queue_sequence ?? 0) + 1,
967
1209
  ).pipe(Effect.mapError(internalFailure(operation)));
1210
+
968
1211
  const now = yield* currentInstant;
969
1212
 
970
1213
  yield* sql`
971
1214
  INSERT INTO effect_agent_submissions (
972
1215
  submission_id,
973
- conversation_id,
1216
+ thread_id,
974
1217
  queue_sequence,
975
1218
  principal,
976
1219
  idempotency_key,
@@ -983,10 +1226,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
983
1226
  state,
984
1227
  created_at,
985
1228
  parent_submission_id,
986
- parent_tool_call_id
1229
+ parent_tool_call_id,
1230
+ admission_group,
1231
+ admission_fence_json,
1232
+ worker_admission_json,
1233
+ message_admission_json
987
1234
  ) VALUES (
988
1235
  ${mintedSubmissionId},
989
- ${validated.conversationId},
1236
+ ${validated.threadId},
990
1237
  ${queueSequence},
991
1238
  ${validated.principal},
992
1239
  ${validated.idempotencyKey},
@@ -999,7 +1246,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
999
1246
  'admitted',
1000
1247
  ${now.iso},
1001
1248
  ${validated.parentLinkage?.parentSubmissionId ?? null},
1002
- ${validated.parentLinkage?.parentToolCallId ?? null}
1249
+ ${validated.parentLinkage?.parentToolCallId ?? null},
1250
+ ${validated.admissionGroup ?? null},
1251
+ ${validated.admissionFence === undefined ? null : JSON.stringify(validated.admissionFence)},
1252
+ ${workerAdmissionJson},
1253
+ ${messageAdmissionJson}
1003
1254
  )
1004
1255
  `.pipe(Effect.mapError(sqlFailure(operation)));
1005
1256
 
@@ -1012,7 +1263,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1012
1263
  }).pipe(Effect.mapError(internalFailure(operation)));
1013
1264
  }),
1014
1265
  );
1266
+
1015
1267
  yield* hitFailpoint("ledger:admit:after", operation);
1268
+
1016
1269
  return result;
1017
1270
  },
1018
1271
  );
@@ -1021,16 +1274,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1021
1274
  "SqliteSubmissionLedger.markReady",
1022
1275
  )(function* (request: MarkReadyRequest) {
1023
1276
  const operation = "ledger mark ready";
1277
+
1024
1278
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkReadyRequest))(
1025
1279
  request,
1026
1280
  ).pipe(Effect.mapError(internalFailure(operation)));
1281
+
1027
1282
  yield* hitFailpoint("ledger:mark-ready:before", operation);
1028
1283
  yield* inWriteTransaction(
1029
1284
  operation,
1030
1285
  Effect.gen(function* () {
1031
1286
  const submission = yield* requireSubmission(operation, validated.submissionId);
1287
+
1032
1288
  if (submission.state !== "admitted") return;
1033
1289
  const now = yield* currentInstant;
1290
+
1034
1291
  yield* sql`
1035
1292
  UPDATE effect_agent_submissions
1036
1293
  SET state = 'ready', ready_at = ${now.iso}
@@ -1044,35 +1301,43 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1044
1301
  const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn("SqliteSubmissionLedger.lookup")(
1045
1302
  function* (request: SubmissionLookup) {
1046
1303
  const operation = "ledger lookup";
1304
+
1047
1305
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookup))(
1048
1306
  request,
1049
1307
  ).pipe(Effect.mapError(internalFailure(operation)));
1308
+
1050
1309
  if (validated._tag === "SubmissionLookupById") {
1051
1310
  const row = yield* readSubmission(operation, validated.submissionId);
1311
+
1052
1312
  if (Option.isNone(row)) return Option.none();
1313
+
1053
1314
  return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
1054
1315
  }
1316
+
1055
1317
  const rows = yield* sql<Record<string, unknown>>`
1056
1318
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1057
1319
  FROM effect_agent_submissions
1058
- WHERE conversation_id = ${validated.conversationId}
1320
+ WHERE thread_id = ${validated.threadId}
1059
1321
  AND principal = ${validated.principal}
1060
1322
  AND idempotency_key = ${validated.idempotencyKey}
1061
1323
  `.pipe(Effect.mapError(sqlFailure(operation)));
1324
+
1062
1325
  const decoded = yield* decodeSubmissionRows(
1063
1326
  operation,
1064
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1327
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1065
1328
  rows,
1066
1329
  );
1330
+
1067
1331
  if (decoded.length > 1) {
1068
1332
  return yield* corruptionFailure(
1069
1333
  operation,
1070
1334
  "effect_agent_submissions",
1071
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1335
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1072
1336
  "An admission idempotency key returned more than one row.",
1073
1337
  );
1074
1338
  }
1075
1339
  if (decoded.length === 0) return Option.none();
1340
+
1076
1341
  return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
1077
1342
  },
1078
1343
  );
@@ -1084,30 +1349,35 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1084
1349
  "SqliteSubmissionLedger.resolveAdmission",
1085
1350
  )(function* (request: SubmissionLookupByKey) {
1086
1351
  const operation = "ledger resolve admission";
1352
+
1087
1353
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookupByKey))(
1088
1354
  request,
1089
1355
  ).pipe(Effect.mapError(internalFailure(operation)));
1356
+
1090
1357
  const rows = yield* sql<Record<string, unknown>>`
1091
1358
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1092
1359
  FROM effect_agent_submissions
1093
- WHERE conversation_id = ${validated.conversationId}
1360
+ WHERE thread_id = ${validated.threadId}
1094
1361
  AND principal = ${validated.principal}
1095
1362
  AND idempotency_key = ${validated.idempotencyKey}
1096
1363
  `.pipe(Effect.mapError(sqlFailure(operation)));
1364
+
1097
1365
  const decoded = yield* decodeSubmissionRows(
1098
1366
  operation,
1099
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1367
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1100
1368
  rows,
1101
1369
  );
1370
+
1102
1371
  if (decoded.length > 1) {
1103
1372
  return yield* corruptionFailure(
1104
1373
  operation,
1105
1374
  "effect_agent_submissions",
1106
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1375
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1107
1376
  "An admission idempotency key returned more than one row.",
1108
1377
  );
1109
1378
  }
1110
1379
  if (decoded.length === 0) return AdmissionNotAdmitted.make();
1380
+
1111
1381
  return AdmissionAdmitted.make({
1112
1382
  submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
1113
1383
  });
@@ -1116,70 +1386,81 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1116
1386
  const claim: SubmissionLedger["Service"]["claim"] = Effect.fn("SqliteSubmissionLedger.claim")(
1117
1387
  function* (request: ClaimRequest) {
1118
1388
  const operation = "ledger claim";
1389
+
1119
1390
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimRequest))(
1120
1391
  request,
1121
1392
  ).pipe(Effect.mapError(internalFailure(operation)));
1393
+
1122
1394
  const attemptId = yield* mintIdentifier("attempt", operation);
1123
1395
  const ownershipToken = yield* mintIdentifier("owner", operation);
1396
+
1124
1397
  yield* hitFailpoint("ledger:claim:before", operation);
1398
+
1125
1399
  const claimed = yield* inWriteTransaction(
1126
1400
  operation,
1127
1401
  Effect.gen(function* () {
1128
1402
  const headRows = yield* sql<Record<string, unknown>>`
1129
1403
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1130
1404
  FROM effect_agent_submissions
1131
- WHERE conversation_id = ${validated.conversationId}
1405
+ WHERE thread_id = ${validated.threadId}
1132
1406
  AND state <> 'settled'
1133
1407
  ORDER BY queue_sequence ASC
1134
1408
  LIMIT 1
1135
1409
  `.pipe(Effect.mapError(sqlFailure(operation)));
1136
- const heads = yield* decodeSubmissionRows(operation, validated.conversationId, headRows);
1410
+
1411
+ const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
1412
+
1137
1413
  if (heads.length === 0) return Option.none<Claim>();
1138
1414
  const head = heads[0];
1139
1415
 
1140
- // A joining/joined head is host-owned and a suspended/unknown head is durably
1141
- // blocked (DUR-017); the lane produces no claim and later ready work is never
1142
- // skipped past the blocked head (DUR-004).
1416
+ // Unknown work is claimable only for a durably requested abort: the coordinator
1417
+ // cleans up children and settles without replaying ordinary Tools. Read the intent
1418
+ // in this claim transaction; retain uncertainty evidence and all ownership fencing.
1143
1419
  if (
1144
1420
  head.state === "joining" ||
1145
1421
  head.state === "joined" ||
1146
1422
  head.state === "suspended" ||
1147
- head.state === "unknown"
1423
+ (head.state === "unknown" &&
1424
+ Option.isNone(yield* readAbortIntent(operation, head.submission_id)))
1148
1425
  ) {
1149
1426
  return Option.none<Claim>();
1150
1427
  }
1151
1428
 
1152
1429
  const now = yield* currentInstant;
1153
1430
  const ownership = yield* readOwnership(operation, head.submission_id);
1431
+
1154
1432
  if (Option.isSome(ownership)) {
1155
1433
  const expiresAt = yield* timestampMillis(
1156
1434
  operation,
1157
1435
  head.submission_id,
1158
1436
  )(ownership.value.lease_expires_at);
1437
+
1159
1438
  // A live lease blocks every new claim; expiry alone only revokes the liveness
1160
1439
  // assumption — correctness stays with producer-epoch fencing (D5).
1161
1440
  if (expiresAt > now.millis) return Option.none<Claim>();
1162
1441
  }
1163
1442
 
1164
- // Bump the Conversation's producer epoch atomically with the claim so every stale
1165
- // Attempt is fenced out of canonical appends (DUR-006). A Conversation that was
1443
+ // Bump the Thread's producer epoch atomically with the claim so every stale
1444
+ // Attempt is fenced out of canonical appends (DUR-006). A Thread that was
1166
1445
  // never materialized (crash between admission and materialization) is created here
1167
1446
  // so recovery can claim first and re-materialize idempotently at this epoch.
1168
- const conversations = yield* journal
1169
- .getConversation(head.conversation_id)
1447
+ const threads = yield* journal
1448
+ .getThread(head.thread_id)
1170
1449
  .pipe(Effect.mapError(internalFailure(operation)));
1450
+
1171
1451
  let producerEpoch: number;
1172
- if (conversations.length === 0) {
1452
+
1453
+ if (threads.length === 0) {
1173
1454
  producerEpoch = 1;
1174
1455
  yield* sql`
1175
- INSERT INTO effect_agent_conversations (
1176
- conversation_id,
1456
+ INSERT INTO effect_agent_threads (
1457
+ thread_id,
1177
1458
  created_at,
1178
1459
  tail_sequence,
1179
1460
  tail_digest,
1180
1461
  producer_epoch
1181
1462
  ) VALUES (
1182
- ${head.conversation_id},
1463
+ ${head.thread_id},
1183
1464
  ${now.iso},
1184
1465
  0,
1185
1466
  ${EMPTY_TAIL_DIGEST},
@@ -1187,15 +1468,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1187
1468
  )
1188
1469
  `.pipe(Effect.mapError(sqlFailure(operation)));
1189
1470
  } else {
1190
- producerEpoch = conversations[0].producer_epoch + 1;
1471
+ producerEpoch = threads[0].producer_epoch + 1;
1191
1472
  yield* sql`
1192
- UPDATE effect_agent_conversations
1473
+ UPDATE effect_agent_threads
1193
1474
  SET producer_epoch = ${producerEpoch}
1194
- WHERE conversation_id = ${head.conversation_id}
1475
+ WHERE thread_id = ${head.thread_id}
1195
1476
  `.pipe(Effect.mapError(sqlFailure(operation)));
1196
1477
  }
1197
1478
 
1198
1479
  const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1480
+
1199
1481
  yield* sql`
1200
1482
  INSERT INTO effect_agent_submission_ownership (
1201
1483
  submission_id,
@@ -1224,14 +1506,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1224
1506
  INSERT INTO effect_agent_attempts (
1225
1507
  attempt_id,
1226
1508
  submission_id,
1227
- conversation_id,
1509
+ thread_id,
1228
1510
  owner_producer_id,
1229
1511
  producer_epoch,
1230
1512
  claimed_at
1231
1513
  ) VALUES (
1232
1514
  ${attemptId},
1233
1515
  ${head.submission_id},
1234
- ${head.conversation_id},
1516
+ ${head.thread_id},
1235
1517
  ${validated.producerId},
1236
1518
  ${producerEpoch},
1237
1519
  ${now.iso}
@@ -1256,6 +1538,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1256
1538
  ),
1257
1539
  ),
1258
1540
  );
1541
+
1259
1542
  return Option.some(
1260
1543
  yield* decodeClaim({
1261
1544
  submissionId: head.submission_id,
@@ -1268,7 +1551,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1268
1551
  );
1269
1552
  }),
1270
1553
  );
1554
+
1271
1555
  yield* hitFailpoint("ledger:claim:after", operation);
1556
+
1272
1557
  return claimed;
1273
1558
  },
1274
1559
  );
@@ -1277,29 +1562,37 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1277
1562
  "SqliteSubmissionLedger.renewOwnership",
1278
1563
  )(function* (request: RenewOwnershipRequest) {
1279
1564
  const operation = "ledger renew ownership";
1565
+
1280
1566
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RenewOwnershipRequest))(
1281
1567
  request,
1282
1568
  ).pipe(Effect.mapError(internalFailure(operation)));
1569
+
1283
1570
  yield* hitFailpoint("ledger:renew:before", operation);
1571
+
1284
1572
  const renewal = yield* inWriteTransaction(
1285
1573
  operation,
1286
1574
  Effect.gen(function* () {
1287
1575
  const submission = yield* requireSubmission(operation, validated.submissionId);
1576
+
1288
1577
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1289
1578
  const now = yield* currentInstant;
1290
1579
  const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1580
+
1291
1581
  yield* sql`
1292
1582
  UPDATE effect_agent_submission_ownership
1293
1583
  SET lease_expires_at = ${leaseExpiresAt}
1294
1584
  WHERE submission_id = ${validated.submissionId}
1295
1585
  `.pipe(Effect.mapError(sqlFailure(operation)));
1586
+
1296
1587
  return yield* decodeOwnershipRenewal({
1297
1588
  ownershipToken: validated.ownershipToken,
1298
1589
  leaseExpiresAt,
1299
1590
  }).pipe(Effect.mapError(internalFailure(operation)));
1300
1591
  }),
1301
1592
  );
1593
+
1302
1594
  yield* hitFailpoint("ledger:renew:after", operation);
1595
+
1303
1596
  return renewal;
1304
1597
  });
1305
1598
 
@@ -1307,14 +1600,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1307
1600
  "SqliteSubmissionLedger.releaseOwnership",
1308
1601
  )(function* (request: ReleaseOwnershipRequest) {
1309
1602
  const operation = "ledger release ownership";
1603
+
1310
1604
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseOwnershipRequest))(
1311
1605
  request,
1312
1606
  ).pipe(Effect.mapError(internalFailure(operation)));
1607
+
1313
1608
  yield* hitFailpoint("ledger:release:before", operation);
1314
1609
  yield* inWriteTransaction(
1315
1610
  operation,
1316
1611
  Effect.gen(function* () {
1317
1612
  const submission = yield* requireSubmission(operation, validated.submissionId);
1613
+
1318
1614
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1319
1615
  yield* sql`
1320
1616
  DELETE FROM effect_agent_submission_ownership
@@ -1336,14 +1632,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1336
1632
  "SqliteSubmissionLedger.markInputApplied",
1337
1633
  )(function* (request: MarkInputAppliedRequest) {
1338
1634
  const operation = "ledger mark input applied";
1635
+
1339
1636
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkInputAppliedRequest))(
1340
1637
  request,
1341
1638
  ).pipe(Effect.mapError(internalFailure(operation)));
1639
+
1342
1640
  yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1343
1641
  yield* inWriteTransaction(
1344
1642
  operation,
1345
1643
  Effect.gen(function* () {
1346
1644
  const submission = yield* requireSubmission(operation, validated.submissionId);
1645
+
1347
1646
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1348
1647
  if (submission.input_applied_record_id !== null) {
1349
1648
  if (
@@ -1352,6 +1651,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1352
1651
  ) {
1353
1652
  return;
1354
1653
  }
1654
+
1355
1655
  return yield* corruptionFailure(
1356
1656
  operation,
1357
1657
  "effect_agent_submissions",
@@ -1379,29 +1679,36 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1379
1679
  "SqliteSubmissionLedger.reserveSettlement",
1380
1680
  )(function* (request: SettlementReservation) {
1381
1681
  const operation = "ledger reserve settlement";
1682
+
1382
1683
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementReservation))(
1383
1684
  request,
1384
1685
  ).pipe(Effect.mapError(internalFailure(operation)));
1686
+
1385
1687
  const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(
1386
1688
  Effect.mapError(internalFailure(operation)),
1387
1689
  );
1690
+
1388
1691
  yield* hitFailpoint("ledger:reserve-settlement:before", operation);
1692
+
1389
1693
  const reserved = yield* inWriteTransaction(
1390
1694
  operation,
1391
1695
  Effect.gen(function* () {
1392
1696
  const existing = yield* readReservation(operation, validated.submissionId);
1697
+
1393
1698
  if (Option.isSome(existing)) {
1394
1699
  const identical =
1395
1700
  existing.value.settlement_id === validated.settlementId &&
1396
1701
  existing.value.outcome === validated.outcome &&
1397
1702
  existing.value.record_digest === validated.recordDigest &&
1398
1703
  existing.value.record_json === recordJson;
1704
+
1399
1705
  if (!identical) {
1400
1706
  return yield* SettlementConflict.make({
1401
1707
  submissionId: validated.submissionId,
1402
1708
  existingOutcome: existing.value.outcome,
1403
1709
  });
1404
1710
  }
1711
+
1405
1712
  const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(
1406
1713
  Effect.mapError((error) =>
1407
1714
  corruptionFailure(
@@ -1412,6 +1719,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1412
1719
  ),
1413
1720
  ),
1414
1721
  );
1722
+
1415
1723
  return ReservedSettlement.make({
1416
1724
  submissionId: validated.submissionId,
1417
1725
  settlementId: validated.settlementId,
@@ -1423,6 +1731,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1423
1731
  }
1424
1732
 
1425
1733
  const submission = yield* requireSubmission(operation, validated.submissionId);
1734
+
1426
1735
  if (submission.state === "settled") {
1427
1736
  if (submission.settled_outcome === null) {
1428
1737
  return yield* corruptionFailure(
@@ -1432,6 +1741,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1432
1741
  "A settled Submission carries no terminal outcome.",
1433
1742
  );
1434
1743
  }
1744
+
1435
1745
  return yield* SettlementConflict.make({
1436
1746
  submissionId: validated.submissionId,
1437
1747
  existingOutcome: submission.settled_outcome,
@@ -1446,13 +1756,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1446
1756
  // ABORTED settlement (`terminalizing` is the same pass's crash replay). Every other
1447
1757
  // reservation stays fenced by the target lane's live ownership.
1448
1758
  let queuedAbortSettlement = false;
1759
+
1449
1760
  if (
1450
1761
  validated.outcome === "aborted" &&
1451
1762
  (submission.state === "ready" || submission.state === "terminalizing")
1452
1763
  ) {
1453
1764
  const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
1765
+
1454
1766
  if (Option.isSome(abortIntent)) {
1455
1767
  const ownership = yield* readOwnership(operation, validated.submissionId);
1768
+
1456
1769
  queuedAbortSettlement = Option.isNone(ownership);
1457
1770
  }
1458
1771
  }
@@ -1461,6 +1774,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1461
1774
  }
1462
1775
  }
1463
1776
  const now = yield* currentInstant;
1777
+
1464
1778
  yield* sql`
1465
1779
  INSERT INTO effect_agent_settlement_reservations (
1466
1780
  submission_id,
@@ -1485,6 +1799,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1485
1799
  SET state = 'terminalizing'
1486
1800
  WHERE submission_id = ${validated.submissionId}
1487
1801
  `.pipe(Effect.mapError(sqlFailure(operation)));
1802
+
1488
1803
  return ReservedSettlement.make({
1489
1804
  submissionId: validated.submissionId,
1490
1805
  settlementId: validated.settlementId,
@@ -1495,7 +1810,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1495
1810
  });
1496
1811
  }),
1497
1812
  );
1813
+
1498
1814
  yield* hitFailpoint("ledger:reserve-settlement:after", operation);
1815
+
1499
1816
  return reserved;
1500
1817
  });
1501
1818
 
@@ -1503,14 +1820,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1503
1820
  "SqliteSubmissionLedger.finalizeSettlement",
1504
1821
  )(function* (request: SettlementFinalization) {
1505
1822
  const operation = "ledger finalize settlement";
1823
+
1506
1824
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementFinalization))(
1507
1825
  request,
1508
1826
  ).pipe(Effect.mapError(internalFailure(operation)));
1827
+
1509
1828
  yield* hitFailpoint("ledger:finalize-settlement:before", operation);
1829
+
1510
1830
  const settlement = yield* inWriteTransaction(
1511
1831
  operation,
1512
1832
  Effect.gen(function* () {
1513
1833
  const reservation = yield* readReservation(operation, validated.submissionId);
1834
+
1514
1835
  if (Option.isNone(reservation)) {
1515
1836
  return yield* LedgerError.make({
1516
1837
  operation,
@@ -1523,7 +1844,32 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1523
1844
  existingOutcome: reservation.value.outcome,
1524
1845
  });
1525
1846
  }
1847
+
1848
+ const reservationRecord = yield* decodeRecordEnvelopeText(
1849
+ reservation.value.record_json,
1850
+ ).pipe(
1851
+ Effect.mapError((error) =>
1852
+ corruptionFailure(
1853
+ operation,
1854
+ "effect_agent_settlement_reservations",
1855
+ validated.submissionId,
1856
+ error.message,
1857
+ ),
1858
+ ),
1859
+ );
1860
+
1861
+ const settlementFailure = settlementFailureFromRecord(reservationRecord);
1862
+
1863
+ if ((reservation.value.outcome === "failed") !== (settlementFailure !== undefined)) {
1864
+ return yield* corruptionFailure(
1865
+ operation,
1866
+ "effect_agent_settlement_reservations",
1867
+ validated.submissionId,
1868
+ "The reserved outcome and canonical failure diagnostic disagree.",
1869
+ );
1870
+ }
1526
1871
  const submission = yield* requireSubmission(operation, validated.submissionId);
1872
+
1527
1873
  if (submission.state === "settled") {
1528
1874
  if (reservation.value.finalized_at === null) {
1529
1875
  return yield* corruptionFailure(
@@ -1533,15 +1879,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1533
1879
  "A settled Submission's reservation carries no finalization timestamp.",
1534
1880
  );
1535
1881
  }
1882
+
1536
1883
  return yield* decodeSettlement({
1537
1884
  submissionId: validated.submissionId,
1538
1885
  settlementId: validated.settlementId,
1539
1886
  receiptId: submission.receipt_id,
1540
1887
  outcome: reservation.value.outcome,
1888
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1541
1889
  settledAt: reservation.value.finalized_at,
1542
1890
  }).pipe(Effect.mapError(internalFailure(operation)));
1543
1891
  }
1544
1892
  const now = yield* currentInstant;
1893
+
1545
1894
  yield* sql`
1546
1895
  UPDATE effect_agent_submissions
1547
1896
  SET state = 'settled', settled_outcome = ${reservation.value.outcome}
@@ -1556,16 +1905,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1556
1905
  DELETE FROM effect_agent_submission_ownership
1557
1906
  WHERE submission_id = ${validated.submissionId}
1558
1907
  `.pipe(Effect.mapError(sqlFailure(operation)));
1908
+
1559
1909
  return yield* decodeSettlement({
1560
1910
  submissionId: validated.submissionId,
1561
1911
  settlementId: validated.settlementId,
1562
1912
  receiptId: submission.receipt_id,
1563
1913
  outcome: reservation.value.outcome,
1914
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1564
1915
  settledAt: now.iso,
1565
1916
  }).pipe(Effect.mapError(internalFailure(operation)));
1566
1917
  }),
1567
1918
  );
1919
+
1568
1920
  yield* hitFailpoint("ledger:finalize-settlement:after", operation);
1921
+
1569
1922
  return settlement;
1570
1923
  });
1571
1924
 
@@ -1573,14 +1926,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1573
1926
  "SqliteSubmissionLedger.requestAbort",
1574
1927
  )(function* (request: AbortCommand) {
1575
1928
  const operation = "ledger request abort";
1929
+
1576
1930
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AbortCommand))(request).pipe(
1577
1931
  Effect.mapError(internalFailure(operation)),
1578
1932
  );
1933
+
1579
1934
  yield* hitFailpoint("ledger:request-abort:before", operation);
1935
+
1580
1936
  const intent = yield* inWriteTransaction(
1581
1937
  operation,
1582
1938
  Effect.gen(function* () {
1583
1939
  const submission = yield* requireSubmission(operation, validated.submissionId);
1940
+
1584
1941
  if (submission.state === "settled") {
1585
1942
  if (submission.settled_outcome === null) {
1586
1943
  return yield* corruptionFailure(
@@ -1590,6 +1947,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1590
1947
  "A settled Submission carries no terminal outcome.",
1591
1948
  );
1592
1949
  }
1950
+
1593
1951
  return yield* SettlementConflict.make({
1594
1952
  submissionId: validated.submissionId,
1595
1953
  existingOutcome: submission.settled_outcome,
@@ -1607,15 +1965,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1607
1965
  "A joined Submission carries no host linkage.",
1608
1966
  );
1609
1967
  }
1968
+
1610
1969
  const hostSubmissionId = yield* decodeSubmissionId(
1611
1970
  submission.joined_host_submission_id,
1612
1971
  ).pipe(Effect.mapError(internalFailure(operation)));
1972
+
1613
1973
  return yield* JoinedToHost.make({
1614
1974
  submissionId: validated.submissionId,
1615
1975
  hostSubmissionId,
1616
1976
  });
1617
1977
  }
1618
1978
  const existing = yield* readAbortIntent(operation, validated.submissionId);
1979
+
1619
1980
  if (Option.isSome(existing)) {
1620
1981
  return yield* abortIntentFromRow(
1621
1982
  operation,
@@ -1625,6 +1986,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1625
1986
  );
1626
1987
  }
1627
1988
  const now = yield* currentInstant;
1989
+
1628
1990
  yield* sql`
1629
1991
  INSERT INTO effect_agent_abort_intents (
1630
1992
  submission_id,
@@ -1638,11 +2000,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1638
2000
  ${now.iso}
1639
2001
  )
1640
2002
  `.pipe(Effect.mapError(sqlFailure(operation)));
2003
+
1641
2004
  const canonicalRecordId = yield* canonicalAbortRecordId(
1642
2005
  operation,
1643
- submission.conversation_id,
2006
+ submission.thread_id,
1644
2007
  validated.submissionId,
1645
2008
  );
2009
+
1646
2010
  return yield* decodeAbortIntent({
1647
2011
  submissionId: validated.submissionId,
1648
2012
  author: validated.author,
@@ -1652,7 +2016,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1652
2016
  }).pipe(Effect.mapError(internalFailure(operation)));
1653
2017
  }),
1654
2018
  );
2019
+
1655
2020
  yield* hitFailpoint("ledger:request-abort:after", operation);
2021
+
1656
2022
  return intent;
1657
2023
  });
1658
2024
 
@@ -1660,31 +2026,38 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1660
2026
  "SqliteSubmissionLedger.claimJoining",
1661
2027
  )(function* (request: ClaimJoiningRequest) {
1662
2028
  const operation = "ledger claim joining";
2029
+
1663
2030
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimJoiningRequest))(
1664
2031
  request,
1665
2032
  ).pipe(Effect.mapError(internalFailure(operation)));
2033
+
1666
2034
  yield* hitFailpoint("ledger:claim-joining:before", operation);
2035
+
1667
2036
  const claims = yield* inWriteTransaction(
1668
2037
  operation,
1669
2038
  Effect.gen(function* () {
1670
2039
  const host = yield* requireSubmission(operation, validated.hostSubmissionId);
1671
- if (host.conversation_id !== validated.conversationId) {
2040
+
2041
+ if (host.thread_id !== validated.threadId) {
1672
2042
  return yield* LedgerError.make({
1673
2043
  operation,
1674
- message: `Host submission ${validated.hostSubmissionId} does not belong to conversation ${validated.conversationId}.`,
2044
+ message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
1675
2045
  });
1676
2046
  }
1677
2047
  // The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
1678
2048
  yield* requireOwnership(operation, host, validated.ownershipToken);
2049
+
1679
2050
  const laterRows = yield* sql<Record<string, unknown>>`
1680
2051
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1681
2052
  FROM effect_agent_submissions
1682
- WHERE conversation_id = ${validated.conversationId}
2053
+ WHERE thread_id = ${validated.threadId}
1683
2054
  AND queue_sequence > ${host.queue_sequence}
1684
2055
  ORDER BY queue_sequence ASC
1685
2056
  `.pipe(Effect.mapError(sqlFailure(operation)));
1686
- const later = yield* decodeSubmissionRows(operation, validated.conversationId, laterRows);
2057
+
2058
+ const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
1687
2059
  const claimed: Array<JoiningClaim> = [];
2060
+
1688
2061
  for (const row of later) {
1689
2062
  if (claimed.length >= validated.maxCount) break;
1690
2063
  // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
@@ -1707,6 +2080,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1707
2080
  SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
1708
2081
  WHERE submission_id = ${row.submission_id}
1709
2082
  `.pipe(Effect.mapError(sqlFailure(operation)));
2083
+
1710
2084
  const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
1711
2085
  Effect.mapError((error) =>
1712
2086
  corruptionFailure(
@@ -1717,6 +2091,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1717
2091
  ),
1718
2092
  ),
1719
2093
  );
2094
+
1720
2095
  claimed.push(
1721
2096
  yield* decodeJoiningClaim({
1722
2097
  submissionId: row.submission_id,
@@ -1725,10 +2100,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1725
2100
  }).pipe(Effect.mapError(internalFailure(operation))),
1726
2101
  );
1727
2102
  }
1728
- return claimed as ReadonlyArray<JoiningClaim>;
2103
+
2104
+ return claimed;
1729
2105
  }),
1730
2106
  );
2107
+
1731
2108
  yield* hitFailpoint("ledger:claim-joining:after", operation);
2109
+
1732
2110
  return claims;
1733
2111
  });
1734
2112
 
@@ -1736,14 +2114,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1736
2114
  "SqliteSubmissionLedger.markJoined",
1737
2115
  )(function* (request: MarkJoinedRequest) {
1738
2116
  const operation = "ledger mark joined";
2117
+
1739
2118
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkJoinedRequest))(
1740
2119
  request,
1741
2120
  ).pipe(Effect.mapError(internalFailure(operation)));
2121
+
1742
2122
  yield* hitFailpoint("ledger:mark-joined:before", operation);
1743
2123
  yield* inWriteTransaction(
1744
2124
  operation,
1745
2125
  Effect.gen(function* () {
1746
2126
  const submission = yield* requireSubmission(operation, validated.submissionId);
2127
+
1747
2128
  if (submission.joined_host_submission_id === null) {
1748
2129
  return yield* LedgerError.make({
1749
2130
  operation,
@@ -1751,6 +2132,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1751
2132
  });
1752
2133
  }
1753
2134
  const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
2135
+
1754
2136
  // The lane is host-owned: the presented token must own the HOST's ownership period,
1755
2137
  // which also lets a later host Attempt repair a lost marker from history (DUR-016).
1756
2138
  yield* requireOwnership(operation, host, validated.ownershipToken);
@@ -1761,6 +2143,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1761
2143
  ) {
1762
2144
  return;
1763
2145
  }
2146
+
1764
2147
  return yield* corruptionFailure(
1765
2148
  operation,
1766
2149
  "effect_agent_submissions",
@@ -1791,14 +2174,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1791
2174
  "SqliteSubmissionLedger.revertJoining",
1792
2175
  )(function* (request: RevertJoiningRequest) {
1793
2176
  const operation = "ledger revert joining";
2177
+
1794
2178
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RevertJoiningRequest))(
1795
2179
  request,
1796
2180
  ).pipe(Effect.mapError(internalFailure(operation)));
2181
+
1797
2182
  yield* hitFailpoint("ledger:revert-joining:before", operation);
1798
2183
  yield* inWriteTransaction(
1799
2184
  operation,
1800
2185
  Effect.gen(function* () {
1801
2186
  const submission = yield* requireSubmission(operation, validated.submissionId);
2187
+
1802
2188
  // Idempotent and recovery-only: only a still-`joining` Submission reverts; an
1803
2189
  // already-joined (or already-reverted) Submission is a no-op (DUR-016).
1804
2190
  if (submission.state !== "joining") return;
@@ -1816,17 +2202,22 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1816
2202
  "SqliteSubmissionLedger.suspend",
1817
2203
  )(function* (request: SuspendRequest) {
1818
2204
  const operation = "ledger suspend";
2205
+
1819
2206
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SuspendRequest))(
1820
2207
  request,
1821
2208
  ).pipe(Effect.mapError(internalFailure(operation)));
2209
+
1822
2210
  const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
1823
2211
  Effect.mapError(internalFailure(operation)),
1824
2212
  );
2213
+
1825
2214
  yield* hitFailpoint("ledger:suspend:before", operation);
2215
+
1826
2216
  const outcome = yield* inWriteTransaction(
1827
2217
  operation,
1828
2218
  Effect.gen(function* () {
1829
2219
  const submission = yield* requireSubmission(operation, validated.submissionId);
2220
+
1830
2221
  if (submission.state === "settled") {
1831
2222
  if (submission.settled_outcome === null) {
1832
2223
  return yield* corruptionFailure(
@@ -1836,6 +2227,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1836
2227
  "A settled Submission carries no terminal outcome.",
1837
2228
  );
1838
2229
  }
2230
+
1839
2231
  return yield* SettlementConflict.make({
1840
2232
  submissionId: validated.submissionId,
1841
2233
  existingOutcome: submission.settled_outcome,
@@ -1844,6 +2236,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1844
2236
  // An exact terminal outcome is already reserved (DUR-011); suspension would
1845
2237
  // contradict it, so the reservation wins.
1846
2238
  const reservation = yield* readReservation(operation, validated.submissionId);
2239
+
1847
2240
  if (Option.isSome(reservation)) {
1848
2241
  return yield* SettlementConflict.make({
1849
2242
  submissionId: validated.submissionId,
@@ -1857,23 +2250,27 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1857
2250
  if (validated.reason._tag === "ApprovalPending") {
1858
2251
  const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
1859
2252
  const decided = new Set(decisions.map((row) => row.tool_call_id));
2253
+
1860
2254
  if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
1861
- return "resume-immediately" as SuspensionOutcome;
2255
+ return RESUME_IMMEDIATELY;
1862
2256
  }
1863
2257
  } else {
1864
2258
  let allSettled = true;
2259
+
1865
2260
  for (const child of validated.reason.children) {
1866
2261
  const childRow = yield* readSubmission(operation, child.childSubmissionId);
2262
+
1867
2263
  if (Option.isNone(childRow) || childRow.value.state !== "settled") {
1868
2264
  allSettled = false;
1869
2265
  break;
1870
2266
  }
1871
2267
  }
1872
2268
  if (allSettled) {
1873
- return "resume-immediately" as SuspensionOutcome;
2269
+ return RESUME_IMMEDIATELY;
1874
2270
  }
1875
2271
  }
1876
2272
  const now = yield* currentInstant;
2273
+
1877
2274
  yield* sql`
1878
2275
  UPDATE effect_agent_submissions
1879
2276
  SET
@@ -1888,10 +2285,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1888
2285
  DELETE FROM effect_agent_submission_ownership
1889
2286
  WHERE submission_id = ${validated.submissionId}
1890
2287
  `.pipe(Effect.mapError(sqlFailure(operation)));
1891
- return "suspended" as SuspensionOutcome;
2288
+
2289
+ return SUSPENDED;
1892
2290
  }),
1893
2291
  );
2292
+
1894
2293
  yield* hitFailpoint("ledger:suspend:after", operation);
2294
+
1895
2295
  return outcome;
1896
2296
  });
1897
2297
 
@@ -1904,6 +2304,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1904
2304
  const wakeSuspendedIfCovered = Effect.fn("SqliteSubmissionLedger.wakeSuspendedIfCovered")(
1905
2305
  function* (operation: string, submission: SubmissionRow): Effect.fn.Return<void, LedgerError> {
1906
2306
  if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
2307
+
1907
2308
  const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
1908
2309
  submission.suspended_reason_json,
1909
2310
  ).pipe(
@@ -1916,9 +2317,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1916
2317
  ),
1917
2318
  ),
1918
2319
  );
2320
+
1919
2321
  if (reason._tag !== "ApprovalPending") return;
1920
2322
  const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
1921
2323
  const decided = new Set(decisions.map((row) => row.tool_call_id));
2324
+
1922
2325
  if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
1923
2326
  yield* sql`
1924
2327
  UPDATE effect_agent_submissions
@@ -1935,14 +2338,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1935
2338
  "SqliteSubmissionLedger.recordApprovalDecision",
1936
2339
  )(function* (command: ApprovalDecisionCommand) {
1937
2340
  const operation = "ledger record approval decision";
2341
+
1938
2342
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ApprovalDecisionCommand))(
1939
2343
  command,
1940
2344
  ).pipe(Effect.mapError(internalFailure(operation)));
2345
+
1941
2346
  yield* hitFailpoint("ledger:approval-decision:before", operation);
2347
+
1942
2348
  const intent = yield* inWriteTransaction(
1943
2349
  operation,
1944
2350
  Effect.gen(function* () {
1945
2351
  const submission = yield* requireSubmission(operation, validated.submissionId);
2352
+
1946
2353
  if (submission.state === "settled") {
1947
2354
  if (submission.settled_outcome === null) {
1948
2355
  return yield* corruptionFailure(
@@ -1952,6 +2359,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1952
2359
  "A settled Submission carries no terminal outcome.",
1953
2360
  );
1954
2361
  }
2362
+
1955
2363
  return yield* SettlementConflict.make({
1956
2364
  submissionId: validated.submissionId,
1957
2365
  existingOutcome: submission.settled_outcome,
@@ -1959,6 +2367,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1959
2367
  }
1960
2368
  const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
1961
2369
  const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
2370
+
1962
2371
  if (existing !== undefined) {
1963
2372
  // Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
1964
2373
  // the recorded intent unchanged; a divergent re-decision conflicts.
@@ -1969,9 +2378,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1969
2378
  existingDecision: existing.decision,
1970
2379
  });
1971
2380
  }
2381
+
1972
2382
  return yield* approvalIntentFromRow(operation, existing);
1973
2383
  }
1974
2384
  const now = yield* currentInstant;
2385
+
1975
2386
  yield* sql`
1976
2387
  INSERT INTO effect_agent_approval_decisions (
1977
2388
  submission_id,
@@ -1990,6 +2401,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1990
2401
  )
1991
2402
  `.pipe(Effect.mapError(sqlFailure(operation)));
1992
2403
  yield* wakeSuspendedIfCovered(operation, submission);
2404
+
1993
2405
  return yield* decodeApprovalDecisionIntent({
1994
2406
  submissionId: validated.submissionId,
1995
2407
  toolCallId: validated.toolCallId,
@@ -2000,7 +2412,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2000
2412
  }).pipe(Effect.mapError(internalFailure(operation)));
2001
2413
  }),
2002
2414
  );
2415
+
2003
2416
  yield* hitFailpoint("ledger:approval-decision:after", operation);
2417
+
2004
2418
  return intent;
2005
2419
  });
2006
2420
 
@@ -2008,14 +2422,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2008
2422
  "SqliteSubmissionLedger.markUnknown",
2009
2423
  )(function* (request: MarkUnknownRequest) {
2010
2424
  const operation = "ledger mark unknown";
2425
+
2011
2426
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkUnknownRequest))(
2012
2427
  request,
2013
2428
  ).pipe(Effect.mapError(internalFailure(operation)));
2429
+
2014
2430
  yield* hitFailpoint("ledger:mark-unknown:before", operation);
2015
2431
  yield* inWriteTransaction(
2016
2432
  operation,
2017
2433
  Effect.gen(function* () {
2018
2434
  const submission = yield* requireSubmission(operation, validated.submissionId);
2435
+
2019
2436
  if (submission.state === "settled") {
2020
2437
  if (submission.settled_outcome === null) {
2021
2438
  return yield* corruptionFailure(
@@ -2025,6 +2442,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2025
2442
  "A settled Submission carries no terminal outcome.",
2026
2443
  );
2027
2444
  }
2445
+
2028
2446
  return yield* SettlementConflict.make({
2029
2447
  submissionId: validated.submissionId,
2030
2448
  existingOutcome: submission.settled_outcome,
@@ -2033,6 +2451,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2033
2451
  // A reserved exact outcome wins over a late Unknown marking (DUR-011); the recovery
2034
2452
  // classifier orders reservation ahead of MarkUnknown for the same reason.
2035
2453
  const reservation = yield* readReservation(operation, validated.submissionId);
2454
+
2036
2455
  if (Option.isSome(reservation)) {
2037
2456
  return yield* SettlementConflict.make({
2038
2457
  submissionId: validated.submissionId,
@@ -2043,13 +2462,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2043
2462
  // set while the first recorded reason is kept.
2044
2463
  const existingIds = yield* storedUnknownToolCallIds(operation, submission);
2045
2464
  const known = new Set(existingIds);
2465
+
2046
2466
  const merged = [
2047
2467
  ...existingIds,
2048
2468
  ...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
2049
2469
  ];
2470
+
2050
2471
  const idsJson = yield* encodeToolCallIdsText(merged).pipe(
2051
2472
  Effect.mapError(internalFailure(operation)),
2052
2473
  );
2474
+
2053
2475
  yield* sql`
2054
2476
  UPDATE effect_agent_submissions
2055
2477
  SET
@@ -2067,17 +2489,22 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2067
2489
  "SqliteSubmissionLedger.recordUnknownResolution",
2068
2490
  )(function* (command: UnknownResolutionCommand) {
2069
2491
  const operation = "ledger record unknown resolution";
2492
+
2070
2493
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(UnknownResolutionCommand))(
2071
2494
  command,
2072
2495
  ).pipe(Effect.mapError(internalFailure(operation)));
2496
+
2073
2497
  const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
2074
2498
  Effect.mapError(internalFailure(operation)),
2075
2499
  );
2500
+
2076
2501
  yield* hitFailpoint("ledger:unknown-resolution:before", operation);
2502
+
2077
2503
  const intent = yield* inWriteTransaction(
2078
2504
  operation,
2079
2505
  Effect.gen(function* () {
2080
2506
  const submission = yield* requireSubmission(operation, validated.submissionId);
2507
+
2081
2508
  if (submission.state === "settled") {
2082
2509
  if (submission.settled_outcome === null) {
2083
2510
  return yield* corruptionFailure(
@@ -2087,6 +2514,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2087
2514
  "A settled Submission carries no terminal outcome.",
2088
2515
  );
2089
2516
  }
2517
+
2090
2518
  return yield* SettlementConflict.make({
2091
2519
  submissionId: validated.submissionId,
2092
2520
  existingOutcome: submission.settled_outcome,
@@ -2094,19 +2522,30 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2094
2522
  }
2095
2523
  const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
2096
2524
  const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
2097
- if (existing !== undefined && existing.resolution_json !== resolutionJson) {
2525
+
2526
+ const existingIntent =
2527
+ existing === undefined
2528
+ ? undefined
2529
+ : yield* unknownResolutionIntentFromRow(operation, existing);
2530
+
2531
+ if (
2532
+ existingIntent !== undefined &&
2533
+ !equivalentUnknownResolution(existingIntent.resolution, validated.resolution)
2534
+ ) {
2098
2535
  return yield* UnknownResolutionConflict.make({
2099
2536
  submissionId: validated.submissionId,
2100
2537
  toolCallId: validated.toolCallId,
2101
2538
  });
2102
2539
  }
2103
2540
  let resolved: UnknownResolutionIntent;
2104
- if (existing !== undefined) {
2541
+
2542
+ if (existingIntent !== undefined) {
2105
2543
  // Idempotent replay of the recorded intent (author/reason may differ; the stored
2106
2544
  // audit fields win, exactly like requestAbort).
2107
- resolved = yield* unknownResolutionIntentFromRow(operation, existing);
2545
+ resolved = existingIntent;
2108
2546
  } else {
2109
2547
  const now = yield* currentInstant;
2548
+
2110
2549
  yield* sql`
2111
2550
  INSERT INTO effect_agent_unknown_resolutions (
2112
2551
  submission_id,
@@ -2124,9 +2563,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2124
2563
  ${now.iso}
2125
2564
  )
2126
2565
  `.pipe(Effect.mapError(sqlFailure(operation)));
2566
+
2127
2567
  const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
2128
2568
  Effect.mapError(internalFailure(operation)),
2129
2569
  );
2570
+
2130
2571
  resolved = yield* decodeUnknownResolutionIntent({
2131
2572
  submissionId: validated.submissionId,
2132
2573
  toolCallId: validated.toolCallId,
@@ -2143,6 +2584,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2143
2584
  const markedIds = yield* storedUnknownToolCallIds(operation, submission);
2144
2585
  const covering = yield* readUnknownResolutions(operation, validated.submissionId);
2145
2586
  const coveredIds = new Set(covering.map((row) => row.tool_call_id));
2587
+
2146
2588
  if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) {
2147
2589
  yield* sql`
2148
2590
  UPDATE effect_agent_submissions
@@ -2154,10 +2596,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2154
2596
  `.pipe(Effect.mapError(sqlFailure(operation)));
2155
2597
  }
2156
2598
  }
2599
+
2157
2600
  return resolved;
2158
2601
  }),
2159
2602
  );
2603
+
2160
2604
  yield* hitFailpoint("ledger:unknown-resolution:after", operation);
2605
+
2161
2606
  return intent;
2162
2607
  });
2163
2608
 
@@ -2165,26 +2610,38 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2165
2610
  "SqliteSubmissionLedger.recordChildSettled",
2166
2611
  )(function* (request: ChildSettledNotification) {
2167
2612
  const operation = "ledger record child settled";
2613
+
2168
2614
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ChildSettledNotification))(
2169
2615
  request,
2170
2616
  ).pipe(Effect.mapError(internalFailure(operation)));
2617
+
2171
2618
  yield* hitFailpoint("ledger:child-settled:before", operation);
2619
+
2172
2620
  const outcome = yield* inWriteTransaction(
2173
2621
  operation,
2174
2622
  Effect.gen(function* () {
2175
2623
  const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2176
- // The child's canonical Settlement is the authority for this wake; a notification for
2177
- // an unsettled (or unknown) child is a caller error in a single-store adapter.
2624
+ // The caller may notify after the canonical Settlement append but before ledger
2625
+ // finalization. An exact reservation plus `terminalizing` is the narrow durable prefix
2626
+ // that makes that ordering admissible; earlier states remain a caller error.
2178
2627
  const child = yield* readSubmission(operation, validated.childSubmissionId);
2179
- if (Option.isNone(child) || child.value.state !== "settled") {
2628
+ const childReservation = yield* readReservation(operation, validated.childSubmissionId);
2629
+
2630
+ const announced =
2631
+ Option.isSome(child) &&
2632
+ (child.value.state === "settled" ||
2633
+ (child.value.state === "terminalizing" && Option.isSome(childReservation)));
2634
+
2635
+ if (!announced) {
2180
2636
  return yield* LedgerError.make({
2181
2637
  operation,
2182
2638
  message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`,
2183
2639
  });
2184
2640
  }
2185
2641
  if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
2186
- return "not-waiting" as ChildSettledOutcome;
2642
+ return NOT_WAITING;
2187
2643
  }
2644
+
2188
2645
  const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
2189
2646
  parent.suspended_reason_json,
2190
2647
  ).pipe(
@@ -2197,20 +2654,28 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2197
2654
  ),
2198
2655
  ),
2199
2656
  );
2657
+
2200
2658
  if (reason._tag !== "WaitingForChild") {
2201
- return "not-waiting" as ChildSettledOutcome;
2659
+ return NOT_WAITING;
2202
2660
  }
2203
2661
  if (
2204
2662
  !reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
2205
2663
  ) {
2206
- return "not-waiting" as ChildSettledOutcome;
2664
+ return NOT_WAITING;
2207
2665
  }
2208
- // The parent wakes exactly when EVERY listed child is settled (spec §12 step 10);
2209
- // replays re-run the coverage check so a recovering caller wakes the lane idempotently.
2666
+ // Every listed child must be either finalized or canonically announced from the exact
2667
+ // terminalizing reservation. Replays re-run this coverage check idempotently.
2210
2668
  for (const entry of reason.children) {
2211
2669
  const listed = yield* readSubmission(operation, entry.childSubmissionId);
2212
- if (Option.isNone(listed) || listed.value.state !== "settled") {
2213
- return "still-waiting" as ChildSettledOutcome;
2670
+ const reservation = yield* readReservation(operation, entry.childSubmissionId);
2671
+
2672
+ const covered =
2673
+ Option.isSome(listed) &&
2674
+ (listed.value.state === "settled" ||
2675
+ (listed.value.state === "terminalizing" && Option.isSome(reservation)));
2676
+
2677
+ if (!covered) {
2678
+ return STILL_WAITING;
2214
2679
  }
2215
2680
  }
2216
2681
  yield* sql`
@@ -2221,10 +2686,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2221
2686
  suspended_at = NULL
2222
2687
  WHERE submission_id = ${validated.parentSubmissionId}
2223
2688
  `.pipe(Effect.mapError(sqlFailure(operation)));
2224
- return "woken" as ChildSettledOutcome;
2689
+
2690
+ return WOKEN;
2225
2691
  }),
2226
2692
  );
2693
+
2227
2694
  yield* hitFailpoint("ledger:child-settled:after", operation);
2695
+
2228
2696
  return outcome;
2229
2697
  });
2230
2698
 
@@ -2232,25 +2700,36 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2232
2700
  "SqliteSubmissionLedger.reserveChildBudget",
2233
2701
  )(function* (request: ChildBudgetReservationRequest) {
2234
2702
  const operation = "ledger reserve child budget";
2703
+
2235
2704
  const validated = yield* Schema.decodeUnknownEffect(
2236
2705
  Schema.toType(ChildBudgetReservationRequest),
2237
2706
  )(request).pipe(Effect.mapError(internalFailure(operation)));
2707
+
2238
2708
  const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
2239
2709
  Effect.mapError(internalFailure(operation)),
2240
2710
  );
2711
+
2241
2712
  yield* hitFailpoint("ledger:child-reservation:before", operation);
2713
+
2242
2714
  const reserved = yield* inWriteTransaction(
2243
2715
  operation,
2244
2716
  Effect.gen(function* () {
2245
2717
  const existing = yield* readChildReservation(operation, validated.reservationId);
2718
+
2246
2719
  if (Option.isSome(existing)) {
2720
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
2721
+ operation,
2722
+ existing.value,
2723
+ );
2724
+
2247
2725
  // Identical replays short-circuit before the fence, mirroring reserveSettlement: a
2248
2726
  // replay creates nothing, so a recovering caller resumes rather than duplicates.
2249
2727
  const identical =
2250
2728
  existing.value.parent_submission_id === validated.parentSubmissionId &&
2251
2729
  existing.value.parent_tool_call_id === validated.parentToolCallId &&
2252
2730
  existing.value.allocation_digest === validated.allocationDigest &&
2253
- existing.value.allocation_json === allocationJson;
2731
+ equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
2732
+
2254
2733
  if (!identical) {
2255
2734
  return yield* ChildReservationConflict.make({
2256
2735
  reservationId: validated.reservationId,
@@ -2259,16 +2738,19 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2259
2738
  "A reservation with this identity exists with a different parent Tool Call or allocation.",
2260
2739
  });
2261
2740
  }
2741
+
2262
2742
  return ReservedChildBudget.make({
2263
- reservation: yield* childReservationSnapshotFromRow(operation, existing.value),
2743
+ reservation: existingSnapshot,
2264
2744
  replayed: true,
2265
2745
  });
2266
2746
  }
2747
+
2267
2748
  const collision = yield* readChildReservationForCall(
2268
2749
  operation,
2269
2750
  validated.parentSubmissionId,
2270
2751
  validated.parentToolCallId,
2271
2752
  );
2753
+
2272
2754
  if (Option.isSome(collision)) {
2273
2755
  return yield* ChildReservationConflict.make({
2274
2756
  reservationId: validated.reservationId,
@@ -2277,10 +2759,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2277
2759
  });
2278
2760
  }
2279
2761
  const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2762
+
2280
2763
  // Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
2281
2764
  // parent Attempt can never create new reservation state.
2282
2765
  yield* requireOwnership(operation, parent, validated.ownershipToken);
2283
2766
  const now = yield* currentInstant;
2767
+
2284
2768
  yield* sql`
2285
2769
  INSERT INTO effect_agent_child_reservations (
2286
2770
  reservation_id,
@@ -2301,6 +2785,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2301
2785
  )
2302
2786
  `.pipe(Effect.mapError(sqlFailure(operation)));
2303
2787
  const inserted = yield* readChildReservation(operation, validated.reservationId);
2788
+
2304
2789
  if (Option.isNone(inserted)) {
2305
2790
  return yield* corruptionFailure(
2306
2791
  operation,
@@ -2309,13 +2794,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2309
2794
  "An inserted child reservation row is missing inside its own transaction.",
2310
2795
  );
2311
2796
  }
2797
+
2312
2798
  return ReservedChildBudget.make({
2313
2799
  reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
2314
2800
  replayed: false,
2315
2801
  });
2316
2802
  }),
2317
2803
  );
2804
+
2318
2805
  yield* hitFailpoint("ledger:child-reservation:after", operation);
2806
+
2319
2807
  return reserved;
2320
2808
  });
2321
2809
 
@@ -2324,14 +2812,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2324
2812
  request: AttachChildToReservationRequest,
2325
2813
  ) {
2326
2814
  const operation = "ledger attach child to reservation";
2815
+
2327
2816
  const validated = yield* Schema.decodeUnknownEffect(
2328
2817
  Schema.toType(AttachChildToReservationRequest),
2329
2818
  )(request).pipe(Effect.mapError(internalFailure(operation)));
2819
+
2330
2820
  yield* hitFailpoint("ledger:child-attach:before", operation);
2821
+
2331
2822
  const attached = yield* inWriteTransaction(
2332
2823
  operation,
2333
2824
  Effect.gen(function* () {
2334
2825
  const existing = yield* readChildReservation(operation, validated.reservationId);
2826
+
2335
2827
  if (Option.isNone(existing)) {
2336
2828
  return yield* LedgerError.make({
2337
2829
  operation,
@@ -2343,6 +2835,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2343
2835
  if (existing.value.child_submission_id === validated.childSubmissionId) {
2344
2836
  return yield* childReservationSnapshotFromRow(operation, existing.value);
2345
2837
  }
2838
+
2346
2839
  return yield* ChildReservationConflict.make({
2347
2840
  reservationId: validated.reservationId,
2348
2841
  status: existing.value.status,
@@ -2350,6 +2843,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2350
2843
  });
2351
2844
  }
2352
2845
  const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
2846
+
2353
2847
  yield* requireOwnership(operation, parent, validated.ownershipToken);
2354
2848
  if (existing.value.status !== "reserved") {
2355
2849
  return yield* ChildReservationConflict.make({
@@ -2361,6 +2855,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2361
2855
  // Single-store latitude: the admitted child must exist here, so a dangling
2362
2856
  // attachment can never enter the recovery view.
2363
2857
  const child = yield* readSubmission(operation, validated.childSubmissionId);
2858
+
2364
2859
  if (Option.isNone(child)) {
2365
2860
  return yield* LedgerError.make({
2366
2861
  operation,
@@ -2373,6 +2868,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2373
2868
  WHERE reservation_id = ${validated.reservationId}
2374
2869
  `.pipe(Effect.mapError(sqlFailure(operation)));
2375
2870
  const updated = yield* readChildReservation(operation, validated.reservationId);
2871
+
2376
2872
  if (Option.isNone(updated)) {
2377
2873
  return yield* corruptionFailure(
2378
2874
  operation,
@@ -2381,10 +2877,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2381
2877
  "An updated child reservation row is missing inside its own transaction.",
2382
2878
  );
2383
2879
  }
2880
+
2384
2881
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2385
2882
  }),
2386
2883
  );
2884
+
2387
2885
  yield* hitFailpoint("ledger:child-attach:after", operation);
2886
+
2388
2887
  return attached;
2389
2888
  });
2390
2889
 
@@ -2392,17 +2891,22 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2392
2891
  "SqliteSubmissionLedger.beginChildBudgetRelease",
2393
2892
  )(function* (request: BeginChildBudgetReleaseRequest) {
2394
2893
  const operation = "ledger begin child budget release";
2894
+
2395
2895
  const validated = yield* Schema.decodeUnknownEffect(
2396
2896
  Schema.toType(BeginChildBudgetReleaseRequest),
2397
2897
  )(request).pipe(Effect.mapError(internalFailure(operation)));
2898
+
2398
2899
  const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
2399
2900
  Effect.mapError(internalFailure(operation)),
2400
2901
  );
2902
+
2401
2903
  yield* hitFailpoint("ledger:child-release-pending:before", operation);
2904
+
2402
2905
  const frozen = yield* inWriteTransaction(
2403
2906
  operation,
2404
2907
  Effect.gen(function* () {
2405
2908
  const existing = yield* readChildReservation(operation, validated.reservationId);
2909
+
2406
2910
  if (Option.isNone(existing)) {
2407
2911
  return yield* LedgerError.make({
2408
2912
  operation,
@@ -2410,11 +2914,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2410
2914
  });
2411
2915
  }
2412
2916
  if (existing.value.status !== "reserved") {
2917
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
2918
+ operation,
2919
+ existing.value,
2920
+ );
2921
+
2413
2922
  // The accounting decision was already frozen exactly once; an identical replay is a
2414
2923
  // no-op and a divergent decision conflicts (spec §12 join step 6).
2415
- if (existing.value.accounting_json === accountingJson) {
2416
- return yield* childReservationSnapshotFromRow(operation, existing.value);
2924
+ if (
2925
+ existingSnapshot.accounting !== undefined &&
2926
+ equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
2927
+ ) {
2928
+ return existingSnapshot;
2417
2929
  }
2930
+
2418
2931
  return yield* ChildReservationConflict.make({
2419
2932
  reservationId: validated.reservationId,
2420
2933
  status: existing.value.status,
@@ -2422,6 +2935,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2422
2935
  });
2423
2936
  }
2424
2937
  const now = yield* currentInstant;
2938
+
2425
2939
  yield* sql`
2426
2940
  UPDATE effect_agent_child_reservations
2427
2941
  SET
@@ -2431,6 +2945,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2431
2945
  WHERE reservation_id = ${validated.reservationId}
2432
2946
  `.pipe(Effect.mapError(sqlFailure(operation)));
2433
2947
  const updated = yield* readChildReservation(operation, validated.reservationId);
2948
+
2434
2949
  if (Option.isNone(updated)) {
2435
2950
  return yield* corruptionFailure(
2436
2951
  operation,
@@ -2439,10 +2954,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2439
2954
  "An updated child reservation row is missing inside its own transaction.",
2440
2955
  );
2441
2956
  }
2957
+
2442
2958
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2443
2959
  }),
2444
2960
  );
2961
+
2445
2962
  yield* hitFailpoint("ledger:child-release-pending:after", operation);
2963
+
2446
2964
  return frozen;
2447
2965
  });
2448
2966
 
@@ -2450,14 +2968,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2450
2968
  "SqliteSubmissionLedger.releaseChildBudget",
2451
2969
  )(function* (request: ReleaseChildBudgetRequest) {
2452
2970
  const operation = "ledger release child budget";
2971
+
2453
2972
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseChildBudgetRequest))(
2454
2973
  request,
2455
2974
  ).pipe(Effect.mapError(internalFailure(operation)));
2975
+
2456
2976
  yield* hitFailpoint("ledger:child-release:before", operation);
2977
+
2457
2978
  const released = yield* inWriteTransaction(
2458
2979
  operation,
2459
2980
  Effect.gen(function* () {
2460
2981
  const existing = yield* readChildReservation(operation, validated.reservationId);
2982
+
2461
2983
  if (Option.isNone(existing)) {
2462
2984
  return yield* LedgerError.make({
2463
2985
  operation,
@@ -2477,12 +2999,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2477
2999
  });
2478
3000
  }
2479
3001
  const now = yield* currentInstant;
3002
+
2480
3003
  yield* sql`
2481
3004
  UPDATE effect_agent_child_reservations
2482
3005
  SET status = 'released', released_at = ${now.iso}
2483
3006
  WHERE reservation_id = ${validated.reservationId}
2484
3007
  `.pipe(Effect.mapError(sqlFailure(operation)));
2485
3008
  const updated = yield* readChildReservation(operation, validated.reservationId);
3009
+
2486
3010
  if (Option.isNone(updated)) {
2487
3011
  return yield* corruptionFailure(
2488
3012
  operation,
@@ -2491,15 +3015,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2491
3015
  "An updated child reservation row is missing inside its own transaction.",
2492
3016
  );
2493
3017
  }
3018
+
2494
3019
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2495
3020
  }),
2496
3021
  );
3022
+
2497
3023
  yield* hitFailpoint("ledger:child-release:after", operation);
3024
+
2498
3025
  return released;
2499
3026
  });
2500
3027
 
2501
3028
  interface ScanCursor {
2502
- readonly conversationId: string;
3029
+ readonly threadId: string;
2503
3030
  readonly queueSequence: number;
2504
3031
  }
2505
3032
 
@@ -2510,13 +3037,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2510
3037
  LedgerError
2511
3038
  > {
2512
3039
  const operation = "ledger scan nonterminal";
3040
+
2513
3041
  const rows = yield* (
2514
3042
  cursor === undefined
2515
3043
  ? sql<Record<string, unknown>>`
2516
3044
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2517
3045
  FROM effect_agent_submissions
2518
3046
  WHERE state <> 'settled'
2519
- ORDER BY conversation_id ASC, queue_sequence ASC
3047
+ ORDER BY thread_id ASC, queue_sequence ASC
2520
3048
  LIMIT ${SCAN_PAGE_SIZE}
2521
3049
  `
2522
3050
  : sql<Record<string, unknown>>`
@@ -2524,51 +3052,131 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2524
3052
  FROM effect_agent_submissions
2525
3053
  WHERE state <> 'settled'
2526
3054
  AND (
2527
- conversation_id > ${cursor.conversationId}
3055
+ thread_id > ${cursor.threadId}
2528
3056
  OR (
2529
- conversation_id = ${cursor.conversationId}
3057
+ thread_id = ${cursor.threadId}
2530
3058
  AND queue_sequence > ${cursor.queueSequence}
2531
3059
  )
2532
3060
  )
2533
- ORDER BY conversation_id ASC, queue_sequence ASC
3061
+ ORDER BY thread_id ASC, queue_sequence ASC
2534
3062
  LIMIT ${SCAN_PAGE_SIZE}
2535
3063
  `
2536
3064
  ).pipe(Effect.mapError(sqlFailure(operation)));
3065
+
2537
3066
  const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
3067
+
2538
3068
  const snapshots = yield* Effect.forEach(decoded, (row) =>
2539
3069
  decodeSubmissionSnapshot(operation, row),
2540
3070
  );
3071
+
2541
3072
  const last = decoded[decoded.length - 1];
3073
+
2542
3074
  const next: Option.Option<ScanCursor | undefined> =
2543
3075
  last === undefined || decoded.length < SCAN_PAGE_SIZE
2544
3076
  ? Option.none()
2545
3077
  : Option.some({
2546
- conversationId: last.conversation_id,
3078
+ threadId: last.thread_id,
2547
3079
  queueSequence: last.queue_sequence,
2548
3080
  });
3081
+
2549
3082
  return [snapshots, next] as const;
2550
3083
  });
2551
3084
 
2552
- const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate(
2553
- undefined as ScanCursor | undefined,
2554
- scanPage,
2555
- );
3085
+ const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate<
3086
+ ScanCursor | undefined,
3087
+ SubmissionSnapshot,
3088
+ LedgerError
3089
+ >(undefined, scanPage);
3090
+
3091
+ const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
3092
+ "SqliteSubmissionLedger.readAbortIntentForSubmission",
3093
+ )(function* (request) {
3094
+ const operation = "ledger read abort intent";
3095
+
3096
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AbortIntentRequest))(
3097
+ request,
3098
+ ).pipe(Effect.mapError(internalFailure(operation)));
3099
+
3100
+ const recordId = submissionAbortRecordId(validated.submissionId);
3101
+
3102
+ const rows = yield* sql<Record<string, unknown>>`
3103
+ SELECT
3104
+ submission.submission_id,
3105
+ abort.submission_id AS abort_submission_id,
3106
+ abort.author,
3107
+ abort.reason,
3108
+ abort.requested_at,
3109
+ canonical.record_id AS canonical_record_id
3110
+ FROM effect_agent_submissions AS submission
3111
+ LEFT JOIN effect_agent_abort_intents AS abort
3112
+ ON abort.submission_id = submission.submission_id
3113
+ LEFT JOIN effect_agent_canonical_records AS canonical
3114
+ ON canonical.thread_id = submission.thread_id
3115
+ AND canonical.record_id = ${recordId}
3116
+ WHERE submission.submission_id = ${validated.submissionId}
3117
+ `.pipe(Effect.mapError(sqlFailure(operation)));
3118
+
3119
+ const decoded = yield* decodeRows(
3120
+ Schema.Array(AbortIntentLookupRow),
3121
+ "effect_agent_abort_intents",
3122
+ validated.submissionId,
3123
+ rows,
3124
+ ).pipe(Effect.mapError(internalFailure(operation)));
3125
+
3126
+ if (decoded.length === 0) {
3127
+ return yield* LedgerError.make({
3128
+ operation,
3129
+ message: `Unknown submission ${validated.submissionId}.`,
3130
+ });
3131
+ }
3132
+ if (decoded.length !== 1) {
3133
+ return yield* corruptionFailure(
3134
+ operation,
3135
+ "effect_agent_abort_intents",
3136
+ validated.submissionId,
3137
+ "An abort intent lookup returned more than one row.",
3138
+ );
3139
+ }
3140
+ const row = decoded[0];
3141
+
3142
+ if (row.abort_submission_id === null) return undefined;
3143
+
3144
+ return yield* decodeAbortIntent({
3145
+ submissionId: row.abort_submission_id,
3146
+ author: row.author,
3147
+ reason: row.reason,
3148
+ requestedAt: row.requested_at,
3149
+ ...(row.canonical_record_id === null ? {} : { canonicalRecordId: row.canonical_record_id }),
3150
+ }).pipe(
3151
+ Effect.mapError((error) =>
3152
+ corruptionFailure(
3153
+ operation,
3154
+ "effect_agent_abort_intents",
3155
+ validated.submissionId,
3156
+ error.message,
3157
+ ),
3158
+ ),
3159
+ );
3160
+ });
2556
3161
 
2557
3162
  const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
2558
3163
  "SqliteSubmissionLedger.loadRecoverySnapshot",
2559
3164
  )(function* (request: RecoverySnapshotRequest) {
2560
3165
  const operation = "ledger load recovery snapshot";
3166
+
2561
3167
  const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RecoverySnapshotRequest))(
2562
3168
  request,
2563
3169
  ).pipe(Effect.mapError(internalFailure(operation)));
2564
- return yield* sql
2565
- .withTransaction(
3170
+
3171
+ return yield* journal
3172
+ .withReadTransaction(operation)(
2566
3173
  Effect.gen(function* () {
2567
3174
  const submissionRow = yield* requireSubmission(operation, validated.submissionId);
2568
3175
  const submission = yield* decodeSubmissionSnapshot(operation, submissionRow);
2569
3176
 
2570
3177
  let ownership: OwnershipSnapshot | undefined;
2571
3178
  const ownershipRow = yield* readOwnership(operation, validated.submissionId);
3179
+
2572
3180
  if (Option.isSome(ownershipRow)) {
2573
3181
  ownership = yield* decodeOwnershipSnapshot({
2574
3182
  attemptId: ownershipRow.value.attempt_id,
@@ -2579,6 +3187,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2579
3187
  }
2580
3188
 
2581
3189
  let inputApplied: InputAppliedMarker | undefined;
3190
+
2582
3191
  if (
2583
3192
  submissionRow.input_applied_record_id !== null &&
2584
3193
  submissionRow.input_applied_sequence !== null
@@ -2591,6 +3200,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2591
3200
 
2592
3201
  let reservation: SettlementReservationSnapshot | undefined;
2593
3202
  const reservationRow = yield* readReservation(operation, validated.submissionId);
3203
+
2594
3204
  if (Option.isSome(reservationRow)) {
2595
3205
  const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(
2596
3206
  Effect.mapError((error) =>
@@ -2602,9 +3212,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2602
3212
  ),
2603
3213
  ),
2604
3214
  );
3215
+
2605
3216
  const settlementId = yield* Schema.decodeUnknownEffect(
2606
3217
  SettlementReservationSnapshot.fields.settlementId,
2607
3218
  )(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
3219
+
2608
3220
  reservation = SettlementReservationSnapshot.make({
2609
3221
  settlementId,
2610
3222
  outcome: reservationRow.value.outcome,
@@ -2616,6 +3228,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2616
3228
 
2617
3229
  let abortIntent: AbortIntent | undefined;
2618
3230
  const abortRow = yield* readAbortIntent(operation, validated.submissionId);
3231
+
2619
3232
  if (Option.isSome(abortRow)) {
2620
3233
  abortIntent = yield* abortIntentFromRow(
2621
3234
  operation,
@@ -2633,11 +3246,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2633
3246
  WHERE joined_host_submission_id = ${validated.submissionId}
2634
3247
  ORDER BY queue_sequence ASC
2635
3248
  `.pipe(Effect.mapError(sqlFailure(operation)));
3249
+
2636
3250
  const joinSubmissions = yield* decodeSubmissionRows(
2637
3251
  operation,
2638
3252
  validated.submissionId,
2639
3253
  joinRows,
2640
3254
  );
3255
+
2641
3256
  const joins = yield* Effect.forEach(joinSubmissions, (row) =>
2642
3257
  decodeJoinSnapshot({
2643
3258
  submissionId: row.submission_id,
@@ -2647,6 +3262,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2647
3262
  );
2648
3263
 
2649
3264
  let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
3265
+
2650
3266
  if (submissionRow.joined_host_submission_id !== null) {
2651
3267
  hostSubmissionId = yield* decodeSubmissionId(
2652
3268
  submissionRow.joined_host_submission_id,
@@ -2654,6 +3270,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2654
3270
  }
2655
3271
 
2656
3272
  let suspension: SuspensionSnapshot | undefined;
3273
+
2657
3274
  if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
2658
3275
  const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(
2659
3276
  Effect.mapError((error) =>
@@ -2665,6 +3282,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2665
3282
  ),
2666
3283
  ),
2667
3284
  );
3285
+
2668
3286
  suspension = yield* decodeSuspensionSnapshot({
2669
3287
  reason,
2670
3288
  suspendedAt: submissionRow.suspended_at,
@@ -2681,11 +3299,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2681
3299
  }
2682
3300
 
2683
3301
  const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
3302
+
2684
3303
  const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
2685
3304
  approvalIntentFromRow(operation, row),
2686
3305
  );
2687
3306
 
2688
3307
  const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
3308
+
2689
3309
  const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
2690
3310
  unknownResolutionIntentFromRow(operation, row),
2691
3311
  );
@@ -2699,18 +3319,23 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2699
3319
  WHERE parent_submission_id = ${validated.submissionId}
2700
3320
  ORDER BY parent_tool_call_id ASC
2701
3321
  `.pipe(Effect.mapError(sqlFailure(operation)));
3322
+
2702
3323
  const decodedChildReservations = yield* decodeChildReservationRows(
2703
3324
  operation,
2704
3325
  validated.submissionId,
2705
3326
  childReservationRows,
2706
3327
  );
3328
+
2707
3329
  const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
2708
3330
  childReservationSnapshotFromRow(operation, row),
2709
3331
  );
3332
+
2710
3333
  const childAttachments: Array<ChildAttachmentSnapshot> = [];
3334
+
2711
3335
  for (const row of decodedChildReservations) {
2712
3336
  if (row.child_submission_id === null) continue;
2713
3337
  const child = yield* readSubmission(operation, row.child_submission_id);
3338
+
2714
3339
  if (Option.isNone(child)) continue;
2715
3340
  childAttachments.push(
2716
3341
  yield* decodeChildAttachmentSnapshot({
@@ -2725,6 +3350,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2725
3350
  }
2726
3351
 
2727
3352
  let parentLinkage: ParentLinkage | undefined;
3353
+
2728
3354
  if (
2729
3355
  submissionRow.parent_submission_id !== null &&
2730
3356
  submissionRow.parent_tool_call_id !== null
@@ -2752,7 +3378,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2752
3378
  });
2753
3379
  }),
2754
3380
  )
2755
- .pipe(Effect.catchTag("SqlError", (error) => Effect.fail(sqlFailure(operation)(error))));
3381
+ .pipe(
3382
+ Effect.catchTag("SqliteStorageError", (error) =>
3383
+ Effect.fail(internalFailure(operation)(error)),
3384
+ ),
3385
+ );
2756
3386
  });
2757
3387
 
2758
3388
  return Context.make(
@@ -2784,6 +3414,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2784
3414
  releaseChildBudget,
2785
3415
  scanNonterminal,
2786
3416
  loadRecoverySnapshot,
3417
+ readAbortIntent: readAbortIntentForSubmission,
2787
3418
  }),
2788
3419
  );
2789
3420
  });
@@ -2801,18 +3432,22 @@ export const submissionLedgerLayer: Layer.Layer<
2801
3432
 
2802
3433
  /**
2803
3434
  * A composition-root convenience Layer for the durable Submission Ledger. Point it at the
2804
- * same database file as the ConversationStore so claims fence the same producer epochs.
3435
+ * same database file as the ThreadStore so claims fence the same producer epochs.
2805
3436
  */
2806
3437
  export const ledgerLayer = (
2807
3438
  options: SqliteStorageOptions,
2808
3439
  ): Layer.Layer<SubmissionLedger, SqliteStorageInitializationError> =>
2809
- submissionLedgerLayer.pipe(
2810
- Layer.provide(
2811
- Layer.mergeAll(
2812
- storageConfigLayer(options),
2813
- storageFailpointLayer(options),
2814
- SqliteClient.layer({ filename: options.filename }),
2815
- NodeCrypto.layer,
3440
+ Layer.unwrap(
3441
+ Effect.map(SqliteStorageConfig, (config) =>
3442
+ submissionLedgerLayer.pipe(
3443
+ Layer.provide(
3444
+ Layer.mergeAll(
3445
+ Layer.succeed(SqliteStorageConfig)(config),
3446
+ storageFailpointLayer(options),
3447
+ SqliteClient.layer({ filename: options.filename }),
3448
+ NodeCrypto.layer,
3449
+ ),
3450
+ ),
2816
3451
  ),
2817
3452
  ),
2818
- );
3453
+ ).pipe(Layer.provide(storageConfigLayer(options)));