@effect-agent/storage-sqlite 0.1.0-beta.12 → 0.1.0-beta.120

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 (63) 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-DVWeaWLm.d.mts +86 -0
  14. package/dist/SqliteStorageError.d.mts +2 -0
  15. package/dist/SqliteStorageError.mjs +150 -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 +14 -0
  21. package/dist/SqliteStorageFailpointTesting.mjs +19 -0
  22. package/dist/SqliteStorageFailpointTesting.mjs.map +1 -0
  23. package/dist/SqliteStorageVersion-BBmy73KE.d.mts +11 -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 +1866 -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 +464 -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-BmgwzT68.mjs +365 -0
  39. package/dist/migrations-BmgwzT68.mjs.map +1 -0
  40. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  41. package/dist/sqlite-journal-DE4MN8uP.mjs +1106 -0
  42. package/dist/sqlite-journal-DE4MN8uP.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} +10 -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} +1105 -240
  53. package/src/SqliteSubscriptionStore.ts +55 -0
  54. package/src/SqliteThreadStore.ts +1004 -0
  55. package/src/index.ts +10 -6
  56. package/src/internal/message-delivery-schema.ts +24 -0
  57. package/src/internal/migrations.ts +373 -0
  58. package/src/internal/recovery-checkpoint-schema.ts +17 -0
  59. package/src/internal/sqlite-journal.ts +1792 -0
  60. package/dist/index.mjs.map +0 -1
  61. package/src/migrations.ts +0 -275
  62. package/src/sqlite-conversation-store.ts +0 -841
  63. package/src/sqlite-journal.ts +0 -1086
@@ -1,18 +1,40 @@
1
+ import { NodeCrypto } from "@effect/platform-node";
2
+ import { SqliteClient } from "@effect/sql-sqlite-node";
3
+ import { Clock, Context, Crypto, DateTime, Effect, Layer, Option, Schema, Stream } from "effect";
4
+ import { EMPTY_TAIL_DIGEST } from "effect-agent/digest";
5
+ import { InputMessage } from "effect-agent/messaging";
6
+ import {
7
+ ApprovalDecision,
8
+ CanonicalSequence,
9
+ DefinitionDigests,
10
+ Digest,
11
+ PersistedJson,
12
+ WorkerAdmission,
13
+ ProducerEpoch,
14
+ RecordEnvelope,
15
+ SettlementOutcome,
16
+ } from "effect-agent/records";
1
17
  import {
2
18
  AbortCommand,
19
+ WorkerStopCommand,
20
+ WorkerLedgerState,
21
+ workerTerminalFromRecord,
3
22
  AbortIntent,
23
+ AbortIntentRequest,
4
24
  AdmissionAdmitted,
5
25
  AdmissionConflict,
26
+ AdmissionFence,
27
+ AdmissionGroup,
28
+ AdmissionPolicyError,
6
29
  AdmissionNotAdmitted,
7
30
  AdmissionRequest,
31
+ SubmissionAdmissionFence,
8
32
  AdmissionResult,
9
33
  ApprovalConflict,
10
- ApprovalDecision,
11
34
  ApprovalDecisionCommand,
12
35
  ApprovalDecisionIntent,
13
36
  AttachChildToReservationRequest,
14
37
  BeginChildBudgetReleaseRequest,
15
- CanonicalSequence,
16
38
  ChildAttachmentSnapshot,
17
39
  ChildBudgetReservationRequest,
18
40
  ChildBudgetReservationSnapshot,
@@ -22,9 +44,6 @@ import {
22
44
  Claim,
23
45
  ClaimJoiningRequest,
24
46
  ClaimRequest,
25
- DefinitionDigests,
26
- Digest,
27
- EMPTY_TAIL_DIGEST,
28
47
  InputAppliedMarker,
29
48
  JoinSnapshot,
30
49
  JoinedToHost,
@@ -39,10 +58,7 @@ import {
39
58
  OwnershipRenewal,
40
59
  OwnershipSnapshot,
41
60
  ParentLinkage,
42
- PersistedJson,
43
- ProducerEpoch,
44
61
  QueueSequence,
45
- RecordEnvelope,
46
62
  RecoverySnapshot,
47
63
  RecoverySnapshotRequest,
48
64
  ReleaseChildBudgetRequest,
@@ -54,14 +70,15 @@ import {
54
70
  Settlement,
55
71
  SettlementConflict,
56
72
  SettlementFinalization,
57
- SettlementOutcome,
58
73
  SettlementReservation,
59
74
  SubmissionLedger,
60
75
  SubmissionLookup,
61
76
  SubmissionLookupByKey,
62
77
  SubmissionSnapshot,
78
+ SubmissionWorkItem,
63
79
  SubmissionState,
64
80
  SettlementReservationSnapshot,
81
+ settlementFailureFromRecord,
65
82
  SuspendRequest,
66
83
  SuspensionReason,
67
84
  SuspensionSnapshot,
@@ -72,29 +89,26 @@ import {
72
89
  submissionAbortRecordId,
73
90
  type ChildSettledOutcome,
74
91
  type SuspensionOutcome,
75
- } from "@effect-agent/session";
76
- import { NodeCrypto } from "@effect/platform-node";
77
- import { SqliteClient } from "@effect/sql-sqlite-node";
78
- import { Clock, Context, Crypto, DateTime, Effect, Layer, Option, Schema, Stream } from "effect";
92
+ } from "effect-agent/submission-ledger";
79
93
  import * as SqlClientService from "effect/unstable/sql/SqlClient";
80
94
  import type { SqlError } from "effect/unstable/sql/SqlError";
81
95
 
96
+ import { decodeRows, initializeSqliteJournal } from "./internal/sqlite-journal.ts";
97
+ import { SqliteStorageConfig } from "./SqliteStorageConfig.ts";
82
98
  import {
83
99
  SqliteLedgerError,
84
100
  SqliteStorageCorruptionError,
85
101
  SqliteStorageError,
86
102
  SqliteWriteContention,
87
103
  type SqliteStorageFailpointLocation,
88
- } from "./errors.ts";
104
+ } from "./SqliteStorageError.ts";
105
+ import { SqliteStorageFailpoint } from "./SqliteStorageFailpoint.ts";
89
106
  import {
90
107
  storageConfigLayer,
91
108
  storageFailpointLayer,
92
109
  type SqliteStorageInitializationError,
93
110
  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";
111
+ } from "./SqliteThreadStore.ts";
98
112
 
99
113
  type SubmissionId = SubmissionSnapshot["submissionId"];
100
114
 
@@ -104,10 +118,15 @@ const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
104
118
 
105
119
  const SCAN_PAGE_SIZE = 256;
106
120
  const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
121
+ const RESUME_IMMEDIATELY: SuspensionOutcome = "resume-immediately";
122
+ const SUSPENDED: SuspensionOutcome = "suspended";
123
+ const NOT_WAITING: ChildSettledOutcome = "not-waiting";
124
+ const STILL_WAITING: ChildSettledOutcome = "still-waiting";
125
+ const WOKEN: ChildSettledOutcome = "woken";
107
126
 
108
127
  class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
109
128
  submission_id: BoundedIdentifier,
110
- conversation_id: BoundedIdentifier,
129
+ thread_id: BoundedIdentifier,
111
130
  queue_sequence: QueueSequence,
112
131
  principal: BoundedIdentifier,
113
132
  idempotency_key: BoundedIdentifier,
@@ -130,6 +149,10 @@ class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
130
149
  unknown_tool_call_ids_json: Schema.NullOr(BoundedStoredText),
131
150
  parent_submission_id: Schema.NullOr(BoundedIdentifier),
132
151
  parent_tool_call_id: Schema.NullOr(BoundedIdentifier),
152
+ admission_group: Schema.NullOr(AdmissionGroup),
153
+ admission_fence_json: Schema.NullOr(BoundedStoredText),
154
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
155
+ message_admission_json: Schema.NullOr(BoundedStoredText),
133
156
  }) {}
134
157
 
135
158
  class ChildReservationRow extends Schema.Class<ChildReservationRow>("ChildReservationRow")({
@@ -196,13 +219,22 @@ class MaxQueueSequenceRow extends Schema.Class<MaxQueueSequenceRow>("MaxQueueSeq
196
219
  max_queue_sequence: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
197
220
  }) {}
198
221
 
222
+ class AbortIntentLookupRow extends Schema.Class<AbortIntentLookupRow>("AbortIntentLookupRow")({
223
+ submission_id: BoundedIdentifier,
224
+ abort_submission_id: Schema.NullOr(BoundedIdentifier),
225
+ author: Schema.NullOr(BoundedIdentifier),
226
+ reason: Schema.NullOr(BoundedStoredText),
227
+ requested_at: Schema.NullOr(BoundedTimestamp),
228
+ canonical_record_id: Schema.NullOr(BoundedIdentifier),
229
+ }) {}
230
+
199
231
  class CanonicalRecordIdRow extends Schema.Class<CanonicalRecordIdRow>("CanonicalRecordIdRow")({
200
232
  record_id: BoundedIdentifier,
201
233
  }) {}
202
234
 
203
235
  const SUBMISSION_COLUMNS = `
204
236
  submission_id,
205
- conversation_id,
237
+ thread_id,
206
238
  queue_sequence,
207
239
  principal,
208
240
  idempotency_key,
@@ -224,7 +256,11 @@ const SUBMISSION_COLUMNS = `
224
256
  unknown_reason,
225
257
  unknown_tool_call_ids_json,
226
258
  parent_submission_id,
227
- parent_tool_call_id
259
+ parent_tool_call_id,
260
+ admission_group,
261
+ admission_fence_json,
262
+ worker_admission_json,
263
+ message_admission_json
228
264
  `;
229
265
 
230
266
  const CHILD_RESERVATION_COLUMNS = `
@@ -241,7 +277,7 @@ const CHILD_RESERVATION_COLUMNS = `
241
277
  released_at
242
278
  `;
243
279
 
244
- /** The branded ToolCallId schema, reached through the session port so no core import is needed. */
280
+ /** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
245
281
  const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
246
282
  const ToolCallIdList = Schema.Array(ToolCallIdSchema);
247
283
 
@@ -271,10 +307,18 @@ const decodeSuspensionSnapshot = Schema.decodeUnknownEffect(SuspensionSnapshot);
271
307
  const decodeApprovalDecisionIntent = Schema.decodeUnknownEffect(ApprovalDecisionIntent);
272
308
  const decodeUnknownResolutionIntent = Schema.decodeUnknownEffect(UnknownResolutionIntent);
273
309
  const decodeParentLinkage = Schema.decodeUnknownEffect(ParentLinkage);
310
+
274
311
  const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
275
312
  ChildBudgetReservationSnapshot,
276
313
  );
314
+
277
315
  const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
316
+ const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
317
+ const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
318
+
319
+ const isSqliteTransactionFailure = Schema.is(
320
+ Schema.Union([SqliteStorageError, SqliteWriteContention]),
321
+ );
278
322
 
279
323
  /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
280
324
  const internalFailure =
@@ -298,6 +342,7 @@ const sqlFailure =
298
342
  operation,
299
343
  message: error.message,
300
344
  });
345
+
301
346
  return internalFailure(operation)(internal);
302
347
  };
303
348
 
@@ -309,7 +354,8 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
309
354
  const failpoint = yield* SqliteStorageFailpoint;
310
355
  const sql = yield* SqlClientService.SqlClient;
311
356
  const crypto = yield* Crypto.Crypto;
312
- const journal = yield* initializeSqliteJournal(sql, failpoint.hit, config.busyTimeout);
357
+ const admissionFence = yield* SubmissionAdmissionFence;
358
+ const journal = yield* initializeSqliteJournal();
313
359
 
314
360
  const hitFailpoint = (
315
361
  location: SqliteStorageFailpointLocation,
@@ -327,6 +373,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
327
373
  A,
328
374
  E extends
329
375
  | AdmissionConflict
376
+ | AdmissionPolicyError
330
377
  | ApprovalConflict
331
378
  | ChildReservationConflict
332
379
  | JoinedToHost
@@ -342,9 +389,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
342
389
  .withWriteTransaction(operation)(effect)
343
390
  .pipe(
344
391
  Effect.mapError((error) =>
345
- error instanceof SqliteStorageError || error instanceof SqliteWriteContention
346
- ? internalFailure(operation)(error)
347
- : error,
392
+ isSqliteTransactionFailure(error) ? internalFailure(operation)(error) : error,
348
393
  ),
349
394
  );
350
395
 
@@ -381,7 +426,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
381
426
  FROM effect_agent_submissions
382
427
  WHERE submission_id = ${submissionId}
383
428
  `.pipe(Effect.mapError(sqlFailure(operation)));
429
+
384
430
  const decoded = yield* decodeSubmissionRows(operation, submissionId, rows);
431
+
385
432
  if (decoded.length > 1) {
386
433
  return yield* corruptionFailure(
387
434
  operation,
@@ -390,6 +437,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
390
437
  "A submission primary key returned more than one row.",
391
438
  );
392
439
  }
440
+
393
441
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
394
442
  });
395
443
 
@@ -398,12 +446,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
398
446
  submissionId: string,
399
447
  ): Effect.fn.Return<SubmissionRow, LedgerError> {
400
448
  const submission = yield* readSubmission(operation, submissionId);
449
+
401
450
  if (Option.isNone(submission)) {
402
451
  return yield* LedgerError.make({
403
452
  operation,
404
453
  message: `Unknown submission ${submissionId}.`,
405
454
  });
406
455
  }
456
+
407
457
  return submission.value;
408
458
  });
409
459
 
@@ -422,12 +472,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
422
472
  FROM effect_agent_submission_ownership
423
473
  WHERE submission_id = ${submissionId}
424
474
  `.pipe(Effect.mapError(sqlFailure(operation)));
475
+
425
476
  const decoded = yield* decodeRows(
426
477
  Schema.Array(OwnershipRow),
427
478
  "effect_agent_submission_ownership",
428
479
  submissionId,
429
480
  rows,
430
481
  ).pipe(Effect.mapError(internalFailure(operation)));
482
+
431
483
  if (decoded.length > 1) {
432
484
  return yield* corruptionFailure(
433
485
  operation,
@@ -436,23 +488,25 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
436
488
  "An ownership primary key returned more than one row.",
437
489
  );
438
490
  }
491
+
439
492
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
440
493
  });
441
494
 
442
- const conversationEpoch = Effect.fn("SqliteSubmissionLedger.conversationEpoch")(function* (
495
+ const threadEpoch = Effect.fn("SqliteSubmissionLedger.threadEpoch")(function* (
443
496
  operation: string,
444
- conversationId: string,
497
+ threadId: string,
445
498
  ): Effect.fn.Return<ProducerEpoch, LedgerError> {
446
- const conversations = yield* journal
447
- .getConversation(conversationId)
499
+ const threads = yield* journal
500
+ .getThread(threadId)
448
501
  .pipe(Effect.mapError(internalFailure(operation)));
449
- return conversations.length === 0 ? EPOCH_ZERO : conversations[0].producer_epoch;
502
+
503
+ return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
450
504
  });
451
505
 
452
506
  /**
453
507
  * Verify inside the surrounding write transaction that the presented token still owns the
454
508
  * Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
455
- * Conversation's current producer epoch (DUR-006).
509
+ * Thread's current producer epoch (DUR-006).
456
510
  */
457
511
  const requireOwnership = Effect.fn("SqliteSubmissionLedger.requireOwnership")(function* (
458
512
  operation: string,
@@ -460,13 +514,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
460
514
  ownershipToken: string,
461
515
  ): Effect.fn.Return<OwnershipRow, OwnershipLost | LedgerError> {
462
516
  const ownership = yield* readOwnership(operation, submission.submission_id);
463
- if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
464
- const actualEpoch = yield* conversationEpoch(operation, submission.conversation_id);
465
- const submissionId = yield* Schema.decodeUnknownEffect(
466
- SubmissionSnapshot.fields.submissionId,
467
- )(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
517
+ const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
518
+
519
+ if (
520
+ Option.isNone(ownership) ||
521
+ ownership.value.ownership_token !== ownershipToken ||
522
+ ownership.value.producer_epoch !== actualEpoch
523
+ ) {
524
+ const submissionId = yield* Schema.decodeEffect(SubmissionSnapshot.fields.submissionId)(
525
+ submission.submission_id,
526
+ ).pipe(Effect.mapError(internalFailure(operation)));
527
+
468
528
  return yield* OwnershipLost.make({ submissionId, actualEpoch });
469
529
  }
530
+
470
531
  return ownership.value;
471
532
  });
472
533
 
@@ -485,6 +546,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
485
546
  ),
486
547
  ),
