@effect-agent/storage-cloudflare 0.1.0-beta.9 → 0.1.0-beta.91

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