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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/DoMemoryStore.d.mts +29 -0
  2. package/dist/DoMemoryStore.mjs +55 -0
  3. package/dist/DoMemoryStore.mjs.map +1 -0
  4. package/dist/DoMessageDeliveryStore.d.mts +17 -0
  5. package/dist/DoMessageDeliveryStore.mjs +32 -0
  6. package/dist/DoMessageDeliveryStore.mjs.map +1 -0
  7. package/dist/DoScheduleStore.d.mts +36 -0
  8. package/dist/DoScheduleStore.mjs +388 -0
  9. package/dist/DoScheduleStore.mjs.map +1 -0
  10. package/dist/DoStorageConfig.d.mts +45 -0
  11. package/dist/DoStorageConfig.mjs +52 -0
  12. package/dist/DoStorageConfig.mjs.map +1 -0
  13. package/dist/DoStorageError-DjpuLHmX.d.mts +101 -0
  14. package/dist/DoStorageError.d.mts +2 -0
  15. package/dist/DoStorageError.mjs +167 -0
  16. package/dist/DoStorageError.mjs.map +1 -0
  17. package/dist/DoStorageFailpoint.d.mts +17 -0
  18. package/dist/DoStorageFailpoint.mjs +14 -0
  19. package/dist/DoStorageFailpoint.mjs.map +1 -0
  20. package/dist/DoStorageFailpointTesting.d.mts +33 -0
  21. package/dist/DoStorageFailpointTesting.mjs +35 -0
  22. package/dist/DoStorageFailpointTesting.mjs.map +1 -0
  23. package/dist/DoStorageVersion-DJwZzqel.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 +1939 -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 +499 -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 +5521 -0
  40. package/dist/PortProtocol.mjs +206 -0
  41. package/dist/PortProtocol.mjs.map +1 -0
  42. package/dist/PortRouting.d.mts +86 -0
  43. package/dist/PortRouting.mjs +461 -0
  44. package/dist/PortRouting.mjs.map +1 -0
  45. package/dist/do-journal-D65Iuln6.mjs +1119 -0
  46. package/dist/do-journal-D65Iuln6.mjs.map +1 -0
  47. package/dist/index.d.mts +14 -1525
  48. package/dist/index.mjs +14 -3695
  49. package/dist/migrations-Br6QqMIf.mjs +306 -0
  50. package/dist/migrations-Br6QqMIf.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} +13 -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} +1119 -262
  61. package/src/DoSubscriptionStore.ts +329 -0
  62. package/src/DoThreadStore.ts +1064 -0
  63. package/src/MemoryProtocol.ts +351 -0
  64. package/src/{port-protocol.ts → PortProtocol.ts} +103 -38
  65. package/src/{routing.ts → PortRouting.ts} +440 -264
  66. package/src/index.ts +13 -32
  67. package/src/internal/do-journal.ts +1789 -0
  68. package/src/internal/message-delivery-schema.ts +24 -0
  69. package/src/{migrations.ts → internal/migrations.ts} +72 -35
  70. package/src/internal/recovery-checkpoint-schema.ts +17 -0
  71. package/dist/index.mjs.map +0 -1
  72. package/src/do-conversation-store.ts +0 -850
  73. package/src/do-journal.ts +0 -1078
  74. /package/src/{do-storage-config.ts → DoStorageConfig.ts} +0 -0
@@ -1,18 +1,40 @@
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,
19
+ WorkerStopCommand,
20
+ WorkerLedgerState,
21
+ workerTerminalFromRecord,
3
22
  AbortIntent,
23
+ AbortIntentRequest,
4
24
  AdmissionAdmitted,
5
25
  AdmissionConflict,
26
+ AdmissionFence,
27
+ AdmissionGroup,
28
+ AdmissionPolicyError,
6
29
  AdmissionNotAdmitted,
7
30
  AdmissionRequest,
31
+ SubmissionAdmissionFence,
8
32
  AdmissionResult,
9
33
  ApprovalConflict,
10
- ApprovalDecision,
11
34
  ApprovalDecisionCommand,
12
35
  ApprovalDecisionIntent,
13
36
  AttachChildToReservationRequest,
14
37
  BeginChildBudgetReleaseRequest,
15
- CanonicalSequence,
16
38
  ChildAttachmentSnapshot,
17
39
  ChildBudgetReservationRequest,
18
40
  ChildBudgetReservationSnapshot,
@@ -22,9 +44,6 @@ import {
22
44
  Claim,
23
45
  ClaimJoiningRequest,
24
46
  ClaimRequest,
25
- DefinitionDigests,
26
- Digest,
27
- EMPTY_TAIL_DIGEST,
28
47
  InputAppliedMarker,
29
48
  JoinSnapshot,
30
49
  JoinedToHost,
@@ -39,10 +58,7 @@ import {
39
58
  OwnershipRenewal,
40
59
  OwnershipSnapshot,
41
60
  ParentLinkage,
42
- PersistedJson,
43
- ProducerEpoch,
44
61
  QueueSequence,
45
- RecordEnvelope,
46
62
  RecoverySnapshot,
47
63
  RecoverySnapshotRequest,
48
64
  ReleaseChildBudgetRequest,
@@ -54,14 +70,15 @@ import {
54
70
  Settlement,
55
71
  SettlementConflict,
56
72
  SettlementFinalization,
57
- SettlementOutcome,
58
73
  SettlementReservation,
59
74
  SubmissionLedger,
60
75
  SubmissionLookup,
61
76
  SubmissionLookupByKey,
62
77
  SubmissionSnapshot,
78
+ SubmissionWorkItem,
63
79
  SubmissionState,
64
80
  SettlementReservationSnapshot,
81
+ settlementFailureFromRecord,
65
82
  SuspendRequest,
66
83
  SuspensionReason,
67
84
  SuspensionSnapshot,
@@ -72,28 +89,26 @@ import {
72
89
  submissionAbortRecordId,
73
90
  type ChildSettledOutcome,
74
91
  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";
92
+ } from "effect-agent/submission-ledger";
93
+ import { ThreadStoreDiagnostic } from "effect-agent/thread-store";
79
94
  import * as SqlClientService from "effect/unstable/sql/SqlClient";
80
95
  import type { SqlError } from "effect/unstable/sql/SqlError";
81
96
 
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";
97
+ import { DoStorageConfig } from "./DoStorageConfig.ts";
91
98
  import {
92
99
  DoLedgerError,
93
100
  DoStorageCorruptionError,
94
101
  DoStorageError,
95
102
  type DoStorageFailpointLocation,
96
- } from "./errors.ts";
103
+ } from "./DoStorageError.ts";
104
+ import { DoStorageFailpoint } from "./DoStorageFailpoint.ts";
105
+ import {
106
+ storageConfigLayer,
107
+ storageFailpointLayer,
108
+ type DoStorageInitializationError,
109
+ type DoStorageOptions,
110
+ } from "./DoThreadStore.ts";
111
+ import { decodeRows, initializeDoJournal } from "./internal/do-journal.ts";
97
112
 
98
113
  type SubmissionId = SubmissionSnapshot["submissionId"];
99
114
 
@@ -107,11 +122,16 @@ const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
107
122
 
108
123
  const SCAN_PAGE_SIZE = 256;
109
124
  const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
125
+ const RESUME_IMMEDIATELY: SuspensionOutcome = "resume-immediately";
126
+ const SUSPENDED: SuspensionOutcome = "suspended";
127
+ const NOT_WAITING: ChildSettledOutcome = "not-waiting";
128
+ const STILL_WAITING: ChildSettledOutcome = "still-waiting";
129
+ const WOKEN: ChildSettledOutcome = "woken";
110
130
  const MAX_IDENTIFIER_LENGTH = 1_024;
111
131
 
112
132
  class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
113
133
  submission_id: BoundedIdentifier,
114
- conversation_id: BoundedIdentifier,
134
+ thread_id: BoundedIdentifier,
115
135
  queue_sequence: QueueSequence,
116
136
  principal: BoundedIdentifier,
117
137
  idempotency_key: BoundedIdentifier,
@@ -134,6 +154,10 @@ class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
134
154
  unknown_tool_call_ids_json: Schema.NullOr(BoundedStoredText),
135
155
  parent_submission_id: Schema.NullOr(BoundedIdentifier),
136
156
  parent_tool_call_id: Schema.NullOr(BoundedIdentifier),
157
+ admission_group: Schema.NullOr(AdmissionGroup),
158
+ admission_fence_json: Schema.NullOr(BoundedStoredText),
159
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
160
+ message_admission_json: Schema.NullOr(BoundedStoredText),
137
161
  }) {}
138
162
 
139
163
  class ChildReservationRow extends Schema.Class<ChildReservationRow>("ChildReservationRow")({
@@ -209,13 +233,22 @@ class MaxQueueSequenceRow extends Schema.Class<MaxQueueSequenceRow>("MaxQueueSeq
209
233
  max_queue_sequence: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
210
234
  }) {}
211
235
 
236
+ class AbortIntentLookupRow extends Schema.Class<AbortIntentLookupRow>("AbortIntentLookupRow")({
237
+ submission_id: BoundedIdentifier,
238
+ abort_submission_id: Schema.NullOr(BoundedIdentifier),
239
+ author: Schema.NullOr(BoundedIdentifier),
240
+ reason: Schema.NullOr(BoundedStoredText),
241
+ requested_at: Schema.NullOr(BoundedTimestamp),
242
+ canonical_record_id: Schema.NullOr(BoundedIdentifier),
243
+ }) {}
244
+
212
245
  class CanonicalRecordIdRow extends Schema.Class<CanonicalRecordIdRow>("CanonicalRecordIdRow")({
213
246
  record_id: BoundedIdentifier,
214
247
  }) {}
215
248
 
216
249
  const SUBMISSION_COLUMNS = `
217
250
  submission_id,
218
- conversation_id,
251
+ thread_id,
219
252
  queue_sequence,
220
253
  principal,
221
254
  idempotency_key,
@@ -237,7 +270,11 @@ const SUBMISSION_COLUMNS = `
237
270
  unknown_reason,
238
271
  unknown_tool_call_ids_json,
239
272
  parent_submission_id,
240
- parent_tool_call_id
273
+ parent_tool_call_id,
274
+ admission_group,
275
+ admission_fence_json,
276
+ worker_admission_json,
277
+ message_admission_json
241
278
  `;
242
279
 
243
280
  const CHILD_RESERVATION_COLUMNS = `
@@ -254,7 +291,7 @@ const CHILD_RESERVATION_COLUMNS = `
254
291
  released_at
255
292
  `;
256
293
 
257
- /** The branded ToolCallId schema, reached through the session port so no core import is needed. */
294
+ /** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
258
295
  const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
259
296
  const ToolCallIdList = Schema.Array(ToolCallIdSchema);
260
297
 
@@ -284,10 +321,15 @@ const decodeSuspensionSnapshot = Schema.decodeUnknownEffect(SuspensionSnapshot);
284
321
  const decodeApprovalDecisionIntent = Schema.decodeUnknownEffect(ApprovalDecisionIntent);
285
322
  const decodeUnknownResolutionIntent = Schema.decodeUnknownEffect(UnknownResolutionIntent);
286
323
  const decodeParentLinkage = Schema.decodeUnknownEffect(ParentLinkage);
324
+
287
325
  const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
288
326
  ChildBudgetReservationSnapshot,
289
327
  );
328
+
290
329
  const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
330
+ const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
331
+ const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
332
+ const isDoStorageError = Schema.is(DoStorageError);
291
333
 
292
334
  /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
293
335
  const internalFailure =
@@ -319,6 +361,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
319
361
  const failpoint = yield* DoStorageFailpoint;
320
362
  const sql = yield* SqlClientService.SqlClient;
321
363
  const crypto = yield* Crypto.Crypto;
364
+ const admissionFence = yield* SubmissionAdmissionFence;
322
365
  const journal = yield* initializeDoJournal(sql, failpoint.hit, config.maxStoredValueBytes);
323
366
 
324
367
  const hitFailpoint = (
@@ -336,6 +379,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
336
379
  A,
337
380
  E extends
338
381
  | AdmissionConflict
382
+ | AdmissionPolicyError
339
383
  | ApprovalConflict
340
384
  | ChildReservationConflict
341
385
  | JoinedToHost
@@ -351,7 +395,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
351
395
  .withWriteTransaction(operation)(effect)
352
396
  .pipe(
353
397
  Effect.mapError((error) =>
354
- error instanceof DoStorageError ? internalFailure(operation)(error) : error,
398
+ isDoStorageError(error) ? internalFailure(operation)(error) : error,
355
399
  ),
356
400
  );
357
401
 
@@ -385,7 +429,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
385
429
  FROM effect_agent_submissions
386
430
  WHERE submission_id = ${submissionId}
387
431
  `.pipe(Effect.mapError(sqlFailure(operation)));
432
+
388
433
  const decoded = yield* decodeSubmissionRows(operation, submissionId, rows);
434
+
389
435
  if (decoded.length > 1) {
390
436
  return yield* corruptionFailure(
391
437
  operation,
@@ -394,6 +440,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
394
440
  "A submission primary key returned more than one row.",
395
441
  );
396
442
  }
443
+
397
444
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
398
445
  });
399
446
 
@@ -402,12 +449,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
402
449
  submissionId: string,
403
450
  ): Effect.fn.Return<SubmissionRow, LedgerError> {
404
451
  const submission = yield* readSubmission(operation, submissionId);
452
+
405
453
  if (Option.isNone(submission)) {
406
454
  return yield* LedgerError.make({
407
455
  operation,
408
456
  message: `Unknown submission ${submissionId}.`,
409
457
  });
410
458
  }
459
+
411
460
  return submission.value;
412
461
  });
413
462
 
@@ -426,12 +475,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
426
475
  FROM effect_agent_submission_ownership
427
476
  WHERE submission_id = ${submissionId}
428
477
  `.pipe(Effect.mapError(sqlFailure(operation)));
478
+
429
479
  const decoded = yield* decodeRows(
430
480
  Schema.Array(OwnershipRow),
431
481
  "effect_agent_submission_ownership",
432
482
  submissionId,
433
483
  rows,
434
484
  ).pipe(Effect.mapError(internalFailure(operation)));
485
+
435
486
  if (decoded.length > 1) {
436
487
  return yield* corruptionFailure(
437
488
  operation,
@@ -440,23 +491,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
440
491
  "An ownership primary key returned more than one row.",
441
492
  );
442
493
  }
494
+
443
495
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
444
496
  });
445
497
 
446
- const conversationEpoch = Effect.fn("DoSubmissionLedger.conversationEpoch")(function* (
498
+ const threadEpoch = Effect.fn("DoSubmissionLedger.threadEpoch")(function* (
447
499
  operation: string,
448
- conversationId: string,
500
+ threadId: string,
449
501
  ): Effect.fn.Return<ProducerEpoch, LedgerError> {
450
- const conversations = yield* journal
451
- .getConversation(conversationId)
502
+ const threads = yield* journal
503
+ .getThread(threadId)
452
504
  .pipe(Effect.mapError(internalFailure(operation)));
453
- return conversations.length === 0 ? EPOCH_ZERO : conversations[0].producer_epoch;
505
+
506
+ return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
454
507
  });
455
508
 
456
509
  /**
457
510
  * Verify inside the surrounding write transaction that the presented token still owns the
458
511
  * Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
459
- * Conversation's current producer epoch (DUR-006).
512
+ * Thread's current producer epoch (DUR-006).
460
513
  */
461
514
  const requireOwnership = Effect.fn("DoSubmissionLedger.requireOwnership")(function* (
462
515
  operation: string,
@@ -464,13 +517,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
464
517
  ownershipToken: string,
465
518
  ): Effect.fn.Return<OwnershipRow, OwnershipLost | LedgerError> {
466
519
  const ownership = yield* readOwnership(operation, submission.submission_id);
467
- if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
468
- const actualEpoch = yield* conversationEpoch(operation, submission.conversation_id);
469
- const submissionId = yield* Schema.decodeUnknownEffect(
470
- SubmissionSnapshot.fields.submissionId,
471
- )(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
520
+ const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
521
+
522
+ if (
523
+ Option.isNone(ownership) ||
524
+ ownership.value.ownership_token !== ownershipToken ||
525
+ ownership.value.producer_epoch !== actualEpoch
526
+ ) {
527
+ const submissionId = yield* Schema.decodeEffect(SubmissionSnapshot.fields.submissionId)(
528
+ submission.submission_id,
529
+ ).pipe(Effect.mapError(internalFailure(operation)));
530
+
472
531
  return yield* OwnershipLost.make({ submissionId, actualEpoch });
473
532
  }
533
+
474
534
  return ownership.value;
475
535
  });
476
536
 
@@ -479,26 +539,29 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
479
539
  operation: string,
480
540
  row: SubmissionRow,
481
541
  ): Effect.fn.Return<SubmissionSnapshot, LedgerError> {
542
+ const decodeFailure = (error: Schema.SchemaError) =>
543
+ internalFailure(operation)(
544
+ DoStorageCorruptionError.make({
545
+ table: "effect_agent_submissions",
546
+ rowKey: row.submission_id,
547
+ message: "Stored submission does not satisfy the ledger schema",
548
+ diagnostic: ThreadStoreDiagnostic.make({
549
+ causeTag: error._tag,
550
+ operation,
551
+ decoder: "SubmissionSnapshot",
552
+ issueTag: error.issue._tag,
553
+ }),
554
+ }),
555
+ );
556
+
482
557
  const agentDigests = yield* parseStoredJsonText(row.agent_digests_json).pipe(
483
- Effect.mapError((error) =>
484
- corruptionFailure(
485
- operation,
486
- "effect_agent_submissions",
487
- row.submission_id,
488
- error.message,
489
- ),
490
- ),
558
+ Effect.mapError(decodeFailure),
491
559
  );
560
+
492
561
  const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
493
- Effect.mapError((error) =>
494
- corruptionFailure(
495
- operation,
496
- "effect_agent_submissions",
497
- row.submission_id,
498
- error.message,
499
- ),
500
- ),
562
+ Effect.mapError(decodeFailure),
501
563
  );
564
+
502
565
  if ((row.parent_submission_id === null) !== (row.parent_tool_call_id === null)) {
503
566
  return yield* corruptionFailure(
504
567
  operation,
@@ -507,9 +570,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
507
570
  "A parent linkage must record both the parent Submission and the parent Tool Call.",
508
571
  );
509
572
  }
573
+
510
574
  return yield* decodeSubmissionSnapshotUnknown({
511
575
  submissionId: row.submission_id,
512
- conversationId: row.conversation_id,
576
+ threadId: row.thread_id,
513
577
  queueSequence: row.queue_sequence,
514
578
  principal: row.principal,
515
579
  idempotencyKey: row.idempotency_key,
@@ -521,6 +585,28 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
521
585
  receiptId: row.receipt_id,
522
586
  state: row.state,
523
587
  createdAt: row.created_at,
588
+ ...(row.admission_group === null ? {} : { admissionGroup: row.admission_group }),
589
+ ...(row.worker_admission_json === null
590
+ ? {}
591
+ : {
592
+ workerAdmission: yield* parseStoredJsonText(row.worker_admission_json).pipe(
593
+ Effect.mapError(decodeFailure),
594
+ ),
595
+ }),
596
+ ...(row.message_admission_json === null
597
+ ? {}
598
+ : {
599
+ messageAdmission: yield* parseStoredJsonText(row.message_admission_json).pipe(
600
+ Effect.mapError(decodeFailure),
601
+ ),
602
+ }),
603
+ ...(row.admission_fence_json === null
604
+ ? {}
605
+ : {
606
+ admissionFence: yield* parseStoredJsonText(row.admission_fence_json).pipe(
607
+ Effect.mapError(decodeFailure),
608
+ ),
609
+ }),
524
610
  ...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
525
611
  ...(row.ready_at === null ? {} : { readyAt: row.ready_at }),
526
612
  ...(row.parent_submission_id === null || row.parent_tool_call_id === null
@@ -531,16 +617,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
531
617
  parentToolCallId: row.parent_tool_call_id,
532
618
  },
533
619
  }),
534
- }).pipe(
535
- Effect.mapError((error) =>
536
- corruptionFailure(
537
- operation,
538
- "effect_agent_submissions",
539
- row.submission_id,
540
- error.message,
541
- ),
542
- ),
543
- );
620
+ }).pipe(Effect.mapError(decodeFailure));
544
621
  },