487
548
  );
549
+
488
550
  const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
489
551
  Effect.mapError((error) =>
490
552
  corruptionFailure(
@@ -495,6 +557,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
495
557
  ),
496
558
  ),
497
559
  );
560
+
498
561
  if ((row.parent_submission_id === null) !== (row.parent_tool_call_id === null)) {
499
562
  return yield* corruptionFailure(
500
563
  operation,
@@ -503,9 +566,10 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
503
566
  "A parent linkage must record both the parent Submission and the parent Tool Call.",
504
567
  );
505
568
  }
569
+
506
570
  return yield* decodeSubmissionSnapshotUnknown({
507
571
  submissionId: row.submission_id,
508
- conversationId: row.conversation_id,
572
+ threadId: row.thread_id,
509
573
  queueSequence: row.queue_sequence,
510
574
  principal: row.principal,
511
575
  idempotencyKey: row.idempotency_key,
@@ -517,6 +581,28 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
517
581
  receiptId: row.receipt_id,
518
582
  state: row.state,
519
583
  createdAt: row.created_at,
584
+ ...(row.admission_group === null ? {} : { admissionGroup: row.admission_group }),
585
+ ...(row.worker_admission_json === null
586
+ ? {}
587
+ : {
588
+ workerAdmission: yield* parseStoredJsonText(row.worker_admission_json).pipe(
589
+ Effect.mapError(internalFailure(operation)),
590
+ ),
591
+ }),
592
+ ...(row.message_admission_json === null
593
+ ? {}
594
+ : {
595
+ messageAdmission: yield* parseStoredJsonText(row.message_admission_json).pipe(
596
+ Effect.mapError(internalFailure(operation)),
597
+ ),
598
+ }),
599
+ ...(row.admission_fence_json === null
600
+ ? {}
601
+ : {
602
+ admissionFence: yield* parseStoredJsonText(row.admission_fence_json).pipe(
603
+ Effect.mapError(internalFailure(operation)),
604
+ ),
605
+ }),
520
606
  ...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
521
607
  ...(row.ready_at === null ? {} : { readyAt: row.ready_at }),
522
608
  ...(row.parent_submission_id === null || row.parent_tool_call_id === null
@@ -557,12 +643,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
557
643
  FROM effect_agent_settlement_reservations
558
644
  WHERE submission_id = ${submissionId}
559
645
  `.pipe(Effect.mapError(sqlFailure(operation)));
646
+
560
647
  const decoded = yield* decodeRows(
561
648
  Schema.Array(ReservationRow),
562
649
  "effect_agent_settlement_reservations",
563
650
  submissionId,
564
651
  rows,
565
652
  ).pipe(Effect.mapError(internalFailure(operation)));
653
+
566
654
  if (decoded.length > 1) {
567
655
  return yield* corruptionFailure(
568
656
  operation,
@@ -571,6 +659,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
571
659
  "A settlement reservation primary key returned more than one row.",
572
660
  );
573
661
  }
662
+
574
663
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
575
664
  });
576
665
 
@@ -588,12 +677,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
588
677
  FROM effect_agent_abort_intents
589
678
  WHERE submission_id = ${submissionId}
590
679
  `.pipe(Effect.mapError(sqlFailure(operation)));
680
+
591
681
  const decoded = yield* decodeRows(
592
682
  Schema.Array(AbortIntentRow),
593
683
  "effect_agent_abort_intents",
594
684
  submissionId,
595
685
  rows,
596
686
  ).pipe(Effect.mapError(internalFailure(operation)));
687
+
597
688
  if (decoded.length > 1) {
598
689
  return yield* corruptionFailure(
599
690
  operation,
@@ -602,6 +693,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
602
693
  "An abort intent primary key returned more than one row.",
603
694
  );
604
695
  }
696
+
605
697
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
606
698
  });
607
699
 
@@ -622,7 +714,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
622
714
  FROM effect_agent_child_reservations
623
715
  WHERE reservation_id = ${reservationId}
624
716
  `.pipe(Effect.mapError(sqlFailure(operation)));
717
+
625
718
  const decoded = yield* decodeChildReservationRows(operation, reservationId, rows);
719
+
626
720
  if (decoded.length > 1) {
627
721
  return yield* corruptionFailure(
628
722
  operation,
@@ -631,6 +725,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
631
725
  "A child reservation primary key returned more than one row.",
632
726
  );
633
727
  }
728
+
634
729
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
635
730
  });
636
731
 
@@ -647,11 +742,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
647
742
  WHERE parent_submission_id = ${parentSubmissionId}
648
743
  AND parent_tool_call_id = ${parentToolCallId}
649
744
  `.pipe(Effect.mapError(sqlFailure(operation)));
745
+
650
746
  const decoded = yield* decodeChildReservationRows(
651
747
  operation,
652
748
  `${parentSubmissionId}/${parentToolCallId}`,
653
749
  rows,
654
750
  );
751
+
655
752
  if (decoded.length > 1) {
656
753
  return yield* corruptionFailure(
657
754
  operation,
@@ -660,6 +757,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
660
757
  "A parent Tool Call returned more than one child reservation.",
661
758
  );
662
759
  }
760
+
663
761
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
664
762
  });
665
763
 
@@ -676,13 +774,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
676
774
  row.reservation_id,
677
775
  error.message,
678
776
  );
777
+
679
778
  const allocation = yield* parseStoredJsonText(row.allocation_json).pipe(
680
779
  Effect.mapError(rowFailure),
681
780
  );
781
+
682
782
  const accounting =
683
783
  row.accounting_json === null
684
784
  ? undefined
685
785
  : yield* parseStoredJsonText(row.accounting_json).pipe(Effect.mapError(rowFailure));
786
+
686
787
  return yield* decodeChildReservationSnapshotUnknown({
687
788
  reservationId: row.reservation_id,
688
789
  parentSubmissionId: row.parent_submission_id,
@@ -715,6 +816,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
715
816
  WHERE submission_id = ${submissionId}
716
817
  ORDER BY tool_call_id ASC
717
818
  `.pipe(Effect.mapError(sqlFailure(operation)));
819
+
718
820
  return yield* decodeRows(
719
821
  Schema.Array(ApprovalDecisionRow),
720
822
  "effect_agent_approval_decisions",
@@ -766,6 +868,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
766
868
  WHERE submission_id = ${submissionId}
767
869
  ORDER BY tool_call_id ASC
768
870
  `.pipe(Effect.mapError(sqlFailure(operation)));
871
+
769
872
  return yield* decodeRows(
770
873
  Schema.Array(UnknownResolutionRow),
771
874
  "effect_agent_unknown_resolutions",
@@ -791,6 +894,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
791
894
  ),
792
895
  ),
793
896
  );
897
+
794
898
  return yield* decodeUnknownResolutionIntent({
795
899
  submissionId: row.submission_id,
796
900
  toolCallId: row.tool_call_id,
@@ -817,6 +921,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
817
921
  submission: SubmissionRow,
818
922
  ): Effect.fn.Return<ReadonlyArray<typeof ToolCallIdSchema.Type>, LedgerError> {
819
923
  if (submission.unknown_tool_call_ids_json === null) return [];
924
+
820
925
  return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(
821
926
  Effect.mapError((error) =>
822
927
  corruptionFailure(
@@ -838,22 +943,25 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
838
943
  const canonicalAbortRecordId = Effect.fn("SqliteSubmissionLedger.canonicalAbortRecordId")(
839
944
  function* (
840
945
  operation: string,
841
- conversationId: string,
946
+ threadId: string,
842
947
  submissionId: SubmissionId,
843
948
  ): Effect.fn.Return<string | undefined, LedgerError> {
844
949
  const recordId = submissionAbortRecordId(submissionId);
950
+
845
951
  const rows = yield* sql<Record<string, unknown>>`
846
952
  SELECT record_id
847
953
  FROM effect_agent_canonical_records
848
- WHERE conversation_id = ${conversationId}
954
+ WHERE thread_id = ${threadId}
849
955
  AND record_id = ${recordId}
850
956
  `.pipe(Effect.mapError(sqlFailure(operation)));
957
+
851
958
  const decoded = yield* decodeRows(
852
959
  Schema.Array(CanonicalRecordIdRow),
853
960
  "effect_agent_canonical_records",
854
- `${conversationId}/${recordId}`,
961
+ `${threadId}/${recordId}`,
855
962
  rows,
856
963
  ).pipe(Effect.mapError(internalFailure(operation)));
964
+
857
965
  return decoded.length === 0 ? undefined : recordId;
858
966
  },
859
967
  );
@@ -866,9 +974,10 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
866
974
  ): Effect.fn.Return<AbortIntent, LedgerError> {
867
975
  const canonicalRecordId = yield* canonicalAbortRecordId(
868
976
  operation,
869
- submission.conversation_id,
977
+ submission.thread_id,
870
978
  submissionId,
871
979
  );
980
+
872
981
  return yield* decodeAbortIntent({
873
982
  submissionId: row.submission_id,
874
983
  author: row.author,
@@ -892,30 +1001,73 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
892
1001
  const admit: SubmissionLedger["Service"]["admit"] = Effect.fn("SqliteSubmissionLedger.admit")(
893
1002
  function* (request: AdmissionRequest) {
894
1003
  const operation = "ledger admit";
895
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AdmissionRequest))(
896
- request,
897
- ).pipe(Effect.mapError(internalFailure(operation)));
1004
+
1005
+ const validated = yield* Schema.decodeEffect(Schema.toType(AdmissionRequest))(request).pipe(
1006
+ Effect.mapError(internalFailure(operation)),
1007
+ );
1008
+
898
1009
  const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(
899
1010
  Effect.mapError(internalFailure(operation)),
900
1011
  );
1012
+
1013
+ const workerAdmissionJson =
1014
+ validated.workerAdmission === undefined
1015
+ ? null
1016
+ : yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
1017
+ validated.workerAdmission,
1018
+ ).pipe(Effect.mapError(internalFailure(operation)));
1019
+
1020
+ if (
1021
+ workerAdmissionJson !== null &&
1022
+ new TextEncoder().encode(workerAdmissionJson).byteLength > 16 * 1024 * 1024
1023
+ ) {
1024
+ return yield* LedgerError.make({
1025
+ operation,
1026
+ message: "Worker admission metadata exceeds the stored value bound",
1027
+ });
1028
+ }
1029
+
1030
+ const messageAdmissionJson =
1031
+ validated.messageAdmission === undefined
1032
+ ? null
1033
+ : yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
1034
+ validated.messageAdmission,
1035
+ ).pipe(Effect.mapError(internalFailure(operation)));
1036
+
1037
+ if (
1038
+ messageAdmissionJson !== null &&
1039
+ new TextEncoder().encode(messageAdmissionJson).byteLength > 16 * 1024 * 1024
1040
+ ) {
1041
+ return yield* LedgerError.make({
1042
+ operation,
1043
+ message: "Message admission metadata exceeds the stored value bound",
1044
+ });
1045
+ }
1046
+
901
1047
  const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
902
1048
  Effect.mapError(internalFailure(operation)),
903
1049
  );
1050
+
904
1051
  const mintedSubmissionId = yield* mintIdentifier("submission", operation);
905
1052
  const mintedReceiptId = yield* mintIdentifier("receipt", operation);
1053
+
906
1054
  yield* hitFailpoint("ledger:admit:before", operation);
1055
+
907
1056
  const result = yield* inWriteTransaction(
908
1057
  operation,
909
1058
  Effect.gen(function* () {
910
- const keyRowKey = `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`;
1059
+ const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
1060
+
911
1061
  const existingRows = yield* sql<Record<string, unknown>>`
912
1062
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
913
1063
  FROM effect_agent_submissions
914
- WHERE conversation_id = ${validated.conversationId}
1064
+ WHERE thread_id = ${validated.threadId}
915
1065
  AND principal = ${validated.principal}
916
1066
  AND idempotency_key = ${validated.idempotencyKey}
917
1067
  `.pipe(Effect.mapError(sqlFailure(operation)));
1068
+
918
1069
  const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
1070
+
919
1071
  if (existing.length > 1) {
920
1072
  return yield* corruptionFailure(
921
1073
  operation,
@@ -933,15 +1085,61 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
933
1085
  existing[0].parent_tool_call_id === null
934
1086
  : existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId &&
935
1087
  existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
1088
+
936
1089
  if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
937
1090
  return yield* AdmissionConflict.make({
938
- conversationId: validated.conversationId,
1091
+ threadId: validated.threadId,
939
1092
  principal: validated.principal,
940
1093
  idempotencyKey: validated.idempotencyKey,
941
1094
  existingInputDigest: existing[0].input_digest,
942
1095
  attemptedInputDigest: validated.inputDigest,
943
1096
  });
944
1097
  }
1098
+
1099
+ const retainedWorkerAdmission =
1100
+ existing[0].worker_admission_json === null
1101
+ ? undefined
1102
+ : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1103
+ existing[0].worker_admission_json,
1104
+ ).pipe(Effect.mapError(internalFailure(operation)));
1105
+
1106
+ const retainedInputMessage =
1107
+ existing[0].message_admission_json === null
1108
+ ? undefined
1109
+ : yield* Schema.decodeEffect(Schema.fromJsonString(InputMessage))(
1110
+ existing[0].message_admission_json,
1111
+ ).pipe(Effect.mapError(internalFailure(operation)));
1112
+
1113
+ const retainedFence =
1114
+ existing[0].admission_fence_json === null
1115
+ ? undefined
1116
+ : yield* Schema.decodeEffect(Schema.fromJsonString(AdmissionFence))(
1117
+ existing[0].admission_fence_json,
1118
+ ).pipe(Effect.mapError(internalFailure(operation)));
1119
+
1120
+ if (
1121
+ (existing[0].admission_group ?? undefined) !== validated.admissionGroup ||
1122
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission))(
1123
+ retainedWorkerAdmission,
1124
+ validated.workerAdmission,
1125
+ ) ||
1126
+ !Schema.toEquivalence(Schema.optional(InputMessage))(
1127
+ retainedInputMessage,
1128
+ validated.messageAdmission,
1129
+ ) ||
1130
+ !Schema.toEquivalence(Schema.optional(AdmissionFence))(
1131
+ retainedFence,
1132
+ validated.admissionFence,
1133
+ )
1134
+ )
1135
+ return yield* AdmissionConflict.make({
1136
+ threadId: validated.threadId,
1137
+ principal: validated.principal,
1138
+ idempotencyKey: validated.idempotencyKey,
1139
+ existingInputDigest: existing[0].input_digest,
1140
+ attemptedInputDigest: validated.inputDigest,
1141
+ });
1142
+
945
1143
  return yield* decodeAdmissionResult({
946
1144
  submissionId: existing[0].submission_id,
947
1145
  receiptId: existing[0].receipt_id,
@@ -951,26 +1149,86 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
951
1149
  }).pipe(Effect.mapError(internalFailure(operation)));
952
1150
  }
953
1151
 
1152
+ const stopped =
1153
+ yield* sql`SELECT thread_id FROM effect_agent_worker_stops WHERE thread_id = ${validated.threadId}`.pipe(
1154
+ Effect.mapError(sqlFailure(operation)),
1155
+ );
1156
+
1157
+ if (stopped.length > 0)
1158
+ return yield* AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" });
1159
+
1160
+ // The first accepted input fixes ordinary/worker lane identity atomically with admission.
1161
+ // Canonical origin materialization can lag admission; a log scan cannot fence that race.
1162
+ const firstRows = yield* sql<Record<string, unknown>>`
1163
+ SELECT worker_admission_json FROM effect_agent_submissions
1164
+ WHERE thread_id=${validated.threadId} ORDER BY queue_sequence LIMIT 1
1165
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1166
+
1167
+ const first = yield* Schema.decodeUnknownEffect(
1168
+ Schema.Array(
1169
+ Schema.Struct({
1170
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
1171
+ }),
1172
+ ),
1173
+ )(firstRows).pipe(Effect.mapError(internalFailure(operation)));
1174
+
1175
+ if (first[0] !== undefined) {
1176
+ const previous =
1177
+ first[0].worker_admission_json === null
1178
+ ? undefined
1179
+ : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1180
+ first[0].worker_admission_json,
1181
+ ).pipe(Effect.mapError(internalFailure(operation)));
1182
+
1183
+ if (
1184
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
1185
+ previous?.origin,
1186
+ validated.workerAdmission?.origin,
1187
+ )
1188
+ )
1189
+ return yield* AdmissionPolicyError.make({
1190
+ reason: "refused",
1191
+ code: "worker-origin-conflict",
1192
+ });
1193
+ }
1194
+
1195
+ yield* admissionFence.check(validated);
1196
+ if (validated.admissionGroup !== undefined) {
1197
+ const occupied = yield* sql<Record<string, unknown>>`
1198
+ SELECT submission_id FROM effect_agent_submissions
1199
+ WHERE thread_id=${validated.threadId} AND admission_group=${validated.admissionGroup} AND state<>'settled' LIMIT 1
1200
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1201
+
1202
+ if (occupied.length > 0)
1203
+ return yield* AdmissionPolicyError.make({
1204
+ reason: "occupied",
1205
+ code: "admission-group",
1206
+ });
1207
+ }
1208
+
954
1209
  const maxRows = yield* sql<Record<string, unknown>>`
955
1210
  SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
956
1211
  FROM effect_agent_submissions
957
- WHERE conversation_id = ${validated.conversationId}
1212
+ WHERE thread_id = ${validated.threadId}
958
1213
  `.pipe(Effect.mapError(sqlFailure(operation)));
1214
+
959
1215
  const decodedMax = yield* decodeRows(
960
1216
  Schema.Array(MaxQueueSequenceRow),
961
1217
  "effect_agent_submissions",
962
- validated.conversationId,
1218
+ validated.threadId,
963
1219
  maxRows,
964
1220
  ).pipe(Effect.mapError(internalFailure(operation)));
1221
+
965
1222
  const queueSequence = yield* decodeQueueSequence(
966
1223
  (decodedMax[0]?.max_queue_sequence ?? 0) + 1,
967
1224
  ).pipe(Effect.mapError(internalFailure(operation)));
1225
+
968
1226
  const now = yield* currentInstant;
969
1227
 
970
1228
  yield* sql`
971
1229
  INSERT INTO effect_agent_submissions (
972
1230
  submission_id,
973
- conversation_id,
1231
+ thread_id,
974
1232
  queue_sequence,
975
1233
  principal,
976
1234
  idempotency_key,
@@ -983,10 +1241,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
983
1241
  state,
984
1242
  created_at,
985
1243
  parent_submission_id,
986
- parent_tool_call_id
1244
+ parent_tool_call_id,
1245
+ admission_group,
1246
+ admission_fence_json,
1247
+ worker_admission_json,
1248
+ message_admission_json
987
1249
  ) VALUES (
988
1250
  ${mintedSubmissionId},
989
- ${validated.conversationId},
1251
+ ${validated.threadId},
990
1252
  ${queueSequence},
991
1253
  ${validated.principal},
992
1254
  ${validated.idempotencyKey},
@@ -999,7 +1261,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
999
1261
  'admitted',
1000
1262
  ${now.iso},
1001
1263
  ${validated.parentLinkage?.parentSubmissionId ?? null},
1002
- ${validated.parentLinkage?.parentToolCallId ?? null}
1264
+ ${validated.parentLinkage?.parentToolCallId ?? null},
1265
+ ${validated.admissionGroup ?? null},
1266
+ ${validated.admissionFence === undefined ? null : JSON.stringify(validated.admissionFence)},
1267
+ ${workerAdmissionJson},
1268
+ ${messageAdmissionJson}
1003
1269
  )
1004
1270
  `.pipe(Effect.mapError(sqlFailure(operation)));
1005
1271
 
@@ -1012,7 +1278,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1012
1278
  }).pipe(Effect.mapError(internalFailure(operation)));
1013
1279
  }),