545
622
  );
546
623
 
@@ -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,86 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1014
1207
  }).pipe(Effect.mapError(internalFailure(operation)));
1015
1208
  }
1016
1209
 
1210
+ const stopped =
1211
+ yield* sql`SELECT thread_id FROM effect_agent_worker_stops WHERE thread_id = ${validated.threadId}`.pipe(
1212
+ Effect.mapError(sqlFailure(operation)),
1213
+ );
1214
+
1215
+ if (stopped.length > 0)
1216
+ return yield* AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" });
1217
+
1218
+ // The first accepted input fixes ordinary/worker lane identity atomically with admission.
1219
+ // Canonical origin materialization can lag admission; a log scan cannot fence that race.
1220
+ const firstRows = yield* sql<Record<string, unknown>>`
1221
+ SELECT worker_admission_json FROM effect_agent_submissions
1222
+ WHERE thread_id=${validated.threadId} ORDER BY queue_sequence LIMIT 1
1223
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1224
+
1225
+ const first = yield* Schema.decodeUnknownEffect(
1226
+ Schema.Array(
1227
+ Schema.Struct({
1228
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
1229
+ }),
1230
+ ),
1231
+ )(firstRows).pipe(Effect.mapError(internalFailure(operation)));
1232
+
1233
+ if (first[0] !== undefined) {
1234
+ const previous =
1235
+ first[0].worker_admission_json === null
1236
+ ? undefined
1237
+ : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1238
+ first[0].worker_admission_json,
1239
+ ).pipe(Effect.mapError(internalFailure(operation)));
1240
+
1241
+ if (
1242
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
1243
+ previous?.origin,
1244
+ validated.workerAdmission?.origin,
1245
+ )
1246
+ )
1247
+ return yield* AdmissionPolicyError.make({
1248
+ reason: "refused",
1249
+ code: "worker-origin-conflict",
1250
+ });
1251
+ }
1252
+
1253
+ yield* admissionFence.check(validated);
1254
+ if (validated.admissionGroup !== undefined) {
1255
+ const occupied = yield* sql<Record<string, unknown>>`
1256
+ SELECT submission_id FROM effect_agent_submissions
1257
+ WHERE thread_id=${validated.threadId} AND admission_group=${validated.admissionGroup} AND state<>'settled' LIMIT 1
1258
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1259
+
1260
+ if (occupied.length > 0)
1261
+ return yield* AdmissionPolicyError.make({
1262
+ reason: "occupied",
1263
+ code: "admission-group",
1264
+ });
1265
+ }
1266
+
1017
1267
  const maxRows = yield* sql<Record<string, unknown>>`
1018
1268
  SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
1019
1269
  FROM effect_agent_submissions
1020
- WHERE conversation_id = ${validated.conversationId}
1270
+ WHERE thread_id = ${validated.threadId}
1021
1271
  `.pipe(Effect.mapError(sqlFailure(operation)));
1272
+
1022
1273
  const decodedMax = yield* decodeRows(
1023
1274
  Schema.Array(MaxQueueSequenceRow),
1024
1275
  "effect_agent_submissions",
1025
- validated.conversationId,
1276
+ validated.threadId,
1026
1277
  maxRows,
1027
1278
  ).pipe(Effect.mapError(internalFailure(operation)));
1279
+
1028
1280
  const queueSequence = yield* decodeQueueSequence(
1029
1281
  (decodedMax[0]?.max_queue_sequence ?? 0) + 1,
1030
1282
  ).pipe(Effect.mapError(internalFailure(operation)));
1283
+
1031
1284
  const now = yield* currentInstant;
1032
1285
 
1033
1286
  yield* sql`
1034
1287
  INSERT INTO effect_agent_submissions (
1035
1288
  submission_id,
1036
- conversation_id,
1289
+ thread_id,
1037
1290
  queue_sequence,
1038
1291
  principal,
1039
1292
  idempotency_key,
@@ -1046,10 +1299,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1046
1299
  state,
1047
1300
  created_at,
1048
1301
  parent_submission_id,
1049
- parent_tool_call_id
1302
+ parent_tool_call_id,
1303
+ admission_group,
1304
+ admission_fence_json,
1305
+ worker_admission_json,
1306
+ message_admission_json
1050
1307
  ) VALUES (
1051
1308
  ${mintedSubmissionId},
1052
- ${validated.conversationId},
1309
+ ${validated.threadId},
1053
1310
  ${queueSequence},
1054
1311
  ${validated.principal},
1055
1312
  ${validated.idempotencyKey},
@@ -1062,7 +1319,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1062
1319
  'admitted',
1063
1320
  ${now.iso},
1064
1321
  ${validated.parentLinkage?.parentSubmissionId ?? null},
1065
- ${validated.parentLinkage?.parentToolCallId ?? null}
1322
+ ${validated.parentLinkage?.parentToolCallId ?? null},
1323
+ ${validated.admissionGroup ?? null},
1324
+ ${validated.admissionFence === undefined ? null : JSON.stringify(validated.admissionFence)},
1325
+ ${workerAdmissionJson},
1326
+ ${messageAdmissionJson}
1066
1327
  )
1067
1328
  `.pipe(Effect.mapError(sqlFailure(operation)));
1068
1329
 
@@ -1075,7 +1336,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1075
1336
  }).pipe(Effect.mapError(internalFailure(operation)));
1076
1337
  }),
1077
1338
  );
1339
+
1078
1340
  yield* hitFailpoint("ledger:admit:after", operation);
1341
+
1079
1342
  return result;
1080
1343
  },
1081
1344
  );
@@ -1084,16 +1347,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1084
1347
  "DoSubmissionLedger.markReady",
1085
1348
  )(function* (request: MarkReadyRequest) {
1086
1349
  const operation = "ledger mark ready";
1087
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkReadyRequest))(
1088
- request,
1089
- ).pipe(Effect.mapError(internalFailure(operation)));
1350
+
1351
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkReadyRequest))(request).pipe(
1352
+ Effect.mapError(internalFailure(operation)),
1353
+ );
1354
+
1090
1355
  yield* hitFailpoint("ledger:mark-ready:before", operation);
1091
1356
  yield* inWriteTransaction(
1092
1357
  operation,
1093
1358
  Effect.gen(function* () {
1094
1359
  const submission = yield* requireSubmission(operation, validated.submissionId);
1360
+
1095
1361
  if (submission.state !== "admitted") return;
1096
1362
  const now = yield* currentInstant;
1363
+
1097
1364
  yield* sql`
1098
1365
  UPDATE effect_agent_submissions
1099
1366
  SET state = 'ready', ready_at = ${now.iso}
@@ -1107,40 +1374,48 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1107
1374
  const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn("DoSubmissionLedger.lookup")(
1108
1375
  function* (request: SubmissionLookup) {
1109
1376
  const operation = "ledger lookup";
1110
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookup))(
1111
- request,
1112
- ).pipe(Effect.mapError(internalFailure(operation)));
1377
+
1378
+ const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookup))(request).pipe(
1379
+ Effect.mapError(internalFailure(operation)),
1380
+ );
1381
+
1113
1382
  if (validated._tag === "SubmissionLookupById") {
1114
1383
  const row = yield* readSubmission(operation, validated.submissionId);
1384
+
1115
1385
  if (Option.isNone(row)) return Option.none();
1386
+
1116
1387
  return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
1117
1388
  }
1389
+
1118
1390
  const rows = yield* sql<Record<string, unknown>>`
1119
1391
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1120
1392
  FROM effect_agent_submissions
1121
- WHERE conversation_id = ${validated.conversationId}
1393
+ WHERE thread_id = ${validated.threadId}
1122
1394
  AND principal = ${validated.principal}
1123
1395
  AND idempotency_key = ${validated.idempotencyKey}
1124
1396
  `.pipe(Effect.mapError(sqlFailure(operation)));
1397
+
1125
1398
  const decoded = yield* decodeSubmissionRows(
1126
1399
  operation,
1127
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1400
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1128
1401
  rows,
1129
1402
  );
1403
+
1130
1404
  if (decoded.length > 1) {
1131
1405
  return yield* corruptionFailure(
1132
1406
  operation,
1133
1407
  "effect_agent_submissions",
1134
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1408
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1135
1409
  "An admission idempotency key returned more than one row.",
1136
1410
  );
1137
1411
  }
1138
1412
  if (decoded.length === 0) return Option.none();
1413
+
1139
1414
  return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
1140
1415
  },
1141
1416
  );
1142
1417
 
1143
- // This LOCAL facet is the authoritative owner of every Conversation stored in this Durable
1418
+ // This LOCAL facet is the authoritative owner of every Thread stored in this Durable
1144
1419
  // Object, so the key-scoped read IS the admission truth and the tri-state degenerates to
1145
1420
  // NotAdmitted or Admitted (SUB-031). `AdmissionIndeterminate` becomes real one layer out:
1146
1421
  // the WP2 routed decorator answers it when the OWNING Durable Object is unreachable.
@@ -1148,30 +1423,35 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1148
1423
  "DoSubmissionLedger.resolveAdmission",
1149
1424
  )(function* (request: SubmissionLookupByKey) {
1150
1425
  const operation = "ledger resolve admission";
1151
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookupByKey))(
1426
+
1427
+ const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookupByKey))(
1152
1428
  request,
1153
1429
  ).pipe(Effect.mapError(internalFailure(operation)));
1430
+
1154
1431
  const rows = yield* sql<Record<string, unknown>>`
1155
1432
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1156
1433
  FROM effect_agent_submissions
1157
- WHERE conversation_id = ${validated.conversationId}
1434
+ WHERE thread_id = ${validated.threadId}
1158
1435
  AND principal = ${validated.principal}
1159
1436
  AND idempotency_key = ${validated.idempotencyKey}
1160
1437
  `.pipe(Effect.mapError(sqlFailure(operation)));
1438
+
1161
1439
  const decoded = yield* decodeSubmissionRows(
1162
1440
  operation,
1163
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1441
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1164
1442
  rows,
1165
1443
  );
1444
+
1166
1445
  if (decoded.length > 1) {
1167
1446
  return yield* corruptionFailure(
1168
1447
  operation,
1169
1448
  "effect_agent_submissions",
1170
- `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1449
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1171
1450
  "An admission idempotency key returned more than one row.",
1172
1451
  );
1173
1452
  }
1174
1453
  if (decoded.length === 0) return AdmissionNotAdmitted.make();
1454
+
1175
1455
  return AdmissionAdmitted.make({
1176
1456
  submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
1177
1457
  });
@@ -1180,71 +1460,103 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1180
1460
  const claim: SubmissionLedger["Service"]["claim"] = Effect.fn("DoSubmissionLedger.claim")(
1181
1461
  function* (request: ClaimRequest) {
1182
1462
  const operation = "ledger claim";
1183
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimRequest))(
1184
- request,
1185
- ).pipe(Effect.mapError(internalFailure(operation)));
1463
+
1464
+ const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
1465
+ Effect.mapError(internalFailure(operation)),
1466
+ );
1467
+
1186
1468
  const attemptId = `attempt-${yield* mintUuid(operation)}`;
1187
1469
  const ownershipToken = `owner-${yield* mintUuid(operation)}`;
1470
+
1188
1471
  yield* hitFailpoint("ledger:claim:before", operation);
1472
+
1189
1473
  const claimed = yield* inWriteTransaction(
1190
1474
  operation,
1191
1475
  Effect.gen(function* () {
1476
+ const now = yield* currentInstant;
1477
+
1478
+ // Ownership belongs to the whole Thread, including unknown work skipped below.
1479
+ // Stored lease instants are normalized UTC strings, so the latest expiry covers
1480
+ // every live lease. This check and the epoch grant share one write transaction.
1481
+ const ownershipRows = yield* sql<Record<string, unknown>>`
1482
+ SELECT ownership.*
1483
+ FROM effect_agent_submission_ownership AS ownership
1484
+ JOIN effect_agent_submissions AS submission
1485
+ ON submission.submission_id = ownership.submission_id
1486
+ WHERE submission.thread_id = ${validated.threadId}
1487
+ ORDER BY ownership.lease_expires_at DESC
1488
+ LIMIT 1
1489
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1490
+
1491
+ const ownership = yield* decodeRows(
1492
+ Schema.Array(OwnershipRow),
1493
+ "effect_agent_submission_ownership",
1494
+ validated.threadId,
1495
+ ownershipRows,
1496
+ ).pipe(Effect.mapError(internalFailure(operation)));
1497
+
1498
+ if (ownership.length > 0) {
1499
+ const expiresAt = yield* timestampMillis(
1500
+ operation,
1501
+ ownership[0].submission_id,
1502
+ )(ownership[0].lease_expires_at);
1503
+
1504
+ if (expiresAt > now.millis) return Option.none<Claim>();
1505
+ }
1506
+
1192
1507
  const headRows = yield* sql<Record<string, unknown>>`
1193
1508
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1194
1509
  FROM effect_agent_submissions
1195
- WHERE conversation_id = ${validated.conversationId}
1510
+ WHERE thread_id = ${validated.threadId}
1196
1511
  AND state <> 'settled'
1512
+ AND (
1513
+ state <> 'unknown' OR EXISTS (
1514
+ SELECT 1 FROM effect_agent_abort_intents
1515
+ WHERE submission_id = effect_agent_submissions.submission_id
1516
+ )
1517
+ )
1197
1518
  ORDER BY queue_sequence ASC
1198
1519
  LIMIT 1
1199
1520
  `.pipe(Effect.mapError(sqlFailure(operation)));
1200
- const heads = yield* decodeSubmissionRows(operation, validated.conversationId, headRows);
1521
+
1522
+ const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
1523
+
1201
1524
  if (heads.length === 0) return Option.none<Claim>();
1202
1525
  const head = heads[0];
1203
1526
 
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).
1527
+ // Approval/delegation suspension and joined work retain their queue barrier.
1528
+ // Unknown work with an abort intent is selected for cleanup without Tool replay.
1207
1529
  if (
1208
1530
  head.state === "joining" ||
1209
1531
  head.state === "joined" ||
1210
1532
  head.state === "suspended" ||
1211
- head.state === "unknown"
1533
+ (head.state === "unknown" &&
1534
+ Option.isNone(yield* readAbortIntent(operation, head.submission_id)))
1212
1535
  ) {
1213
1536
  return Option.none<Claim>();
1214
1537
  }
1215
1538
 
1216
- const now = yield* currentInstant;
1217
- const ownership = yield* readOwnership(operation, head.submission_id);
1218
- if (Option.isSome(ownership)) {
1219
- const expiresAt = yield* timestampMillis(
1220
- operation,
1221
- head.submission_id,
1222
- )(ownership.value.lease_expires_at);
1223
- // A live lease blocks every new claim; expiry alone only revokes the liveness
1224
- // assumption — correctness stays with producer-epoch fencing (D5). In DC a live
1225
- // lease under another token can only come from an evicted incarnation.
1226
- if (expiresAt > now.millis) return Option.none<Claim>();
1227
- }
1228
-
1229
- // Bump the Conversation's producer epoch atomically with the claim so every stale
1230
- // Attempt is fenced out of canonical appends (DUR-006). A Conversation that was
1539
+ // Bump the Thread's producer epoch atomically with the claim so every stale
1540
+ // Attempt is fenced out of canonical appends (DUR-006). A Thread that was
1231
1541
  // never materialized (eviction between admission and materialization) is created
1232
1542
  // here so recovery can claim first and re-materialize idempotently at this epoch.
1233
- const conversations = yield* journal
1234
- .getConversation(head.conversation_id)
1543
+ const threads = yield* journal
1544
+ .getThread(head.thread_id)
1235
1545
  .pipe(Effect.mapError(internalFailure(operation)));
1546
+
1236
1547
  let producerEpoch: number;
1237
- if (conversations.length === 0) {
1548
+
1549
+ if (threads.length === 0) {
1238
1550
  producerEpoch = 1;
1239
1551
  yield* sql`
1240
- INSERT INTO effect_agent_conversations (
1241
- conversation_id,
1552
+ INSERT INTO effect_agent_threads (
1553
+ thread_id,
1242
1554
  created_at,
1243
1555
  tail_sequence,
1244
1556
  tail_digest,
1245
1557
  producer_epoch
1246
1558
  ) VALUES (
1247
- ${head.conversation_id},
1559
+ ${head.thread_id},
1248
1560
  ${now.iso},
1249
1561
  0,
1250
1562
  ${EMPTY_TAIL_DIGEST},
@@ -1252,15 +1564,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1252
1564
  )
1253
1565
  `.pipe(Effect.mapError(sqlFailure(operation)));
1254
1566
  } else {
1255
- producerEpoch = conversations[0].producer_epoch + 1;
1567
+ producerEpoch = threads[0].producer_epoch + 1;
1256
1568
  yield* sql`
1257
- UPDATE effect_agent_conversations
1569
+ UPDATE effect_agent_threads
1258
1570
  SET producer_epoch = ${producerEpoch}
1259
- WHERE conversation_id = ${head.conversation_id}
1571
+ WHERE thread_id = ${head.thread_id}
1260
1572
  `.pipe(Effect.mapError(sqlFailure(operation)));
1261
1573
  }
1262
1574
 
1263
1575
  const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1576
+
1264
1577
  yield* sql`
1265
1578
  INSERT INTO effect_agent_submission_ownership (
1266
1579
  submission_id,
@@ -1289,14 +1602,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1289
1602
  INSERT INTO effect_agent_attempts (
1290
1603
  attempt_id,
1291
1604
  submission_id,
1292
- conversation_id,
1605
+ thread_id,
1293
1606
  owner_producer_id,
1294
1607
  producer_epoch,
1295
1608
  claimed_at
1296
1609
  ) VALUES (
1297
1610
  ${attemptId},
1298
1611
  ${head.submission_id},
1299
- ${head.conversation_id},
1612
+ ${head.thread_id},
1300
1613
  ${validated.producerId},
1301
1614
  ${producerEpoch},
1302
1615
  ${now.iso}
@@ -1321,6 +1634,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1321
1634
  ),
1322
1635
  ),
1323
1636
  );
1637
+
1324
1638
  return Option.some(
1325
1639
  yield* decodeClaim({
1326
1640
  submissionId: head.submission_id,
@@ -1333,7 +1647,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1333
1647
  );
1334
1648
  }),
1335
1649
  );
1650
+
1336
1651
  yield* hitFailpoint("ledger:claim:after", operation);
1652
+
1337
1653
  return claimed;
1338
1654
  },
1339
1655
  );
@@ -1342,29 +1658,37 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1342
1658
  "DoSubmissionLedger.renewOwnership",
1343
1659
  )(function* (request: RenewOwnershipRequest) {
1344
1660
  const operation = "ledger renew ownership";
1345
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RenewOwnershipRequest))(
1661
+
1662
+ const validated = yield* Schema.decodeEffect(Schema.toType(RenewOwnershipRequest))(
1346
1663
  request,
1347
1664
  ).pipe(Effect.mapError(internalFailure(operation)));
1665
+
1348
1666
  yield* hitFailpoint("ledger:renew:before", operation);
1667
+
1349
1668
  const renewal = yield* inWriteTransaction(
1350
1669
  operation,
1351
1670
  Effect.gen(function* () {
1352
1671
  const submission = yield* requireSubmission(operation, validated.submissionId);
1672
+
1353
1673
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1354
1674
  const now = yield* currentInstant;
1355
1675
  const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1676
+
1356
1677
  yield* sql`
1357
1678
  UPDATE effect_agent_submission_ownership
1358
1679
  SET lease_expires_at = ${leaseExpiresAt}
1359
1680
  WHERE submission_id = ${validated.submissionId}
1360
1681
  `.pipe(Effect.mapError(sqlFailure(operation)));
1682
+
1361
1683
  return yield* decodeOwnershipRenewal({
1362
1684
  ownershipToken: validated.ownershipToken,
1363
1685
  leaseExpiresAt,
1364
1686
  }).pipe(Effect.mapError(internalFailure(operation)));
1365
1687
  }),
1366
1688
  );
1689
+
1367
1690
  yield* hitFailpoint("ledger:renew:after", operation);
1691
+
1368
1692
  return renewal;
1369
1693
  });
1370
1694
 
@@ -1372,14 +1696,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1372
1696
  "DoSubmissionLedger.releaseOwnership",
1373
1697
  )(function* (request: ReleaseOwnershipRequest) {
1374
1698
  const operation = "ledger release ownership";
1375
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseOwnershipRequest))(
1699
+
1700
+ const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseOwnershipRequest))(
1376
1701
  request,
1377
1702
  ).pipe(Effect.mapError(internalFailure(operation)));
1703
+
1378
1704
  yield* hitFailpoint("ledger:release:before", operation);