1014
1280
  );
1281
+
1015
1282
  yield* hitFailpoint("ledger:admit:after", operation);
1283
+
1016
1284
  return result;
1017
1285
  },
1018
1286
  );
@@ -1021,16 +1289,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1021
1289
  "SqliteSubmissionLedger.markReady",
1022
1290
  )(function* (request: MarkReadyRequest) {
1023
1291
  const operation = "ledger mark ready";
1024
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkReadyRequest))(
1025
- request,
1026
- ).pipe(Effect.mapError(internalFailure(operation)));
1292
+
1293
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkReadyRequest))(request).pipe(
1294
+ Effect.mapError(internalFailure(operation)),
1295
+ );
1296
+
1027
1297
  yield* hitFailpoint("ledger:mark-ready:before", operation);
1028
1298
  yield* inWriteTransaction(
1029
1299
  operation,
1030
1300
  Effect.gen(function* () {
1031
1301
  const submission = yield* requireSubmission(operation, validated.submissionId);
1302
+
1032
1303
  if (submission.state !== "admitted") return;
1033
1304
  const now = yield* currentInstant;
1305
+
1034
1306
  yield* sql`
1035
1307
  UPDATE effect_agent_submissions
1036
1308
  SET state = 'ready', ready_at = ${now.iso}
@@ -1044,35 +1316,43 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1044
1316
  const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn("SqliteSubmissionLedger.lookup")(
1045
1317
  function* (request: SubmissionLookup) {
1046
1318
  const operation = "ledger lookup";
1047
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookup))(
1048
- request,
1049
- ).pipe(Effect.mapError(internalFailure(operation)));
1319
+
1320
+ const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookup))(request).pipe(
1321
+ Effect.mapError(internalFailure(operation)),
1322
+ );
1323
+
1050
1324
  if (validated._tag === "SubmissionLookupById") {
1051
1325
  const row = yield* readSubmission(operation, validated.submissionId);
1326
+
1052
1327
  if (Option.isNone(row)) return Option.none();
1328
+
1053
1329
  return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
1054
1330
  }
1331
+
1055
1332
  const rows = yield* sql<Record<string, unknown>>`
1056
1333
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1057
1334
  FROM effect_agent_submissions
1058
- WHERE conversation_id = ${validated.conversationId}
1335
+ WHERE thread_id = ${validated.threadId}
1059
1336
  AND principal = ${validated.principal}
1060
1337
  AND idempotency_key = ${validated.idempotencyKey}
1061
1338
  `.pipe(Effect.mapError(sqlFailure(operation)));
1339
+
1062
1340
  const decoded = yield* decodeSubmissionRows(
1063
1341
  operation,
1064
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1342
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1065
1343
  rows,
1066
1344
  );
1345
+
1067
1346
  if (decoded.length > 1) {
1068
1347
  return yield* corruptionFailure(
1069
1348
  operation,
1070
1349
  "effect_agent_submissions",
1071
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1350
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1072
1351
  "An admission idempotency key returned more than one row.",
1073
1352
  );
1074
1353
  }
1075
1354
  if (decoded.length === 0) return Option.none();
1355
+
1076
1356
  return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
1077
1357
  },
1078
1358
  );
@@ -1084,30 +1364,35 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1084
1364
  "SqliteSubmissionLedger.resolveAdmission",
1085
1365
  )(function* (request: SubmissionLookupByKey) {
1086
1366
  const operation = "ledger resolve admission";
1087
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookupByKey))(
1367
+
1368
+ const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookupByKey))(
1088
1369
  request,
1089
1370
  ).pipe(Effect.mapError(internalFailure(operation)));
1371
+
1090
1372
  const rows = yield* sql<Record<string, unknown>>`
1091
1373
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1092
1374
  FROM effect_agent_submissions
1093
- WHERE conversation_id = ${validated.conversationId}
1375
+ WHERE thread_id = ${validated.threadId}
1094
1376
  AND principal = ${validated.principal}
1095
1377
  AND idempotency_key = ${validated.idempotencyKey}
1096
1378
  `.pipe(Effect.mapError(sqlFailure(operation)));
1379
+
1097
1380
  const decoded = yield* decodeSubmissionRows(
1098
1381
  operation,
1099
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1382
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1100
1383
  rows,
1101
1384
  );
1385
+
1102
1386
  if (decoded.length > 1) {
1103
1387
  return yield* corruptionFailure(
1104
1388
  operation,
1105
1389
  "effect_agent_submissions",
1106
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1390
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1107
1391
  "An admission idempotency key returned more than one row.",
1108
1392
  );
1109
1393
  }
1110
1394
  if (decoded.length === 0) return AdmissionNotAdmitted.make();
1395
+
1111
1396
  return AdmissionAdmitted.make({
1112
1397
  submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
1113
1398
  });
@@ -1116,70 +1401,103 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1116
1401
  const claim: SubmissionLedger["Service"]["claim"] = Effect.fn("SqliteSubmissionLedger.claim")(
1117
1402
  function* (request: ClaimRequest) {
1118
1403
  const operation = "ledger claim";
1119
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimRequest))(
1120
- request,
1121
- ).pipe(Effect.mapError(internalFailure(operation)));
1404
+
1405
+ const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
1406
+ Effect.mapError(internalFailure(operation)),
1407
+ );
1408
+
1122
1409
  const attemptId = yield* mintIdentifier("attempt", operation);
1123
1410
  const ownershipToken = yield* mintIdentifier("owner", operation);
1411
+
1124
1412
  yield* hitFailpoint("ledger:claim:before", operation);
1413
+
1125
1414
  const claimed = yield* inWriteTransaction(
1126
1415
  operation,
1127
1416
  Effect.gen(function* () {
1417
+ const now = yield* currentInstant;
1418
+
1419
+ // Ownership belongs to the whole Thread, including unknown work skipped below.
1420
+ // Stored lease instants are normalized UTC strings, so the latest expiry covers
1421
+ // every live lease. This check and the epoch grant share one write transaction.
1422
+ const ownershipRows = yield* sql<Record<string, unknown>>`
1423
+ SELECT ownership.*
1424
+ FROM effect_agent_submission_ownership AS ownership
1425
+ JOIN effect_agent_submissions AS submission
1426
+ ON submission.submission_id = ownership.submission_id
1427
+ WHERE submission.thread_id = ${validated.threadId}
1428
+ ORDER BY ownership.lease_expires_at DESC
1429
+ LIMIT 1
1430
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1431
+
1432
+ const ownership = yield* decodeRows(
1433
+ Schema.Array(OwnershipRow),
1434
+ "effect_agent_submission_ownership",
1435
+ validated.threadId,
1436
+ ownershipRows,
1437
+ ).pipe(Effect.mapError(internalFailure(operation)));
1438
+
1439
+ if (ownership.length > 0) {
1440
+ const expiresAt = yield* timestampMillis(
1441
+ operation,
1442
+ ownership[0].submission_id,
1443
+ )(ownership[0].lease_expires_at);
1444
+
1445
+ if (expiresAt > now.millis) return Option.none<Claim>();
1446
+ }
1447
+
1128
1448
  const headRows = yield* sql<Record<string, unknown>>`
1129
1449
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1130
1450
  FROM effect_agent_submissions
1131
- WHERE conversation_id = ${validated.conversationId}
1451
+ WHERE thread_id = ${validated.threadId}
1132
1452
  AND state <> 'settled'
1453
+ AND (
1454
+ state <> 'unknown' OR EXISTS (
1455
+ SELECT 1 FROM effect_agent_abort_intents
1456
+ WHERE submission_id = effect_agent_submissions.submission_id
1457
+ )
1458
+ )
1133
1459
  ORDER BY queue_sequence ASC
1134
1460
  LIMIT 1
1135
1461
  `.pipe(Effect.mapError(sqlFailure(operation)));
1136
- const heads = yield* decodeSubmissionRows(operation, validated.conversationId, headRows);
1462
+
1463
+ const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
1464
+
1137
1465
  if (heads.length === 0) return Option.none<Claim>();
1138
1466
  const head = heads[0];
1139
1467
 
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).
1468
+ // Approval/delegation suspension and joined work retain their queue barrier.
1469
+ // Unknown work with an abort intent is selected for cleanup without Tool replay.
1143
1470
  if (
1144
1471
  head.state === "joining" ||
1145
1472
  head.state === "joined" ||
1146
1473
  head.state === "suspended" ||
1147
- head.state === "unknown"
1474
+ (head.state === "unknown" &&
1475
+ Option.isNone(yield* readAbortIntent(operation, head.submission_id)))
1148
1476
  ) {
1149
1477
  return Option.none<Claim>();
1150
1478
  }
1151
1479
 
1152
- const now = yield* currentInstant;
1153
- const ownership = yield* readOwnership(operation, head.submission_id);
1154
- if (Option.isSome(ownership)) {
1155
- const expiresAt = yield* timestampMillis(
1156
- operation,
1157
- head.submission_id,
1158
- )(ownership.value.lease_expires_at);
1159
- // A live lease blocks every new claim; expiry alone only revokes the liveness
1160
- // assumption — correctness stays with producer-epoch fencing (D5).
1161
- if (expiresAt > now.millis) return Option.none<Claim>();
1162
- }
1163
-
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
1480
+ // Bump the Thread's producer epoch atomically with the claim so every stale
1481
+ // Attempt is fenced out of canonical appends (DUR-006). A Thread that was
1166
1482
  // never materialized (crash between admission and materialization) is created here
1167
1483
  // so recovery can claim first and re-materialize idempotently at this epoch.
1168
- const conversations = yield* journal
1169
- .getConversation(head.conversation_id)
1484
+ const threads = yield* journal
1485
+ .getThread(head.thread_id)
1170
1486
  .pipe(Effect.mapError(internalFailure(operation)));
1487
+
1171
1488
  let producerEpoch: number;
1172
- if (conversations.length === 0) {
1489
+
1490
+ if (threads.length === 0) {
1173
1491
  producerEpoch = 1;
1174
1492
  yield* sql`
1175
- INSERT INTO effect_agent_conversations (
1176
- conversation_id,
1493
+ INSERT INTO effect_agent_threads (
1494
+ thread_id,
1177
1495
  created_at,
1178
1496
  tail_sequence,
1179
1497
  tail_digest,
1180
1498
  producer_epoch
1181
1499
  ) VALUES (
1182
- ${head.conversation_id},
1500
+ ${head.thread_id},
1183
1501
  ${now.iso},
1184
1502
  0,
1185
1503
  ${EMPTY_TAIL_DIGEST},
@@ -1187,15 +1505,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1187
1505
  )
1188
1506
  `.pipe(Effect.mapError(sqlFailure(operation)));
1189
1507
  } else {
1190
- producerEpoch = conversations[0].producer_epoch + 1;
1508
+ producerEpoch = threads[0].producer_epoch + 1;
1191
1509
  yield* sql`
1192
- UPDATE effect_agent_conversations
1510
+ UPDATE effect_agent_threads
1193
1511
  SET producer_epoch = ${producerEpoch}
1194
- WHERE conversation_id = ${head.conversation_id}
1512
+ WHERE thread_id = ${head.thread_id}
1195
1513
  `.pipe(Effect.mapError(sqlFailure(operation)));
1196
1514
  }
1197
1515
 
1198
1516
  const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1517
+
1199
1518
  yield* sql`
1200
1519
  INSERT INTO effect_agent_submission_ownership (
1201
1520
  submission_id,
@@ -1224,14 +1543,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1224
1543
  INSERT INTO effect_agent_attempts (
1225
1544
  attempt_id,
1226
1545
  submission_id,
1227
- conversation_id,
1546
+ thread_id,
1228
1547
  owner_producer_id,
1229
1548
  producer_epoch,
1230
1549
  claimed_at
1231
1550
  ) VALUES (
1232
1551
  ${attemptId},
1233
1552
  ${head.submission_id},
1234
- ${head.conversation_id},
1553
+ ${head.thread_id},
1235
1554
  ${validated.producerId},
1236
1555
  ${producerEpoch},
1237
1556
  ${now.iso}
@@ -1256,6 +1575,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1256
1575
  ),
1257
1576
  ),
1258
1577
  );
1578
+
1259
1579
  return Option.some(
1260
1580
  yield* decodeClaim({
1261
1581
  submissionId: head.submission_id,
@@ -1268,7 +1588,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1268
1588
  );
1269
1589
  }),
1270
1590
  );
1591
+
1271
1592
  yield* hitFailpoint("ledger:claim:after", operation);
1593
+
1272
1594
  return claimed;
1273
1595
  },
1274
1596
  );
@@ -1277,29 +1599,37 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1277
1599
  "SqliteSubmissionLedger.renewOwnership",
1278
1600
  )(function* (request: RenewOwnershipRequest) {
1279
1601
  const operation = "ledger renew ownership";
1280
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RenewOwnershipRequest))(
1602
+
1603
+ const validated = yield* Schema.decodeEffect(Schema.toType(RenewOwnershipRequest))(
1281
1604
  request,
1282
1605
  ).pipe(Effect.mapError(internalFailure(operation)));
1606
+
1283
1607
  yield* hitFailpoint("ledger:renew:before", operation);
1608
+
1284
1609
  const renewal = yield* inWriteTransaction(
1285
1610
  operation,
1286
1611
  Effect.gen(function* () {
1287
1612
  const submission = yield* requireSubmission(operation, validated.submissionId);
1613
+
1288
1614
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1289
1615
  const now = yield* currentInstant;
1290
1616
  const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1617
+
1291
1618
  yield* sql`
1292
1619
  UPDATE effect_agent_submission_ownership
1293
1620
  SET lease_expires_at = ${leaseExpiresAt}
1294
1621
  WHERE submission_id = ${validated.submissionId}
1295
1622
  `.pipe(Effect.mapError(sqlFailure(operation)));
1623
+
1296
1624
  return yield* decodeOwnershipRenewal({
1297
1625
  ownershipToken: validated.ownershipToken,
1298
1626
  leaseExpiresAt,
1299
1627
  }).pipe(Effect.mapError(internalFailure(operation)));
1300
1628
  }),
1301
1629
  );
1630
+
1302
1631
  yield* hitFailpoint("ledger:renew:after", operation);
1632
+
1303
1633
  return renewal;
1304
1634
  });
1305
1635
 
@@ -1307,14 +1637,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1307
1637
  "SqliteSubmissionLedger.releaseOwnership",
1308
1638
  )(function* (request: ReleaseOwnershipRequest) {
1309
1639
  const operation = "ledger release ownership";
1310
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseOwnershipRequest))(
1640
+
1641
+ const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseOwnershipRequest))(
1311
1642
  request,
1312
1643
  ).pipe(Effect.mapError(internalFailure(operation)));
1644
+
1313
1645
  yield* hitFailpoint("ledger:release:before", operation);
1314
1646
  yield* inWriteTransaction(
1315
1647
  operation,
1316
1648
  Effect.gen(function* () {
1317
1649
  const submission = yield* requireSubmission(operation, validated.submissionId);
1650
+
1318
1651
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1319
1652
  yield* sql`
1320
1653
  DELETE FROM effect_agent_submission_ownership
@@ -1336,14 +1669,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1336
1669
  "SqliteSubmissionLedger.markInputApplied",
1337
1670
  )(function* (request: MarkInputAppliedRequest) {
1338
1671
  const operation = "ledger mark input applied";
1339
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkInputAppliedRequest))(
1672
+
1673
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
1340
1674
  request,
1341
1675
  ).pipe(Effect.mapError(internalFailure(operation)));
1676
+
1342
1677
  yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1343
1678
  yield* inWriteTransaction(
1344
1679
  operation,
1345
1680
  Effect.gen(function* () {
1346
1681
  const submission = yield* requireSubmission(operation, validated.submissionId);
1682
+
1347
1683
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1348
1684
  if (submission.input_applied_record_id !== null) {
1349
1685
  if (
@@ -1352,6 +1688,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1352
1688
  ) {
1353
1689
  return;
1354
1690
  }
1691
+
1355
1692
  return yield* corruptionFailure(
1356
1693
  operation,
1357
1694
  "effect_agent_submissions",
@@ -1379,29 +1716,36 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1379
1716
  "SqliteSubmissionLedger.reserveSettlement",
1380
1717
  )(function* (request: SettlementReservation) {
1381
1718
  const operation = "ledger reserve settlement";
1382
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementReservation))(
1719
+
1720
+ const validated = yield* Schema.decodeEffect(Schema.toType(SettlementReservation))(
1383
1721
  request,
1384
1722
  ).pipe(Effect.mapError(internalFailure(operation)));
1723
+
1385
1724
  const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(
1386
1725
  Effect.mapError(internalFailure(operation)),
1387
1726
  );
1727
+
1388
1728
  yield* hitFailpoint("ledger:reserve-settlement:before", operation);
1729
+
1389
1730
  const reserved = yield* inWriteTransaction(
1390
1731
  operation,
1391
1732
  Effect.gen(function* () {
1392
1733
  const existing = yield* readReservation(operation, validated.submissionId);
1734
+
1393
1735
  if (Option.isSome(existing)) {
1394
1736
  const identical =
1395
1737
  existing.value.settlement_id === validated.settlementId &&
1396
1738
  existing.value.outcome === validated.outcome &&
1397
1739
  existing.value.record_digest === validated.recordDigest &&
1398
1740
  existing.value.record_json === recordJson;
1741
+
1399
1742
  if (!identical) {
1400
1743
  return yield* SettlementConflict.make({
1401
1744
  submissionId: validated.submissionId,
1402
1745
  existingOutcome: existing.value.outcome,
1403
1746
  });
1404
1747
  }
1748
+
1405
1749
  const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(
1406
1750
  Effect.mapError((error) =>
1407
1751
  corruptionFailure(
@@ -1412,6 +1756,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1412
1756
  ),
1413
1757
  ),
1414
1758
  );
1759
+
1415
1760
  return ReservedSettlement.make({
1416
1761
  submissionId: validated.submissionId,
1417
1762
  settlementId: validated.settlementId,
@@ -1423,6 +1768,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1423
1768
  }
1424
1769
 
1425
1770
  const submission = yield* requireSubmission(operation, validated.submissionId);
1771
+
1426
1772
  if (submission.state === "settled") {
1427
1773
  if (submission.settled_outcome === null) {
1428
1774
  return yield* corruptionFailure(
@@ -1432,6 +1778,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1432
1778
  "A settled Submission carries no terminal outcome.",
1433
1779
  );
1434
1780
  }
1781
+
1435
1782
  return yield* SettlementConflict.make({
1436
1783
  submissionId: validated.submissionId,
1437
1784
  existingOutcome: submission.settled_outcome,
@@ -1446,13 +1793,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1446
1793
  // ABORTED settlement (`terminalizing` is the same pass's crash replay). Every other
1447
1794
  // reservation stays fenced by the target lane's live ownership.
1448
1795
  let queuedAbortSettlement = false;
1796
+
1449
1797
  if (
1450
1798
  validated.outcome === "aborted" &&
1451
1799
  (submission.state === "ready" || submission.state === "terminalizing")
1452
1800
  ) {
1453
1801
  const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
1802
+
1454
1803
  if (Option.isSome(abortIntent)) {
1455
1804
  const ownership = yield* readOwnership(operation, validated.submissionId);
1805
+
1456
1806
  queuedAbortSettlement = Option.isNone(ownership);
1457
1807
  }
1458
1808
  }
@@ -1461,6 +1811,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1461
1811
  }
1462
1812
  }
1463
1813
  const now = yield* currentInstant;
1814
+
1464
1815
  yield* sql`
1465
1816
  INSERT INTO effect_agent_settlement_reservations (
1466
1817
  submission_id,
@@ -1485,6 +1836,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1485
1836
  SET state = 'terminalizing'
1486
1837
  WHERE submission_id = ${validated.submissionId}
1487
1838
  `.pipe(Effect.mapError(sqlFailure(operation)));
1839
+
1488
1840
  return ReservedSettlement.make({
1489
1841
  submissionId: validated.submissionId,
1490
1842
  settlementId: validated.settlementId,
@@ -1495,56 +1847,197 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1495
1847
  });
1496
1848
  }),
1497
1849
  );
1850
+
1498
1851
  yield* hitFailpoint("ledger:reserve-settlement:after", operation);
1852
+
1499
1853
  return reserved;
1500
1854
  });
1501
1855
 
1502
- const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
1503
- "SqliteSubmissionLedger.finalizeSettlement",
1504
- )(function* (request: SettlementFinalization) {
1856
+ const validateFinalization = Effect.fn("SqliteSubmissionLedger.validateFinalization")(function* (
1857
+ validated: SettlementFinalization,
1858
+ reservation: Option.Option<ReservationRow>,
1859
+ ) {
1505
1860
  const operation = "ledger finalize settlement";
1506
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementFinalization))(
1507
- request,
1508
- ).pipe(Effect.mapError(internalFailure(operation)));
1509
- yield* hitFailpoint("ledger:finalize-settlement:before", operation);
1510
- const settlement = yield* inWriteTransaction(
1511
- operation,
1512
- Effect.gen(function* () {
1513
- const reservation = yield* readReservation(operation, validated.submissionId);
1514
- if (Option.isNone(reservation)) {
1515
- return yield* LedgerError.make({
1516
- operation,
1517
- message: `No settlement reservation exists for submission ${validated.submissionId}.`,
1518
- });
1519
- }
1520
- if (reservation.value.settlement_id !== validated.settlementId) {
1521
- return yield* SettlementConflict.make({
1522
- submissionId: validated.submissionId,
1523
- existingOutcome: reservation.value.outcome,
1524
- });
1525
- }
1526
- const submission = yield* requireSubmission(operation, validated.submissionId);
1527
- if (submission.state === "settled") {
1528
- if (reservation.value.finalized_at === null) {
1529
- return yield* corruptionFailure(
1530
- operation,
1531
- "effect_agent_settlement_reservations",
1532
- validated.submissionId,
1533
- "A settled Submission's reservation carries no finalization timestamp.",
1861
+
1862
+ if (Option.isNone(reservation)) {
1863
+ return yield* LedgerError.make({
1864
+ operation,
1865
+ message: `No settlement reservation exists for submission ${validated.submissionId}.`,
1866
+ });
1867
+ }
1868
+ if (reservation.value.settlement_id !== validated.settlementId) {
1869
+ return yield* SettlementConflict.make({
1870
+ submissionId: validated.submissionId,
1871
+ existingOutcome: reservation.value.outcome,
1872
+ });
1873
+ }
1874
+
1875
+ const reservationRecord = yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(
1876
+ Effect.mapError((error) =>
1877
+ corruptionFailure(
1878
+ operation,
1879
+ "effect_agent_settlement_reservations",
1880
+ validated.submissionId,
1881
+ error.message,
1882
+ ),
1883
+ ),
1884
+ );
1885
+
1886
+ const settlementFailure = settlementFailureFromRecord(reservationRecord);
1887
+
1888
+ if ((reservation.value.outcome === "failed") !== (settlementFailure !== undefined)) {
1889
+ return yield* corruptionFailure(
1890
+ operation,
1891
+ "effect_agent_settlement_reservations",
1892
+ validated.submissionId,
1893
+ "The reserved outcome and canonical failure diagnostic disagree.",
1894
+ );
1895
+ }
1896
+
1897
+ return { reservation: reservation.value, reservationRecord, settlementFailure };
1898
+ });
1899
+
1900
+ const replayFinalization = Effect.fn("SqliteSubmissionLedger.replayFinalization")(function* (
1901
+ validated: SettlementFinalization,
1902
+ submission: SubmissionRow,
1903
+ { reservation, settlementFailure }: Effect.Success<ReturnType<typeof validateFinalization>>,
1904
+ ) {
1905
+ const operation = "ledger finalize settlement";
1906
+
1907
+ if (reservation.finalized_at === null) {
1908
+ return yield* corruptionFailure(
1909
+ operation,
1910
+ "effect_agent_settlement_reservations",
1911
+ validated.submissionId,
1912
+ "A settled Submission's reservation carries no finalization timestamp.",
1913
+ );
1914
+ }
1915
+
1916
+ return yield* decodeSettlement({
1917
+ submissionId: validated.submissionId,
1918
+ settlementId: validated.settlementId,
1919
+ receiptId: submission.receipt_id,
1920
+ outcome: reservation.outcome,
1921
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1922
+ settledAt: reservation.finalized_at,
1923
+ }).pipe(Effect.mapError(internalFailure(operation)));
1924
+ });
1925
+
1926
+ const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
1927
+ "SqliteSubmissionLedger.finalizeSettlement",
1928
+ )(function* (request: SettlementFinalization) {
1929
+ const operation = "ledger finalize settlement";
1930
+
1931
+ const validated = yield* Schema.decodeEffect(Schema.toType(SettlementFinalization))(
1932
+ request,
1933
+ ).pipe(Effect.mapError(internalFailure(operation)));
1934
+
1935
+ yield* hitFailpoint("ledger:finalize-settlement:before", operation);
1936
+
1937
+ // A single statement captures the settled row and its immutable reservation together.
1938
+ // A miss does not authorize finalization: the write path re-reads under its transaction.
1939
+ const replayRows = yield* sql<Record<string, unknown>>`
1940
+ SELECT submission.*, reservation.settlement_id, reservation.outcome,
1941
+ reservation.record_id, reservation.record_json, reservation.record_digest,
1942
+ reservation.reserved_at, reservation.finalized_at
1943
+ FROM effect_agent_submissions AS submission
1944
+ INNER JOIN effect_agent_settlement_reservations AS reservation
1945
+ ON reservation.submission_id = submission.submission_id
1946
+ WHERE submission.submission_id = ${validated.submissionId} AND submission.state = 'settled'
1947
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1948
+
1949
+ if (replayRows.length > 1) {
1950
+ return yield* corruptionFailure(
1951
+ operation,
1952
+ "effect_agent_submissions",
1953
+ validated.submissionId,
1954
+ "A submission primary key returned more than one row.",
1955
+ );
1956
+ }
1957
+ const replayRow = replayRows[0];
1958
+
1959
+ if (replayRow !== undefined) {
1960
+ const reservations = yield* decodeRows(
1961
+ Schema.Array(ReservationRow),
1962
+ "effect_agent_settlement_reservations",
1963
+ validated.submissionId,
1964
+ replayRows,
1965
+ ).pipe(Effect.mapError(internalFailure(operation)));
1966
+
1967
+ const state = yield* validateFinalization(validated, Option.fromUndefinedOr(reservations[0]));
1968
+
1969
+ const submission = yield* Schema.decodeUnknownEffect(SubmissionRow)(replayRow).pipe(
1970
+ Effect.mapError((error) =>
1971
+ corruptionFailure(
1972
+ operation,
1973
+ "effect_agent_submissions",
1974
+ validated.submissionId,
1975
+ error.message,
1976
+ ),
1977
+ ),
1978
+ );
1979
+
1980
+ const settlement = yield* replayFinalization(validated, submission, state);
1981
+
1982
+ yield* hitFailpoint("ledger:finalize-settlement:after", operation);
1983
+
1984
+ return settlement;
1985
+ }
1986
+
1987
+ const settlement = yield* inWriteTransaction(
1988
+ operation,
1989
+ Effect.gen(function* () {
1990
+ const state = yield* validateFinalization(
1991
+ validated,
1992
+ yield* readReservation(operation, validated.submissionId),
1993
+ );
1994
+
1995
+ const { reservation, reservationRecord, settlementFailure } = state;
1996
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1997
+
1998
+ if (submission.state === "settled")
1999
+ return yield* replayFinalization(validated, submission, state);
2000
+ const now = yield* currentInstant;
2001
+
2002
+ const terminal =
2003
+ submission.worker_admission_json === null
2004
+ ? undefined
2005
+ : workerTerminalFromRecord(
2006
+ yield* decodeSubmissionSnapshot(operation, submission),
2007
+ reservationRecord,
2008
+ );
2009
+
2010
+ if (terminal !== undefined) {
2011
+ // Admission and finalization serialize here. An accepted correction that this Run
2012
+ // has not applied vetoes its completion, including admission after RunCompleted.
2013
+ const pending =
2014
+ terminal === "completed"
2015
+ ? yield* sql`SELECT submission_id FROM effect_agent_submissions
2016
+ WHERE thread_id = ${submission.thread_id} AND queue_sequence > ${submission.queue_sequence}
2017
+ AND queue_sequence = (SELECT MAX(queue_sequence) FROM effect_agent_submissions WHERE thread_id = ${submission.thread_id})
2018
+ AND (joined_host_submission_id IS NULL OR joined_host_submission_id <> ${submission.submission_id}
2019
+ OR input_applied_record_id IS NULL) LIMIT 1`.pipe(
2020
+ Effect.mapError(sqlFailure(operation)),
2021
+ )
2022
+ : [];
2023
+
2024
+ if (pending.length === 0) {
2025
+ yield* sql`INSERT OR IGNORE INTO effect_agent_worker_stops (thread_id, terminal)
2026
+ VALUES (${submission.thread_id}, ${terminal})`.pipe(
2027
+ Effect.mapError(sqlFailure(operation)),
1534
2028
  );
2029
+ yield* sql`INSERT OR IGNORE INTO effect_agent_abort_intents (submission_id, author, reason, requested_at)
2030
+ SELECT submission_id, ${submission.principal}, ${`Worker assignment ${terminal}`}, ${now.iso}
2031
+ FROM effect_agent_submissions WHERE thread_id = ${submission.thread_id} AND state <> 'settled'
2032
+ AND submission_id <> ${submission.submission_id}
2033
+ AND (joined_host_submission_id IS NULL OR joined_host_submission_id <> ${submission.submission_id}
2034
+ OR input_applied_record_id IS NULL)`.pipe(Effect.mapError(sqlFailure(operation)));
1535
2035
  }
1536
- return yield* decodeSettlement({
1537
- submissionId: validated.submissionId,
1538
- settlementId: validated.settlementId,
1539
- receiptId: submission.receipt_id,
1540
- outcome: reservation.value.outcome,
1541
- settledAt: reservation.value.finalized_at,
1542
- }).pipe(Effect.mapError(internalFailure(operation)));
1543
2036
  }
1544
- const now = yield* currentInstant;
2037
+
1545
2038
  yield* sql`
1546
2039
  UPDATE effect_agent_submissions
1547
- SET state = 'settled', settled_outcome = ${reservation.value.outcome}
2040
+ SET state = 'settled', settled_outcome = ${reservation.outcome}
1548
2041
  WHERE submission_id = ${validated.submissionId}
1549
2042
  `.pipe(Effect.mapError(sqlFailure(operation)));
1550
2043
  yield* sql`
@@ -1556,31 +2049,117 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1556
2049
  DELETE FROM effect_agent_submission_ownership
1557
2050
  WHERE submission_id = ${validated.submissionId}
1558
2051
  `.pipe(Effect.mapError(sqlFailure(operation)));
2052
+
1559
2053
  return yield* decodeSettlement({
1560
2054
  submissionId: validated.submissionId,
1561
2055
  settlementId: validated.settlementId,
1562
2056
  receiptId: submission.receipt_id,
1563
- outcome: reservation.value.outcome,
2057
+ outcome: reservation.outcome,
2058
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1564
2059
  settledAt: now.iso,
1565
2060
  }).pipe(Effect.mapError(internalFailure(operation)));
1566
2061
  }),
1567
2062
  );
2063
+
1568
2064
  yield* hitFailpoint("ledger:finalize-settlement:after", operation);
2065
+
1569
2066
  return settlement;
1570
2067
  });
1571
2068
 
2069
+ const inspectWorker = Effect.fn("SqliteSubmissionLedger.inspectWorker")(function* (
2070
+ threadId: SubmissionSnapshot["threadId"],
2071
+ ) {
2072
+ const operation = "inspect worker";
2073
+
2074
+ return yield* sql
2075
+ .withTransaction(
2076
+ Effect.gen(function* () {
2077
+ const read = Effect.fnUntraced(function* (active: boolean) {
2078
+ const rows =
2079
+ yield* sql`SELECT ${sql.literal(SUBMISSION_COLUMNS)} FROM effect_agent_submissions
2080
+ WHERE thread_id = ${threadId} ${active ? sql`AND state <> 'settled'` : sql``}
2081
+ ORDER BY queue_sequence ${active ? sql`ASC` : sql`DESC`} LIMIT 1`.pipe(
2082
+ Effect.mapError(sqlFailure(operation)),
2083
+ );
2084
+
2085
+ const decoded = yield* decodeSubmissionRows(operation, threadId, rows);
2086
+
2087
+ return decoded[0] === undefined
2088
+ ? null
2089
+ : yield* decodeSubmissionSnapshot(operation, decoded[0]);
2090
+ });
2091
+
2092
+ const latest = yield* read(false);
2093
+ const active = yield* read(true);
2094
+
2095
+ const stops =
2096
+ yield* sql`SELECT terminal FROM effect_agent_worker_stops WHERE thread_id = ${threadId}`.pipe(
2097
+ Effect.mapError(sqlFailure(operation)),
2098
+ );
2099
+
2100
+ return yield* Schema.decodeUnknownEffect(Schema.toType(WorkerLedgerState))({
2101
+ latest,
2102
+ active,
2103
+ stopped: stops.length > 0,
2104
+ ...(stops[0] === undefined || stops[0].terminal === null
2105
+ ? {}
2106
+ : { terminal: stops[0].terminal }),
2107
+ }).pipe(Effect.mapError(internalFailure(operation)));
2108
+ }),
2109
+ )
2110
+ .pipe(Effect.catchTag("SqlError", (cause) => sqlFailure(operation)(cause)));
2111
+ });
2112
+
2113
+ const stopWorker = Effect.fn("SqliteSubmissionLedger.stopWorker")(function* (
2114
+ request: WorkerStopCommand,
2115
+ ) {
2116
+ const operation = "ledger stop worker";
2117
+
2118
+ const validated = yield* Schema.decodeEffect(WorkerStopCommand)(request).pipe(
2119
+ Effect.mapError(internalFailure(operation)),
2120
+ );
2121
+
2122
+ return yield* inWriteTransaction(
2123
+ operation,
2124
+ Effect.gen(function* () {
2125
+ const now = yield* currentInstant;
2126
+
2127
+ yield* sql`INSERT OR IGNORE INTO effect_agent_worker_stops (thread_id) VALUES (${validated.threadId})`.pipe(
2128
+ Effect.mapError(sqlFailure(operation)),
2129
+ );
2130
+ yield* sql`INSERT OR IGNORE INTO effect_agent_abort_intents (submission_id, author, reason, requested_at)
2131
+ SELECT submission_id, ${validated.author}, 'Worker owner stopped the worker', ${now.iso}
2132
+ FROM effect_agent_submissions WHERE thread_id = ${validated.threadId} AND state <> 'settled'`.pipe(
2133
+ Effect.mapError(sqlFailure(operation)),
2134
+ );
2135
+
2136
+ const rows = yield* sql`SELECT o.submission_id FROM effect_agent_submission_ownership o
2137
+ JOIN effect_agent_submissions s ON s.submission_id = o.submission_id
2138
+ WHERE s.thread_id = ${validated.threadId} AND s.state <> 'settled'`.pipe(
2139
+ Effect.mapError(sqlFailure(operation)),
2140
+ );
2141
+
2142
+ return rows.length;
2143
+ }),
2144
+ );
2145
+ });
2146
+
1572
2147
  const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fn(
1573
2148
  "SqliteSubmissionLedger.requestAbort",
1574
2149
  )(function* (request: AbortCommand) {
1575
2150
  const operation = "ledger request abort";
1576
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AbortCommand))(request).pipe(
2151
+
2152
+ const validated = yield* Schema.decodeEffect(Schema.toType(AbortCommand))(request).pipe(
1577
2153
  Effect.mapError(internalFailure(operation)),
1578
2154
  );
2155
+
1579
2156
  yield* hitFailpoint("ledger:request-abort:before", operation);
2157
+
1580
2158
  const intent = yield* inWriteTransaction(
1581
2159
  operation,
1582
2160
  Effect.gen(function* () {
1583
2161
  const submission = yield* requireSubmission(operation, validated.submissionId);
2162
+
1584
2163
  if (submission.state === "settled") {
1585
2164
  if (submission.settled_outcome === null) {
1586
2165
  return yield* corruptionFailure(
@@ -1590,6 +2169,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1590
2169
  "A settled Submission carries no terminal outcome.",
1591
2170
  );
1592
2171
  }
2172
+
1593
2173
  return yield* SettlementConflict.make({
1594
2174
  submissionId: validated.submissionId,
1595
2175
  existingOutcome: submission.settled_outcome,
@@ -1607,15 +2187,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1607
2187
  "A joined Submission carries no host linkage.",
1608
2188
  );
1609
2189
  }
2190
+
1610
2191
  const hostSubmissionId = yield* decodeSubmissionId(
1611
2192
  submission.joined_host_submission_id,
1612
2193
  ).pipe(Effect.mapError(internalFailure(operation)));
2194
+
1613
2195
  return yield* JoinedToHost.make({
1614
2196
  submissionId: validated.submissionId,
1615
2197
  hostSubmissionId,
1616
2198
  });
1617
2199
  }
1618
2200
  const existing = yield* readAbortIntent(operation, validated.submissionId);
2201
+
1619
2202
  if (Option.isSome(existing)) {
1620
2203
  return yield* abortIntentFromRow(
1621
2204
  operation,
@@ -1625,6 +2208,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1625
2208
  );
1626
2209
  }
1627
2210
  const now = yield* currentInstant;
2211
+
1628
2212
  yield* sql`
1629
2213
  INSERT INTO effect_agent_abort_intents (
1630
2214
  submission_id,
@@ -1638,11 +2222,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1638
2222
  ${now.iso}
1639
2223
  )
1640
2224
  `.pipe(Effect.mapError(sqlFailure(operation)));
2225
+
1641
2226
  const canonicalRecordId = yield* canonicalAbortRecordId(
1642
2227
  operation,
1643
- submission.conversation_id,
2228
+ submission.thread_id,
1644
2229
  validated.submissionId,
1645
2230
  );
2231
+
1646
2232
  return yield* decodeAbortIntent({
1647
2233
  submissionId: validated.submissionId,
1648
2234
  author: validated.author,
@@ -1652,7 +2238,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1652
2238
  }).pipe(Effect.mapError(internalFailure(operation)));
1653
2239
  }),
1654
2240
  );
2241
+
1655
2242
  yield* hitFailpoint("ledger:request-abort:after", operation);
2243
+
1656
2244
  return intent;
1657
2245
  });
1658
2246
 
@@ -1660,31 +2248,45 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1660
2248
  "SqliteSubmissionLedger.claimJoining",
1661
2249
  )(function* (request: ClaimJoiningRequest) {
1662
2250
  const operation = "ledger claim joining";
1663
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimJoiningRequest))(
1664
- request,
1665
- ).pipe(Effect.mapError(internalFailure(operation)));
2251
+
2252
+ const validated = yield* Schema.decodeEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(
2253
+ Effect.mapError(internalFailure(operation)),
2254
+ );
2255
+
1666
2256
  yield* hitFailpoint("ledger:claim-joining:before", operation);
2257
+
1667
2258
  const claims = yield* inWriteTransaction(
1668
2259
  operation,
1669
2260
  Effect.gen(function* () {
1670
2261
  const host = yield* requireSubmission(operation, validated.hostSubmissionId);
1671
- if (host.conversation_id !== validated.conversationId) {
2262
+
2263
+ if (host.thread_id !== validated.threadId) {
1672
2264
  return yield* LedgerError.make({
1673
2265
  operation,
1674
- message: `Host submission ${validated.hostSubmissionId} does not belong to conversation ${validated.conversationId}.`,
2266
+ message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
1675
2267
  });
1676
2268
  }
1677
2269
  // The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
1678
2270
  yield* requireOwnership(operation, host, validated.ownershipToken);
2271
+
2272
+ const stopped =
2273
+ yield* sql`SELECT thread_id FROM effect_agent_worker_stops WHERE thread_id = ${validated.threadId}`.pipe(
2274
+ Effect.mapError(sqlFailure(operation)),
2275
+ );
2276
+
2277
+ if (stopped.length > 0) return [];
2278
+
1679
2279
  const laterRows = yield* sql<Record<string, unknown>>`
1680
2280
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1681
2281
  FROM effect_agent_submissions
1682
- WHERE conversation_id = ${validated.conversationId}
2282
+ WHERE thread_id = ${validated.threadId}
1683
2283
  AND queue_sequence > ${host.queue_sequence}
1684
2284
  ORDER BY queue_sequence ASC
1685
2285
  `.pipe(Effect.mapError(sqlFailure(operation)));
1686
- const later = yield* decodeSubmissionRows(operation, validated.conversationId, laterRows);
2286
+
2287
+ const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
1687
2288
  const claimed: Array<JoiningClaim> = [];
2289
+
1688
2290
  for (const row of later) {
1689
2291
  if (claimed.length >= validated.maxCount) break;
1690
2292
  // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
@@ -1707,6 +2309,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1707
2309
  SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
1708
2310
  WHERE submission_id = ${row.submission_id}
1709
2311
  `.pipe(Effect.mapError(sqlFailure(operation)));
2312
+
1710
2313
  const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
1711
2314
  Effect.mapError((error) =>
1712
2315
  corruptionFailure(
@@ -1717,6 +2320,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1717
2320
  ),
1718
2321
  ),
1719
2322
  );
2323
+
1720
2324
  claimed.push(
1721
2325
  yield* decodeJoiningClaim({
1722
2326
  submissionId: row.submission_id,
@@ -1725,10 +2329,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1725
2329
  }).pipe(Effect.mapError(internalFailure(operation))),
1726
2330
  );
1727
2331
  }
1728
- return claimed as ReadonlyArray<JoiningClaim>;
2332
+
2333
+ return claimed;
1729
2334
  }),
1730
2335
  );
2336
+
1731
2337
  yield* hitFailpoint("ledger:claim-joining:after", operation);
2338
+
1732
2339
  return claims;
1733
2340
  });
1734
2341
 
@@ -1736,14 +2343,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1736
2343
  "SqliteSubmissionLedger.markJoined",
1737
2344
  )(function* (request: MarkJoinedRequest) {
1738
2345
  const operation = "ledger mark joined";
1739
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkJoinedRequest))(
1740
- request,
1741
- ).pipe(Effect.mapError(internalFailure(operation)));
2346
+
2347
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkJoinedRequest))(request).pipe(
2348
+ Effect.mapError(internalFailure(operation)),
2349
+ );
2350
+
1742
2351
  yield* hitFailpoint("ledger:mark-joined:before", operation);
1743
2352
  yield* inWriteTransaction(
1744
2353
  operation,
1745
2354
  Effect.gen(function* () {
1746
2355
  const submission = yield* requireSubmission(operation, validated.submissionId);
2356
+
1747
2357
  if (submission.joined_host_submission_id === null) {
1748
2358
  return yield* LedgerError.make({
1749
2359
  operation,
@@ -1751,6 +2361,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1751
2361
  });
1752
2362
  }
1753
2363
  const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
2364
+
1754
2365
  // The lane is host-owned: the presented token must own the HOST's ownership period,
1755
2366
  // which also lets a later host Attempt repair a lost marker from history (DUR-016).
1756
2367
  yield* requireOwnership(operation, host, validated.ownershipToken);
@@ -1761,6 +2372,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1761
2372
  ) {
1762
2373
  return;
1763
2374
  }
2375
+
1764
2376
  return yield* corruptionFailure(
1765
2377
  operation,
1766
2378
  "effect_agent_submissions",
@@ -1791,14 +2403,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1791
2403
  "SqliteSubmissionLedger.revertJoining",
1792
2404
  )(function* (request: RevertJoiningRequest) {
1793
2405
  const operation = "ledger revert joining";
1794
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RevertJoiningRequest))(
1795
- request,
1796
- ).pipe(Effect.mapError(internalFailure(operation)));
2406
+
2407
+ const validated = yield* Schema.decodeEffect(Schema.toType(RevertJoiningRequest))(request).pipe(
2408
+ Effect.mapError(internalFailure(operation)),
2409
+ );
2410
+
1797
2411
  yield* hitFailpoint("ledger:revert-joining:before", operation);
1798
2412
  yield* inWriteTransaction(
1799
2413
  operation,
1800
2414
  Effect.gen(function* () {
1801
2415
  const submission = yield* requireSubmission(operation, validated.submissionId);
2416
+
1802
2417
  // Idempotent and recovery-only: only a still-`joining` Submission reverts; an
1803
2418
  // already-joined (or already-reverted) Submission is a no-op (DUR-016).
1804
2419
  if (submission.state !== "joining") return;
@@ -1816,17 +2431,22 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1816
2431
  "SqliteSubmissionLedger.suspend",
1817
2432
  )(function* (request: SuspendRequest) {
1818
2433
  const operation = "ledger suspend";
1819
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SuspendRequest))(
1820
- request,
1821
- ).pipe(Effect.mapError(internalFailure(operation)));
2434
+
2435
+ const validated = yield* Schema.decodeEffect(Schema.toType(SuspendRequest))(request).pipe(
2436
+ Effect.mapError(internalFailure(operation)),
2437
+ );
2438
+
1822
2439
  const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
1823
2440
  Effect.mapError(internalFailure(operation)),
1824
2441
  );
2442
+
1825
2443
  yield* hitFailpoint("ledger:suspend:before", operation);
2444
+
1826
2445
  const outcome = yield* inWriteTransaction(
1827
2446
  operation,
1828
2447
  Effect.gen(function* () {
1829
2448
  const submission = yield* requireSubmission(operation, validated.submissionId);
2449
+
1830
2450
  if (submission.state === "settled") {
1831
2451
  if (submission.settled_outcome === null) {
1832
2452
  return yield* corruptionFailure(
@@ -1836,6 +2456,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1836
2456
  "A settled Submission carries no terminal outcome.",
1837
2457
  );
1838
2458
  }
2459
+
1839
2460
  return yield* SettlementConflict.make({
1840
2461
  submissionId: validated.submissionId,
1841
2462
  existingOutcome: submission.settled_outcome,
@@ -1844,6 +2465,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1844
2465
  // An exact terminal outcome is already reserved (DUR-011); suspension would
1845
2466
  // contradict it, so the reservation wins.
1846
2467
  const reservation = yield* readReservation(operation, validated.submissionId);
2468
+
1847
2469
  if (Option.isSome(reservation)) {
1848
2470
  return yield* SettlementConflict.make({
1849
2471
  submissionId: validated.submissionId,
@@ -1857,23 +2479,27 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1857
2479
  if (validated.reason._tag === "ApprovalPending") {
1858
2480
  const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
1859
2481
  const decided = new Set(decisions.map((row) => row.tool_call_id));
2482
+
1860
2483
  if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
1861
- return "resume-immediately" as SuspensionOutcome;
2484
+ return RESUME_IMMEDIATELY;
1862
2485
  }
1863
2486
  } else {
1864
2487
  let allSettled = true;
2488
+
1865
2489
  for (const child of validated.reason.children) {
1866
2490
  const childRow = yield* readSubmission(operation, child.childSubmissionId);
2491
+
1867
2492
  if (Option.isNone(childRow) || childRow.value.state !== "settled") {
1868
2493
  allSettled = false;
1869
2494
  break;
1870
2495
  }
1871
2496
  }
1872
2497
  if (allSettled) {
1873
- return "resume-immediately" as SuspensionOutcome;
2498
+ return RESUME_IMMEDIATELY;
1874
2499
  }
1875
2500
  }
1876
2501
  const now = yield* currentInstant;
2502
+
1877
2503
  yield* sql`
1878
2504
  UPDATE effect_agent_submissions
1879
2505
  SET
@@ -1888,10 +2514,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1888
2514
  DELETE FROM effect_agent_submission_ownership
1889
2515
  WHERE submission_id = ${validated.submissionId}
1890
2516
  `.pipe(Effect.mapError(sqlFailure(operation)));
1891
- return "suspended" as SuspensionOutcome;
2517
+
2518
+ return SUSPENDED;
1892
2519
  }),
1893
2520
  );
2521
+
1894
2522
  yield* hitFailpoint("ledger:suspend:after", operation);
2523
+
1895
2524
  return outcome;
1896
2525
  });
1897
2526
 
@@ -1904,6 +2533,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1904
2533
  const wakeSuspendedIfCovered = Effect.fn("SqliteSubmissionLedger.wakeSuspendedIfCovered")(
1905
2534
  function* (operation: string, submission: SubmissionRow): Effect.fn.Return<void, LedgerError> {
1906
2535
  if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
2536
+
1907
2537
  const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
1908
2538
  submission.suspended_reason_json,
1909
2539
  ).pipe(
@@ -1916,9 +2546,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1916
2546
  ),
1917
2547
  ),
1918
2548
  );
2549
+
1919
2550
  if (reason._tag !== "ApprovalPending") return;
1920
2551
  const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
1921
2552
  const decided = new Set(decisions.map((row) => row.tool_call_id));
2553
+
1922
2554
  if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
1923
2555
  yield* sql`
1924
2556
  UPDATE effect_agent_submissions
@@ -1935,14 +2567,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1935
2567
  "SqliteSubmissionLedger.recordApprovalDecision",
1936
2568
  )(function* (command: ApprovalDecisionCommand) {
1937
2569
  const operation = "ledger record approval decision";
1938
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ApprovalDecisionCommand))(
2570
+
2571
+ const validated = yield* Schema.decodeEffect(Schema.toType(ApprovalDecisionCommand))(
1939
2572
  command,
1940
2573
  ).pipe(Effect.mapError(internalFailure(operation)));
2574
+
1941
2575
  yield* hitFailpoint("ledger:approval-decision:before", operation);
2576
+
1942
2577
  const intent = yield* inWriteTransaction(
1943
2578
  operation,
1944
2579
  Effect.gen(function* () {
1945
2580
  const submission = yield* requireSubmission(operation, validated.submissionId);
2581
+
1946
2582
  if (submission.state === "settled") {
1947
2583
  if (submission.settled_outcome === null) {
1948
2584
  return yield* corruptionFailure(
@@ -1952,6 +2588,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1952
2588
  "A settled Submission carries no terminal outcome.",
1953
2589
  );
1954
2590
  }
2591
+
1955
2592
  return yield* SettlementConflict.make({
1956
2593
  submissionId: validated.submissionId,
1957
2594
  existingOutcome: submission.settled_outcome,
@@ -1959,6 +2596,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1959
2596
  }
1960
2597
  const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
1961
2598
  const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
2599
+
1962
2600
  if (existing !== undefined) {
1963
2601
  // Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
1964
2602
  // the recorded intent unchanged; a divergent re-decision conflicts.
@@ -1969,9 +2607,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1969
2607
  existingDecision: existing.decision,
1970
2608
  });
1971
2609
  }
2610
+
1972
2611
  return yield* approvalIntentFromRow(operation, existing);
1973
2612
  }
1974
2613
  const now = yield* currentInstant;
2614
+
1975
2615
  yield* sql`
1976
2616
  INSERT INTO effect_agent_approval_decisions (
1977
2617
  submission_id,
@@ -1990,6 +2630,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
1990
2630
  )
1991
2631
  `.pipe(Effect.mapError(sqlFailure(operation)));
1992
2632
  yield* wakeSuspendedIfCovered(operation, submission);
2633
+
1993
2634
  return yield* decodeApprovalDecisionIntent({
1994
2635
  submissionId: validated.submissionId,
1995
2636
  toolCallId: validated.toolCallId,
@@ -2000,7 +2641,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2000
2641
  }).pipe(Effect.mapError(internalFailure(operation)));
2001
2642
  }),
2002
2643
  );
2644
+
2003
2645
  yield* hitFailpoint("ledger:approval-decision:after", operation);
2646
+
2004
2647
  return intent;
2005
2648
  });
2006
2649
 
@@ -2008,14 +2651,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2008
2651
  "SqliteSubmissionLedger.markUnknown",
2009
2652
  )(function* (request: MarkUnknownRequest) {
2010
2653
  const operation = "ledger mark unknown";
2011
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkUnknownRequest))(
2012
- request,
2013
- ).pipe(Effect.mapError(internalFailure(operation)));
2654
+
2655
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkUnknownRequest))(request).pipe(
2656
+ Effect.mapError(internalFailure(operation)),
2657
+ );
2658
+
2014
2659
  yield* hitFailpoint("ledger:mark-unknown:before", operation);
2015
2660
  yield* inWriteTransaction(
2016
2661
  operation,
2017
2662
  Effect.gen(function* () {
2018
2663
  const submission = yield* requireSubmission(operation, validated.submissionId);
2664
+
2019
2665
  if (submission.state === "settled") {
2020
2666
  if (submission.settled_outcome === null) {
2021
2667
  return yield* corruptionFailure(
@@ -2025,6 +2671,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2025
2671
  "A settled Submission carries no terminal outcome.",
2026
2672
  );
2027
2673
  }
2674
+
2028
2675
  return yield* SettlementConflict.make({
2029
2676
  submissionId: validated.submissionId,
2030
2677
  existingOutcome: submission.settled_outcome,
@@ -2033,6 +2680,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2033
2680
  // A reserved exact outcome wins over a late Unknown marking (DUR-011); the recovery
2034
2681
  // classifier orders reservation ahead of MarkUnknown for the same reason.
2035
2682
  const reservation = yield* readReservation(operation, validated.submissionId);
2683
+
2036
2684
  if (Option.isSome(reservation)) {
2037
2685
  return yield* SettlementConflict.make({
2038
2686
  submissionId: validated.submissionId,
@@ -2043,13 +2691,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2043
2691
  // set while the first recorded reason is kept.
2044
2692
  const existingIds = yield* storedUnknownToolCallIds(operation, submission);
2045
2693
  const known = new Set(existingIds);
2694
+
2046
2695
  const merged = [
2047
2696
  ...existingIds,
2048
2697
  ...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
2049
2698
  ];
2699
+
2050
2700
  const idsJson = yield* encodeToolCallIdsText(merged).pipe(
2051
2701
  Effect.mapError(internalFailure(operation)),
2052
2702
  );
2703
+
2053
2704
  yield* sql`
2054
2705
  UPDATE effect_agent_submissions
2055
2706
  SET
@@ -2067,17 +2718,22 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2067
2718
  "SqliteSubmissionLedger.recordUnknownResolution",
2068
2719
  )(function* (command: UnknownResolutionCommand) {
2069
2720
  const operation = "ledger record unknown resolution";
2070
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(UnknownResolutionCommand))(
2721
+
2722
+ const validated = yield* Schema.decodeEffect(Schema.toType(UnknownResolutionCommand))(
2071
2723
  command,
2072
2724
  ).pipe(Effect.mapError(internalFailure(operation)));
2725
+
2073
2726
  const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
2074
2727
  Effect.mapError(internalFailure(operation)),
2075
2728
  );
2729
+
2076
2730
  yield* hitFailpoint("ledger:unknown-resolution:before", operation);
2731
+
2077
2732
  const intent = yield* inWriteTransaction(
2078
2733
  operation,
2079
2734
  Effect.gen(function* () {
2080
2735
  const submission = yield* requireSubmission(operation, validated.submissionId);
2736
+
2081
2737
  if (submission.state === "settled") {
2082
2738
  if (submission.settled_outcome === null) {
2083
2739
  return yield* corruptionFailure(
@@ -2087,6 +2743,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2087
2743
  "A settled Submission carries no terminal outcome.",
2088
2744
  );
2089
2745
  }
2746
+
2090
2747
  return yield* SettlementConflict.make({
2091
2748
  submissionId: validated.submissionId,
2092
2749
  existingOutcome: submission.settled_outcome,
@@ -2094,19 +2751,30 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2094
2751
  }
2095
2752
  const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
2096
2753
  const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
2097
- if (existing !== undefined && existing.resolution_json !== resolutionJson) {
2754
+
2755
+ const existingIntent =
2756
+ existing === undefined
2757
+ ? undefined
2758
+ : yield* unknownResolutionIntentFromRow(operation, existing);
2759
+
2760
+ if (
2761
+ existingIntent !== undefined &&
2762
+ !equivalentUnknownResolution(existingIntent.resolution, validated.resolution)
2763
+ ) {
2098
2764
  return yield* UnknownResolutionConflict.make({
2099
2765
  submissionId: validated.submissionId,
2100
2766
  toolCallId: validated.toolCallId,
2101
2767
  });
2102
2768
  }
2103
2769
  let resolved: UnknownResolutionIntent;
2104
- if (existing !== undefined) {
2770
+
2771
+ if (existingIntent !== undefined) {
2105
2772
  // Idempotent replay of the recorded intent (author/reason may differ; the stored
2106
2773
  // audit fields win, exactly like requestAbort).
2107
- resolved = yield* unknownResolutionIntentFromRow(operation, existing);
2774
+ resolved = existingIntent;
2108
2775
  } else {
2109
2776
  const now = yield* currentInstant;
2777
+
2110
2778
  yield* sql`
2111
2779
  INSERT INTO effect_agent_unknown_resolutions (
2112
2780
  submission_id,
@@ -2124,9 +2792,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2124
2792
  ${now.iso}
2125
2793
  )
2126
2794
  `.pipe(Effect.mapError(sqlFailure(operation)));
2795
+
2127
2796
  const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
2128
2797
  Effect.mapError(internalFailure(operation)),
2129
2798
  );
2799
+
2130
2800
  resolved = yield* decodeUnknownResolutionIntent({
2131
2801
  submissionId: validated.submissionId,
2132
2802
  toolCallId: validated.toolCallId,
@@ -2143,6 +2813,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2143
2813
  const markedIds = yield* storedUnknownToolCallIds(operation, submission);
2144
2814
  const covering = yield* readUnknownResolutions(operation, validated.submissionId);
2145
2815
  const coveredIds = new Set(covering.map((row) => row.tool_call_id));
2816
+
2146
2817
  if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) {
2147
2818
  yield* sql`
2148
2819
  UPDATE effect_agent_submissions
@@ -2154,10 +2825,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2154
2825
  `.pipe(Effect.mapError(sqlFailure(operation)));
2155
2826
  }
2156
2827
  }
2828
+
2157
2829
  return resolved;
2158
2830
  }),
2159
2831
  );
2832
+
2160
2833
  yield* hitFailpoint("ledger:unknown-resolution:after", operation);
2834
+
2161
2835
  return intent;
2162
2836
  });
2163
2837
 
@@ -2165,26 +2839,38 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2165
2839
  "SqliteSubmissionLedger.recordChildSettled",
2166
2840
  )(function* (request: ChildSettledNotification) {
2167
2841
  const operation = "ledger record child settled";
2168
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ChildSettledNotification))(
2842
+
2843
+ const validated = yield* Schema.decodeEffect(Schema.toType(ChildSettledNotification))(
2169
2844
  request,
2170
2845
  ).pipe(Effect.mapError(internalFailure(operation)));
2846
+
2171
2847
  yield* hitFailpoint("ledger:child-settled:before", operation);
2848
+
2172
2849
  const outcome = yield* inWriteTransaction(
2173
2850
  operation,
2174
2851
  Effect.gen(function* () {
2175
2852
  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.
2853
+ // The caller may notify after the canonical Settlement append but before ledger
2854
+ // finalization. An exact reservation plus `terminalizing` is the narrow durable prefix
2855
+ // that makes that ordering admissible; earlier states remain a caller error.
2178
2856
  const child = yield* readSubmission(operation, validated.childSubmissionId);
2179
- if (Option.isNone(child) || child.value.state !== "settled") {
2857
+ const childReservation = yield* readReservation(operation, validated.childSubmissionId);
2858
+
2859
+ const announced =
2860
+ Option.isSome(child) &&
2861
+ (child.value.state === "settled" ||
2862
+ (child.value.state === "terminalizing" && Option.isSome(childReservation)));
2863
+
2864
+ if (!announced) {
2180
2865
  return yield* LedgerError.make({
2181
2866
  operation,
2182
2867
  message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`,
2183
2868
  });
2184
2869
  }
2185
2870
  if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
2186
- return "not-waiting" as ChildSettledOutcome;
2871
+ return NOT_WAITING;
2187
2872
  }
2873
+
2188
2874
  const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
2189
2875
  parent.suspended_reason_json,
2190
2876
  ).pipe(
@@ -2197,20 +2883,28 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2197
2883
  ),
2198
2884
  ),
2199
2885
  );
2886
+
2200
2887
  if (reason._tag !== "WaitingForChild") {
2201
- return "not-waiting" as ChildSettledOutcome;
2888
+ return NOT_WAITING;
2202
2889
  }
2203
2890
  if (
2204
2891
  !reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
2205
2892
  ) {
2206
- return "not-waiting" as ChildSettledOutcome;
2893
+ return NOT_WAITING;
2207
2894
  }
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.
2895
+ // Every listed child must be either finalized or canonically announced from the exact
2896
+ // terminalizing reservation. Replays re-run this coverage check idempotently.
2210
2897
  for (const entry of reason.children) {
2211
2898
  const listed = yield* readSubmission(operation, entry.childSubmissionId);
2212
- if (Option.isNone(listed) || listed.value.state !== "settled") {
2213
- return "still-waiting" as ChildSettledOutcome;
2899
+ const reservation = yield* readReservation(operation, entry.childSubmissionId);
2900
+
2901
+ const covered =
2902
+ Option.isSome(listed) &&
2903
+ (listed.value.state === "settled" ||
2904
+ (listed.value.state === "terminalizing" && Option.isSome(reservation)));
2905
+
2906
+ if (!covered) {
2907
+ return STILL_WAITING;
2214
2908
  }
2215
2909
  }
2216
2910
  yield* sql`
@@ -2221,10 +2915,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2221
2915
  suspended_at = NULL
2222
2916
  WHERE submission_id = ${validated.parentSubmissionId}
2223
2917
  `.pipe(Effect.mapError(sqlFailure(operation)));
2224
- return "woken" as ChildSettledOutcome;
2918
+
2919
+ return WOKEN;
2225
2920
  }),
2226
2921
  );
2922
+
2227
2923
  yield* hitFailpoint("ledger:child-settled:after", operation);
2924
+
2228
2925
  return outcome;
2229
2926
  });
2230
2927
 
@@ -2232,25 +2929,36 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2232
2929
  "SqliteSubmissionLedger.reserveChildBudget",
2233
2930
  )(function* (request: ChildBudgetReservationRequest) {
2234
2931
  const operation = "ledger reserve child budget";
2235
- const validated = yield* Schema.decodeUnknownEffect(
2236
- Schema.toType(ChildBudgetReservationRequest),
2237
- )(request).pipe(Effect.mapError(internalFailure(operation)));
2932
+
2933
+ const validated = yield* Schema.decodeEffect(Schema.toType(ChildBudgetReservationRequest))(
2934
+ request,
2935
+ ).pipe(Effect.mapError(internalFailure(operation)));
2936
+
2238
2937
  const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
2239
2938
  Effect.mapError(internalFailure(operation)),
2240
2939
  );
2940
+
2241
2941
  yield* hitFailpoint("ledger:child-reservation:before", operation);
2942
+
2242
2943
  const reserved = yield* inWriteTransaction(
2243
2944
  operation,
2244
2945
  Effect.gen(function* () {
2245
2946
  const existing = yield* readChildReservation(operation, validated.reservationId);
2947
+
2246
2948
  if (Option.isSome(existing)) {
2949
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
2950
+ operation,
2951
+ existing.value,
2952
+ );
2953
+
2247
2954
  // Identical replays short-circuit before the fence, mirroring reserveSettlement: a
2248
2955
  // replay creates nothing, so a recovering caller resumes rather than duplicates.
2249
2956
  const identical =
2250
2957
  existing.value.parent_submission_id === validated.parentSubmissionId &&
2251
2958
  existing.value.parent_tool_call_id === validated.parentToolCallId &&
2252
2959
  existing.value.allocation_digest === validated.allocationDigest &&
2253
- existing.value.allocation_json === allocationJson;
2960
+ equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
2961
+
2254
2962
  if (!identical) {
2255
2963
  return yield* ChildReservationConflict.make({
2256
2964
  reservationId: validated.reservationId,
@@ -2259,16 +2967,19 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2259
2967
  "A reservation with this identity exists with a different parent Tool Call or allocation.",
2260
2968
  });
2261
2969
  }
2970
+
2262
2971
  return ReservedChildBudget.make({
2263
- reservation: yield* childReservationSnapshotFromRow(operation, existing.value),
2972
+ reservation: existingSnapshot,
2264
2973
  replayed: true,
2265
2974
  });
2266
2975
  }
2976
+
2267
2977
  const collision = yield* readChildReservationForCall(
2268
2978
  operation,
2269
2979
  validated.parentSubmissionId,
2270
2980
  validated.parentToolCallId,
2271
2981
  );
2982
+
2272
2983
  if (Option.isSome(collision)) {
2273
2984
  return yield* ChildReservationConflict.make({
2274
2985
  reservationId: validated.reservationId,
@@ -2277,10 +2988,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2277
2988
  });
2278
2989
  }
2279
2990
  const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2991
+
2280
2992
  // Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
2281
2993
  // parent Attempt can never create new reservation state.
2282
2994
  yield* requireOwnership(operation, parent, validated.ownershipToken);
2283
2995
  const now = yield* currentInstant;
2996
+
2284
2997
  yield* sql`
2285
2998
  INSERT INTO effect_agent_child_reservations (
2286
2999
  reservation_id,
@@ -2301,6 +3014,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2301
3014
  )
2302
3015
  `.pipe(Effect.mapError(sqlFailure(operation)));
2303
3016
  const inserted = yield* readChildReservation(operation, validated.reservationId);
3017
+
2304
3018
  if (Option.isNone(inserted)) {
2305
3019
  return yield* corruptionFailure(
2306
3020
  operation,
@@ -2309,13 +3023,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2309
3023
  "An inserted child reservation row is missing inside its own transaction.",
2310
3024
  );
2311
3025
  }
3026
+
2312
3027
  return ReservedChildBudget.make({
2313
3028
  reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
2314
3029
  replayed: false,
2315
3030
  });
2316
3031
  }),
2317
3032
  );
3033
+
2318
3034
  yield* hitFailpoint("ledger:child-reservation:after", operation);
3035
+
2319
3036
  return reserved;
2320
3037
  });
2321
3038
 
@@ -2324,14 +3041,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2324
3041
  request: AttachChildToReservationRequest,
2325
3042
  ) {
2326
3043
  const operation = "ledger attach child to reservation";
2327
- const validated = yield* Schema.decodeUnknownEffect(
2328
- Schema.toType(AttachChildToReservationRequest),
2329
- )(request).pipe(Effect.mapError(internalFailure(operation)));
3044
+
3045
+ const validated = yield* Schema.decodeEffect(Schema.toType(AttachChildToReservationRequest))(
3046
+ request,
3047
+ ).pipe(Effect.mapError(internalFailure(operation)));
3048
+
2330
3049
  yield* hitFailpoint("ledger:child-attach:before", operation);
3050
+
2331
3051
  const attached = yield* inWriteTransaction(
2332
3052
  operation,
2333
3053
  Effect.gen(function* () {
2334
3054
  const existing = yield* readChildReservation(operation, validated.reservationId);
3055
+
2335
3056
  if (Option.isNone(existing)) {
2336
3057
  return yield* LedgerError.make({
2337
3058
  operation,
@@ -2343,6 +3064,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2343
3064
  if (existing.value.child_submission_id === validated.childSubmissionId) {
2344
3065
  return yield* childReservationSnapshotFromRow(operation, existing.value);
2345
3066
  }
3067
+
2346
3068
  return yield* ChildReservationConflict.make({
2347
3069
  reservationId: validated.reservationId,
2348
3070
  status: existing.value.status,
@@ -2350,6 +3072,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2350
3072
  });
2351
3073
  }
2352
3074
  const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
3075
+
2353
3076
  yield* requireOwnership(operation, parent, validated.ownershipToken);
2354
3077
  if (existing.value.status !== "reserved") {
2355
3078
  return yield* ChildReservationConflict.make({
@@ -2361,6 +3084,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2361
3084
  // Single-store latitude: the admitted child must exist here, so a dangling
2362
3085
  // attachment can never enter the recovery view.
2363
3086
  const child = yield* readSubmission(operation, validated.childSubmissionId);
3087
+
2364
3088
  if (Option.isNone(child)) {
2365
3089
  return yield* LedgerError.make({
2366
3090
  operation,
@@ -2373,6 +3097,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2373
3097
  WHERE reservation_id = ${validated.reservationId}
2374
3098
  `.pipe(Effect.mapError(sqlFailure(operation)));
2375
3099
  const updated = yield* readChildReservation(operation, validated.reservationId);
3100
+
2376
3101
  if (Option.isNone(updated)) {
2377
3102
  return yield* corruptionFailure(
2378
3103
  operation,
@@ -2381,10 +3106,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2381
3106
  "An updated child reservation row is missing inside its own transaction.",
2382
3107
  );
2383
3108
  }
3109
+
2384
3110
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2385
3111
  }),
2386
3112
  );
3113
+
2387
3114
  yield* hitFailpoint("ledger:child-attach:after", operation);
3115
+
2388
3116
  return attached;
2389
3117
  });
2390
3118
 
@@ -2392,17 +3120,22 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2392
3120
  "SqliteSubmissionLedger.beginChildBudgetRelease",
2393
3121
  )(function* (request: BeginChildBudgetReleaseRequest) {
2394
3122
  const operation = "ledger begin child budget release";
2395
- const validated = yield* Schema.decodeUnknownEffect(
2396
- Schema.toType(BeginChildBudgetReleaseRequest),
2397
- )(request).pipe(Effect.mapError(internalFailure(operation)));
3123
+
3124
+ const validated = yield* Schema.decodeEffect(Schema.toType(BeginChildBudgetReleaseRequest))(
3125
+ request,
3126
+ ).pipe(Effect.mapError(internalFailure(operation)));
3127
+
2398
3128
  const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
2399
3129
  Effect.mapError(internalFailure(operation)),
2400
3130
  );
3131
+
2401
3132
  yield* hitFailpoint("ledger:child-release-pending:before", operation);
3133
+
2402
3134
  const frozen = yield* inWriteTransaction(
2403
3135
  operation,
2404
3136
  Effect.gen(function* () {
2405
3137
  const existing = yield* readChildReservation(operation, validated.reservationId);
3138
+
2406
3139
  if (Option.isNone(existing)) {
2407
3140
  return yield* LedgerError.make({
2408
3141
  operation,
@@ -2410,11 +3143,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2410
3143
  });
2411
3144
  }
2412
3145
  if (existing.value.status !== "reserved") {
3146
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
3147
+ operation,
3148
+ existing.value,
3149
+ );
3150
+
2413
3151
  // The accounting decision was already frozen exactly once; an identical replay is a
2414
3152
  // 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);
3153
+ if (
3154
+ existingSnapshot.accounting !== undefined &&
3155
+ equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
3156
+ ) {
3157
+ return existingSnapshot;
2417
3158
  }
3159
+
2418
3160
  return yield* ChildReservationConflict.make({
2419
3161
  reservationId: validated.reservationId,
2420
3162
  status: existing.value.status,
@@ -2422,6 +3164,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2422
3164
  });
2423
3165
  }
2424
3166
  const now = yield* currentInstant;
3167
+
2425
3168
  yield* sql`
2426
3169
  UPDATE effect_agent_child_reservations
2427
3170
  SET
@@ -2431,6 +3174,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2431
3174
  WHERE reservation_id = ${validated.reservationId}
2432
3175
  `.pipe(Effect.mapError(sqlFailure(operation)));
2433
3176
  const updated = yield* readChildReservation(operation, validated.reservationId);
3177
+
2434
3178
  if (Option.isNone(updated)) {
2435
3179
  return yield* corruptionFailure(
2436
3180
  operation,
@@ -2439,10 +3183,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2439
3183
  "An updated child reservation row is missing inside its own transaction.",
2440
3184
  );
2441
3185
  }
3186
+
2442
3187
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2443
3188
  }),
2444
3189
  );
3190
+
2445
3191
  yield* hitFailpoint("ledger:child-release-pending:after", operation);
3192
+
2446
3193
  return frozen;
2447
3194
  });
2448
3195
 
@@ -2450,14 +3197,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2450
3197
  "SqliteSubmissionLedger.releaseChildBudget",
2451
3198
  )(function* (request: ReleaseChildBudgetRequest) {
2452
3199
  const operation = "ledger release child budget";
2453
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseChildBudgetRequest))(
3200
+
3201
+ const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseChildBudgetRequest))(
2454
3202
  request,
2455
3203
  ).pipe(Effect.mapError(internalFailure(operation)));
3204
+
2456
3205
  yield* hitFailpoint("ledger:child-release:before", operation);
3206
+
2457
3207
  const released = yield* inWriteTransaction(
2458
3208
  operation,
2459
3209
  Effect.gen(function* () {
2460
3210
  const existing = yield* readChildReservation(operation, validated.reservationId);
3211
+
2461
3212
  if (Option.isNone(existing)) {
2462
3213
  return yield* LedgerError.make({
2463
3214
  operation,
@@ -2477,12 +3228,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2477
3228
  });
2478
3229
  }
2479
3230
  const now = yield* currentInstant;
3231
+
2480
3232
  yield* sql`
2481
3233
  UPDATE effect_agent_child_reservations
2482
3234
  SET status = 'released', released_at = ${now.iso}
2483
3235
  WHERE reservation_id = ${validated.reservationId}
2484
3236
  `.pipe(Effect.mapError(sqlFailure(operation)));
2485
3237
  const updated = yield* readChildReservation(operation, validated.reservationId);
3238
+
2486
3239
  if (Option.isNone(updated)) {
2487
3240
  return yield* corruptionFailure(
2488
3241
  operation,
@@ -2491,84 +3244,167 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2491
3244
  "An updated child reservation row is missing inside its own transaction.",
2492
3245
  );
2493
3246
  }
3247
+
2494
3248
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2495
3249
  }),
2496
3250
  );
3251
+
2497
3252
  yield* hitFailpoint("ledger:child-release:after", operation);
3253
+
2498
3254
  return released;
2499
3255
  });
2500
3256
 
2501
3257
  interface ScanCursor {
2502
- readonly conversationId: string;
3258
+ readonly threadId: string;
2503
3259
  readonly queueSequence: number;
2504
3260
  }
2505
3261
 
2506
3262
  const scanPage = Effect.fn("SqliteSubmissionLedger.scanPage")(function* (
2507
3263
  cursor: ScanCursor | undefined,
2508
3264
  ): Effect.fn.Return<
2509
- readonly [ReadonlyArray<SubmissionSnapshot>, Option.Option<ScanCursor | undefined>],
3265
+ readonly [ReadonlyArray<SubmissionWorkItem>, Option.Option<ScanCursor | undefined>],
2510
3266
  LedgerError
2511
3267
  > {
2512
3268
  const operation = "ledger scan nonterminal";
3269
+
2513
3270
  const rows = yield* (
2514
3271
  cursor === undefined
2515
3272
  ? sql<Record<string, unknown>>`
2516
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3273
+ SELECT submission_id AS "submissionId", thread_id AS "threadId",
3274
+ queue_sequence AS "queueSequence", principal, idempotency_key AS "idempotencyKey",
3275
+ deployment_id AS "deploymentId", receipt_id AS "receiptId", state
2517
3276
  FROM effect_agent_submissions
2518
3277
  WHERE state <> 'settled'
2519
- ORDER BY conversation_id ASC, queue_sequence ASC
3278
+ ORDER BY thread_id ASC, queue_sequence ASC
2520
3279
  LIMIT ${SCAN_PAGE_SIZE}
2521
3280
  `
2522
3281
  : sql<Record<string, unknown>>`
2523
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3282
+ SELECT submission_id AS "submissionId", thread_id AS "threadId",
3283
+ queue_sequence AS "queueSequence", principal, idempotency_key AS "idempotencyKey",
3284
+ deployment_id AS "deploymentId", receipt_id AS "receiptId", state
2524
3285
  FROM effect_agent_submissions
2525
3286
  WHERE state <> 'settled'
2526
- AND (
2527
- conversation_id > ${cursor.conversationId}
2528
- OR (
2529
- conversation_id = ${cursor.conversationId}
2530
- AND queue_sequence > ${cursor.queueSequence}
2531
- )
2532
- )
2533
- ORDER BY conversation_id ASC, queue_sequence ASC
3287
+ AND (thread_id, queue_sequence) > (${cursor.threadId}, ${cursor.queueSequence})
3288
+ ORDER BY thread_id ASC, queue_sequence ASC
2534
3289
  LIMIT ${SCAN_PAGE_SIZE}
2535
3290
  `
2536
3291
  ).pipe(Effect.mapError(sqlFailure(operation)));
2537
- const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
2538
- const snapshots = yield* Effect.forEach(decoded, (row) =>
2539
- decodeSubmissionSnapshot(operation, row),
2540
- );
3292
+
3293
+ const decoded = yield* decodeRows(
3294
+ Schema.Array(SubmissionWorkItem),
3295
+ "effect_agent_submissions",
3296
+ "nonterminal_scan",
3297
+ rows,
3298
+ ).pipe(Effect.mapError(internalFailure(operation)));
3299
+
2541
3300
  const last = decoded[decoded.length - 1];
3301
+
2542
3302
  const next: Option.Option<ScanCursor | undefined> =
2543
3303
  last === undefined || decoded.length < SCAN_PAGE_SIZE
2544
3304
  ? Option.none()
2545
3305
  : Option.some({
2546
- conversationId: last.conversation_id,
2547
- queueSequence: last.queue_sequence,
3306
+ threadId: last.threadId,
3307
+ queueSequence: last.queueSequence,
2548
3308
  });
2549
- return [snapshots, next] as const;
3309
+
3310
+ return [decoded, next] as const;
2550
3311
  });
2551
3312
 
2552
- const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate(
2553
- undefined as ScanCursor | undefined,
2554
- scanPage,
2555
- );
3313
+ const scanNonterminal: Stream.Stream<SubmissionWorkItem, LedgerError> = Stream.paginate<
3314
+ ScanCursor | undefined,
3315
+ SubmissionWorkItem,
3316
+ LedgerError
3317
+ >(undefined, scanPage);
3318
+
3319
+ const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
3320
+ "SqliteSubmissionLedger.readAbortIntentForSubmission",
3321
+ )(function* (request) {
3322
+ const operation = "ledger read abort intent";
3323
+
3324
+ const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
3325
+ Effect.mapError(internalFailure(operation)),
3326
+ );
3327
+
3328
+ const recordId = submissionAbortRecordId(validated.submissionId);
3329
+
3330
+ const rows = yield* sql<Record<string, unknown>>`
3331
+ SELECT
3332
+ submission.submission_id,
3333
+ abort.submission_id AS abort_submission_id,
3334
+ abort.author,
3335
+ abort.reason,
3336
+ abort.requested_at,
3337
+ canonical.record_id AS canonical_record_id
3338
+ FROM effect_agent_submissions AS submission
3339
+ LEFT JOIN effect_agent_abort_intents AS abort
3340
+ ON abort.submission_id = submission.submission_id
3341
+ LEFT JOIN effect_agent_canonical_records AS canonical
3342
+ ON canonical.thread_id = submission.thread_id
3343
+ AND canonical.record_id = ${recordId}
3344
+ WHERE submission.submission_id = ${validated.submissionId}
3345
+ `.pipe(Effect.mapError(sqlFailure(operation)));
3346
+
3347
+ const decoded = yield* decodeRows(
3348
+ Schema.Array(AbortIntentLookupRow),
3349
+ "effect_agent_abort_intents",
3350
+ validated.submissionId,
3351
+ rows,
3352
+ ).pipe(Effect.mapError(internalFailure(operation)));
3353
+
3354
+ if (decoded.length === 0) {
3355
+ return yield* LedgerError.make({
3356
+ operation,
3357
+ message: `Unknown submission ${validated.submissionId}.`,
3358
+ });
3359
+ }
3360
+ if (decoded.length !== 1) {
3361
+ return yield* corruptionFailure(
3362
+ operation,
3363
+ "effect_agent_abort_intents",
3364
+ validated.submissionId,
3365
+ "An abort intent lookup returned more than one row.",
3366
+ );
3367
+ }
3368
+ const row = decoded[0];
3369
+
3370
+ if (row.abort_submission_id === null) return undefined;
3371
+
3372
+ return yield* decodeAbortIntent({
3373
+ submissionId: row.abort_submission_id,
3374
+ author: row.author,
3375
+ reason: row.reason,
3376
+ requestedAt: row.requested_at,
3377
+ ...(row.canonical_record_id === null ? {} : { canonicalRecordId: row.canonical_record_id }),
3378
+ }).pipe(
3379
+ Effect.mapError((error) =>
3380
+ corruptionFailure(
3381
+ operation,
3382
+ "effect_agent_abort_intents",
3383
+ validated.submissionId,
3384
+ error.message,
3385
+ ),
3386
+ ),
3387
+ );
3388
+ });
2556
3389
 
2557
3390
  const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
2558
3391
  "SqliteSubmissionLedger.loadRecoverySnapshot",
2559
3392
  )(function* (request: RecoverySnapshotRequest) {
2560
3393
  const operation = "ledger load recovery snapshot";
2561
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RecoverySnapshotRequest))(
3394
+
3395
+ const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
2562
3396
  request,
2563
3397
  ).pipe(Effect.mapError(internalFailure(operation)));
2564
- return yield* sql
2565
- .withTransaction(
3398
+
3399
+ return yield* journal
3400
+ .withReadTransaction(operation)(
2566
3401
  Effect.gen(function* () {
2567
3402
  const submissionRow = yield* requireSubmission(operation, validated.submissionId);
2568
3403
  const submission = yield* decodeSubmissionSnapshot(operation, submissionRow);
2569
3404
 
2570
3405
  let ownership: OwnershipSnapshot | undefined;
2571
3406
  const ownershipRow = yield* readOwnership(operation, validated.submissionId);
3407
+
2572
3408
  if (Option.isSome(ownershipRow)) {
2573
3409
  ownership = yield* decodeOwnershipSnapshot({
2574
3410
  attemptId: ownershipRow.value.attempt_id,
@@ -2579,6 +3415,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2579
3415
  }
2580
3416
 
2581
3417
  let inputApplied: InputAppliedMarker | undefined;
3418
+
2582
3419
  if (
2583
3420
  submissionRow.input_applied_record_id !== null &&
2584
3421
  submissionRow.input_applied_sequence !== null
@@ -2591,6 +3428,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2591
3428
 
2592
3429
  let reservation: SettlementReservationSnapshot | undefined;
2593
3430
  const reservationRow = yield* readReservation(operation, validated.submissionId);
3431
+
2594
3432
  if (Option.isSome(reservationRow)) {
2595
3433
  const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(
2596
3434
  Effect.mapError((error) =>
@@ -2602,9 +3440,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2602
3440
  ),
2603
3441
  ),
2604
3442
  );
2605
- const settlementId = yield* Schema.decodeUnknownEffect(
3443
+
3444
+ const settlementId = yield* Schema.decodeEffect(
2606
3445
  SettlementReservationSnapshot.fields.settlementId,
2607
3446
  )(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
3447
+
2608
3448
  reservation = SettlementReservationSnapshot.make({
2609
3449
  settlementId,
2610
3450
  outcome: reservationRow.value.outcome,
@@ -2616,6 +3456,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2616
3456
 
2617
3457
  let abortIntent: AbortIntent | undefined;
2618
3458
  const abortRow = yield* readAbortIntent(operation, validated.submissionId);
3459
+
2619
3460
  if (Option.isSome(abortRow)) {
2620
3461
  abortIntent = yield* abortIntentFromRow(
2621
3462
  operation,
@@ -2633,11 +3474,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2633
3474
  WHERE joined_host_submission_id = ${validated.submissionId}
2634
3475
  ORDER BY queue_sequence ASC
2635
3476
  `.pipe(Effect.mapError(sqlFailure(operation)));
3477
+
2636
3478
  const joinSubmissions = yield* decodeSubmissionRows(
2637
3479
  operation,
2638
3480
  validated.submissionId,
2639
3481
  joinRows,
2640
3482
  );
3483
+
2641
3484
  const joins = yield* Effect.forEach(joinSubmissions, (row) =>
2642
3485
  decodeJoinSnapshot({
2643
3486
  submissionId: row.submission_id,
@@ -2647,6 +3490,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2647
3490
  );
2648
3491
 
2649
3492
  let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
3493
+
2650
3494
  if (submissionRow.joined_host_submission_id !== null) {
2651
3495
  hostSubmissionId = yield* decodeSubmissionId(
2652
3496
  submissionRow.joined_host_submission_id,
@@ -2654,6 +3498,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2654
3498
  }
2655
3499
 
2656
3500
  let suspension: SuspensionSnapshot | undefined;
3501
+
2657
3502
  if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
2658
3503
  const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(
2659
3504
  Effect.mapError((error) =>
@@ -2665,6 +3510,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2665
3510
  ),
2666
3511
  ),
2667
3512
  );
3513
+
2668
3514
  suspension = yield* decodeSuspensionSnapshot({
2669
3515
  reason,
2670
3516
  suspendedAt: submissionRow.suspended_at,
@@ -2681,11 +3527,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2681
3527
  }
2682
3528
 
2683
3529
  const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
3530
+
2684
3531
  const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
2685
3532
  approvalIntentFromRow(operation, row),
2686
3533
  );
2687
3534
 
2688
3535
  const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
3536
+
2689
3537
  const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
2690
3538
  unknownResolutionIntentFromRow(operation, row),
2691
3539
  );
@@ -2699,18 +3547,23 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2699
3547
  WHERE parent_submission_id = ${validated.submissionId}
2700
3548
  ORDER BY parent_tool_call_id ASC
2701
3549
  `.pipe(Effect.mapError(sqlFailure(operation)));
3550
+
2702
3551
  const decodedChildReservations = yield* decodeChildReservationRows(
2703
3552
  operation,
2704
3553
  validated.submissionId,
2705
3554
  childReservationRows,
2706
3555
  );
3556
+
2707
3557
  const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
2708
3558
  childReservationSnapshotFromRow(operation, row),
2709
3559
  );
3560
+
2710
3561
  const childAttachments: Array<ChildAttachmentSnapshot> = [];
3562
+
2711
3563
  for (const row of decodedChildReservations) {
2712
3564
  if (row.child_submission_id === null) continue;
2713
3565
  const child = yield* readSubmission(operation, row.child_submission_id);
3566
+
2714
3567
  if (Option.isNone(child)) continue;
2715
3568
  childAttachments.push(
2716
3569
  yield* decodeChildAttachmentSnapshot({
@@ -2725,6 +3578,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2725
3578
  }
2726
3579
 
2727
3580
  let parentLinkage: ParentLinkage | undefined;
3581
+
2728
3582
  if (
2729
3583
  submissionRow.parent_submission_id !== null &&
2730
3584
  submissionRow.parent_tool_call_id !== null
@@ -2752,7 +3606,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2752
3606
  });
2753
3607
  }),
2754
3608
  )
2755
- .pipe(Effect.catchTag("SqlError", (error) => Effect.fail(sqlFailure(operation)(error))));
3609
+ .pipe(
3610
+ Effect.catchTag("SqliteStorageError", (error) =>
3611
+ Effect.fail(internalFailure(operation)(error)),
3612
+ ),
3613
+ );
2756
3614
  });
2757
3615
 
2758
3616
  return Context.make(
@@ -2770,6 +3628,8 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2770
3628
  reserveSettlement,
2771
3629
  finalizeSettlement,
2772
3630
  requestAbort,
3631
+ stopWorker,
3632
+ inspectWorker,
2773
3633
  claimJoining,
2774
3634
  markJoined,
2775
3635
  revertJoining,
@@ -2784,6 +3644,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
2784
3644
  releaseChildBudget,
2785
3645
  scanNonterminal,
2786
3646
  loadRecoverySnapshot,
3647
+ readAbortIntent: readAbortIntentForSubmission,
2787
3648
  }),
2788
3649
  );
2789
3650
  });
@@ -2801,18 +3662,22 @@ export const submissionLedgerLayer: Layer.Layer<
2801
3662
 
2802
3663
  /**
2803
3664
  * 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.
3665
+ * same database file as the ThreadStore so claims fence the same producer epochs.
2805
3666
  */
2806
3667
  export const ledgerLayer = (
2807
3668
  options: SqliteStorageOptions,
2808
3669
  ): 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,
3670
+ Layer.unwrap(
3671
+ Effect.map(SqliteStorageConfig, (config) =>
3672
+ submissionLedgerLayer.pipe(
3673
+ Layer.provide(
3674
+ Layer.mergeAll(
3675
+ Layer.succeed(SqliteStorageConfig)(config),
3676
+ storageFailpointLayer(options),
3677
+ SqliteClient.layer({ filename: options.filename }),
3678
+ NodeCrypto.layer,
3679
+ ),
3680
+ ),
2816
3681
  ),
2817
3682
  ),
2818
- );
3683
+ ).pipe(Layer.provide(storageConfigLayer(options)));