1379
1705
  yield* inWriteTransaction(
1380
1706
  operation,
1381
1707
  Effect.gen(function* () {
1382
1708
  const submission = yield* requireSubmission(operation, validated.submissionId);
1709
+
1383
1710
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1384
1711
  yield* sql`
1385
1712
  DELETE FROM effect_agent_submission_ownership
@@ -1401,14 +1728,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1401
1728
  "DoSubmissionLedger.markInputApplied",
1402
1729
  )(function* (request: MarkInputAppliedRequest) {
1403
1730
  const operation = "ledger mark input applied";
1404
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkInputAppliedRequest))(
1731
+
1732
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
1405
1733
  request,
1406
1734
  ).pipe(Effect.mapError(internalFailure(operation)));
1735
+
1407
1736
  yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1408
1737
  yield* inWriteTransaction(
1409
1738
  operation,
1410
1739
  Effect.gen(function* () {
1411
1740
  const submission = yield* requireSubmission(operation, validated.submissionId);
1741
+
1412
1742
  yield* requireOwnership(operation, submission, validated.ownershipToken);
1413
1743
  if (submission.input_applied_record_id !== null) {
1414
1744
  if (
@@ -1417,6 +1747,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1417
1747
  ) {
1418
1748
  return;
1419
1749
  }
1750
+
1420
1751
  return yield* corruptionFailure(
1421
1752
  operation,
1422
1753
  "effect_agent_submissions",
@@ -1444,34 +1775,41 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1444
1775
  "DoSubmissionLedger.reserveSettlement",
1445
1776
  )(function* (request: SettlementReservation) {
1446
1777
  const operation = "ledger reserve settlement";
1447
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementReservation))(
1778
+
1779
+ const validated = yield* Schema.decodeEffect(Schema.toType(SettlementReservation))(
1448
1780
  request,
1449
1781
  ).pipe(Effect.mapError(internalFailure(operation)));
1782
+
1450
1783
  const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(
1451
1784
  Effect.mapError(internalFailure(operation)),
1452
1785
  );
1786
+
1453
1787
  // The reserved record is appended canonically later; refuse over-bound payloads typed
1454
1788
  // before the reservation row exists.
1455
1789
  yield* journal
1456
1790
  .checkValueBound(operation, recordJson)
1457
1791
  .pipe(Effect.mapError(internalFailure(operation)));
1458
1792
  yield* hitFailpoint("ledger:reserve-settlement:before", operation);
1793
+
1459
1794
  const reserved = yield* inWriteTransaction(
1460
1795
  operation,
1461
1796
  Effect.gen(function* () {
1462
1797
  const existing = yield* readReservation(operation, validated.submissionId);
1798
+
1463
1799
  if (Option.isSome(existing)) {
1464
1800
  const identical =
1465
1801
  existing.value.settlement_id === validated.settlementId &&
1466
1802
  existing.value.outcome === validated.outcome &&
1467
1803
  existing.value.record_digest === validated.recordDigest &&
1468
1804
  existing.value.record_json === recordJson;
1805
+
1469
1806
  if (!identical) {
1470
1807
  return yield* SettlementConflict.make({
1471
1808
  submissionId: validated.submissionId,
1472
1809
  existingOutcome: existing.value.outcome,
1473
1810
  });
1474
1811
  }
1812
+
1475
1813
  const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(
1476
1814
  Effect.mapError((error) =>
1477
1815
  corruptionFailure(
@@ -1482,6 +1820,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1482
1820
  ),
1483
1821
  ),
1484
1822
  );
1823
+
1485
1824
  return ReservedSettlement.make({
1486
1825
  submissionId: validated.submissionId,
1487
1826
  settlementId: validated.settlementId,
@@ -1493,6 +1832,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1493
1832
  }
1494
1833
 
1495
1834
  const submission = yield* requireSubmission(operation, validated.submissionId);
1835
+
1496
1836
  if (submission.state === "settled") {
1497
1837
  if (submission.settled_outcome === null) {
1498
1838
  return yield* corruptionFailure(
@@ -1502,6 +1842,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1502
1842
  "A settled Submission carries no terminal outcome.",
1503
1843
  );
1504
1844
  }
1845
+
1505
1846
  return yield* SettlementConflict.make({
1506
1847
  submissionId: validated.submissionId,
1507
1848
  existingOutcome: submission.settled_outcome,
@@ -1516,13 +1857,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1516
1857
  // ABORTED settlement (`terminalizing` is the same pass's crash replay). Every other
1517
1858
  // reservation stays fenced by the target lane's live ownership.
1518
1859
  let queuedAbortSettlement = false;
1860
+
1519
1861
  if (
1520
1862
  validated.outcome === "aborted" &&
1521
1863
  (submission.state === "ready" || submission.state === "terminalizing")
1522
1864
  ) {
1523
1865
  const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
1866
+
1524
1867
  if (Option.isSome(abortIntent)) {
1525
1868
  const ownership = yield* readOwnership(operation, validated.submissionId);
1869
+
1526
1870
  queuedAbortSettlement = Option.isNone(ownership);
1527
1871
  }
1528
1872
  }
@@ -1531,6 +1875,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1531
1875
  }
1532
1876
  }
1533
1877
  const now = yield* currentInstant;
1878
+
1534
1879
  yield* sql`
1535
1880
  INSERT INTO effect_agent_settlement_reservations (
1536
1881
  submission_id,
@@ -1555,6 +1900,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1555
1900
  SET state = 'terminalizing'
1556
1901
  WHERE submission_id = ${validated.submissionId}
1557
1902
  `.pipe(Effect.mapError(sqlFailure(operation)));
1903
+
1558
1904
  return ReservedSettlement.make({
1559
1905
  submissionId: validated.submissionId,
1560
1906
  settlementId: validated.settlementId,
@@ -1565,56 +1911,197 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1565
1911
  });
1566
1912
  }),
1567
1913
  );
1914
+
1568
1915
  yield* hitFailpoint("ledger:reserve-settlement:after", operation);
1916
+
1569
1917
  return reserved;
1570
1918
  });
1571
1919
 
1920
+ const validateFinalization = Effect.fn("DoSubmissionLedger.validateFinalization")(function* (
1921
+ validated: SettlementFinalization,
1922
+ reservation: Option.Option<ReservationRow>,
1923
+ ) {
1924
+ const operation = "ledger finalize settlement";
1925
+
1926
+ if (Option.isNone(reservation)) {
1927
+ return yield* LedgerError.make({
1928
+ operation,
1929
+ message: `No settlement reservation exists for submission ${validated.submissionId}.`,
1930
+ });
1931
+ }
1932
+ if (reservation.value.settlement_id !== validated.settlementId) {
1933
+ return yield* SettlementConflict.make({
1934
+ submissionId: validated.submissionId,
1935
+ existingOutcome: reservation.value.outcome,
1936
+ });
1937
+ }
1938
+
1939
+ const reservationRecord = yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(
1940
+ Effect.mapError((error) =>
1941
+ corruptionFailure(
1942
+ operation,
1943
+ "effect_agent_settlement_reservations",
1944
+ validated.submissionId,
1945
+ error.message,
1946
+ ),
1947
+ ),
1948
+ );
1949
+
1950
+ const settlementFailure = settlementFailureFromRecord(reservationRecord);
1951
+
1952
+ if ((reservation.value.outcome === "failed") !== (settlementFailure !== undefined)) {
1953
+ return yield* corruptionFailure(
1954
+ operation,
1955
+ "effect_agent_settlement_reservations",
1956
+ validated.submissionId,
1957
+ "The reserved outcome and canonical failure diagnostic disagree.",
1958
+ );
1959
+ }
1960
+
1961
+ return { reservation: reservation.value, reservationRecord, settlementFailure };
1962
+ });
1963
+
1964
+ const replayFinalization = Effect.fn("DoSubmissionLedger.replayFinalization")(function* (
1965
+ validated: SettlementFinalization,
1966
+ submission: SubmissionRow,
1967
+ { reservation, settlementFailure }: Effect.Success<ReturnType<typeof validateFinalization>>,
1968
+ ) {
1969
+ const operation = "ledger finalize settlement";
1970
+
1971
+ if (reservation.finalized_at === null) {
1972
+ return yield* corruptionFailure(
1973
+ operation,
1974
+ "effect_agent_settlement_reservations",
1975
+ validated.submissionId,
1976
+ "A settled Submission's reservation carries no finalization timestamp.",
1977
+ );
1978
+ }
1979
+
1980
+ return yield* decodeSettlement({
1981
+ submissionId: validated.submissionId,
1982
+ settlementId: validated.settlementId,
1983
+ receiptId: submission.receipt_id,
1984
+ outcome: reservation.outcome,
1985
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1986
+ settledAt: reservation.finalized_at,
1987
+ }).pipe(Effect.mapError(internalFailure(operation)));
1988
+ });
1989
+
1572
1990
  const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
1573
1991
  "DoSubmissionLedger.finalizeSettlement",
1574
1992
  )(function* (request: SettlementFinalization) {
1575
1993
  const operation = "ledger finalize settlement";
1576
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementFinalization))(
1994
+
1995
+ const validated = yield* Schema.decodeEffect(Schema.toType(SettlementFinalization))(
1577
1996
  request,
1578
1997
  ).pipe(Effect.mapError(internalFailure(operation)));
1998
+
1579
1999
  yield* hitFailpoint("ledger:finalize-settlement:before", operation);
2000
+
2001
+ // A single statement captures the settled row and its immutable reservation together.
2002
+ // A miss does not authorize finalization: the write path re-reads under its transaction.
2003
+ const replayRows = yield* sql<Record<string, unknown>>`
2004
+ SELECT submission.*, reservation.settlement_id, reservation.outcome,
2005
+ reservation.record_id, reservation.record_json, reservation.record_digest,
2006
+ reservation.reserved_at, reservation.finalized_at
2007
+ FROM effect_agent_submissions AS submission
2008
+ INNER JOIN effect_agent_settlement_reservations AS reservation
2009
+ ON reservation.submission_id = submission.submission_id
2010
+ WHERE submission.submission_id = ${validated.submissionId} AND submission.state = 'settled'
2011
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2012
+
2013
+ if (replayRows.length > 1) {
2014
+ return yield* corruptionFailure(
2015
+ operation,
2016
+ "effect_agent_submissions",
2017
+ validated.submissionId,
2018
+ "A submission primary key returned more than one row.",
2019
+ );
2020
+ }
2021
+ const replayRow = replayRows[0];
2022
+
2023
+ if (replayRow !== undefined) {
2024
+ const reservations = yield* decodeRows(
2025
+ Schema.Array(ReservationRow),
2026
+ "effect_agent_settlement_reservations",
2027
+ validated.submissionId,
2028
+ replayRows,
2029
+ ).pipe(Effect.mapError(internalFailure(operation)));
2030
+
2031
+ const state = yield* validateFinalization(validated, Option.fromUndefinedOr(reservations[0]));
2032
+
2033
+ const submission = yield* Schema.decodeUnknownEffect(SubmissionRow)(replayRow).pipe(
2034
+ Effect.mapError((error) =>
2035
+ corruptionFailure(
2036
+ operation,
2037
+ "effect_agent_submissions",
2038
+ validated.submissionId,
2039
+ error.message,
2040
+ ),
2041
+ ),
2042
+ );
2043
+
2044
+ const settlement = yield* replayFinalization(validated, submission, state);
2045
+
2046
+ yield* hitFailpoint("ledger:finalize-settlement:after", operation);
2047
+
2048
+ return settlement;
2049
+ }
2050
+
1580
2051
  const settlement = yield* inWriteTransaction(
1581
2052
  operation,
1582
2053
  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
- }
2054
+ const state = yield* validateFinalization(
2055
+ validated,
2056
+ yield* readReservation(operation, validated.submissionId),
2057
+ );
2058
+
2059
+ const { reservation, reservationRecord, settlementFailure } = state;
1596
2060
  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.",
2061
+
2062
+ if (submission.state === "settled")
2063
+ return yield* replayFinalization(validated, submission, state);
2064
+ const now = yield* currentInstant;
2065
+
2066
+ const terminal =
2067
+ submission.worker_admission_json === null
2068
+ ? undefined
2069
+ : workerTerminalFromRecord(
2070
+ yield* decodeSubmissionSnapshot(operation, submission),
2071
+ reservationRecord,
2072
+ );
2073
+
2074
+ if (terminal !== undefined) {
2075
+ // Admission and finalization serialize here. An accepted correction that this Run
2076
+ // has not applied vetoes its completion, including admission after RunCompleted.
2077
+ const pending =
2078
+ terminal === "completed"
2079
+ ? yield* sql`SELECT submission_id FROM effect_agent_submissions
2080
+ WHERE thread_id = ${submission.thread_id} AND queue_sequence > ${submission.queue_sequence}
2081
+ AND queue_sequence = (SELECT MAX(queue_sequence) FROM effect_agent_submissions WHERE thread_id = ${submission.thread_id})
2082
+ AND (joined_host_submission_id IS NULL OR joined_host_submission_id <> ${submission.submission_id}
2083
+ OR input_applied_record_id IS NULL) LIMIT 1`.pipe(
2084
+ Effect.mapError(sqlFailure(operation)),
2085
+ )
2086
+ : [];
2087
+
2088
+ if (pending.length === 0) {
2089
+ yield* sql`INSERT OR IGNORE INTO effect_agent_worker_stops (thread_id, terminal)
2090
+ VALUES (${submission.thread_id}, ${terminal})`.pipe(
2091
+ Effect.mapError(sqlFailure(operation)),
1604
2092
  );
2093
+ yield* sql`INSERT OR IGNORE INTO effect_agent_abort_intents (submission_id, author, reason, requested_at)
2094
+ SELECT submission_id, ${submission.principal}, ${`Worker assignment ${terminal}`}, ${now.iso}
2095
+ FROM effect_agent_submissions WHERE thread_id = ${submission.thread_id} AND state <> 'settled'
2096
+ AND submission_id <> ${submission.submission_id}
2097
+ AND (joined_host_submission_id IS NULL OR joined_host_submission_id <> ${submission.submission_id}
2098
+ OR input_applied_record_id IS NULL)`.pipe(Effect.mapError(sqlFailure(operation)));
1605
2099
  }
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
2100
  }
1614
- const now = yield* currentInstant;
2101
+
1615
2102
  yield* sql`
1616
2103
  UPDATE effect_agent_submissions
1617
- SET state = 'settled', settled_outcome = ${reservation.value.outcome}
2104
+ SET state = 'settled', settled_outcome = ${reservation.outcome}
1618
2105
  WHERE submission_id = ${validated.submissionId}
1619
2106
  `.pipe(Effect.mapError(sqlFailure(operation)));
1620
2107
  yield* sql`
@@ -1626,31 +2113,117 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1626
2113
  DELETE FROM effect_agent_submission_ownership
1627
2114
  WHERE submission_id = ${validated.submissionId}
1628
2115
  `.pipe(Effect.mapError(sqlFailure(operation)));
2116
+
1629
2117
  return yield* decodeSettlement({
1630
2118
  submissionId: validated.submissionId,
1631
2119
  settlementId: validated.settlementId,
1632
2120
  receiptId: submission.receipt_id,
1633
- outcome: reservation.value.outcome,
2121
+ outcome: reservation.outcome,
2122
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1634
2123
  settledAt: now.iso,
1635
2124
  }).pipe(Effect.mapError(internalFailure(operation)));
1636
2125
  }),
1637
2126
  );
2127
+
1638
2128
  yield* hitFailpoint("ledger:finalize-settlement:after", operation);
2129
+
1639
2130
  return settlement;
1640
2131
  });
1641
2132
 
2133
+ const inspectWorker = Effect.fn("DoSubmissionLedger.inspectWorker")(function* (
2134
+ threadId: SubmissionSnapshot["threadId"],
2135
+ ) {
2136
+ const operation = "inspect worker";
2137
+
2138
+ return yield* sql
2139
+ .withTransaction(
2140
+ Effect.gen(function* () {
2141
+ const read = Effect.fnUntraced(function* (active: boolean) {
2142
+ const rows =
2143
+ yield* sql`SELECT ${sql.literal(SUBMISSION_COLUMNS)} FROM effect_agent_submissions
2144
+ WHERE thread_id = ${threadId} ${active ? sql`AND state <> 'settled'` : sql``}
2145
+ ORDER BY queue_sequence ${active ? sql`ASC` : sql`DESC`} LIMIT 1`.pipe(
2146
+ Effect.mapError(sqlFailure(operation)),
2147
+ );
2148
+
2149
+ const decoded = yield* decodeSubmissionRows(operation, threadId, rows);
2150
+
2151
+ return decoded[0] === undefined
2152
+ ? null
2153
+ : yield* decodeSubmissionSnapshot(operation, decoded[0]);
2154
+ });
2155
+
2156
+ const latest = yield* read(false);
2157
+ const active = yield* read(true);
2158
+
2159
+ const stops =
2160
+ yield* sql`SELECT terminal FROM effect_agent_worker_stops WHERE thread_id = ${threadId}`.pipe(
2161
+ Effect.mapError(sqlFailure(operation)),
2162
+ );
2163
+
2164
+ return yield* Schema.decodeUnknownEffect(Schema.toType(WorkerLedgerState))({
2165
+ latest,
2166
+ active,
2167
+ stopped: stops.length > 0,
2168
+ ...(stops[0] === undefined || stops[0].terminal === null
2169
+ ? {}
2170
+ : { terminal: stops[0].terminal }),
2171
+ }).pipe(Effect.mapError(internalFailure(operation)));
2172
+ }),
2173
+ )
2174
+ .pipe(Effect.catchTag("SqlError", (cause) => sqlFailure(operation)(cause)));
2175
+ });
2176
+
2177
+ const stopWorker = Effect.fn("DoSubmissionLedger.stopWorker")(function* (
2178
+ request: WorkerStopCommand,
2179
+ ) {
2180
+ const operation = "ledger stop worker";
2181
+
2182
+ const validated = yield* Schema.decodeEffect(WorkerStopCommand)(request).pipe(
2183
+ Effect.mapError(internalFailure(operation)),
2184
+ );
2185
+
2186
+ return yield* inWriteTransaction(
2187
+ operation,
2188
+ Effect.gen(function* () {
2189
+ const now = yield* currentInstant;
2190
+
2191
+ yield* sql`INSERT OR IGNORE INTO effect_agent_worker_stops (thread_id) VALUES (${validated.threadId})`.pipe(
2192
+ Effect.mapError(sqlFailure(operation)),
2193
+ );
2194
+ yield* sql`INSERT OR IGNORE INTO effect_agent_abort_intents (submission_id, author, reason, requested_at)
2195
+ SELECT submission_id, ${validated.author}, 'Worker owner stopped the worker', ${now.iso}
2196
+ FROM effect_agent_submissions WHERE thread_id = ${validated.threadId} AND state <> 'settled'`.pipe(
2197
+ Effect.mapError(sqlFailure(operation)),
2198
+ );
2199
+
2200
+ const rows = yield* sql`SELECT o.submission_id FROM effect_agent_submission_ownership o
2201
+ JOIN effect_agent_submissions s ON s.submission_id = o.submission_id
2202
+ WHERE s.thread_id = ${validated.threadId} AND s.state <> 'settled'`.pipe(
2203
+ Effect.mapError(sqlFailure(operation)),
2204
+ );
2205
+
2206
+ return rows.length;
2207
+ }),
2208
+ );
2209
+ });
2210
+
1642
2211
  const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fn(
1643
2212
  "DoSubmissionLedger.requestAbort",
1644
2213
  )(function* (request: AbortCommand) {
1645
2214
  const operation = "ledger request abort";
1646
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AbortCommand))(request).pipe(
2215
+
2216
+ const validated = yield* Schema.decodeEffect(Schema.toType(AbortCommand))(request).pipe(
1647
2217
  Effect.mapError(internalFailure(operation)),
1648
2218
  );
2219
+
1649
2220
  yield* hitFailpoint("ledger:request-abort:before", operation);
2221
+
1650
2222
  const intent = yield* inWriteTransaction(
1651
2223
  operation,
1652
2224
  Effect.gen(function* () {
1653
2225
  const submission = yield* requireSubmission(operation, validated.submissionId);
2226
+
1654
2227
  if (submission.state === "settled") {
1655
2228
  if (submission.settled_outcome === null) {
1656
2229
  return yield* corruptionFailure(
@@ -1660,6 +2233,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1660
2233
  "A settled Submission carries no terminal outcome.",
1661
2234
  );
1662
2235
  }
2236
+
1663
2237
  return yield* SettlementConflict.make({
1664
2238
  submissionId: validated.submissionId,
1665
2239
  existingOutcome: submission.settled_outcome,
@@ -1677,15 +2251,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1677
2251
  "A joined Submission carries no host linkage.",
1678
2252
  );
1679
2253
  }
2254
+
1680
2255
  const hostSubmissionId = yield* decodeSubmissionId(
1681
2256
  submission.joined_host_submission_id,
1682
2257
  ).pipe(Effect.mapError(internalFailure(operation)));
2258
+
1683
2259
  return yield* JoinedToHost.make({
1684
2260
  submissionId: validated.submissionId,
1685
2261
  hostSubmissionId,
1686
2262
  });
1687
2263
  }
1688
2264
  const existing = yield* readAbortIntent(operation, validated.submissionId);
2265
+
1689
2266
  if (Option.isSome(existing)) {
1690
2267
  return yield* abortIntentFromRow(
1691
2268
  operation,
@@ -1695,6 +2272,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1695
2272
  );
1696
2273
  }
1697
2274
  const now = yield* currentInstant;
2275
+
1698
2276
  yield* sql`
1699
2277
  INSERT INTO effect_agent_abort_intents (
1700
2278
  submission_id,
@@ -1708,11 +2286,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1708
2286
  ${now.iso}
1709
2287
  )
1710
2288
  `.pipe(Effect.mapError(sqlFailure(operation)));
2289
+
1711
2290
  const canonicalRecordId = yield* canonicalAbortRecordId(
1712
2291
  operation,
1713
- submission.conversation_id,
2292
+ submission.thread_id,
1714
2293
  validated.submissionId,
1715
2294
  );
2295
+
1716
2296
  return yield* decodeAbortIntent({
1717
2297
  submissionId: validated.submissionId,
1718
2298
  author: validated.author,
@@ -1722,7 +2302,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1722
2302
  }).pipe(Effect.mapError(internalFailure(operation)));
1723
2303
  }),
1724
2304
  );
2305
+
1725
2306
  yield* hitFailpoint("ledger:request-abort:after", operation);
2307
+
1726
2308
  return intent;
1727
2309
  });
1728
2310
 
@@ -1730,31 +2312,45 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1730
2312
  "DoSubmissionLedger.claimJoining",
1731
2313
  )(function* (request: ClaimJoiningRequest) {
1732
2314
  const operation = "ledger claim joining";
1733
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimJoiningRequest))(
1734
- request,
1735
- ).pipe(Effect.mapError(internalFailure(operation)));
2315
+
2316
+ const validated = yield* Schema.decodeEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(
2317
+ Effect.mapError(internalFailure(operation)),
2318
+ );
2319
+
1736
2320
  yield* hitFailpoint("ledger:claim-joining:before", operation);
2321
+
1737
2322
  const claims = yield* inWriteTransaction(
1738
2323
  operation,
1739
2324
  Effect.gen(function* () {
1740
2325
  const host = yield* requireSubmission(operation, validated.hostSubmissionId);
1741
- if (host.conversation_id !== validated.conversationId) {
2326
+
2327
+ if (host.thread_id !== validated.threadId) {
1742
2328
  return yield* LedgerError.make({
1743
2329
  operation,
1744
- message: `Host submission ${validated.hostSubmissionId} does not belong to conversation ${validated.conversationId}.`,
2330
+ message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
1745
2331
  });
1746
2332
  }
1747
2333
  // The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
1748
2334
  yield* requireOwnership(operation, host, validated.ownershipToken);
2335
+
2336
+ const stopped =
2337
+ yield* sql`SELECT thread_id FROM effect_agent_worker_stops WHERE thread_id = ${validated.threadId}`.pipe(
2338
+ Effect.mapError(sqlFailure(operation)),
2339
+ );
2340
+
2341
+ if (stopped.length > 0) return [];
2342
+
1749
2343
  const laterRows = yield* sql<Record<string, unknown>>`
1750
2344
  SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1751
2345
  FROM effect_agent_submissions
1752
- WHERE conversation_id = ${validated.conversationId}
2346
+ WHERE thread_id = ${validated.threadId}
1753
2347
  AND queue_sequence > ${host.queue_sequence}
1754
2348
  ORDER BY queue_sequence ASC
1755
2349
  `.pipe(Effect.mapError(sqlFailure(operation)));
1756
- const later = yield* decodeSubmissionRows(operation, validated.conversationId, laterRows);
2350
+
2351
+ const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
1757
2352
  const claimed: Array<JoiningClaim> = [];
2353
+
1758
2354
  for (const row of later) {
1759
2355
  if (claimed.length >= validated.maxCount) break;
1760
2356
  // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
@@ -1777,6 +2373,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1777
2373
  SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
1778
2374
  WHERE submission_id = ${row.submission_id}
1779
2375
  `.pipe(Effect.mapError(sqlFailure(operation)));
2376
+
1780
2377
  const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
1781
2378
  Effect.mapError((error) =>
1782
2379
  corruptionFailure(
@@ -1787,6 +2384,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1787
2384
  ),
1788
2385
  ),
1789
2386
  );
2387
+
1790
2388
  claimed.push(
1791
2389
  yield* decodeJoiningClaim({
1792
2390
  submissionId: row.submission_id,
@@ -1795,10 +2393,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1795
2393
  }).pipe(Effect.mapError(internalFailure(operation))),
1796
2394
  );
1797
2395
  }
1798
- return claimed as ReadonlyArray<JoiningClaim>;
2396
+
2397
+ return claimed;
1799
2398
  }),
1800
2399
  );
2400
+
1801
2401
  yield* hitFailpoint("ledger:claim-joining:after", operation);
2402
+
1802
2403
  return claims;
1803
2404
  });
1804
2405
 
@@ -1806,14 +2407,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1806
2407
  "DoSubmissionLedger.markJoined",
1807
2408
  )(function* (request: MarkJoinedRequest) {
1808
2409
  const operation = "ledger mark joined";
1809
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkJoinedRequest))(
1810
- request,
1811
- ).pipe(Effect.mapError(internalFailure(operation)));
2410
+
2411
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkJoinedRequest))(request).pipe(
2412
+ Effect.mapError(internalFailure(operation)),
2413
+ );
2414
+
1812
2415
  yield* hitFailpoint("ledger:mark-joined:before", operation);
1813
2416
  yield* inWriteTransaction(
1814
2417
  operation,
1815
2418
  Effect.gen(function* () {
1816
2419
  const submission = yield* requireSubmission(operation, validated.submissionId);
2420
+
1817
2421
  if (submission.joined_host_submission_id === null) {
1818
2422
  return yield* LedgerError.make({
1819
2423
  operation,
@@ -1821,6 +2425,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1821
2425
  });
1822
2426
  }
1823
2427
  const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
2428
+
1824
2429
  // The lane is host-owned: the presented token must own the HOST's ownership period,
1825
2430
  // which also lets a later host Attempt repair a lost marker from history (DUR-016).
1826
2431
  yield* requireOwnership(operation, host, validated.ownershipToken);
@@ -1831,6 +2436,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1831
2436
  ) {
1832
2437
  return;
1833
2438
  }
2439
+
1834
2440
  return yield* corruptionFailure(
1835
2441
  operation,
1836
2442
  "effect_agent_submissions",
@@ -1861,14 +2467,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1861
2467
  "DoSubmissionLedger.revertJoining",
1862
2468
  )(function* (request: RevertJoiningRequest) {
1863
2469
  const operation = "ledger revert joining";
1864
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RevertJoiningRequest))(
1865
- request,
1866
- ).pipe(Effect.mapError(internalFailure(operation)));
2470
+
2471
+ const validated = yield* Schema.decodeEffect(Schema.toType(RevertJoiningRequest))(request).pipe(
2472
+ Effect.mapError(internalFailure(operation)),
2473
+ );
2474
+
1867
2475
  yield* hitFailpoint("ledger:revert-joining:before", operation);
1868
2476
  yield* inWriteTransaction(
1869
2477
  operation,
1870
2478
  Effect.gen(function* () {
1871
2479
  const submission = yield* requireSubmission(operation, validated.submissionId);
2480
+
1872
2481
  // Idempotent and recovery-only: only a still-`joining` Submission reverts; an
1873
2482
  // already-joined (or already-reverted) Submission is a no-op (DUR-016).
1874
2483
  if (submission.state !== "joining") return;
@@ -1885,17 +2494,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1885
2494
  const suspend: SubmissionLedger["Service"]["suspend"] = Effect.fn("DoSubmissionLedger.suspend")(
1886
2495
  function* (request: SuspendRequest) {
1887
2496
  const operation = "ledger suspend";
1888
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SuspendRequest))(
1889
- request,
1890
- ).pipe(Effect.mapError(internalFailure(operation)));
2497
+
2498
+ const validated = yield* Schema.decodeEffect(Schema.toType(SuspendRequest))(request).pipe(
2499
+ Effect.mapError(internalFailure(operation)),
2500
+ );
2501
+
1891
2502
  const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
1892
2503
  Effect.mapError(internalFailure(operation)),
1893
2504
  );
2505
+
1894
2506
  yield* hitFailpoint("ledger:suspend:before", operation);
2507
+
1895
2508
  const outcome = yield* inWriteTransaction(
1896
2509
  operation,
1897
2510
  Effect.gen(function* () {
1898
2511
  const submission = yield* requireSubmission(operation, validated.submissionId);
2512
+
1899
2513
  if (submission.state === "settled") {
1900
2514
  if (submission.settled_outcome === null) {
1901
2515
  return yield* corruptionFailure(
@@ -1905,6 +2519,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1905
2519
  "A settled Submission carries no terminal outcome.",
1906
2520
  );
1907
2521
  }
2522
+
1908
2523
  return yield* SettlementConflict.make({
1909
2524
  submissionId: validated.submissionId,
1910
2525
  existingOutcome: submission.settled_outcome,
@@ -1913,6 +2528,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1913
2528
  // An exact terminal outcome is already reserved (DUR-011); suspension would
1914
2529
  // contradict it, so the reservation wins.
1915
2530
  const reservation = yield* readReservation(operation, validated.submissionId);
2531
+
1916
2532
  if (Option.isSome(reservation)) {
1917
2533
  return yield* SettlementConflict.make({
1918
2534
  submissionId: validated.submissionId,
@@ -1923,35 +2539,39 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1923
2539
  // A covering event that raced ahead of the suspend transaction resumes the caller
1924
2540
  // immediately WITHOUT releasing the lane (plan §2.6, §12). For WaitingForChild the
1925
2541
  // 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
2542
+ // notification marker: parent and child Threads live in different Durable
1927
2543
  // Objects, and the port contract requires that a child settlement reported (via
1928
2544
  // `recordChildSettled` → marker) before this suspend commits is observed here.
1929
2545
  if (validated.reason._tag === "ApprovalPending") {
1930
2546
  const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
1931
2547
  const decided = new Set(decisions.map((row) => row.tool_call_id));
2548
+
1932
2549
  if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
1933
- return "resume-immediately" as SuspensionOutcome;
2550
+ return RESUME_IMMEDIATELY;
1934
2551
  }
1935
2552
  } else {
1936
2553
  const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
1937
2554
  const markerChildren = new Set(markers.map((row) => row.child_submission_id));
1938
2555
  let allSettled = true;
2556
+
1939
2557
  for (const child of validated.reason.children) {
1940
2558
  const settled = yield* childProvablySettled(
1941
2559
  operation,
1942
2560
  markerChildren,
1943
2561
  child.childSubmissionId,
1944
2562
  );
2563
+
1945
2564
  if (!settled) {
1946
2565
  allSettled = false;
1947
2566
  break;
1948
2567
  }
1949
2568
  }
1950
2569
  if (allSettled) {
1951
- return "resume-immediately" as SuspensionOutcome;
2570
+ return RESUME_IMMEDIATELY;
1952
2571
  }
1953
2572
  }
1954
2573
  const now = yield* currentInstant;
2574
+
1955
2575
  yield* sql`
1956
2576
  UPDATE effect_agent_submissions
1957
2577
  SET
@@ -1966,10 +2586,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1966
2586
  DELETE FROM effect_agent_submission_ownership
1967
2587
  WHERE submission_id = ${validated.submissionId}
1968
2588
  `.pipe(Effect.mapError(sqlFailure(operation)));
1969
- return "suspended" as SuspensionOutcome;
2589
+
2590
+ return SUSPENDED;
1970
2591
  }),
1971
2592
  );
2593
+
1972
2594
  yield* hitFailpoint("ledger:suspend:after", operation);
2595
+
1973
2596
  return outcome;
1974
2597
  },
1975
2598
  );
@@ -1985,6 +2608,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1985
2608
  submission: SubmissionRow,
1986
2609
  ): Effect.fn.Return<void, LedgerError> {
1987
2610
  if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
2611
+
1988
2612
  const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
1989
2613
  submission.suspended_reason_json,
1990
2614
  ).pipe(
@@ -1997,9 +2621,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1997
2621
  ),
1998
2622
  ),
1999
2623
  );
2624
+
2000
2625
  if (reason._tag !== "ApprovalPending") return;
2001
2626
  const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
2002
2627
  const decided = new Set(decisions.map((row) => row.tool_call_id));
2628
+
2003
2629
  if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
2004
2630
  yield* sql`
2005
2631
  UPDATE effect_agent_submissions
@@ -2015,14 +2641,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2015
2641
  "DoSubmissionLedger.recordApprovalDecision",
2016
2642
  )(function* (command: ApprovalDecisionCommand) {
2017
2643
  const operation = "ledger record approval decision";
2018
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ApprovalDecisionCommand))(
2644
+
2645
+ const validated = yield* Schema.decodeEffect(Schema.toType(ApprovalDecisionCommand))(
2019
2646
  command,
2020
2647
  ).pipe(Effect.mapError(internalFailure(operation)));
2648
+
2021
2649
  yield* hitFailpoint("ledger:approval-decision:before", operation);
2650
+
2022
2651
  const intent = yield* inWriteTransaction(
2023
2652
  operation,
2024
2653
  Effect.gen(function* () {
2025
2654
  const submission = yield* requireSubmission(operation, validated.submissionId);
2655
+
2026
2656
  if (submission.state === "settled") {
2027
2657
  if (submission.settled_outcome === null) {
2028
2658
  return yield* corruptionFailure(
@@ -2032,6 +2662,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2032
2662
  "A settled Submission carries no terminal outcome.",
2033
2663
  );
2034
2664
  }
2665
+
2035
2666
  return yield* SettlementConflict.make({
2036
2667
  submissionId: validated.submissionId,
2037
2668
  existingOutcome: submission.settled_outcome,
@@ -2039,6 +2670,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2039
2670
  }
2040
2671
  const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
2041
2672
  const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
2673
+
2042
2674
  if (existing !== undefined) {
2043
2675
  // Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
2044
2676
  // the recorded intent unchanged; a divergent re-decision conflicts.
@@ -2049,9 +2681,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2049
2681
  existingDecision: existing.decision,
2050
2682
  });
2051
2683
  }
2684
+
2052
2685
  return yield* approvalIntentFromRow(operation, existing);
2053
2686
  }
2054
2687
  const now = yield* currentInstant;
2688
+
2055
2689
  yield* sql`
2056
2690
  INSERT INTO effect_agent_approval_decisions (
2057
2691
  submission_id,
@@ -2070,6 +2704,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2070
2704
  )
2071
2705
  `.pipe(Effect.mapError(sqlFailure(operation)));
2072
2706
  yield* wakeSuspendedIfCovered(operation, submission);
2707
+
2073
2708
  return yield* decodeApprovalDecisionIntent({
2074
2709
  submissionId: validated.submissionId,
2075
2710
  toolCallId: validated.toolCallId,
@@ -2080,7 +2715,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2080
2715
  }).pipe(Effect.mapError(internalFailure(operation)));
2081
2716
  }),
2082
2717
  );
2718
+
2083
2719
  yield* hitFailpoint("ledger:approval-decision:after", operation);
2720
+
2084
2721
  return intent;
2085
2722
  });
2086
2723
 
@@ -2088,14 +2725,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2088
2725
  "DoSubmissionLedger.markUnknown",
2089
2726
  )(function* (request: MarkUnknownRequest) {
2090
2727
  const operation = "ledger mark unknown";
2091
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkUnknownRequest))(
2092
- request,
2093
- ).pipe(Effect.mapError(internalFailure(operation)));
2728
+
2729
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkUnknownRequest))(request).pipe(
2730
+ Effect.mapError(internalFailure(operation)),
2731
+ );
2732
+
2094
2733
  yield* hitFailpoint("ledger:mark-unknown:before", operation);
2095
2734
  yield* inWriteTransaction(
2096
2735
  operation,
2097
2736
  Effect.gen(function* () {
2098
2737
  const submission = yield* requireSubmission(operation, validated.submissionId);
2738
+
2099
2739
  if (submission.state === "settled") {
2100
2740
  if (submission.settled_outcome === null) {
2101
2741
  return yield* corruptionFailure(
@@ -2105,6 +2745,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2105
2745
  "A settled Submission carries no terminal outcome.",
2106
2746
  );
2107
2747
  }
2748
+
2108
2749
  return yield* SettlementConflict.make({
2109
2750
  submissionId: validated.submissionId,
2110
2751
  existingOutcome: submission.settled_outcome,
@@ -2113,6 +2754,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2113
2754
  // A reserved exact outcome wins over a late Unknown marking (DUR-011); the recovery
2114
2755
  // classifier orders reservation ahead of MarkUnknown for the same reason.
2115
2756
  const reservation = yield* readReservation(operation, validated.submissionId);
2757
+
2116
2758
  if (Option.isSome(reservation)) {
2117
2759
  return yield* SettlementConflict.make({
2118
2760
  submissionId: validated.submissionId,
@@ -2123,13 +2765,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2123
2765
  // set while the first recorded reason is kept.
2124
2766
  const existingIds = yield* storedUnknownToolCallIds(operation, submission);
2125
2767
  const known = new Set(existingIds);
2768
+
2126
2769
  const merged = [
2127
2770
  ...existingIds,
2128
2771
  ...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
2129
2772
  ];
2773
+
2130
2774
  const idsJson = yield* encodeToolCallIdsText(merged).pipe(
2131
2775
  Effect.mapError(internalFailure(operation)),
2132
2776
  );
2777
+
2133
2778
  yield* sql`
2134
2779
  UPDATE effect_agent_submissions
2135
2780
  SET
@@ -2147,17 +2792,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2147
2792
  "DoSubmissionLedger.recordUnknownResolution",
2148
2793
  )(function* (command: UnknownResolutionCommand) {
2149
2794
  const operation = "ledger record unknown resolution";
2150
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(UnknownResolutionCommand))(
2795
+
2796
+ const validated = yield* Schema.decodeEffect(Schema.toType(UnknownResolutionCommand))(
2151
2797
  command,
2152
2798
  ).pipe(Effect.mapError(internalFailure(operation)));
2799
+
2153
2800
  const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
2154
2801
  Effect.mapError(internalFailure(operation)),
2155
2802
  );
2803
+
2156
2804
  yield* hitFailpoint("ledger:unknown-resolution:before", operation);
2805
+
2157
2806
  const intent = yield* inWriteTransaction(
2158
2807
  operation,
2159
2808
  Effect.gen(function* () {
2160
2809
  const submission = yield* requireSubmission(operation, validated.submissionId);
2810
+
2161
2811
  if (submission.state === "settled") {
2162
2812
  if (submission.settled_outcome === null) {
2163
2813
  return yield* corruptionFailure(
@@ -2167,6 +2817,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2167
2817
  "A settled Submission carries no terminal outcome.",
2168
2818
  );
2169
2819
  }
2820
+
2170
2821
  return yield* SettlementConflict.make({
2171
2822
  submissionId: validated.submissionId,
2172
2823
  existingOutcome: submission.settled_outcome,
@@ -2174,19 +2825,30 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2174
2825
  }
2175
2826
  const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
2176
2827
  const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
2177
- if (existing !== undefined && existing.resolution_json !== resolutionJson) {
2828
+
2829
+ const existingIntent =
2830
+ existing === undefined
2831
+ ? undefined
2832
+ : yield* unknownResolutionIntentFromRow(operation, existing);
2833
+
2834
+ if (
2835
+ existingIntent !== undefined &&
2836
+ !equivalentUnknownResolution(existingIntent.resolution, validated.resolution)
2837
+ ) {
2178
2838
  return yield* UnknownResolutionConflict.make({
2179
2839
  submissionId: validated.submissionId,
2180
2840
  toolCallId: validated.toolCallId,
2181
2841
  });
2182
2842
  }
2183
2843
  let resolved: UnknownResolutionIntent;
2184
- if (existing !== undefined) {
2844
+
2845
+ if (existingIntent !== undefined) {
2185
2846
  // Idempotent replay of the recorded intent (author/reason may differ; the stored
2186
2847
  // audit fields win, exactly like requestAbort).
2187
- resolved = yield* unknownResolutionIntentFromRow(operation, existing);
2848
+ resolved = existingIntent;
2188
2849
  } else {
2189
2850
  const now = yield* currentInstant;
2851
+
2190
2852
  yield* sql`
2191
2853
  INSERT INTO effect_agent_unknown_resolutions (
2192
2854
  submission_id,
@@ -2204,9 +2866,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2204
2866
  ${now.iso}
2205
2867
  )
2206
2868
  `.pipe(Effect.mapError(sqlFailure(operation)));
2869
+
2207
2870
  const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
2208
2871
  Effect.mapError(internalFailure(operation)),
2209
2872
  );
2873
+
2210
2874
  resolved = yield* decodeUnknownResolutionIntent({
2211
2875
  submissionId: validated.submissionId,
2212
2876
  toolCallId: validated.toolCallId,
@@ -2223,6 +2887,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2223
2887
  const markedIds = yield* storedUnknownToolCallIds(operation, submission);
2224
2888
  const covering = yield* readUnknownResolutions(operation, validated.submissionId);
2225
2889
  const coveredIds = new Set(covering.map((row) => row.tool_call_id));
2890
+
2226
2891
  if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) {
2227
2892
  yield* sql`
2228
2893
  UPDATE effect_agent_submissions
@@ -2234,10 +2899,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2234
2899
  `.pipe(Effect.mapError(sqlFailure(operation)));
2235
2900
  }
2236
2901
  }
2902
+
2237
2903
  return resolved;
2238
2904
  }),
2239
2905
  );
2906
+
2240
2907
  yield* hitFailpoint("ledger:unknown-resolution:after", operation);
2908
+
2241
2909
  return intent;
2242
2910
  });
2243
2911
 
@@ -2245,21 +2913,31 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2245
2913
  "DoSubmissionLedger.recordChildSettled",
2246
2914
  )(function* (request: ChildSettledNotification) {
2247
2915
  const operation = "ledger record child settled";
2248
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ChildSettledNotification))(
2916
+
2917
+ const validated = yield* Schema.decodeEffect(Schema.toType(ChildSettledNotification))(
2249
2918
  request,
2250
2919
  ).pipe(Effect.mapError(internalFailure(operation)));
2920
+
2251
2921
  yield* hitFailpoint("ledger:child-settled:before", operation);
2922
+
2252
2923
  const outcome = yield* inWriteTransaction(
2253
2924
  operation,
2254
2925
  Effect.gen(function* () {
2255
2926
  const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2256
2927
  // 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.
2928
+ // row lives in THIS store (single-store latitude, and every conformance lane), either a
2929
+ // finalized row or an exact terminalizing reservation admits the notification: the
2930
+ // runtime calls only after the canonical append and before ledger finalization. When the
2931
+ // row does not live here — the normal cross-DO case the routed notification from the
2932
+ // child's owning Durable Object is the settlement evidence this store records durably.
2261
2933
  const child = yield* readSubmission(operation, validated.childSubmissionId);
2262
- if (Option.isSome(child) && child.value.state !== "settled") {
2934
+ const childReservation = yield* readReservation(operation, validated.childSubmissionId);
2935
+
2936
+ if (
2937
+ Option.isSome(child) &&
2938
+ child.value.state !== "settled" &&
2939
+ !(child.value.state === "terminalizing" && Option.isSome(childReservation))
2940
+ ) {
2263
2941
  return yield* LedgerError.make({
2264
2942
  operation,
2265
2943
  message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`,
@@ -2270,6 +2948,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2270
2948
  // before the parent's suspend transaction is observed by that suspend's covering
2271
2949
  // check, even when the parent is not (or not yet) suspended.
2272
2950
  const now = yield* currentInstant;
2951
+
2273
2952
  yield* sql`
2274
2953
  INSERT INTO effect_agent_child_settlements (
2275
2954
  parent_submission_id,
@@ -2279,15 +2958,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2279
2958
  ) VALUES (
2280
2959
  ${validated.parentSubmissionId},
2281
2960
  ${validated.childSubmissionId},
2282
- ${Option.isSome(child) ? child.value.settled_outcome : null},
2961
+ ${
2962
+ Option.isSome(child) && child.value.state === "settled"
2963
+ ? child.value.settled_outcome
2964
+ : Option.isSome(childReservation)
2965
+ ? childReservation.value.outcome
2966
+ : null
2967
+ },
2283
2968
  ${now.iso}
2284
2969
  )
2285
2970
  ON CONFLICT (parent_submission_id, child_submission_id) DO NOTHING
2286
2971
  `.pipe(Effect.mapError(sqlFailure(operation)));
2287
2972
 
2288
2973
  if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
2289
- return "not-waiting" as ChildSettledOutcome;
2974
+ return NOT_WAITING;
2290
2975
  }
2976
+
2291
2977
  const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
2292
2978
  parent.suspended_reason_json,
2293
2979
  ).pipe(
@@ -2300,27 +2986,30 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2300
2986
  ),
2301
2987
  ),
2302
2988
  );
2989
+
2303
2990
  if (reason._tag !== "WaitingForChild") {
2304
- return "not-waiting" as ChildSettledOutcome;
2991
+ return NOT_WAITING;
2305
2992
  }
2306
2993
  if (
2307
2994
  !reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
2308
2995
  ) {
2309
- return "not-waiting" as ChildSettledOutcome;
2996
+ return NOT_WAITING;
2310
2997
  }
2311
2998
  // The parent wakes exactly when EVERY listed child is provably settled — from its
2312
2999
  // local row or a recorded marker (spec §12 step 10); replays re-run the coverage
2313
3000
  // check so a recovering caller wakes the lane idempotently.
2314
3001
  const markers = yield* readChildSettlementMarkers(operation, validated.parentSubmissionId);
2315
3002
  const markerChildren = new Set(markers.map((row) => row.child_submission_id));
3003
+
2316
3004
  for (const entry of reason.children) {
2317
3005
  const settled = yield* childProvablySettled(
2318
3006
  operation,
2319
3007
  markerChildren,
2320
3008
  entry.childSubmissionId,
2321
3009
  );
3010
+
2322
3011
  if (!settled) {
2323
- return "still-waiting" as ChildSettledOutcome;
3012
+ return STILL_WAITING;
2324
3013
  }
2325
3014
  }
2326
3015
  yield* sql`
@@ -2331,10 +3020,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2331
3020
  suspended_at = NULL
2332
3021
  WHERE submission_id = ${validated.parentSubmissionId}
2333
3022
  `.pipe(Effect.mapError(sqlFailure(operation)));
2334
- return "woken" as ChildSettledOutcome;
3023
+
3024
+ return WOKEN;
2335
3025
  }),
2336
3026
  );
3027
+
2337
3028
  yield* hitFailpoint("ledger:child-settled:after", operation);
3029
+
2338
3030
  return outcome;
2339
3031
  });
2340
3032
 
@@ -2342,25 +3034,36 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2342
3034
  "DoSubmissionLedger.reserveChildBudget",
2343
3035
  )(function* (request: ChildBudgetReservationRequest) {
2344
3036
  const operation = "ledger reserve child budget";
2345
- const validated = yield* Schema.decodeUnknownEffect(
2346
- Schema.toType(ChildBudgetReservationRequest),
2347
- )(request).pipe(Effect.mapError(internalFailure(operation)));
3037
+
3038
+ const validated = yield* Schema.decodeEffect(Schema.toType(ChildBudgetReservationRequest))(
3039
+ request,
3040
+ ).pipe(Effect.mapError(internalFailure(operation)));
3041
+
2348
3042
  const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
2349
3043
  Effect.mapError(internalFailure(operation)),
2350
3044
  );
3045
+
2351
3046
  yield* hitFailpoint("ledger:child-reservation:before", operation);
3047
+
2352
3048
  const reserved = yield* inWriteTransaction(
2353
3049
  operation,
2354
3050
  Effect.gen(function* () {
2355
3051
  const existing = yield* readChildReservation(operation, validated.reservationId);
3052
+
2356
3053
  if (Option.isSome(existing)) {
3054
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
3055
+ operation,
3056
+ existing.value,
3057
+ );
3058
+
2357
3059
  // Identical replays short-circuit before the fence, mirroring reserveSettlement: a
2358
3060
  // replay creates nothing, so a recovering caller resumes rather than duplicates.
2359
3061
  const identical =
2360
3062
  existing.value.parent_submission_id === validated.parentSubmissionId &&
2361
3063
  existing.value.parent_tool_call_id === validated.parentToolCallId &&
2362
3064
  existing.value.allocation_digest === validated.allocationDigest &&
2363
- existing.value.allocation_json === allocationJson;
3065
+ equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
3066
+
2364
3067
  if (!identical) {
2365
3068
  return yield* ChildReservationConflict.make({
2366
3069
  reservationId: validated.reservationId,
@@ -2369,16 +3072,19 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2369
3072
  "A reservation with this identity exists with a different parent Tool Call or allocation.",
2370
3073
  });
2371
3074
  }
3075
+
2372
3076
  return ReservedChildBudget.make({
2373
- reservation: yield* childReservationSnapshotFromRow(operation, existing.value),
3077
+ reservation: existingSnapshot,
2374
3078
  replayed: true,
2375
3079
  });
2376
3080
  }
3081
+
2377
3082
  const collision = yield* readChildReservationForCall(
2378
3083
  operation,
2379
3084
  validated.parentSubmissionId,
2380
3085
  validated.parentToolCallId,
2381
3086
  );
3087
+
2382
3088
  if (Option.isSome(collision)) {
2383
3089
  return yield* ChildReservationConflict.make({
2384
3090
  reservationId: validated.reservationId,
@@ -2387,10 +3093,12 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2387
3093
  });
2388
3094
  }
2389
3095
  const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
3096
+
2390
3097
  // Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
2391
3098
  // parent Attempt can never create new reservation state.
2392
3099
  yield* requireOwnership(operation, parent, validated.ownershipToken);
2393
3100
  const now = yield* currentInstant;
3101
+
2394
3102
  yield* sql`
2395
3103
  INSERT INTO effect_agent_child_reservations (
2396
3104
  reservation_id,
@@ -2411,6 +3119,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2411
3119
  )
2412
3120
  `.pipe(Effect.mapError(sqlFailure(operation)));
2413
3121
  const inserted = yield* readChildReservation(operation, validated.reservationId);
3122
+
2414
3123
  if (Option.isNone(inserted)) {
2415
3124
  return yield* corruptionFailure(
2416
3125
  operation,
@@ -2419,13 +3128,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2419
3128
  "An inserted child reservation row is missing inside its own transaction.",
2420
3129
  );
2421
3130
  }
3131
+
2422
3132
  return ReservedChildBudget.make({
2423
3133
  reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
2424
3134
  replayed: false,
2425
3135
  });
2426
3136
  }),
2427
3137
  );
3138
+
2428
3139
  yield* hitFailpoint("ledger:child-reservation:after", operation);
3140
+
2429
3141
  return reserved;
2430
3142
  });
2431
3143
 
@@ -2434,14 +3146,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2434
3146
  request: AttachChildToReservationRequest,
2435
3147
  ) {
2436
3148
  const operation = "ledger attach child to reservation";
2437
- const validated = yield* Schema.decodeUnknownEffect(
2438
- Schema.toType(AttachChildToReservationRequest),
2439
- )(request).pipe(Effect.mapError(internalFailure(operation)));
3149
+
3150
+ const validated = yield* Schema.decodeEffect(Schema.toType(AttachChildToReservationRequest))(
3151
+ request,
3152
+ ).pipe(Effect.mapError(internalFailure(operation)));
3153
+
2440
3154
  yield* hitFailpoint("ledger:child-attach:before", operation);
3155
+
2441
3156
  const attached = yield* inWriteTransaction(
2442
3157
  operation,
2443
3158
  Effect.gen(function* () {
2444
3159
  const existing = yield* readChildReservation(operation, validated.reservationId);
3160
+
2445
3161
  if (Option.isNone(existing)) {
2446
3162
  return yield* LedgerError.make({
2447
3163
  operation,
@@ -2453,6 +3169,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2453
3169
  if (existing.value.child_submission_id === validated.childSubmissionId) {
2454
3170
  return yield* childReservationSnapshotFromRow(operation, existing.value);
2455
3171
  }
3172
+
2456
3173
  return yield* ChildReservationConflict.make({
2457
3174
  reservationId: validated.reservationId,
2458
3175
  status: existing.value.status,
@@ -2460,6 +3177,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2460
3177
  });
2461
3178
  }
2462
3179
  const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
3180
+
2463
3181
  yield* requireOwnership(operation, parent, validated.ownershipToken);
2464
3182
  if (existing.value.status !== "reserved") {
2465
3183
  return yield* ChildReservationConflict.make({
@@ -2478,6 +3196,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2478
3196
  WHERE reservation_id = ${validated.reservationId}
2479
3197
  `.pipe(Effect.mapError(sqlFailure(operation)));
2480
3198
  const updated = yield* readChildReservation(operation, validated.reservationId);
3199
+
2481
3200
  if (Option.isNone(updated)) {
2482
3201
  return yield* corruptionFailure(
2483
3202
  operation,
@@ -2486,10 +3205,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2486
3205
  "An updated child reservation row is missing inside its own transaction.",
2487
3206
  );
2488
3207
  }
3208
+
2489
3209
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2490
3210
  }),
2491
3211
  );
3212
+
2492
3213
  yield* hitFailpoint("ledger:child-attach:after", operation);
3214
+
2493
3215
  return attached;
2494
3216
  });
2495
3217
 
@@ -2497,17 +3219,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2497
3219
  "DoSubmissionLedger.beginChildBudgetRelease",
2498
3220
  )(function* (request: BeginChildBudgetReleaseRequest) {
2499
3221
  const operation = "ledger begin child budget release";
2500
- const validated = yield* Schema.decodeUnknownEffect(
2501
- Schema.toType(BeginChildBudgetReleaseRequest),
2502
- )(request).pipe(Effect.mapError(internalFailure(operation)));
3222
+
3223
+ const validated = yield* Schema.decodeEffect(Schema.toType(BeginChildBudgetReleaseRequest))(
3224
+ request,
3225
+ ).pipe(Effect.mapError(internalFailure(operation)));
3226
+
2503
3227
  const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
2504
3228
  Effect.mapError(internalFailure(operation)),
2505
3229
  );
3230
+
2506
3231
  yield* hitFailpoint("ledger:child-release-pending:before", operation);
3232
+
2507
3233
  const frozen = yield* inWriteTransaction(
2508
3234
  operation,
2509
3235
  Effect.gen(function* () {
2510
3236
  const existing = yield* readChildReservation(operation, validated.reservationId);
3237
+
2511
3238
  if (Option.isNone(existing)) {
2512
3239
  return yield* LedgerError.make({
2513
3240
  operation,
@@ -2515,11 +3242,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2515
3242
  });
2516
3243
  }
2517
3244
  if (existing.value.status !== "reserved") {
3245
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
3246
+ operation,
3247
+ existing.value,
3248
+ );
3249
+
2518
3250
  // The accounting decision was already frozen exactly once; an identical replay is a
2519
3251
  // 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);
3252
+ if (
3253
+ existingSnapshot.accounting !== undefined &&
3254
+ equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
3255
+ ) {
3256
+ return existingSnapshot;
2522
3257
  }
3258
+
2523
3259
  return yield* ChildReservationConflict.make({
2524
3260
  reservationId: validated.reservationId,
2525
3261
  status: existing.value.status,
@@ -2527,6 +3263,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2527
3263
  });
2528
3264
  }
2529
3265
  const now = yield* currentInstant;
3266
+
2530
3267
  yield* sql`
2531
3268
  UPDATE effect_agent_child_reservations
2532
3269
  SET
@@ -2536,6 +3273,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2536
3273
  WHERE reservation_id = ${validated.reservationId}
2537
3274
  `.pipe(Effect.mapError(sqlFailure(operation)));
2538
3275
  const updated = yield* readChildReservation(operation, validated.reservationId);
3276
+
2539
3277
  if (Option.isNone(updated)) {
2540
3278
  return yield* corruptionFailure(
2541
3279
  operation,
@@ -2544,10 +3282,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2544
3282
  "An updated child reservation row is missing inside its own transaction.",
2545
3283
  );
2546
3284
  }
3285
+
2547
3286
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2548
3287
  }),
2549
3288
  );
3289
+
2550
3290
  yield* hitFailpoint("ledger:child-release-pending:after", operation);
3291
+
2551
3292
  return frozen;
2552
3293
  });
2553
3294
 
@@ -2555,14 +3296,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2555
3296
  "DoSubmissionLedger.releaseChildBudget",
2556
3297
  )(function* (request: ReleaseChildBudgetRequest) {
2557
3298
  const operation = "ledger release child budget";
2558
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseChildBudgetRequest))(
3299
+
3300
+ const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseChildBudgetRequest))(
2559
3301
  request,
2560
3302
  ).pipe(Effect.mapError(internalFailure(operation)));
3303
+
2561
3304
  yield* hitFailpoint("ledger:child-release:before", operation);
3305
+
2562
3306
  const released = yield* inWriteTransaction(
2563
3307
  operation,
2564
3308
  Effect.gen(function* () {
2565
3309
  const existing = yield* readChildReservation(operation, validated.reservationId);
3310
+
2566
3311
  if (Option.isNone(existing)) {
2567
3312
  return yield* LedgerError.make({
2568
3313
  operation,
@@ -2582,12 +3327,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2582
3327
  });
2583
3328
  }
2584
3329
  const now = yield* currentInstant;
3330
+
2585
3331
  yield* sql`
2586
3332
  UPDATE effect_agent_child_reservations
2587
3333
  SET status = 'released', released_at = ${now.iso}
2588
3334
  WHERE reservation_id = ${validated.reservationId}
2589
3335
  `.pipe(Effect.mapError(sqlFailure(operation)));
2590
3336
  const updated = yield* readChildReservation(operation, validated.reservationId);
3337
+
2591
3338
  if (Option.isNone(updated)) {
2592
3339
  return yield* corruptionFailure(
2593
3340
  operation,
@@ -2596,76 +3343,159 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2596
3343
  "An updated child reservation row is missing inside its own transaction.",
2597
3344
  );
2598
3345
  }
3346
+
2599
3347
  return yield* childReservationSnapshotFromRow(operation, updated.value);
2600
3348
  }),
2601
3349
  );
3350
+
2602
3351
  yield* hitFailpoint("ledger:child-release:after", operation);
3352
+
2603
3353
  return released;
2604
3354
  });
2605
3355
 
2606
3356
  interface ScanCursor {
2607
- readonly conversationId: string;
3357
+ readonly threadId: string;
2608
3358
  readonly queueSequence: number;
2609
3359
  }
2610
3360
 
2611
3361
  const scanPage = Effect.fn("DoSubmissionLedger.scanPage")(function* (
2612
3362
  cursor: ScanCursor | undefined,
2613
3363
  ): Effect.fn.Return<
2614
- readonly [ReadonlyArray<SubmissionSnapshot>, Option.Option<ScanCursor | undefined>],
3364
+ readonly [ReadonlyArray<SubmissionWorkItem>, Option.Option<ScanCursor | undefined>],
2615
3365
  LedgerError
2616
3366
  > {
2617
3367
  const operation = "ledger scan nonterminal";
3368
+
2618
3369
  const rows = yield* (
2619
3370
  cursor === undefined
2620
3371
  ? sql<Record<string, unknown>>`
2621
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3372
+ SELECT submission_id AS "submissionId", thread_id AS "threadId",
3373
+ queue_sequence AS "queueSequence", principal, idempotency_key AS "idempotencyKey",
3374
+ deployment_id AS "deploymentId", receipt_id AS "receiptId", state
2622
3375
  FROM effect_agent_submissions
2623
3376
  WHERE state <> 'settled'
2624
- ORDER BY conversation_id ASC, queue_sequence ASC
3377
+ ORDER BY thread_id ASC, queue_sequence ASC
2625
3378
  LIMIT ${SCAN_PAGE_SIZE}
2626
3379
  `
2627
3380
  : sql<Record<string, unknown>>`
2628
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
3381
+ SELECT submission_id AS "submissionId", thread_id AS "threadId",
3382
+ queue_sequence AS "queueSequence", principal, idempotency_key AS "idempotencyKey",
3383
+ deployment_id AS "deploymentId", receipt_id AS "receiptId", state
2629
3384
  FROM effect_agent_submissions
2630
3385
  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
3386
+ AND (thread_id, queue_sequence) > (${cursor.threadId}, ${cursor.queueSequence})
3387
+ ORDER BY thread_id ASC, queue_sequence ASC
2639
3388
  LIMIT ${SCAN_PAGE_SIZE}
2640
3389
  `
2641
3390
  ).pipe(Effect.mapError(sqlFailure(operation)));
2642
- const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
2643
- const snapshots = yield* Effect.forEach(decoded, (row) =>
2644
- decodeSubmissionSnapshot(operation, row),
2645
- );
3391
+
3392
+ const decoded = yield* decodeRows(
3393
+ Schema.Array(SubmissionWorkItem),
3394
+ "effect_agent_submissions",
3395
+ "nonterminal_scan",
3396
+ rows,
3397
+ "SubmissionWorkItem",
3398
+ ).pipe(Effect.mapError(internalFailure(operation)));
3399
+
2646
3400
  const last = decoded[decoded.length - 1];
3401
+
2647
3402
  const next: Option.Option<ScanCursor | undefined> =
2648
3403
  last === undefined || decoded.length < SCAN_PAGE_SIZE
2649
3404
  ? Option.none()
2650
3405
  : Option.some({
2651
- conversationId: last.conversation_id,
2652
- queueSequence: last.queue_sequence,
3406
+ threadId: last.threadId,
3407
+ queueSequence: last.queueSequence,
2653
3408
  });
2654
- return [snapshots, next] as const;
3409
+
3410
+ return [decoded, next] as const;
2655
3411
  });
2656
3412
 
2657
- const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate(
2658
- undefined as ScanCursor | undefined,
2659
- scanPage,
2660
- );
3413
+ const scanNonterminal: Stream.Stream<SubmissionWorkItem, LedgerError> = Stream.paginate<
3414
+ ScanCursor | undefined,
3415
+ SubmissionWorkItem,
3416
+ LedgerError
3417
+ >(undefined, scanPage);
3418
+
3419
+ const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
3420
+ "DoSubmissionLedger.readAbortIntentForSubmission",
3421
+ )(function* (request) {
3422
+ const operation = "ledger read abort intent";
3423
+
3424
+ const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
3425
+ Effect.mapError(internalFailure(operation)),
3426
+ );
3427
+
3428
+ const recordId = submissionAbortRecordId(validated.submissionId);
3429
+
3430
+ const rows = yield* sql<Record<string, unknown>>`
3431
+ SELECT
3432
+ submission.submission_id,
3433
+ abort.submission_id AS abort_submission_id,
3434
+ abort.author,
3435
+ abort.reason,
3436
+ abort.requested_at,
3437
+ canonical.record_id AS canonical_record_id
3438
+ FROM effect_agent_submissions AS submission
3439
+ LEFT JOIN effect_agent_abort_intents AS abort
3440
+ ON abort.submission_id = submission.submission_id
3441
+ LEFT JOIN effect_agent_canonical_records AS canonical
3442
+ ON canonical.thread_id = submission.thread_id
3443
+ AND canonical.record_id = ${recordId}
3444
+ WHERE submission.submission_id = ${validated.submissionId}
3445
+ `.pipe(Effect.mapError(sqlFailure(operation)));
3446
+
3447
+ const decoded = yield* decodeRows(
3448
+ Schema.Array(AbortIntentLookupRow),
3449
+ "effect_agent_abort_intents",
3450
+ validated.submissionId,
3451
+ rows,
3452
+ ).pipe(Effect.mapError(internalFailure(operation)));
3453
+
3454
+ if (decoded.length === 0) {
3455
+ return yield* LedgerError.make({
3456
+ operation,
3457
+ message: `Unknown submission ${validated.submissionId}.`,
3458
+ });
3459
+ }
3460
+ if (decoded.length !== 1) {
3461
+ return yield* corruptionFailure(
3462
+ operation,
3463
+ "effect_agent_abort_intents",
3464
+ validated.submissionId,
3465
+ "An abort intent lookup returned more than one row.",
3466
+ );
3467
+ }
3468
+ const row = decoded[0];
3469
+
3470
+ if (row.abort_submission_id === null) return undefined;
3471
+
3472
+ return yield* decodeAbortIntent({
3473
+ submissionId: row.abort_submission_id,
3474
+ author: row.author,
3475
+ reason: row.reason,
3476
+ requestedAt: row.requested_at,
3477
+ ...(row.canonical_record_id === null ? {} : { canonicalRecordId: row.canonical_record_id }),
3478
+ }).pipe(
3479
+ Effect.mapError((error) =>
3480
+ corruptionFailure(
3481
+ operation,
3482
+ "effect_agent_abort_intents",
3483
+ validated.submissionId,
3484
+ error.message,
3485
+ ),
3486
+ ),
3487
+ );
3488
+ });
2661
3489
 
2662
3490
  const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
2663
3491
  "DoSubmissionLedger.loadRecoverySnapshot",
2664
3492
  )(function* (request: RecoverySnapshotRequest) {
2665
3493
  const operation = "ledger load recovery snapshot";
2666
- const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RecoverySnapshotRequest))(
3494
+
3495
+ const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
2667
3496
  request,
2668
3497
  ).pipe(Effect.mapError(internalFailure(operation)));
3498
+
2669
3499
  return yield* sql
2670
3500
  .withTransaction(
2671
3501
  Effect.gen(function* () {
@@ -2674,6 +3504,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2674
3504
 
2675
3505
  let ownership: OwnershipSnapshot | undefined;
2676
3506
  const ownershipRow = yield* readOwnership(operation, validated.submissionId);
3507
+
2677
3508
  if (Option.isSome(ownershipRow)) {
2678
3509
  ownership = yield* decodeOwnershipSnapshot({
2679
3510
  attemptId: ownershipRow.value.attempt_id,
@@ -2684,6 +3515,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2684
3515
  }
2685
3516
 
2686
3517
  let inputApplied: InputAppliedMarker | undefined;
3518
+
2687
3519
  if (
2688
3520
  submissionRow.input_applied_record_id !== null &&
2689
3521
  submissionRow.input_applied_sequence !== null
@@ -2696,6 +3528,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2696
3528
 
2697
3529
  let reservation: SettlementReservationSnapshot | undefined;
2698
3530
  const reservationRow = yield* readReservation(operation, validated.submissionId);
3531
+
2699
3532
  if (Option.isSome(reservationRow)) {
2700
3533
  const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(
2701
3534
  Effect.mapError((error) =>
@@ -2707,9 +3540,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2707
3540
  ),
2708
3541
  ),
2709
3542
  );
2710
- const settlementId = yield* Schema.decodeUnknownEffect(
3543
+
3544
+ const settlementId = yield* Schema.decodeEffect(
2711
3545
  SettlementReservationSnapshot.fields.settlementId,
2712
3546
  )(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
3547
+
2713
3548
  reservation = SettlementReservationSnapshot.make({
2714
3549
  settlementId,
2715
3550
  outcome: reservationRow.value.outcome,
@@ -2721,6 +3556,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2721
3556
 
2722
3557
  let abortIntent: AbortIntent | undefined;
2723
3558
  const abortRow = yield* readAbortIntent(operation, validated.submissionId);
3559
+
2724
3560
  if (Option.isSome(abortRow)) {
2725
3561
  abortIntent = yield* abortIntentFromRow(
2726
3562
  operation,
@@ -2738,11 +3574,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2738
3574
  WHERE joined_host_submission_id = ${validated.submissionId}
2739
3575
  ORDER BY queue_sequence ASC
2740
3576
  `.pipe(Effect.mapError(sqlFailure(operation)));
3577
+
2741
3578
  const joinSubmissions = yield* decodeSubmissionRows(
2742
3579
  operation,
2743
3580
  validated.submissionId,
2744
3581
  joinRows,
2745
3582
  );
3583
+
2746
3584
  const joins = yield* Effect.forEach(joinSubmissions, (row) =>
2747
3585
  decodeJoinSnapshot({
2748
3586
  submissionId: row.submission_id,
@@ -2752,6 +3590,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2752
3590
  );
2753
3591
 
2754
3592
  let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
3593
+
2755
3594
  if (submissionRow.joined_host_submission_id !== null) {
2756
3595
  hostSubmissionId = yield* decodeSubmissionId(
2757
3596
  submissionRow.joined_host_submission_id,
@@ -2759,6 +3598,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2759
3598
  }
2760
3599
 
2761
3600
  let suspension: SuspensionSnapshot | undefined;
3601
+
2762
3602
  if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
2763
3603
  const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(
2764
3604
  Effect.mapError((error) =>
@@ -2770,6 +3610,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2770
3610
  ),
2771
3611
  ),
2772
3612
  );
3613
+
2773
3614
  suspension = yield* decodeSuspensionSnapshot({
2774
3615
  reason,
2775
3616
  suspendedAt: submissionRow.suspended_at,
@@ -2786,11 +3627,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2786
3627
  }
2787
3628
 
2788
3629
  const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
3630
+
2789
3631
  const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
2790
3632
  approvalIntentFromRow(operation, row),
2791
3633
  );
2792
3634
 
2793
3635
  const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
3636
+
2794
3637
  const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
2795
3638
  unknownResolutionIntentFromRow(operation, row),
2796
3639
  );
@@ -2807,20 +3650,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2807
3650
  WHERE parent_submission_id = ${validated.submissionId}
2808
3651
  ORDER BY parent_tool_call_id ASC
2809
3652
  `.pipe(Effect.mapError(sqlFailure(operation)));
3653
+
2810
3654
  const decodedChildReservations = yield* decodeChildReservationRows(
2811
3655
  operation,
2812
3656
  validated.submissionId,
2813
3657
  childReservationRows,
2814
3658
  );
3659
+
2815
3660
  const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
2816
3661
  childReservationSnapshotFromRow(operation, row),
2817
3662
  );
3663
+
2818
3664
  const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
2819
3665
  const markersByChild = new Map(markers.map((row) => [row.child_submission_id, row]));
2820
3666
  const childAttachments: Array<ChildAttachmentSnapshot> = [];
3667
+
2821
3668
  for (const row of decodedChildReservations) {
2822
3669
  if (row.child_submission_id === null) continue;
2823
3670
  const child = yield* readSubmission(operation, row.child_submission_id);
3671
+
2824
3672
  if (Option.isSome(child)) {
2825
3673
  childAttachments.push(
2826
3674
  yield* decodeChildAttachmentSnapshot({
@@ -2835,6 +3683,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2835
3683
  continue;
2836
3684
  }
2837
3685
  const marker = markersByChild.get(row.child_submission_id);
3686
+
2838
3687
  if (marker === undefined) continue;
2839
3688
  childAttachments.push(
2840
3689
  yield* decodeChildAttachmentSnapshot({
@@ -2847,6 +3696,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2847
3696
  }
2848
3697
 
2849
3698
  let parentLinkage: ParentLinkage | undefined;
3699
+
2850
3700
  if (
2851
3701
  submissionRow.parent_submission_id !== null &&
2852
3702
  submissionRow.parent_tool_call_id !== null
@@ -2892,6 +3742,8 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2892
3742
  reserveSettlement,
2893
3743
  finalizeSettlement,
2894
3744
  requestAbort,
3745
+ stopWorker,
3746
+ inspectWorker,
2895
3747
  claimJoining,
2896
3748
  markJoined,
2897
3749
  revertJoining,
@@ -2906,6 +3758,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2906
3758
  releaseChildBudget,
2907
3759
  scanNonterminal,
2908
3760
  loadRecoverySnapshot,
3761
+ readAbortIntent: readAbortIntentForSubmission,
2909
3762
  }),
2910
3763
  );
2911
3764
  });
@@ -2923,18 +3776,22 @@ export const submissionLedgerLayer: Layer.Layer<
2923
3776
 
2924
3777
  /**
2925
3778
  * 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.
3779
+ * same `ctx.storage` as the ThreadStore so claims fence the same producer epochs.
2927
3780
  */
2928
3781
  export const ledgerLayer = (
2929
3782
  options: DoStorageOptions,
2930
3783
  ): 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,
3784
+ Layer.unwrap(
3785
+ Effect.map(DoStorageConfig, (config) =>
3786
+ submissionLedgerLayer.pipe(
3787
+ Layer.provide(
3788
+ Layer.mergeAll(
3789
+ Layer.succeed(DoStorageConfig)(config),
3790
+ storageFailpointLayer(options),
3791
+ SqliteClient.layer({ storage: options.storage }),
3792
+ BrowserCrypto.layer,
3793
+ ),
3794
+ ),
2938
3795
  ),
2939
3796
  ),
2940
- );
3797
+ ).pipe(Layer.provide(storageConfigLayer(options)));