@yielded/agent-storage-cloudflare 0.1.0-beta.167 → 0.1.0-beta.168

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 (71) hide show
  1. package/dist/DoMemoryStore.mjs +1 -1
  2. package/dist/DoMessageDeliveryStore.d.mts +1 -1
  3. package/dist/DoMessageDeliveryStore.mjs +3 -2
  4. package/dist/DoMessageDeliveryStore.mjs.map +1 -1
  5. package/dist/DoScheduleStore.mjs +16 -37
  6. package/dist/DoScheduleStore.mjs.map +1 -1
  7. package/dist/DoStorageConfig.d.mts +2 -10
  8. package/dist/DoStorageConfig.mjs +2 -11
  9. package/dist/DoStorageConfig.mjs.map +1 -1
  10. package/dist/{DoStorageError-CGuAfSwF.d.mts → DoStorageError-CxjMnzUA.d.mts} +7 -18
  11. package/dist/DoStorageError.d.mts +2 -2
  12. package/dist/DoStorageError.mjs +14 -30
  13. package/dist/DoStorageError.mjs.map +1 -1
  14. package/dist/DoStorageFailpoint.d.mts +1 -1
  15. package/dist/DoStorageFailpointTesting.d.mts +1 -1
  16. package/dist/{DoStorageVersion-iNSyi3cJ.d.mts → DoStorageVersion-DTFoLglB.d.mts} +3 -4
  17. package/dist/DoStorageVersion.d.mts +1 -1
  18. package/dist/DoStorageVersion.mjs +2 -2
  19. package/dist/DoStorageVersion.mjs.map +1 -1
  20. package/dist/DoSubmissionLedger.d.mts +7 -5
  21. package/dist/DoSubmissionLedger.mjs +378 -528
  22. package/dist/DoSubmissionLedger.mjs.map +1 -1
  23. package/dist/DoSubscriptionStore.mjs +5 -24
  24. package/dist/DoSubscriptionStore.mjs.map +1 -1
  25. package/dist/{DoThreadStore-D_83OjCH.d.mts → DoThreadStore-Bd4fTns3.d.mts} +17 -18
  26. package/dist/DoThreadStore-CwBvTLoq.mjs +468 -0
  27. package/dist/DoThreadStore-CwBvTLoq.mjs.map +1 -0
  28. package/dist/DoThreadStore.d.mts +2 -2
  29. package/dist/DoThreadStore.mjs +4 -529
  30. package/dist/MemoryProtocol.mjs +3 -3
  31. package/dist/MemoryProtocol.mjs.map +1 -1
  32. package/dist/PortProtocol.d.mts +11727 -3410
  33. package/dist/PortProtocol.mjs +103 -15
  34. package/dist/PortProtocol.mjs.map +1 -1
  35. package/dist/PortRouting.d.mts +14 -9
  36. package/dist/PortRouting.mjs +155 -29
  37. package/dist/PortRouting.mjs.map +1 -1
  38. package/dist/do-journal-Cawnsetj.mjs +761 -0
  39. package/dist/do-journal-Cawnsetj.mjs.map +1 -0
  40. package/dist/index.d.mts +3 -3
  41. package/dist/index.mjs +1 -1
  42. package/dist/migrations-D_EP3KYf.mjs +148 -0
  43. package/dist/migrations-D_EP3KYf.mjs.map +1 -0
  44. package/dist/{owned-state-DHm2aRXx.mjs → owned-state-BHzUi0eb.mjs} +50 -19
  45. package/dist/owned-state-BHzUi0eb.mjs.map +1 -0
  46. package/package.json +1 -1
  47. package/src/DoMessageDeliveryStore.ts +2 -1
  48. package/src/DoScheduleStore.ts +19 -59
  49. package/src/DoStorageConfig.ts +2 -11
  50. package/src/DoStorageError.ts +3 -26
  51. package/src/DoStorageVersion.ts +1 -1
  52. package/src/DoSubmissionLedger.ts +1806 -2030
  53. package/src/DoSubscriptionStore.ts +5 -47
  54. package/src/DoThreadStore.ts +276 -622
  55. package/src/MemoryProtocol.ts +3 -3
  56. package/src/PortProtocol.ts +198 -12
  57. package/src/PortRouting.ts +527 -108
  58. package/src/internal/canonical-append.ts +15 -0
  59. package/src/internal/do-journal.ts +548 -1440
  60. package/src/internal/migrations.ts +184 -287
  61. package/src/internal/owned-state.ts +81 -30
  62. package/src/internal/prompt-checkpoint.ts +158 -0
  63. package/src/internal/storage-span.ts +2 -5
  64. package/dist/DoThreadStore.mjs.map +0 -1
  65. package/dist/do-journal-DUhjZkOf.mjs +0 -1352
  66. package/dist/do-journal-DUhjZkOf.mjs.map +0 -1
  67. package/dist/migrations-DtKz_Gxr.mjs +0 -306
  68. package/dist/migrations-DtKz_Gxr.mjs.map +0 -1
  69. package/dist/owned-state-DHm2aRXx.mjs.map +0 -1
  70. package/src/internal/message-delivery-schema.ts +0 -24
  71. package/src/internal/recovery-checkpoint-schema.ts +0 -17
@@ -1,10 +1,22 @@
1
1
  import { BrowserCrypto } from "@effect/platform-browser";
2
2
  import { SqliteClient } from "@effect/sql-sqlite-do";
3
+ import {
4
+ decodeAbortFact,
5
+ decodeAdmissionFact,
6
+ decodeApprovalFact,
7
+ decodeResolutionFact,
8
+ } from "@yielded/agent-storage-sql/sql-admission-facts";
9
+ import {
10
+ definitionJson,
11
+ makeCanonicalStorage,
12
+ recordBatchId,
13
+ runKey,
14
+ } from "@yielded/agent-storage-sql/sql-canonical-storage";
3
15
  import {
4
16
  SqlStorageProgress,
5
17
  type SqlStorageProgressKind,
6
18
  } from "@yielded/agent-storage-sql/sql-storage-progress";
7
- import { EMPTY_TAIL_DIGEST } from "@yielded/agent/digest";
19
+ import { canonicalRecordJson } from "@yielded/agent-storage-sql/sql-thread-archive-range";
8
20
  import type { LifecyclePublicationFact } from "@yielded/agent/lifecycle-publication";
9
21
  import { InputMessage } from "@yielded/agent/messaging";
10
22
  import {
@@ -14,10 +26,18 @@ import {
14
26
  Digest,
15
27
  PersistedJson,
16
28
  WorkerAdmission,
17
- ProducerEpoch,
18
29
  RecordEnvelope,
19
30
  SettlementOutcome,
20
31
  } from "@yielded/agent/records";
32
+ import { runIdForSubmission } from "@yielded/agent/run-journal";
33
+ import { StorageOwner } from "@yielded/agent/run-storage";
34
+ import {
35
+ SettlementPublicationResult,
36
+ SettlementPublisher,
37
+ validatePublication,
38
+ validateCanonicalSettlement,
39
+ validateJoinedSettlement,
40
+ } from "@yielded/agent/settlement-publisher";
21
41
  import {
22
42
  AbortCommand,
23
43
  WorkerStopCommand,
@@ -37,6 +57,7 @@ import {
37
57
  ApprovalConflict,
38
58
  ApprovalDecisionCommand,
39
59
  ApprovalDecisionIntent,
60
+ approvalDecisionsCover,
40
61
  AttachChildToReservationRequest,
41
62
  BeginChildBudgetReleaseRequest,
42
63
  ChildAttachmentSnapshot,
@@ -59,29 +80,24 @@ import {
59
80
  MarkReadyRequest,
60
81
  MarkUnknownRequest,
61
82
  OwnershipLost,
62
- OwnershipRenewal,
63
- OwnershipSnapshot,
64
83
  ParentLinkage,
65
84
  QueueSequence,
66
85
  RecoverySnapshot,
67
86
  RecoverySnapshotRequest,
68
87
  ReleaseChildBudgetRequest,
69
- ReleaseOwnershipRequest,
70
- RenewOwnershipRequest,
71
88
  ReservedChildBudget,
72
- ReservedSettlement,
73
89
  RevertJoiningRequest,
74
90
  Settlement,
75
91
  SettlementConflict,
76
92
  SettlementFinalization,
77
- SettlementReservation,
78
93
  SubmissionLedger,
79
94
  SubmissionLookup,
80
95
  SubmissionLookupByKey,
81
96
  SubmissionSnapshot,
97
+ FundingOwnerRequest,
98
+ FundingOwner,
82
99
  SubmissionWorkItem,
83
100
  SubmissionState,
84
- SettlementReservationSnapshot,
85
101
  settlementFailureFromRecord,
86
102
  SuspendRequest,
87
103
  SuspensionReason,
@@ -90,11 +106,22 @@ import {
90
106
  UnknownResolutionCommand,
91
107
  UnknownResolutionConflict,
92
108
  UnknownResolutionIntent,
109
+ UnknownResolutionKind,
110
+ unknownResolutionKind,
111
+ unknownResolutionMatchesResult,
93
112
  submissionAbortRecordId,
113
+ submissionSettlementRecordId,
114
+ submissionSettlementBatchId,
94
115
  type ChildSettledOutcome,
95
116
  type SuspensionOutcome,
96
117
  } from "@yielded/agent/submission-ledger";
97
- import { ThreadStoreDiagnostic } from "@yielded/agent/thread-store";
118
+ import {
119
+ makeTransferFactCheck,
120
+ AppendConflict,
121
+ ThreadNotMaterialized,
122
+ ThreadStoreDiagnostic,
123
+ ThreadStoreError,
124
+ } from "@yielded/agent/thread-store";
98
125
  import { AssignmentTerminal } from "@yielded/agent/worker";
99
126
  import {
100
127
  Cause,
@@ -125,9 +152,9 @@ import {
125
152
  type DoStorageInitializationError,
126
153
  type DoStorageOptions,
127
154
  } from "./DoThreadStore.ts";
155
+ import { prepareCanonicalAppend } from "./internal/canonical-append.ts";
128
156
  import { decodeRows, initializeDoJournal } from "./internal/do-journal.ts";
129
157
  import { ownedRows } from "./internal/owned-state.ts";
130
- import { withStorageSpan } from "./internal/storage-span.ts";
131
158
 
132
159
  type SubmissionId = SubmissionSnapshot["submissionId"];
133
160
 
@@ -140,7 +167,7 @@ const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
140
167
  const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
141
168
 
142
169
  const SCAN_PAGE_SIZE = 256;
143
- const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
170
+ const admissionFitsTransfer = makeTransferFactCheck(AdmissionRequest);
144
171
  const RESUME_IMMEDIATELY: SuspensionOutcome = "resume-immediately";
145
172
  const SUSPENDED: SuspensionOutcome = "suspended";
146
173
  const NOT_WAITING: ChildSettledOutcome = "not-waiting";
@@ -162,6 +189,8 @@ class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
162
189
  receipt_id: BoundedIdentifier,
163
190
  state: SubmissionState,
164
191
  settled_outcome: Schema.NullOr(SettlementOutcome),
192
+ settled_record_id: Schema.NullOr(BoundedIdentifier),
193
+ finalized_at: Schema.NullOr(BoundedTimestamp),
165
194
  created_at: BoundedTimestamp,
166
195
  ready_at: Schema.NullOr(BoundedTimestamp),
167
196
  input_applied_record_id: Schema.NullOr(BoundedIdentifier),
@@ -226,32 +255,13 @@ class ApprovalDecisionRow extends Schema.Class<ApprovalDecisionRow>("ApprovalDec
226
255
  class UnknownResolutionRow extends Schema.Class<UnknownResolutionRow>("UnknownResolutionRow")({
227
256
  submission_id: BoundedIdentifier,
228
257
  tool_call_id: BoundedIdentifier,
258
+ resolution_kind: UnknownResolutionKind,
229
259
  author: BoundedIdentifier,
230
260
  reason: BoundedStoredText,
231
261
  resolution_json: BoundedStoredText,
232
262
  resolved_at: BoundedTimestamp,
233
263
  }) {}
234
264
 
235
- class OwnershipRow extends Schema.Class<OwnershipRow>("OwnershipRow")({
236
- submission_id: BoundedIdentifier,
237
- attempt_id: BoundedIdentifier,
238
- ownership_token: BoundedIdentifier,
239
- producer_epoch: ProducerEpoch,
240
- owner_producer_id: BoundedIdentifier,
241
- lease_expires_at: BoundedTimestamp,
242
- }) {}
243
-
244
- class ReservationRow extends Schema.Class<ReservationRow>("ReservationRow")({
245
- submission_id: BoundedIdentifier,
246
- settlement_id: BoundedIdentifier,
247
- outcome: SettlementOutcome,
248
- record_id: BoundedIdentifier,
249
- record_json: BoundedStoredText,
250
- record_digest: Digest,
251
- reserved_at: BoundedTimestamp,
252
- finalized_at: Schema.NullOr(BoundedTimestamp),
253
- }) {}
254
-
255
265
  class AbortIntentRow extends Schema.Class<AbortIntentRow>("AbortIntentRow")({
256
266
  submission_id: BoundedIdentifier,
257
267
  author: BoundedIdentifier,
@@ -276,13 +286,14 @@ const SUBMISSION_COLUMNS = `
276
286
  principal,
277
287
  idempotency_key,
278
288
  agent_id,
279
- agent_digests_json,
280
289
  deployment_id,
281
290
  input_json,
282
291
  input_digest,
283
292
  receipt_id,
284
293
  state,
285
294
  settled_outcome,
295
+ settled_record_id,
296
+ finalized_at,
286
297
  created_at,
287
298
  ready_at,
288
299
  input_applied_record_id,
@@ -320,7 +331,6 @@ const ToolCallIdList = Schema.Array(ToolCallIdSchema);
320
331
 
321
332
  const encodePersistedJsonText = Schema.encodeEffect(Schema.fromJsonString(PersistedJson));
322
333
  const encodeDefinitionDigestsText = Schema.encodeEffect(Schema.fromJsonString(DefinitionDigests));
323
- const encodeRecordEnvelopeText = Schema.encodeEffect(Schema.fromJsonString(RecordEnvelope));
324
334
  const decodeRecordEnvelopeText = Schema.decodeEffect(Schema.fromJsonString(RecordEnvelope));
325
335
  const encodeSuspensionReasonText = Schema.encodeEffect(Schema.fromJsonString(SuspensionReason));
326
336
  const encodeUnknownResolutionText = Schema.encodeEffect(Schema.fromJsonString(UnknownResolution));
@@ -329,12 +339,15 @@ const decodeToolCallIdsText = Schema.decodeEffect(Schema.fromJsonString(ToolCall
329
339
  const parseStoredJsonText = Schema.decodeEffect(Schema.fromJsonString(Schema.Json));
330
340
  const decodeAdmissionResult = Schema.decodeUnknownEffect(AdmissionResult);
331
341
  const decodeClaim = Schema.decodeUnknownEffect(Claim);
332
- const decodeOwnershipRenewal = Schema.decodeUnknownEffect(OwnershipRenewal);
333
342
  const decodeSettlement = Schema.decodeUnknownEffect(Settlement);
334
343
  const decodeAbortIntent = Schema.decodeUnknownEffect(AbortIntent);
335
- const decodeOwnershipSnapshot = Schema.decodeUnknownEffect(OwnershipSnapshot);
336
344
  const decodeInputAppliedMarker = Schema.decodeUnknownEffect(InputAppliedMarker);
337
- const decodeSubmissionSnapshotUnknown = Schema.decodeUnknownEffect(SubmissionSnapshot);
345
+
346
+ const decodeSubmissionSnapshotType = (input: typeof SubmissionSnapshot.Type) =>
347
+ Schema.decodeEffect(Schema.toType(Schema.Struct(SubmissionSnapshot.fields)))(input).pipe(
348
+ Effect.map((fields) => SubmissionSnapshot.make(fields)),
349
+ );
350
+
338
351
  const decodeSubmissionId = Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId);
339
352
  const decodeQueueSequence = Schema.decodeUnknownEffect(QueueSequence);
340
353
  const decodeUtcInstant = Schema.decodeUnknownEffect(Schema.DateTimeUtcFromString);
@@ -385,20 +398,7 @@ const submissionsRows = ownedRows(
385
398
  (row) => row.submission_id,
386
399
  "submission_id",
387
400
  [["thread_id", "principal", "idempotency_key"]],
388
- );
389
-
390
- const ownershipRows = ownedRows(
391
- OwnershipRow,
392
- "effect_agent_submission_ownership",
393
- (row) => row.submission_id,
394
- "submission_id",
395
- );
396
-
397
- const reservationsRows = ownedRows(
398
- ReservationRow,
399
- "effect_agent_settlement_reservations",
400
- (row) => row.submission_id,
401
- "submission_id",
401
+ (sql) => sql`*, ${definitionJson(sql)} AS agent_digests_json`,
402
402
  );
403
403
 
404
404
  const abortsRows = ownedRows(
@@ -413,7 +413,7 @@ const approvalsRows = ownedRows(ApprovalDecisionRow, "effect_agent_approval_deci
413
413
  );
414
414
 
415
415
  const resolutionsRows = ownedRows(UnknownResolutionRow, "effect_agent_unknown_resolutions", (row) =>
416
- JSON.stringify([row.submission_id, row.tool_call_id]),
416
+ JSON.stringify([row.submission_id, row.tool_call_id, row.resolution_kind]),
417
417
  );
418
418
 
419
419
  const childSettlementsRows = ownedRows(
@@ -442,7 +442,7 @@ const submissionWorkRows = ownedRows(
442
442
  (row) => row.submission_id,
443
443
  );
444
444
 
445
- const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
445
+ const makeServices = Effect.fnUntraced(function* () {
446
446
  const progress = yield* SqlStorageProgress;
447
447
 
448
448
  const recordProgress = (kind: SqlStorageProgressKind, operation: string) =>
@@ -458,10 +458,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
458
458
  const failpoint = yield* DoStorageFailpoint;
459
459
  const sql = yield* SqlClientService.SqlClient;
460
460
  const crypto = yield* Crypto.Crypto;
461
+ const physical = yield* makeCanonicalStorage();
462
+ const submissionColumns = sql`${sql.literal(SUBMISSION_COLUMNS)}, ${definitionJson(sql)} AS agent_digests_json`;
461
463
  const admissionFence = yield* SubmissionAdmissionFence;
462
464
  const journal = yield* initializeDoJournal(sql, failpoint.hit, config.maxStoredValueBytes);
463
465
  const lifecycle = journal.lifecycle;
464
466
  const state = journal.state;
467
+ const owner = state.owner;
465
468
  const submissionViews = submissionsRows(state, sql);
466
469
  const workRows = submissionWorkRows(state, sql);
467
470
 
@@ -476,8 +479,6 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
476
479
  .write(effect)
477
480
  .pipe(Effect.tap((changed) => workRows.write(Effect.succeed(changed)))),
478
481
  },
479
- ownership: ownershipRows(state, sql),
480
- reservations: reservationsRows(state, sql),
481
482
  aborts: abortsRows(state, sql),
482
483
  approvals: approvalsRows(state, sql),
483
484
  resolutions: resolutionsRows(state, sql),
@@ -492,46 +493,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
492
493
  operation: string,
493
494
  ) => effect.pipe(Effect.mapError(internalFailure(operation)));
494
495
 
495
- // Small lanes share control metadata; large retained histories keep indexed SQL reads.
496
- const laneWork = Effect.fnUntraced(function* (threadId: string, operation: string) {
497
- const work = yield* cached(
498
- workRows.matching(
499
- JSON.stringify(["lane", threadId]),
500
- (row) => row.thread_id === threadId,
501
- sql`SELECT submission_id, thread_id, queue_sequence, principal, idempotency_key,
502
- deployment_id, receipt_id, state FROM effect_agent_submissions WHERE thread_id = ${threadId} LIMIT 129`,
503
- 128,
504
- ),
505
- operation,
506
- );
507
-
508
- return work.length > 128
509
- ? undefined
510
- : [...work].sort((a, b) => a.queue_sequence - b.queue_sequence);
511
- });
512
-
513
496
  const byKey = Effect.fnUntraced(function* (
514
497
  request: Pick<SubmissionLookupByKey, "threadId" | "principal" | "idempotencyKey">,
515
498
  operation: string,
516
499
  ) {
517
- const lane = yield* laneWork(request.threadId, operation);
518
-
519
- if (lane === undefined)
520
- return yield* cached(
521
- rows.submissions.byFields([
522
- ["thread_id", request.threadId],
523
- ["principal", request.principal],
524
- ["idempotency_key", request.idempotencyKey],
525
- ]),
526
- operation,
527
- );
528
-
529
- const matches = lane.filter(
530
- (row) =>
531
- row.principal === request.principal && row.idempotency_key === request.idempotencyKey,
500
+ return yield* cached(
501
+ rows.submissions.byFields([
502
+ ["thread_id", request.threadId],
503
+ ["principal", request.principal],
504
+ ["idempotency_key", request.idempotencyKey],
505
+ ]),
506
+ operation,
532
507
  );
533
-
534
- return yield* Effect.forEach(matches, (row) => requireSubmission(operation, row.submission_id));
535
508
  }, state.read);
536
509
 
537
510
  const retainLifecycle = (submission: SubmissionRow, fact: LifecyclePublicationFact) =>
@@ -624,27 +597,19 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
624
597
  );
625
598
 
626
599
  const mintUuid = (operation: string): Effect.Effect<string, LedgerError> =>
627
- crypto.randomUUIDv7.pipe(Effect.mapError((error) => internalFailure(operation)(error)));
600
+ crypto.randomUUIDv7().pipe(Effect.mapError((error) => internalFailure(operation)(error)));
628
601
 
629
602
  const currentInstant = Effect.map(Clock.currentTimeMillis, (millis) => ({
630
603
  millis,
631
604
  iso: new Date(millis).toISOString(),
632
605
  }));
633
606
 
634
- const timestampMillis = (operation: string, rowKey: string) => (timestamp: string) =>
635
- decodeUtcInstant(timestamp).pipe(
636
- Effect.map(DateTime.toEpochMillis),
637
- Effect.mapError((error) =>
638
- corruptionFailure(operation, "effect_agent_submission_ownership", rowKey, error.message),
639
- ),
640
- );
641
-
642
607
  const decodeSubmissionRows = (operation: string, rowKey: string, rows: unknown) =>
643
608
  decodeRows(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(
644
609
  Effect.mapError(internalFailure(operation)),
645
610
  );
646
611
 
647
- const readSubmission = Effect.fn("DoSubmissionLedger.readSubmission")(function* (
612
+ const readSubmission = Effect.fnUntraced(function* (
648
613
  operation: string,
649
614
  submissionId: string,
650
615
  ): Effect.fn.Return<Option.Option<SubmissionRow>, LedgerError> {
@@ -653,7 +618,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
653
618
  return Option.fromUndefinedOr(found[0]);
654
619
  });
655
620
 
656
- const requireSubmission = Effect.fn("DoSubmissionLedger.requireSubmission")(function* (
621
+ const requireSubmission = Effect.fnUntraced(function* (
657
622
  operation: string,
658
623
  submissionId: string,
659
624
  ): Effect.fn.Return<SubmissionRow, LedgerError> {
@@ -669,167 +634,122 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
669
634
  return submission.value;
670
635
  });
671
636
 
672
- const readOwnership = Effect.fn("DoSubmissionLedger.readOwnership")(function* (
637
+ const requireOwner = Effect.fnUntraced(function* (
673
638
  operation: string,
674
- submissionId: string,
675
- ): Effect.fn.Return<Option.Option<OwnershipRow>, LedgerError> {
676
- const found = yield* cached(rows.ownership.by("submission_id", submissionId), operation);
639
+ submission: SubmissionRow,
640
+ ): Effect.fn.Return<void, OwnershipLost | LedgerError> {
641
+ if (owner.hasOwner(submission.thread_id, submission.submission_id)) return;
677
642
 
678
- return Option.fromUndefinedOr(found[0]);
643
+ const submissionId = yield* decodeSubmissionId(submission.submission_id).pipe(
644
+ Effect.mapError(internalFailure(operation)),
645
+ );
646
+
647
+ return yield* OwnershipLost.make({ submissionId });
679
648
  });
680
649
 
681
- const threadEpoch = Effect.fn("DoSubmissionLedger.threadEpoch")(function* (
682
- operation: string,
683
- threadId: string,
684
- ): Effect.fn.Return<ProducerEpoch, LedgerError> {
685
- const threads = yield* journal
686
- .getThread(threadId)
687
- .pipe(Effect.mapError(internalFailure(operation)));
650
+ const submissions = new WeakMap<SubmissionRow, SubmissionSnapshot>();
688
651
 
689
- return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
690
- });
652
+ const decodeSubmissionSnapshot = Effect.fnUntraced(function* (
653
+ operation: string,
654
+ row: SubmissionRow,
655
+ ): Effect.fn.Return<SubmissionSnapshot, LedgerError> {
656
+ const existing = submissions.get(row);
657
+
658
+ if (existing !== undefined) return existing;
659
+
660
+ const decodeFailure = (error: Schema.SchemaError) =>
661
+ internalFailure(operation)(
662
+ DoStorageCorruptionError.make({
663
+ table: "effect_agent_submissions",
664
+ rowKey: row.submission_id,
665
+ message: "Stored submission does not satisfy the ledger schema",
666
+ diagnostic: ThreadStoreDiagnostic.make({
667
+ causeTag: error._tag,
668
+ operation,
669
+ decoder: "SubmissionSnapshot",
670
+ issueTag: error.issue._tag,
671
+ }),
672
+ }),
673
+ );
691
674
 
692
- /**
693
- * Verify inside the surrounding write transaction that the presented token still owns the
694
- * Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
695
- * Thread's current producer epoch (DUR-006).
696
- */
697
- const requireOwnership = Effect.fnUntraced(
698
- function* (
699
- operation: string,
700
- submission: SubmissionRow,
701
- ownershipToken: string,
702
- ): Effect.fn.Return<OwnershipRow, OwnershipLost | LedgerError> {
703
- const ownership = yield* readOwnership(operation, submission.submission_id);
704
- const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
675
+ if ((row.parent_submission_id === null) !== (row.parent_tool_call_id === null)) {
676
+ return yield* corruptionFailure(
677
+ operation,
678
+ "effect_agent_submissions",
679
+ row.submission_id,
680
+ "A parent linkage must record both the parent Submission and the parent Tool Call.",
681
+ );
682
+ }
705
683
 
706
- if (
707
- Option.isNone(ownership) ||
708
- ownership.value.ownership_token !== ownershipToken ||
709
- ownership.value.producer_epoch !== actualEpoch
710
- ) {
711
- const submissionId = yield* Schema.decodeEffect(SubmissionSnapshot.fields.submissionId)(
712
- submission.submission_id,
713
- ).pipe(Effect.mapError(internalFailure(operation)));
684
+ const admission = yield* decodeAdmissionFact(row).pipe(Effect.mapError(decodeFailure));
714
685
 
715
- return yield* OwnershipLost.make({ submissionId, actualEpoch });
716
- }
686
+ const readyAt =
687
+ row.ready_at === null
688
+ ? undefined
689
+ : yield* decodeUtcInstant(row.ready_at).pipe(Effect.mapError(decodeFailure));
717
690
 
718
- return ownership.value;
719
- },
720
- withStorageSpan(
721
- "DoSubmissionLedger.requireOwnership",
722
- (error) => error._tag === "OwnershipLost",
723
- ),
724
- );
691
+ const snapshot = yield* decodeSubmissionSnapshotType({
692
+ ...admission,
693
+ state: row.state,
694
+ ...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
695
+ ...(readyAt === undefined ? {} : { readyAt }),
696
+ }).pipe(Effect.mapError(decodeFailure));
725
697
 
726
- const submissions = new WeakMap<SubmissionRow, SubmissionSnapshot>();
698
+ submissions.set(row, snapshot);
727
699
 
728
- const decodeSubmissionSnapshot = Effect.fn("DoSubmissionLedger.decodeSubmissionSnapshot")(
729
- function* (
730
- operation: string,
731
- row: SubmissionRow,
732
- ): Effect.fn.Return<SubmissionSnapshot, LedgerError> {
733
- const existing = submissions.get(row);
734
-
735
- if (existing !== undefined) return existing;
736
-
737
- const decodeFailure = (error: Schema.SchemaError) =>
738
- internalFailure(operation)(
739
- DoStorageCorruptionError.make({
740
- table: "effect_agent_submissions",
741
- rowKey: row.submission_id,
742
- message: "Stored submission does not satisfy the ledger schema",
743
- diagnostic: ThreadStoreDiagnostic.make({
744
- causeTag: error._tag,
745
- operation,
746
- decoder: "SubmissionSnapshot",
747
- issueTag: error.issue._tag,
748
- }),
749
- }),
750
- );
700
+ return snapshot;
701
+ });
751
702
 
752
- const agentDigests = yield* parseStoredJsonText(row.agent_digests_json).pipe(
753
- Effect.mapError(decodeFailure),
754
- );
703
+ const readCanonicalSettlement = Effect.fnUntraced(function* (
704
+ operation: string,
705
+ submission: SubmissionRow,
706
+ ) {
707
+ const expected = yield* decodeSubmissionSnapshot(operation, submission);
708
+ const recordId = submissionSettlementRecordId(expected.submissionId);
755
709
 
756
- const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
757
- Effect.mapError(decodeFailure),
758
- );
710
+ const found =
711
+ yield* sql`SELECT ${recordBatchId(sql)} AS batch_id, record_id, ${canonicalRecordJson(sql)} AS record_json
712
+ FROM effect_agent_canonical_records WHERE thread_id = ${submission.thread_id}
713
+ AND record_id = ${JSON.stringify(recordId)}`.pipe(Effect.mapError(sqlFailure(operation)));
759
714
 
760
- if ((row.parent_submission_id === null) !== (row.parent_tool_call_id === null)) {
761
- return yield* corruptionFailure(
762
- operation,
763
- "effect_agent_submissions",
764
- row.submission_id,
765
- "A parent linkage must record both the parent Submission and the parent Tool Call.",
766
- );
767
- }
715
+ const decoded = yield* decodeRows(
716
+ Schema.Array(
717
+ Schema.Struct({
718
+ batch_id: Schema.fromJsonString(BoundedIdentifier),
719
+ record_id: Schema.fromJsonString(BoundedIdentifier),
720
+ record_json: BoundedStoredText,
721
+ }),
722
+ ),
723
+ "effect_agent_canonical_records",
724
+ recordId,
725
+ found,
726
+ ).pipe(Effect.mapError(internalFailure(operation)));
768
727
 
769
- const snapshot = yield* decodeSubmissionSnapshotUnknown({
770
- submissionId: row.submission_id,
771
- threadId: row.thread_id,
772
- queueSequence: row.queue_sequence,
773
- principal: row.principal,
774
- idempotencyKey: row.idempotency_key,
775
- agentId: row.agent_id,
776
- agentDigests,
777
- deploymentId: row.deployment_id,
778
- inputPayload,
779
- inputDigest: row.input_digest,
780
- receiptId: row.receipt_id,
781
- state: row.state,
782
- createdAt: row.created_at,
783
- ...(row.admission_group === null ? {} : { admissionGroup: row.admission_group }),
784
- ...(row.worker_admission_json === null
785
- ? {}
786
- : {
787
- workerAdmission: yield* parseStoredJsonText(row.worker_admission_json).pipe(
788
- Effect.mapError(decodeFailure),
789
- ),
790
- }),
791
- ...(row.message_admission_json === null
792
- ? {}
793
- : {
794
- messageAdmission: yield* parseStoredJsonText(row.message_admission_json).pipe(
795
- Effect.mapError(decodeFailure),
796
- ),
797
- }),
798
- ...(row.admission_fence_json === null
799
- ? {}
800
- : {
801
- admissionFence: yield* parseStoredJsonText(row.admission_fence_json).pipe(
802
- Effect.mapError(decodeFailure),
803
- ),
804
- }),
805
- ...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
806
- ...(row.ready_at === null ? {} : { readyAt: row.ready_at }),
807
- ...(row.parent_submission_id === null || row.parent_tool_call_id === null
808
- ? {}
809
- : {
810
- parentLinkage: {
811
- parentSubmissionId: row.parent_submission_id,
812
- parentToolCallId: row.parent_tool_call_id,
813
- },
814
- }),
815
- }).pipe(Effect.mapError(decodeFailure));
728
+ if (decoded.length === 0) return undefined;
729
+ const row = decoded[0];
816
730
 
817
- submissions.set(row, snapshot);
731
+ if (
732
+ decoded.length !== 1 ||
733
+ row.batch_id !== submissionSettlementBatchId(expected.submissionId) ||
734
+ row.record_id !== recordId
735
+ )
736
+ return yield* corruptionFailure(
737
+ operation,
738
+ "effect_agent_canonical_records",
739
+ recordId,
740
+ "Canonical settlement identity is inconsistent.",
741
+ );
818
742
 
819
- return snapshot;
820
- },
821
- );
743
+ const record = yield* decodeRecordEnvelopeText(row.record_json).pipe(
744
+ Effect.mapError(internalFailure(operation)),
745
+ );
822
746
 
823
- const readReservation = Effect.fn("DoSubmissionLedger.readReservation")(function* (
824
- operation: string,
825
- submissionId: string,
826
- ): Effect.fn.Return<Option.Option<ReservationRow>, LedgerError> {
827
- const found = yield* cached(rows.reservations.by("submission_id", submissionId), operation);
747
+ const settlement = yield* validateCanonicalSettlement(record, expected);
828
748
 
829
- return Option.fromUndefinedOr(found[0]);
749
+ return { record, settlement };
830
750
  });
831
751
 
832
- const readAbortIntent = Effect.fn("DoSubmissionLedger.readAbortIntent")(function* (
752
+ const readAbortIntent = Effect.fnUntraced(function* (
833
753
  operation: string,
834
754
  submissionId: string,
835
755
  ): Effect.fn.Return<Option.Option<AbortIntentRow>, LedgerError> {
@@ -843,25 +763,23 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
843
763
  * Submission (the DC realization of the port's "cross-store adapters record a durable
844
764
  * notification marker" contract).
845
765
  */
846
- const readChildSettlementMarkers = Effect.fn("DoSubmissionLedger.readChildSettlementMarkers")(
847
- function* (
848
- operation: string,
849
- parentSubmissionId: string,
850
- ): Effect.fn.Return<ReadonlyArray<ChildSettlementMarkerRow>, LedgerError> {
851
- const found = yield* cached(
852
- rows.childSettlements.by("parent_submission_id", parentSubmissionId),
853
- operation,
854
- );
766
+ const readChildSettlementMarkers = Effect.fnUntraced(function* (
767
+ operation: string,
768
+ parentSubmissionId: string,
769
+ ): Effect.fn.Return<ReadonlyArray<ChildSettlementMarkerRow>, LedgerError> {
770
+ const found = yield* cached(
771
+ rows.childSettlements.by("parent_submission_id", parentSubmissionId),
772
+ operation,
773
+ );
855
774
 
856
- return found.toSorted((a, b) =>
857
- a.child_submission_id < b.child_submission_id
858
- ? -1
859
- : a.child_submission_id > b.child_submission_id
860
- ? 1
861
- : 0,
862
- );
863
- },
864
- );
775
+ return found.toSorted((a, b) =>
776
+ a.child_submission_id < b.child_submission_id
777
+ ? -1
778
+ : a.child_submission_id > b.child_submission_id
779
+ ? 1
780
+ : 0,
781
+ );
782
+ });
865
783
 
866
784
  /**
867
785
  * Whether one listed child is provably settled from THIS store: either its own row lives
@@ -869,7 +787,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
869
787
  * cross-store notification marker was recorded for it (the child's row lives in another
870
788
  * Durable Object and its owner reported the settlement through `recordChildSettled`).
871
789
  */
872
- const childProvablySettled = Effect.fn("DoSubmissionLedger.childProvablySettled")(function* (
790
+ const childProvablySettled = Effect.fnUntraced(function* (
873
791
  operation: string,
874
792
  markerChildren: ReadonlySet<string>,
875
793
  childSubmissionId: string,
@@ -877,7 +795,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
877
795
  if (markerChildren.has(childSubmissionId)) return true;
878
796
  const childRow = yield* readSubmission(operation, childSubmissionId);
879
797
 
880
- return Option.isSome(childRow) && childRow.value.state === "settled";
798
+ return (
799
+ Option.isSome(childRow) &&
800
+ (yield* readCanonicalSettlement(operation, childRow.value)) !== undefined
801
+ );
881
802
  });
882
803
 
883
804
  const decodeChildReservationRows = (operation: string, rowKey: string, rows: unknown) =>
@@ -888,7 +809,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
888
809
  rows,
889
810
  ).pipe(Effect.mapError(internalFailure(operation)));
890
811
 
891
- const readChildReservation = Effect.fn("DoSubmissionLedger.readChildReservation")(function* (
812
+ const readChildReservation = Effect.fnUntraced(function* (
892
813
  operation: string,
893
814
  reservationId: string,
894
815
  ): Effect.fn.Return<Option.Option<ChildReservationRow>, LedgerError> {
@@ -912,41 +833,37 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
912
833
  return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
913
834
  });
914
835
 
915
- const readChildReservationForCall = Effect.fn("DoSubmissionLedger.readChildReservationForCall")(
916
- function* (
917
- operation: string,
918
- parentSubmissionId: string,
919
- parentToolCallId: string,
920
- ): Effect.fn.Return<Option.Option<ChildReservationRow>, LedgerError> {
921
- const rows = yield* sql<Record<string, unknown>>`
836
+ const readChildReservationForCall = Effect.fnUntraced(function* (
837
+ operation: string,
838
+ parentSubmissionId: string,
839
+ parentToolCallId: string,
840
+ ): Effect.fn.Return<Option.Option<ChildReservationRow>, LedgerError> {
841
+ const rows = yield* sql<Record<string, unknown>>`
922
842
  SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
923
843
  FROM effect_agent_child_reservations
924
844
  WHERE parent_submission_id = ${parentSubmissionId}
925
845
  AND parent_tool_call_id = ${parentToolCallId}
926
846
  `.pipe(Effect.mapError(sqlFailure(operation)));
927
847
 
928
- const decoded = yield* decodeChildReservationRows(
848
+ const decoded = yield* decodeChildReservationRows(
849
+ operation,
850
+ `${parentSubmissionId}/${parentToolCallId}`,
851
+ rows,
852
+ );
853
+
854
+ if (decoded.length > 1) {
855
+ return yield* corruptionFailure(
929
856
  operation,
857
+ "effect_agent_child_reservations",
930
858
  `${parentSubmissionId}/${parentToolCallId}`,
931
- rows,
859
+ "A parent Tool Call returned more than one child reservation.",
932
860
  );
861
+ }
933
862
 
934
- if (decoded.length > 1) {
935
- return yield* corruptionFailure(
936
- operation,
937
- "effect_agent_child_reservations",
938
- `${parentSubmissionId}/${parentToolCallId}`,
939
- "A parent Tool Call returned more than one child reservation.",
940
- );
941
- }
942
-
943
- return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
944
- },
945
- );
863
+ return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
864
+ });
946
865
 
947
- const childReservationSnapshotFromRow = Effect.fn(
948
- "DoSubmissionLedger.childReservationSnapshotFromRow",
949
- )(function* (
866
+ const childReservationSnapshotFromRow = Effect.fnUntraced(function* (
950
867
  operation: string,
951
868
  row: ChildReservationRow,
952
869
  ): Effect.fn.Return<ChildBudgetReservationSnapshot, LedgerError> {
@@ -982,7 +899,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
982
899
  }).pipe(Effect.mapError(rowFailure));
983
900
  });
984
901
 
985
- const readApprovalDecisions = Effect.fn("DoSubmissionLedger.readApprovalDecisions")(function* (
902
+ const readApprovalDecisions = Effect.fnUntraced(function* (
986
903
  operation: string,
987
904
  submissionId: string,
988
905
  ): Effect.fn.Return<ReadonlyArray<ApprovalDecisionRow>, LedgerError> {
@@ -993,18 +910,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
993
910
  );
994
911
  });
995
912
 
996
- const approvalIntentFromRow = Effect.fn("DoSubmissionLedger.approvalIntentFromRow")(function* (
913
+ const approvalIntentFromRow = (
997
914
  operation: string,
998
915
  row: ApprovalDecisionRow,
999
- ): Effect.fn.Return<ApprovalDecisionIntent, LedgerError> {
1000
- return yield* decodeApprovalDecisionIntent({
1001
- submissionId: row.submission_id,
1002
- toolCallId: row.tool_call_id,
1003
- decision: row.decision,
1004
- resolver: row.resolver,
1005
- reason: row.reason,
1006
- decidedAt: row.decided_at,
1007
- }).pipe(
916
+ ): Effect.Effect<ApprovalDecisionIntent, LedgerError> =>
917
+ decodeApprovalFact(row).pipe(
1008
918
  Effect.mapError((error) =>
1009
919
  corruptionFailure(
1010
920
  operation,
@@ -1014,9 +924,8 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1014
924
  ),
1015
925
  ),
1016
926
  );
1017
- });
1018
927
 
1019
- const readUnknownResolutions = Effect.fn("DoSubmissionLedger.readUnknownResolutions")(function* (
928
+ const readUnknownResolutions = Effect.fnUntraced(function* (
1020
929
  operation: string,
1021
930
  submissionId: string,
1022
931
  ): Effect.fn.Return<ReadonlyArray<UnknownResolutionRow>, LedgerError> {
@@ -1027,13 +936,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1027
936
  );
1028
937
  });
1029
938
 
1030
- const unknownResolutionIntentFromRow = Effect.fn(
1031
- "DoSubmissionLedger.unknownResolutionIntentFromRow",
1032
- )(function* (
939
+ const unknownResolutionIntentFromRow = Effect.fnUntraced(function* (
1033
940
  operation: string,
1034
941
  row: UnknownResolutionRow,
1035
942
  ): Effect.fn.Return<UnknownResolutionIntent, LedgerError> {
1036
- const resolution = yield* parseStoredJsonText(row.resolution_json).pipe(
943
+ return yield* decodeResolutionFact(row).pipe(
1037
944
  Effect.mapError((error) =>
1038
945
  corruptionFailure(
1039
946
  operation,
@@ -1043,57 +950,37 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1043
950
  ),
1044
951
  ),
1045
952
  );
953
+ });
954
+
955
+ /** The Submission's marked-unknown open Tool Call identities, empty when never marked. */
956
+ const storedUnknownToolCallIds = Effect.fnUntraced(function* (
957
+ operation: string,
958
+ submission: SubmissionRow,
959
+ ): Effect.fn.Return<ReadonlyArray<typeof ToolCallIdSchema.Type>, LedgerError> {
960
+ if (submission.unknown_tool_call_ids_json === null) return [];
1046
961
 
1047
- return yield* decodeUnknownResolutionIntent({
1048
- submissionId: row.submission_id,
1049
- toolCallId: row.tool_call_id,
1050
- author: row.author,
1051
- reason: row.reason,
1052
- resolution,
1053
- resolvedAt: row.resolved_at,
1054
- }).pipe(
962
+ return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(
1055
963
  Effect.mapError((error) =>
1056
964
  corruptionFailure(
1057
965
  operation,
1058
- "effect_agent_unknown_resolutions",
1059
- `${row.submission_id}/${row.tool_call_id}`,
966
+ "effect_agent_submissions",
967
+ submission.submission_id,
1060
968
  error.message,
1061
969
  ),
1062
970
  ),
1063
971
  );
1064
972
  });
1065
973
 
1066
- /** The Submission's marked-unknown open Tool Call identities, empty when never marked. */
1067
- const storedUnknownToolCallIds = Effect.fn("DoSubmissionLedger.storedUnknownToolCallIds")(
1068
- function* (
1069
- operation: string,
1070
- submission: SubmissionRow,
1071
- ): Effect.fn.Return<ReadonlyArray<typeof ToolCallIdSchema.Type>, LedgerError> {
1072
- if (submission.unknown_tool_call_ids_json === null) return [];
1073
-
1074
- return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(
1075
- Effect.mapError((error) =>
1076
- corruptionFailure(
1077
- operation,
1078
- "effect_agent_submissions",
1079
- submission.submission_id,
1080
- error.message,
1081
- ),
1082
- ),
1083
- );
1084
- },
1085
- );
1086
-
1087
974
  /**
1088
975
  * Canonical history is the abort authority (DUR-015): the intent's canonicalRecordId is
1089
976
  * derived from the shared canonical-records table using the deterministic abort record
1090
977
  * identity, never from a cached ledger marker.
1091
978
  */
1092
- const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (
979
+ const canonicalAbortRecordId = Effect.fnUntraced(function* (
1093
980
  operation: string,
1094
981
  threadId: string,
1095
982
  submissionId: SubmissionId,
1096
- ): Effect.fn.Return<string | undefined, LedgerError> {
983
+ ): Effect.fn.Return<AbortIntent["canonicalRecordId"], LedgerError> {
1097
984
  const recordId = submissionAbortRecordId(submissionId);
1098
985
 
1099
986
  const present = yield* journal
@@ -1103,7 +990,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1103
990
  return present ? recordId : undefined;
1104
991
  });
1105
992
 
1106
- const abortIntentFromRow = Effect.fn("DoSubmissionLedger.abortIntentFromRow")(function* (
993
+ const abortIntentFromRow = Effect.fnUntraced(function* (
1107
994
  operation: string,
1108
995
  submission: SubmissionRow,
1109
996
  submissionId: SubmissionId,
@@ -1115,13 +1002,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1115
1002
  submissionId,
1116
1003
  );
1117
1004
 
1118
- return yield* decodeAbortIntent({
1119
- submissionId: row.submission_id,
1120
- author: row.author,
1121
- reason: row.reason,
1122
- requestedAt: row.requested_at,
1123
- ...(canonicalRecordId === undefined ? {} : { canonicalRecordId }),
1124
- }).pipe(
1005
+ const fact = yield* decodeAbortFact(row).pipe(
1125
1006
  Effect.mapError((error) =>
1126
1007
  corruptionFailure(
1127
1008
  operation,
@@ -1131,6 +1012,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1131
1012
  ),
1132
1013
  ),
1133
1014
  );
1015
+
1016
+ return AbortIntent.make({
1017
+ ...fact,
1018
+ ...(canonicalRecordId === undefined ? {} : { canonicalRecordId }),
1019
+ });
1134
1020
  });
1135
1021
 
1136
1022
  // Durable Object storage is the single serialized owner: writes confirm through output
@@ -1140,257 +1026,254 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1140
1026
  LedgerCapabilities.make({ durability: "durable-cloudflare" }),
1141
1027
  );
1142
1028
 
1143
- const admit: SubmissionLedger["Service"]["admit"] = Effect.fn("DoSubmissionLedger.admit")(
1144
- function* (request: AdmissionRequest) {
1145
- const operation = "ledger admit";
1146
-
1147
- const validated = yield* Schema.decodeEffect(Schema.toType(AdmissionRequest))(request).pipe(
1148
- Effect.mapError(internalFailure(operation)),
1149
- );
1150
-
1151
- const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(
1152
- Effect.mapError(internalFailure(operation)),
1153
- );
1029
+ const admit: SubmissionLedger["Service"]["admit"] = Effect.fnUntraced(function* (
1030
+ request: AdmissionRequest,
1031
+ ) {
1032
+ const operation = "ledger admit";
1154
1033
 
1155
- // The platform's ~2 MB per-value bound, refused typed BEFORE any durable mutation
1156
- // (resource-limits gate; oversized payloads are the designed R2 overflow path).
1157
- yield* journal
1158
- .checkValueBound(operation, inputJson)
1159
- .pipe(Effect.mapError(internalFailure(operation)));
1034
+ const validated = yield* Schema.decodeEffect(Schema.toType(AdmissionRequest))(request).pipe(
1035
+ Effect.mapError(internalFailure(operation)),
1036
+ );
1160
1037
 
1161
- const workerAdmissionJson =
1162
- validated.workerAdmission === undefined
1163
- ? null
1164
- : yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
1165
- validated.workerAdmission,
1166
- ).pipe(Effect.mapError(internalFailure(operation)));
1038
+ const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(
1039
+ Effect.mapError(internalFailure(operation)),
1040
+ );
1167
1041
 
1168
- if (
1169
- workerAdmissionJson !== null &&
1170
- new TextEncoder().encode(workerAdmissionJson).byteLength > config.maxStoredValueBytes
1171
- ) {
1172
- return yield* LedgerError.make({
1173
- operation,
1174
- message: "Worker admission metadata exceeds the stored value bound",
1175
- });
1176
- }
1042
+ // The platform's ~2 MB per-value bound, refused typed BEFORE any durable mutation
1043
+ // (resource-limits gate; oversized payloads are the designed R2 overflow path).
1044
+ yield* journal
1045
+ .checkValueBound(operation, inputJson)
1046
+ .pipe(Effect.mapError(internalFailure(operation)));
1177
1047
 
1178
- const messageAdmissionJson =
1179
- validated.messageAdmission === undefined
1180
- ? null
1181
- : yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
1182
- validated.messageAdmission,
1183
- ).pipe(Effect.mapError(internalFailure(operation)));
1048
+ const workerAdmissionJson =
1049
+ validated.workerAdmission === undefined
1050
+ ? null
1051
+ : yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
1052
+ validated.workerAdmission,
1053
+ ).pipe(Effect.mapError(internalFailure(operation)));
1184
1054
 
1185
- if (
1186
- messageAdmissionJson !== null &&
1187
- new TextEncoder().encode(messageAdmissionJson).byteLength > config.maxStoredValueBytes
1188
- ) {
1189
- return yield* LedgerError.make({
1190
- operation,
1191
- message: "Message admission metadata exceeds the stored value bound",
1192
- });
1193
- }
1055
+ if (
1056
+ workerAdmissionJson !== null &&
1057
+ new TextEncoder().encode(workerAdmissionJson).byteLength > config.maxStoredValueBytes
1058
+ ) {
1059
+ return yield* LedgerError.make({
1060
+ operation,
1061
+ message: "Worker admission metadata exceeds the stored value bound",
1062
+ });
1063
+ }
1194
1064
 
1195
- const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
1196
- Effect.mapError(internalFailure(operation)),
1197
- );
1065
+ const messageAdmissionJson =
1066
+ validated.messageAdmission === undefined
1067
+ ? null
1068
+ : yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
1069
+ validated.messageAdmission,
1070
+ ).pipe(Effect.mapError(internalFailure(operation)));
1198
1071
 
1199
- // Routable Submission identity (D-P6-5): `{uuidv7}:{threadId}`. The cross-DO
1200
- // routing layer parses ITS OWN minted format (split at the first ":") to address
1201
- // submissionId-only operations to the owning Thread Object; the id stays opaque
1202
- // to every other component, exactly like DN's `submission-{uuid}` prefix.
1203
- const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.threadId}`;
1072
+ if (
1073
+ messageAdmissionJson !== null &&
1074
+ new TextEncoder().encode(messageAdmissionJson).byteLength > config.maxStoredValueBytes
1075
+ ) {
1076
+ return yield* LedgerError.make({
1077
+ operation,
1078
+ message: "Message admission metadata exceeds the stored value bound",
1079
+ });
1080
+ }
1204
1081
 
1205
- if (mintedSubmissionId.length > MAX_IDENTIFIER_LENGTH) {
1206
- return yield* LedgerError.make({
1207
- operation,
1208
- message:
1209
- `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds ` +
1210
- `the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Thread identity.`,
1211
- });
1212
- }
1213
- const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
1082
+ const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
1083
+ Effect.mapError(internalFailure(operation)),
1084
+ );
1214
1085
 
1215
- yield* hitFailpoint("ledger:admit:before", operation);
1086
+ // Routable Submission identity (D-P6-5): `{uuidv7}:{threadId}`. The cross-DO
1087
+ // routing layer parses ITS OWN minted format (split at the first ":") to address
1088
+ // submissionId-only operations to the owning Thread Object; the id stays opaque
1089
+ // to every other component, exactly like DN's `submission-{uuid}` prefix.
1090
+ const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.threadId}`;
1216
1091
 
1217
- const result = yield* inWriteTransaction(
1092
+ if (mintedSubmissionId.length > MAX_IDENTIFIER_LENGTH) {
1093
+ return yield* LedgerError.make({
1218
1094
  operation,
1219
- Effect.gen(function* () {
1220
- const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
1095
+ message:
1096
+ `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds ` +
1097
+ `the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Thread identity.`,
1098
+ });
1099
+ }
1100
+ const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
1221
1101
 
1222
- const existingRows = yield* byKey(validated, operation);
1102
+ yield* hitFailpoint("ledger:admit:before", operation);
1223
1103
 
1224
- const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
1104
+ const result = yield* inWriteTransaction(
1105
+ operation,
1106
+ Effect.gen(function* () {
1107
+ const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
1225
1108
 
1226
- if (existing.length > 1) {
1227
- return yield* corruptionFailure(
1228
- operation,
1229
- "effect_agent_submissions",
1230
- keyRowKey,
1231
- "An admission idempotency key returned more than one row.",
1232
- );
1233
- }
1234
- if (existing.length === 1) {
1235
- // A replay must repeat the exact canonical input AND the exact parent linkage (or
1236
- // its absence): linkage is immutable child lineage (spec §12 step 5, SUB-016).
1237
- const sameLinkage =
1238
- validated.parentLinkage === undefined
1239
- ? existing[0].parent_submission_id === null &&
1240
- existing[0].parent_tool_call_id === null
1241
- : existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId &&
1242
- existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
1243
-
1244
- if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
1245
- return yield* AdmissionConflict.make({
1246
- threadId: validated.threadId,
1247
- principal: validated.principal,
1248
- idempotencyKey: validated.idempotencyKey,
1249
- existingInputDigest: existing[0].input_digest,
1250
- attemptedInputDigest: validated.inputDigest,
1251
- });
1252
- }
1109
+ const existingRows = yield* byKey(validated, operation);
1253
1110
 
1254
- const retainedWorkerAdmission =
1255
- existing[0].worker_admission_json === null
1256
- ? undefined
1257
- : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1258
- existing[0].worker_admission_json,
1259
- ).pipe(Effect.mapError(internalFailure(operation)));
1111
+ const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
1260
1112
 
1261
- const retainedInputMessage =
1262
- existing[0].message_admission_json === null
1263
- ? undefined
1264
- : yield* Schema.decodeEffect(Schema.fromJsonString(InputMessage))(
1265
- existing[0].message_admission_json,
1266
- ).pipe(Effect.mapError(internalFailure(operation)));
1113
+ if (existing.length > 1) {
1114
+ return yield* corruptionFailure(
1115
+ operation,
1116
+ "effect_agent_submissions",
1117
+ keyRowKey,
1118
+ "An admission idempotency key returned more than one row.",
1119
+ );
1120
+ }
1121
+ if (existing.length === 1) {
1122
+ // A replay must repeat the exact canonical input AND the exact parent linkage (or
1123
+ // its absence): linkage is immutable child lineage (spec §12 step 5, SUB-016).
1124
+ const sameLinkage =
1125
+ validated.parentLinkage === undefined
1126
+ ? existing[0].parent_submission_id === null &&
1127
+ existing[0].parent_tool_call_id === null
1128
+ : existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId &&
1129
+ existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
1130
+
1131
+ if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
1132
+ return yield* AdmissionConflict.make({
1133
+ threadId: validated.threadId,
1134
+ principal: validated.principal,
1135
+ idempotencyKey: validated.idempotencyKey,
1136
+ existingInputDigest: existing[0].input_digest,
1137
+ attemptedInputDigest: validated.inputDigest,
1138
+ });
1139
+ }
1267
1140
 
1268
- const retainedFence =
1269
- existing[0].admission_fence_json === null
1270
- ? undefined
1271
- : yield* Schema.decodeEffect(Schema.fromJsonString(AdmissionFence))(
1272
- existing[0].admission_fence_json,
1273
- ).pipe(Effect.mapError(internalFailure(operation)));
1141
+ const retainedWorkerAdmission =
1142
+ existing[0].worker_admission_json === null
1143
+ ? undefined
1144
+ : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1145
+ existing[0].worker_admission_json,
1146
+ ).pipe(Effect.mapError(internalFailure(operation)));
1147
+
1148
+ const retainedInputMessage =
1149
+ existing[0].message_admission_json === null
1150
+ ? undefined
1151
+ : yield* Schema.decodeEffect(Schema.fromJsonString(InputMessage))(
1152
+ existing[0].message_admission_json,
1153
+ ).pipe(Effect.mapError(internalFailure(operation)));
1154
+
1155
+ const retainedFence =
1156
+ existing[0].admission_fence_json === null
1157
+ ? undefined
1158
+ : yield* Schema.decodeEffect(Schema.fromJsonString(AdmissionFence))(
1159
+ existing[0].admission_fence_json,
1160
+ ).pipe(Effect.mapError(internalFailure(operation)));
1274
1161
 
1275
- if (
1276
- (existing[0].admission_group ?? undefined) !== validated.admissionGroup ||
1277
- !Schema.toEquivalence(Schema.optional(WorkerAdmission))(
1278
- retainedWorkerAdmission,
1279
- validated.workerAdmission,
1280
- ) ||
1281
- !Schema.toEquivalence(Schema.optional(InputMessage))(
1282
- retainedInputMessage,
1283
- validated.messageAdmission,
1284
- ) ||
1285
- !Schema.toEquivalence(Schema.optional(AdmissionFence))(
1286
- retainedFence,
1287
- validated.admissionFence,
1288
- )
1162
+ if (
1163
+ (existing[0].admission_group ?? undefined) !== validated.admissionGroup ||
1164
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission))(
1165
+ retainedWorkerAdmission,
1166
+ validated.workerAdmission,
1167
+ ) ||
1168
+ !Schema.toEquivalence(Schema.optional(InputMessage))(
1169
+ retainedInputMessage,
1170
+ validated.messageAdmission,
1171
+ ) ||
1172
+ !Schema.toEquivalence(Schema.optional(AdmissionFence))(
1173
+ retainedFence,
1174
+ validated.admissionFence,
1289
1175
  )
1290
- return yield* AdmissionConflict.make({
1291
- threadId: validated.threadId,
1292
- principal: validated.principal,
1293
- idempotencyKey: validated.idempotencyKey,
1294
- existingInputDigest: existing[0].input_digest,
1295
- attemptedInputDigest: validated.inputDigest,
1296
- });
1176
+ )
1177
+ return yield* AdmissionConflict.make({
1178
+ threadId: validated.threadId,
1179
+ principal: validated.principal,
1180
+ idempotencyKey: validated.idempotencyKey,
1181
+ existingInputDigest: existing[0].input_digest,
1182
+ attemptedInputDigest: validated.inputDigest,
1183
+ });
1297
1184
 
1298
- return yield* decodeAdmissionResult({
1299
- submissionId: existing[0].submission_id,
1300
- receiptId: existing[0].receipt_id,
1301
- queueSequence: existing[0].queue_sequence,
1302
- state: existing[0].state,
1303
- replayed: true,
1304
- }).pipe(Effect.mapError(internalFailure(operation)));
1305
- }
1185
+ return yield* decodeAdmissionResult({
1186
+ submissionId: existing[0].submission_id,
1187
+ receiptId: existing[0].receipt_id,
1188
+ queueSequence: existing[0].queue_sequence,
1189
+ state: existing[0].state,
1190
+ replayed: true,
1191
+ }).pipe(Effect.mapError(internalFailure(operation)));
1192
+ }
1306
1193
 
1307
- const stopped = yield* cached(
1308
- rows.workerStops.by("thread_id", validated.threadId),
1194
+ if (!admissionFitsTransfer(validated.threadId, validated))
1195
+ return yield* LedgerError.make({
1309
1196
  operation,
1310
- );
1311
-
1312
- if (stopped.length > 0)
1313
- return yield* AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" });
1197
+ message: "Admission exceeds the complete transfer page byte bound",
1198
+ });
1314
1199
 
1315
- // The first accepted input fixes ordinary/worker lane identity atomically with admission.
1316
- // Canonical origin materialization can lag admission; a log scan cannot fence that race.
1317
- const lane = yield* laneWork(validated.threadId, operation);
1200
+ const stopped = yield* cached(
1201
+ rows.workerStops.by("thread_id", validated.threadId),
1202
+ operation,
1203
+ );
1318
1204
 
1319
- const firstRows =
1320
- lane === undefined
1321
- ? yield* sql<Record<string, unknown>>`
1205
+ if (stopped.length > 0)
1206
+ return yield* AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" });
1207
+
1208
+ // The first accepted input fixes ordinary/worker lane identity atomically with admission.
1209
+ // Canonical origin materialization can lag admission; a log scan cannot fence that race.
1210
+ const firstRows = yield* sql<Record<string, unknown>>`
1322
1211
  SELECT worker_admission_json FROM effect_agent_submissions
1323
1212
  WHERE thread_id=${validated.threadId} ORDER BY queue_sequence LIMIT 1
1324
- `.pipe(Effect.mapError(sqlFailure(operation)))
1325
- : lane[0] === undefined
1326
- ? []
1327
- : [yield* requireSubmission(operation, lane[0].submission_id)];
1328
-
1329
- const first = yield* Schema.decodeUnknownEffect(
1330
- Schema.Array(
1331
- Schema.Struct({
1332
- worker_admission_json: Schema.NullOr(BoundedStoredText),
1333
- }),
1334
- ),
1335
- )(firstRows).pipe(Effect.mapError(internalFailure(operation)));
1213
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1336
1214
 
1337
- if (first[0] !== undefined) {
1338
- const previous =
1339
- first[0].worker_admission_json === null
1340
- ? undefined
1341
- : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1342
- first[0].worker_admission_json,
1343
- ).pipe(Effect.mapError(internalFailure(operation)));
1215
+ const first = yield* Schema.decodeUnknownEffect(
1216
+ Schema.Array(
1217
+ Schema.Struct({
1218
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
1219
+ }),
1220
+ ),
1221
+ )(firstRows).pipe(Effect.mapError(internalFailure(operation)));
1344
1222
 
1345
- if (
1346
- !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
1347
- previous?.origin,
1348
- validated.workerAdmission?.origin,
1349
- )
1223
+ if (first[0] !== undefined) {
1224
+ const previous =
1225
+ first[0].worker_admission_json === null
1226
+ ? undefined
1227
+ : yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
1228
+ first[0].worker_admission_json,
1229
+ ).pipe(Effect.mapError(internalFailure(operation)));
1230
+
1231
+ if (
1232
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
1233
+ previous?.origin,
1234
+ validated.workerAdmission?.origin,
1350
1235
  )
1351
- return yield* AdmissionPolicyError.make({
1352
- reason: "refused",
1353
- code: "worker-origin-conflict",
1354
- });
1355
- }
1236
+ )
1237
+ return yield* AdmissionPolicyError.make({
1238
+ reason: "refused",
1239
+ code: "worker-origin-conflict",
1240
+ });
1241
+ }
1356
1242
 
1357
- yield* admissionFence.check(validated);
1358
- if (validated.admissionGroup !== undefined) {
1359
- const occupied = yield* sql<Record<string, unknown>>`
1243
+ yield* admissionFence.check(validated);
1244
+ if (validated.admissionGroup !== undefined) {
1245
+ const occupied = yield* sql<Record<string, unknown>>`
1360
1246
  SELECT submission_id FROM effect_agent_submissions
1361
1247
  WHERE thread_id=${validated.threadId} AND admission_group=${validated.admissionGroup} AND state<>'settled' LIMIT 1
1362
1248
  `.pipe(Effect.mapError(sqlFailure(operation)));
1363
1249
 
1364
- if (occupied.length > 0)
1365
- return yield* AdmissionPolicyError.make({
1366
- reason: "occupied",
1367
- code: "admission-group",
1368
- });
1369
- }
1250
+ if (occupied.length > 0)
1251
+ return yield* AdmissionPolicyError.make({
1252
+ reason: "occupied",
1253
+ code: "admission-group",
1254
+ });
1255
+ }
1370
1256
 
1371
- const maxRows =
1372
- lane === undefined
1373
- ? yield* sql<Record<string, unknown>>`
1257
+ const maxRows = yield* sql<Record<string, unknown>>`
1374
1258
  SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
1375
1259
  FROM effect_agent_submissions
1376
1260
  WHERE thread_id = ${validated.threadId}
1377
- `.pipe(Effect.mapError(sqlFailure(operation)))
1378
- : [{ max_queue_sequence: lane.at(-1)?.queue_sequence ?? 0 }];
1261
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1379
1262
 
1380
- const decodedMax = yield* decodeRows(
1381
- Schema.Array(MaxQueueSequenceRow),
1382
- "effect_agent_submissions",
1383
- validated.threadId,
1384
- maxRows,
1385
- ).pipe(Effect.mapError(internalFailure(operation)));
1263
+ const decodedMax = yield* decodeRows(
1264
+ Schema.Array(MaxQueueSequenceRow),
1265
+ "effect_agent_submissions",
1266
+ validated.threadId,
1267
+ maxRows,
1268
+ ).pipe(Effect.mapError(internalFailure(operation)));
1386
1269
 
1387
- const queueSequence = yield* decodeQueueSequence(
1388
- (decodedMax[0]?.max_queue_sequence ?? 0) + 1,
1389
- ).pipe(Effect.mapError(internalFailure(operation)));
1270
+ const queueSequence = yield* decodeQueueSequence(
1271
+ (decodedMax[0]?.max_queue_sequence ?? 0) + 1,
1272
+ ).pipe(Effect.mapError(internalFailure(operation)));
1390
1273
 
1391
- const now = yield* currentInstant;
1274
+ const now = yield* currentInstant;
1392
1275
 
1393
- yield* sql`
1276
+ yield* sql`
1394
1277
  INSERT INTO effect_agent_submissions (
1395
1278
  submission_id,
1396
1279
  thread_id,
@@ -1398,7 +1281,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1398
1281
  principal,
1399
1282
  idempotency_key,
1400
1283
  agent_id,
1401
- agent_digests_json,
1284
+ definition_key,
1402
1285
  deployment_id,
1403
1286
  input_json,
1404
1287
  input_digest,
@@ -1418,7 +1301,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1418
1301
  ${validated.principal},
1419
1302
  ${validated.idempotencyKey},
1420
1303
  ${validated.agentId},
1421
- ${agentDigestsJson},
1304
+ ${yield* physical.definition(agentDigestsJson).pipe(Effect.mapError(internalFailure(operation)))},
1422
1305
  ${validated.deploymentId},
1423
1306
  ${inputJson},
1424
1307
  ${validated.inputDigest},
@@ -1432,40 +1315,51 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1432
1315
  ${workerAdmissionJson},
1433
1316
  ${messageAdmissionJson}
1434
1317
  )
1435
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
1318
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
1319
+ rows.submissions.write,
1320
+ Effect.mapError(internalFailure(operation)),
1321
+ );
1436
1322
 
1437
- // The INSERT (never replay or hydration) proves these new submission-owned sets
1438
- // empty. Gated writes update them; rollback/invalidation discards that proof.
1439
- rows.aborts.seed("submission_id", mintedSubmissionId, []);
1440
- rows.ownership.seed("submission_id", mintedSubmissionId, []);
1441
- rows.reservations.seed("submission_id", mintedSubmissionId, []);
1442
- rows.submissions.seed("joined_host_submission_id", mintedSubmissionId, []);
1443
- rows.approvals.seed("submission_id", mintedSubmissionId, []);
1444
- rows.resolutions.seed("submission_id", mintedSubmissionId, []);
1445
- rows.childReservations.seed("parent_submission_id", mintedSubmissionId, []);
1446
- rows.childSettlements.seed("parent_submission_id", mintedSubmissionId, []);
1323
+ if (queueSequence === 1) {
1324
+ const threads = yield* journal
1325
+ .getThread(validated.threadId)
1326
+ .pipe(Effect.mapError(internalFailure(operation)));
1447
1327
 
1448
- yield* recordProgress("submission", operation);
1328
+ if (threads.length === 0)
1329
+ yield* journal.work
1330
+ .initialize(validated.threadId)
1331
+ .pipe(Effect.mapError(internalFailure(operation)));
1332
+ }
1449
1333
 
1450
- return yield* decodeAdmissionResult({
1451
- submissionId: mintedSubmissionId,
1452
- receiptId: mintedReceiptId,
1453
- queueSequence,
1454
- state: "admitted",
1455
- replayed: false,
1456
- }).pipe(Effect.mapError(internalFailure(operation)));
1457
- }),
1458
- );
1334
+ // The INSERT (never replay or hydration) proves these new submission-owned sets
1335
+ // empty. Gated writes update them; rollback/invalidation discards that proof.
1336
+ rows.aborts.seed("submission_id", mintedSubmissionId, []);
1337
+ rows.submissions.seed("joined_host_submission_id", mintedSubmissionId, []);
1338
+ rows.approvals.seed("submission_id", mintedSubmissionId, []);
1339
+ rows.resolutions.seed("submission_id", mintedSubmissionId, []);
1340
+ rows.childReservations.seed("parent_submission_id", mintedSubmissionId, []);
1341
+ rows.childSettlements.seed("parent_submission_id", mintedSubmissionId, []);
1459
1342
 
1460
- yield* hitFailpoint("ledger:admit:after", operation);
1343
+ yield* recordProgress("submission", operation);
1461
1344
 
1462
- return result;
1463
- },
1464
- );
1345
+ return yield* decodeAdmissionResult({
1346
+ submissionId: mintedSubmissionId,
1347
+ receiptId: mintedReceiptId,
1348
+ queueSequence,
1349
+ state: "admitted",
1350
+ replayed: false,
1351
+ }).pipe(Effect.mapError(internalFailure(operation)));
1352
+ }),
1353
+ );
1465
1354
 
1466
- const markReady: SubmissionLedger["Service"]["markReady"] = Effect.fn(
1467
- "DoSubmissionLedger.markReady",
1468
- )(function* (request: MarkReadyRequest) {
1355
+ yield* hitFailpoint("ledger:admit:after", operation);
1356
+
1357
+ return result;
1358
+ });
1359
+
1360
+ const markReady: SubmissionLedger["Service"]["markReady"] = Effect.fnUntraced(function* (
1361
+ request: MarkReadyRequest,
1362
+ ) {
1469
1363
  const operation = "ledger mark ready";
1470
1364
 
1471
1365
  const validated = yield* Schema.decodeEffect(Schema.toType(MarkReadyRequest))(request).pipe(
@@ -1485,7 +1379,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1485
1379
  UPDATE effect_agent_submissions
1486
1380
  SET state = 'ready', ready_at = ${now.iso}
1487
1381
  WHERE submission_id = ${validated.submissionId}
1488
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
1382
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
1383
+ rows.submissions.write,
1384
+ Effect.mapError(internalFailure(operation)),
1385
+ );
1489
1386
  yield* recordProgress("submission", operation);
1490
1387
  yield* retainLifecycle(submission, {
1491
1388
  _tag: "SubmissionReady",
@@ -1496,56 +1393,82 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1496
1393
  yield* hitFailpoint("ledger:mark-ready:after", operation);
1497
1394
  });
1498
1395
 
1499
- const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn("DoSubmissionLedger.lookup")(
1500
- function* (request: SubmissionLookup) {
1501
- const operation = "ledger lookup";
1396
+ const resolveFundingOwner: NonNullable<SubmissionLedger["Service"]["resolveFundingOwner"]> =
1397
+ Effect.fnUntraced(function* (input) {
1398
+ const operation = "resolve funding owner";
1502
1399
 
1503
- const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookup))(request).pipe(
1400
+ const request = yield* Schema.decodeEffect(FundingOwnerRequest)(input).pipe(
1504
1401
  Effect.mapError(internalFailure(operation)),
1505
1402
  );
1506
1403
 
1507
- if (validated._tag === "SubmissionLookupById") {
1508
- const row = yield* readSubmission(operation, validated.submissionId);
1509
-
1510
- if (Option.isNone(row)) return Option.none();
1511
-
1512
- return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
1513
- }
1404
+ const selected = yield* requireSubmission(operation, request.submissionId);
1514
1405
 
1515
- const foundRows = yield* byKey(validated, operation);
1406
+ if (selected.thread_id !== request.threadId)
1407
+ return yield* LedgerError.make({
1408
+ operation,
1409
+ message: "Selected funding input belongs to another Thread.",
1410
+ });
1411
+ if (selected.state === "joining")
1412
+ return yield* LedgerError.make({
1413
+ operation,
1414
+ message: "Selected funding input is still joining; retry after durable binding.",
1415
+ });
1416
+ const hostId = selected.joined_host_submission_id;
1516
1417
 
1517
- const decoded = yield* decodeSubmissionRows(
1518
- operation,
1519
- `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1520
- foundRows,
1521
- );
1418
+ if (hostId === null && selected.state === "joined")
1419
+ return yield* corruptionFailure(
1420
+ operation,
1421
+ "effect_agent_submissions",
1422
+ selected.submission_id,
1423
+ "Joined input has no stable host identity.",
1424
+ );
1425
+ if (hostId !== null && selected.state !== "joined" && selected.state !== "settled")
1426
+ return yield* corruptionFailure(
1427
+ operation,
1428
+ "effect_agent_submissions",
1429
+ selected.submission_id,
1430
+ "Host linkage is inconsistent with selected input state.",
1431
+ );
1432
+ const owner = hostId === null ? selected : yield* requireSubmission(operation, hostId);
1522
1433
 
1523
- if (decoded.length > 1) {
1434
+ if (
1435
+ owner.thread_id !== request.threadId ||
1436
+ (hostId !== null &&
1437
+ (owner.submission_id === selected.submission_id ||
1438
+ owner.joined_host_submission_id !== null ||
1439
+ owner.state === "joining" ||
1440
+ owner.state === "joined" ||
1441
+ owner.queue_sequence >= selected.queue_sequence))
1442
+ )
1524
1443
  return yield* corruptionFailure(
1525
1444
  operation,
1526
1445
  "effect_agent_submissions",
1527
- `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1528
- "An admission idempotency key returned more than one row.",
1446
+ selected.submission_id,
1447
+ "Funding host is not a stable earlier input in this Thread.",
1529
1448
  );
1530
- }
1531
- if (decoded.length === 0) return Option.none();
1532
1449
 
1533
- return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
1534
- },
1535
- );
1450
+ return yield* Schema.decodeEffect(Schema.toType(FundingOwner))({
1451
+ selected: yield* decodeSubmissionSnapshot(operation, selected),
1452
+ owner: yield* decodeSubmissionSnapshot(operation, owner),
1453
+ }).pipe(Effect.mapError(internalFailure(operation)));
1454
+ }, state.read);
1536
1455
 
1537
- // This LOCAL facet is the authoritative owner of every Thread stored in this Durable
1538
- // Object, so the key-scoped read IS the admission truth and the tri-state degenerates to
1539
- // NotAdmitted or Admitted (SUB-031). `AdmissionIndeterminate` becomes real one layer out:
1540
- // the WP2 routed decorator answers it when the OWNING Durable Object is unreachable.
1541
- const resolveAdmission: SubmissionLedger["Service"]["resolveAdmission"] = Effect.fn(
1542
- "DoSubmissionLedger.resolveAdmission",
1543
- )(function* (request: SubmissionLookupByKey) {
1544
- const operation = "ledger resolve admission";
1456
+ const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fnUntraced(function* (
1457
+ request: SubmissionLookup,
1458
+ ) {
1459
+ const operation = "ledger lookup";
1545
1460
 
1546
- const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookupByKey))(
1547
- request,
1548
- ).pipe(Effect.mapError(internalFailure(operation)));
1461
+ const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookup))(request).pipe(
1462
+ Effect.mapError(internalFailure(operation)),
1463
+ );
1464
+
1465
+ if (validated._tag === "SubmissionLookupById") {
1466
+ const row = yield* readSubmission(operation, validated.submissionId);
1467
+
1468
+ if (Option.isNone(row)) return Option.none();
1469
+
1470
+ return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
1471
+ }
1549
1472
 
1550
1473
  const foundRows = yield* byKey(validated, operation);
1551
1474
 
@@ -1563,76 +1486,66 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1563
1486
  "An admission idempotency key returned more than one row.",
1564
1487
  );
1565
1488
  }
1566
- if (decoded.length === 0) return AdmissionNotAdmitted.make();
1489
+ if (decoded.length === 0) return Option.none();
1567
1490
 
1568
- return AdmissionAdmitted.make({
1569
- submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
1570
- });
1491
+ return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
1571
1492
  });
1572
1493
 
1573
- const claim: SubmissionLedger["Service"]["claim"] = Effect.fn("DoSubmissionLedger.claim")(
1574
- function* (request: ClaimRequest) {
1575
- const operation = "ledger claim";
1494
+ // This LOCAL facet is the authoritative owner of every Thread stored in this Durable
1495
+ // Object, so the key-scoped read IS the admission truth and the tri-state degenerates to
1496
+ // NotAdmitted or Admitted (SUB-031). `AdmissionIndeterminate` becomes real one layer out:
1497
+ // the WP2 routed decorator answers it when the OWNING Durable Object is unreachable.
1498
+ const resolveAdmission: SubmissionLedger["Service"]["resolveAdmission"] = Effect.fnUntraced(
1499
+ function* (request: SubmissionLookupByKey) {
1500
+ const operation = "ledger resolve admission";
1501
+
1502
+ const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookupByKey))(
1503
+ request,
1504
+ ).pipe(Effect.mapError(internalFailure(operation)));
1576
1505
 
1577
- const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
1578
- Effect.mapError(internalFailure(operation)),
1506
+ const foundRows = yield* byKey(validated, operation);
1507
+
1508
+ const decoded = yield* decodeSubmissionRows(
1509
+ operation,
1510
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1511
+ foundRows,
1579
1512
  );
1580
1513
 
1581
- const attemptId = `attempt-${yield* mintUuid(operation)}`;
1582
- const ownershipToken = `owner-${yield* mintUuid(operation)}`;
1514
+ if (decoded.length > 1) {
1515
+ return yield* corruptionFailure(
1516
+ operation,
1517
+ "effect_agent_submissions",
1518
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1519
+ "An admission idempotency key returned more than one row.",
1520
+ );
1521
+ }
1522
+ if (decoded.length === 0) return AdmissionNotAdmitted.make();
1523
+
1524
+ return AdmissionAdmitted.make({
1525
+ submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
1526
+ });
1527
+ },
1528
+ );
1583
1529
 
1584
- yield* hitFailpoint("ledger:claim:before", operation);
1530
+ const selectHead: SubmissionLedger["Service"]["selectHead"] = Effect.fn(
1531
+ "DoSubmissionLedger.selectHead",
1532
+ )(function* (request: ClaimRequest) {
1533
+ const operation = "ledger select head";
1585
1534
 
1586
- const claimed = yield* inWriteTransaction(
1587
- operation,
1588
- Effect.gen(function* () {
1589
- const now = yield* currentInstant;
1535
+ const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
1536
+ Effect.mapError(internalFailure(operation)),
1537
+ );
1590
1538
 
1591
- // Ownership belongs to the whole Thread, including unknown work skipped below.
1592
- // Stored lease instants are normalized UTC strings, so the latest expiry covers
1593
- // every live lease. This check and the epoch grant share one write transaction.
1594
- const lane = yield* laneWork(validated.threadId, operation);
1595
-
1596
- const ownershipRows =
1597
- lane === undefined
1598
- ? yield* sql<Record<string, unknown>>`
1599
- SELECT ownership.*
1600
- FROM effect_agent_submission_ownership AS ownership
1601
- JOIN effect_agent_submissions AS submission
1602
- ON submission.submission_id = ownership.submission_id
1603
- WHERE submission.thread_id = ${validated.threadId}
1604
- ORDER BY ownership.lease_expires_at DESC
1605
- LIMIT 1
1606
- `.pipe(Effect.mapError(sqlFailure(operation)))
1607
- : (yield* Effect.forEach(
1608
- lane.filter((item) => item.state !== "settled"),
1609
- (item) =>
1610
- cached(rows.ownership.by("submission_id", item.submission_id), operation),
1611
- ))
1612
- .flat()
1613
- .sort((a, b) => b.lease_expires_at.localeCompare(a.lease_expires_at))
1614
- .slice(0, 1);
1615
-
1616
- const ownership = yield* decodeRows(
1617
- Schema.Array(OwnershipRow),
1618
- "effect_agent_submission_ownership",
1619
- validated.threadId,
1620
- ownershipRows,
1621
- ).pipe(Effect.mapError(internalFailure(operation)));
1539
+ const attemptId = `attempt-${yield* mintUuid(operation)}`;
1622
1540
 
1623
- if (ownership.length > 0) {
1624
- const expiresAt = yield* timestampMillis(
1625
- operation,
1626
- ownership[0].submission_id,
1627
- )(ownership[0].lease_expires_at);
1541
+ yield* hitFailpoint("ledger:claim:before", operation);
1628
1542
 
1629
- if (expiresAt > now.millis) return Option.none<Claim>();
1630
- }
1543
+ const claimed = yield* owner.withMutation(
1544
+ Effect.gen(function* () {
1545
+ if (owner.hasOwner(validated.threadId)) return Option.none<Claim>();
1631
1546
 
1632
- const headRows =
1633
- lane === undefined
1634
- ? yield* sql<Record<string, unknown>>`
1635
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1547
+ const headRows = yield* sql<Record<string, unknown>>`
1548
+ SELECT ${submissionColumns}
1636
1549
  FROM effect_agent_submissions
1637
1550
  WHERE thread_id = ${validated.threadId}
1638
1551
  AND state <> 'settled'
@@ -1644,533 +1557,330 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
1644
1557
  )
1645
1558
  ORDER BY queue_sequence ASC
1646
1559
  LIMIT ${validated.handoff === undefined ? 1 : validated.handoff.deferredSubmissionIds.length + 1}
1647
- `.pipe(Effect.mapError(sqlFailure(operation)))
1648
- : yield* Effect.gen(function* () {
1649
- const heads: Array<SubmissionRow> = [];
1650
-
1651
- for (const item of lane) {
1652
- if (item.state === "settled") continue;
1653
- if (
1654
- item.state === "unknown" &&
1655
- Option.isNone(yield* readAbortIntent(operation, item.submission_id))
1656
- )
1657
- continue;
1658
- heads.push(yield* requireSubmission(operation, item.submission_id));
1659
- if (
1660
- heads.length >=
1661
- (validated.handoff === undefined
1662
- ? 1
1663
- : validated.handoff.deferredSubmissionIds.length + 1)
1664
- )
1665
- break;
1666
- }
1667
-
1668
- return heads;
1669
- });
1560
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1670
1561
 
1671
- const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
1562
+ const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
1672
1563
 
1673
- if (heads.length === 0) return Option.none<Claim>();
1674
- let head = heads[0];
1564
+ if (heads.length === 0) return Option.none<Claim>();
1565
+ let head = heads[0];
1675
1566
 
1676
- if (validated.handoff !== undefined) {
1677
- const handoff = validated.handoff;
1567
+ if (validated.handoff !== undefined) {
1568
+ const handoff = validated.handoff;
1678
1569
 
1679
- for (const candidate of heads) {
1680
- if (candidate.submission_id === handoff.submissionId) {
1681
- head = candidate;
1682
- break;
1683
- }
1684
- if (
1685
- !handoff.deferredSubmissionIds.some((id) => id === candidate.submission_id) ||
1686
- candidate.state !== "input-applied" ||
1687
- Option.isSome(yield* readAbortIntent(operation, candidate.submission_id))
1688
- )
1689
- return Option.none<Claim>();
1570
+ for (const candidate of heads) {
1571
+ if (candidate.submission_id === handoff.submissionId) {
1572
+ head = candidate;
1573
+ break;
1690
1574
  }
1691
1575
  if (
1692
- head.submission_id !== handoff.submissionId ||
1693
- (head.state !== "ready" && head.state !== "running" && head.state !== "input-applied")
1576
+ !handoff.deferredSubmissionIds.some((id) => id === candidate.submission_id) ||
1577
+ candidate.state !== "input-applied" ||
1578
+ Option.isSome(yield* readAbortIntent(operation, candidate.submission_id))
1694
1579
  )
1695
1580
  return Option.none<Claim>();
1696
1581
  }
1697
-
1698
- // Approval/delegation suspension and joined work retain their queue barrier.
1699
- // Unknown work with an abort intent is selected for cleanup without Tool replay.
1700
- if (
1701
- head.state === "joining" ||
1702
- head.state === "joined" ||
1703
- head.state === "suspended" ||
1704
- (head.state === "unknown" &&
1705
- Option.isNone(yield* readAbortIntent(operation, head.submission_id)))
1706
- ) {
1707
- return Option.none<Claim>();
1708
- }
1709
-
1710
- // Bump the Thread's producer epoch atomically with the claim so every stale
1711
- // Attempt is fenced out of canonical appends (DUR-006). A Thread that was
1712
- // never materialized (eviction between admission and materialization) is created
1713
- // here so recovery can claim first and re-materialize idempotently at this epoch.
1714
- const threads = yield* journal
1715
- .getThread(head.thread_id)
1716
- .pipe(Effect.mapError(internalFailure(operation)));
1717
-
1718
1582
  if (
1719
- validated.handoff !== undefined &&
1720
- threads[0]?.producer_epoch !== validated.handoff.producerEpoch
1583
+ head.submission_id !== handoff.submissionId ||
1584
+ (head.state !== "ready" && head.state !== "input-applied")
1721
1585
  )
1722
1586
  return Option.none<Claim>();
1587
+ }
1723
1588
 
1724
- let producerEpoch: number;
1725
-
1726
- if (threads.length === 0) {
1727
- producerEpoch = 1;
1728
- yield* sql`
1729
- INSERT INTO effect_agent_threads (
1730
- thread_id,
1731
- created_at,
1732
- tail_sequence,
1733
- tail_digest,
1734
- producer_epoch
1735
- ) VALUES (
1736
- ${head.thread_id},
1737
- ${now.iso},
1738
- 0,
1739
- ${EMPTY_TAIL_DIGEST},
1740
- ${producerEpoch}
1741
- )
1742
- RETURNING *`.pipe(rows.threads.write, Effect.mapError(internalFailure(operation)));
1743
- } else {
1744
- producerEpoch = threads[0].producer_epoch + 1;
1745
- yield* sql`
1746
- UPDATE effect_agent_threads
1747
- SET producer_epoch = ${producerEpoch}
1748
- WHERE thread_id = ${head.thread_id}
1749
- RETURNING *`.pipe(rows.threads.write, Effect.mapError(internalFailure(operation)));
1750
- }
1751
-
1752
- const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1753
-
1754
- yield* sql`
1755
- INSERT INTO effect_agent_submission_ownership (
1756
- submission_id,
1757
- attempt_id,
1758
- ownership_token,
1759
- producer_epoch,
1760
- owner_producer_id,
1761
- lease_expires_at
1762
- ) VALUES (
1763
- ${head.submission_id},
1764
- ${attemptId},
1765
- ${ownershipToken},
1766
- ${producerEpoch},
1767
- ${validated.producerId},
1768
- ${leaseExpiresAt}
1769
- )
1770
- ON CONFLICT (submission_id) DO UPDATE SET
1771
- attempt_id = excluded.attempt_id,
1772
- ownership_token = excluded.ownership_token,
1773
- producer_epoch = excluded.producer_epoch,
1774
- owner_producer_id = excluded.owner_producer_id,
1775
- lease_expires_at = excluded.lease_expires_at
1776
- RETURNING *`.pipe(rows.ownership.write, Effect.mapError(internalFailure(operation)));
1777
-
1778
- yield* sql`
1779
- INSERT INTO effect_agent_attempts (
1780
- attempt_id,
1781
- submission_id,
1782
- thread_id,
1783
- owner_producer_id,
1784
- producer_epoch,
1785
- claimed_at
1786
- ) VALUES (
1787
- ${attemptId},
1788
- ${head.submission_id},
1789
- ${head.thread_id},
1790
- ${validated.producerId},
1791
- ${producerEpoch},
1792
- ${now.iso}
1793
- )
1794
- `.pipe(Effect.mapError(sqlFailure(operation)));
1795
-
1796
- if (head.state === "ready") {
1797
- yield* sql`
1798
- UPDATE effect_agent_submissions
1799
- SET state = 'running'
1800
- WHERE submission_id = ${head.submission_id}
1801
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
1802
- }
1589
+ // Approval/delegation suspension and joined work retain their queue barrier.
1590
+ // Unknown work with an abort intent is selected for cleanup without Tool replay.
1591
+ if (
1592
+ head.state === "joining" ||
1593
+ head.state === "joined" ||
1594
+ head.state === "suspended" ||
1595
+ (head.state === "unknown" &&
1596
+ Option.isNone(yield* readAbortIntent(operation, head.submission_id)))
1597
+ ) {
1598
+ return Option.none<Claim>();
1599
+ }
1803
1600
 
1804
- const inputPayload = yield* parseStoredJsonText(head.input_json).pipe(
1805
- Effect.mapError((error) =>
1806
- corruptionFailure(
1807
- operation,
1808
- "effect_agent_submissions",
1809
- head.submission_id,
1810
- error.message,
1811
- ),
1601
+ const inputPayload = yield* parseStoredJsonText(head.input_json).pipe(
1602
+ Effect.mapError((error) =>
1603
+ corruptionFailure(
1604
+ operation,
1605
+ "effect_agent_submissions",
1606
+ head.submission_id,
1607
+ error.message,
1812
1608
  ),
1813
- );
1814
-
1815
- return Option.some(
1816
- yield* decodeClaim({
1817
- submissionId: head.submission_id,
1818
- attemptId,
1819
- ownershipToken,
1820
- producerEpoch,
1821
- leaseExpiresAt,
1822
- inputPayload,
1823
- }).pipe(Effect.mapError(internalFailure(operation))),
1824
- );
1825
- }),
1826
- );
1827
-
1828
- yield* hitFailpoint("ledger:claim:after", operation);
1829
-
1830
- return claimed;
1831
- },
1832
- );
1833
-
1834
- const renewOwnership: SubmissionLedger["Service"]["renewOwnership"] = Effect.fn(
1835
- "DoSubmissionLedger.renewOwnership",
1836
- )(function* (request: RenewOwnershipRequest) {
1837
- const operation = "ledger renew ownership";
1838
-
1839
- const validated = yield* Schema.decodeEffect(Schema.toType(RenewOwnershipRequest))(
1840
- request,
1841
- ).pipe(Effect.mapError(internalFailure(operation)));
1842
-
1843
- yield* hitFailpoint("ledger:renew:before", operation);
1844
-
1845
- const renewal = yield* inWriteTransaction(
1846
- operation,
1847
- Effect.gen(function* () {
1848
- const submission = yield* requireSubmission(operation, validated.submissionId);
1849
-
1850
- yield* requireOwnership(operation, submission, validated.ownershipToken);
1851
- const now = yield* currentInstant;
1852
- const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1853
-
1854
- yield* sql`
1855
- UPDATE effect_agent_submission_ownership
1856
- SET lease_expires_at = ${leaseExpiresAt}
1857
- WHERE submission_id = ${validated.submissionId}
1858
- RETURNING *`.pipe(rows.ownership.write, Effect.mapError(internalFailure(operation)));
1609
+ ),
1610
+ );
1859
1611
 
1860
- return yield* decodeOwnershipRenewal({
1861
- ownershipToken: validated.ownershipToken,
1862
- leaseExpiresAt,
1863
- }).pipe(Effect.mapError(internalFailure(operation)));
1612
+ return Option.some(
1613
+ yield* decodeClaim({
1614
+ submissionId: head.submission_id,
1615
+ attemptId,
1616
+ inputPayload,
1617
+ }).pipe(Effect.mapError(internalFailure(operation))),
1618
+ );
1864
1619
  }),
1865
1620
  );
1866
1621
 
1867
- yield* hitFailpoint("ledger:renew:after", operation);
1622
+ yield* hitFailpoint("ledger:claim:after", operation);
1868
1623
 
1869
- return renewal;
1624
+ return claimed;
1870
1625
  });
1871
1626
 
1872
- const releaseOwnership: SubmissionLedger["Service"]["releaseOwnership"] = Effect.fnUntraced(
1873
- function* (request: ReleaseOwnershipRequest) {
1874
- const operation = "ledger release ownership";
1627
+ const markInputApplied: SubmissionLedger["Service"]["markInputApplied"] = Effect.fnUntraced(
1628
+ function* (request: MarkInputAppliedRequest) {
1629
+ const operation = "ledger mark input applied";
1875
1630
 
1876
- const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseOwnershipRequest))(
1631
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
1877
1632
  request,
1878
1633
  ).pipe(Effect.mapError(internalFailure(operation)));
1879
1634
 
1880
- yield* hitFailpoint("ledger:release:before", operation);
1881
- // Settlement and suspension already remove ownership. Cleanup can observe that without
1882
- // opening a transaction; an owned lane must still pass the atomic check below.
1883
- const current = yield* requireSubmission(operation, validated.submissionId);
1884
-
1885
- yield* requireOwnership(operation, current, validated.ownershipToken);
1635
+ yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1886
1636
  yield* inWriteTransaction(
1887
1637
  operation,
1888
1638
  Effect.gen(function* () {
1889
1639
  const submission = yield* requireSubmission(operation, validated.submissionId);
1890
1640
 
1891
- yield* requireOwnership(operation, submission, validated.ownershipToken);
1641
+ yield* requireOwner(operation, submission);
1642
+ if (submission.input_applied_record_id !== null) {
1643
+ if (
1644
+ submission.input_applied_record_id === validated.recordId &&
1645
+ submission.input_applied_sequence === validated.sequence
1646
+ ) {
1647
+ return;
1648
+ }
1649
+
1650
+ return yield* corruptionFailure(
1651
+ operation,
1652
+ "effect_agent_submissions",
1653
+ validated.submissionId,
1654
+ "A different canonical input marker is already recorded for this Submission.",
1655
+ );
1656
+ }
1892
1657
  yield* sql`
1893
- DELETE FROM effect_agent_submission_ownership
1658
+ UPDATE effect_agent_submissions
1659
+ SET
1660
+ input_applied_record_id = ${validated.recordId},
1661
+ input_applied_sequence = ${validated.sequence},
1662
+ state = CASE
1663
+ WHEN state IN ('admitted', 'ready') THEN 'input-applied'
1664
+ ELSE state
1665
+ END
1894
1666
  WHERE submission_id = ${validated.submissionId}
1895
- RETURNING *`.pipe(rows.ownership.remove, Effect.mapError(internalFailure(operation)));
1896
- if (submission.state === "running") {
1897
- yield* sql`
1898
- UPDATE effect_agent_submissions
1899
- SET state = 'ready'
1900
- WHERE submission_id = ${validated.submissionId}
1901
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
1902
- }
1667
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
1668
+ rows.submissions.write,
1669
+ Effect.mapError(internalFailure(operation)),
1670
+ );
1903
1671
  }),
1904
- (error) => error._tag === "OwnershipLost",
1905
1672
  );
1906
- yield* hitFailpoint("ledger:release:after", operation);
1673
+ yield* hitFailpoint("ledger:mark-input-applied:after", operation);
1907
1674
  },
1908
- withStorageSpan(
1909
- "DoSubmissionLedger.releaseOwnership",
1910
- (error) => error._tag === "OwnershipLost",
1911
- ),
1912
1675
  );
1913
1676
 
1914
- const markInputApplied: SubmissionLedger["Service"]["markInputApplied"] = Effect.fn(
1915
- "DoSubmissionLedger.markInputApplied",
1916
- )(function* (request: MarkInputAppliedRequest) {
1917
- const operation = "ledger mark input applied";
1677
+ const publish: SettlementPublisher["Service"]["publish"] = Effect.fn(
1678
+ "DoSubmissionLedger.publishSettlement",
1679
+ )(function* (input) {
1680
+ const operation = "publish settlement";
1681
+ const { request, record, settlement } = yield* validatePublication(input);
1918
1682
 
1919
- const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
1920
- request,
1921
- ).pipe(Effect.mapError(internalFailure(operation)));
1683
+ const prepared = yield* prepareCanonicalAppend(request.append).pipe(
1684
+ Effect.provideService(Crypto.Crypto, crypto),
1685
+ );
1922
1686
 
1923
- yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1924
- yield* inWriteTransaction(
1925
- operation,
1926
- Effect.gen(function* () {
1927
- const submission = yield* requireSubmission(operation, validated.submissionId);
1928
-
1929
- yield* requireOwnership(operation, submission, validated.ownershipToken);
1930
- if (submission.input_applied_record_id !== null) {
1931
- if (
1932
- submission.input_applied_record_id === validated.recordId &&
1933
- submission.input_applied_sequence === validated.sequence
1934
- ) {
1935
- return;
1936
- }
1937
-
1938
- return yield* corruptionFailure(
1939
- operation,
1940
- "effect_agent_submissions",
1941
- validated.submissionId,
1942
- "A different canonical input marker is already recorded for this Submission.",
1943
- );
1944
- }
1945
- yield* sql`
1946
- UPDATE effect_agent_submissions
1947
- SET
1948
- input_applied_record_id = ${validated.recordId},
1949
- input_applied_sequence = ${validated.sequence},
1950
- state = CASE
1951
- WHEN state IN ('admitted', 'ready', 'running') THEN 'input-applied'
1952
- ELSE state
1953
- END
1954
- WHERE submission_id = ${validated.submissionId}
1955
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
1956
- }),
1957
- );
1958
- yield* hitFailpoint("ledger:mark-input-applied:after", operation);
1959
- });
1960
-
1961
- const reserveSettlement: SubmissionLedger["Service"]["reserveSettlement"] = Effect.fn(
1962
- "DoSubmissionLedger.reserveSettlement",
1963
- )(function* (request: SettlementReservation) {
1964
- const operation = "ledger reserve settlement";
1965
-
1966
- const validated = yield* Schema.decodeEffect(Schema.toType(SettlementReservation))(
1967
- request,
1968
- ).pipe(Effect.mapError(internalFailure(operation)));
1687
+ const mapAppendError = (error: { readonly _tag: string; readonly message: string }) =>
1688
+ ThreadStoreError.make({ operation, message: error.message, cause: error });
1969
1689
 
1970
- const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(
1971
- Effect.mapError(internalFailure(operation)),
1972
- );
1973
-
1974
- // The reserved record is appended canonically later; refuse over-bound payloads typed
1975
- // before the reservation row exists.
1976
- yield* journal
1977
- .checkValueBound(operation, recordJson)
1978
- .pipe(Effect.mapError(internalFailure(operation)));
1979
- yield* hitFailpoint("ledger:reserve-settlement:before", operation);
1980
-
1981
- const reserved = yield* inWriteTransaction(
1982
- operation,
1983
- Effect.gen(function* () {
1984
- const existing = yield* readReservation(operation, validated.submissionId);
1985
-
1986
- if (Option.isSome(existing)) {
1987
- const identical =
1988
- existing.value.settlement_id === validated.settlementId &&
1989
- existing.value.outcome === validated.outcome &&
1990
- existing.value.record_digest === validated.recordDigest &&
1991
- existing.value.record_json === recordJson;
1992
-
1993
- if (!identical) {
1994
- return yield* SettlementConflict.make({
1995
- submissionId: validated.submissionId,
1996
- existingOutcome: existing.value.outcome,
1997
- });
1998
- }
1999
-
2000
- const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(
2001
- Effect.mapError((error) =>
2002
- corruptionFailure(
2003
- operation,
2004
- "effect_agent_settlement_reservations",
2005
- validated.submissionId,
2006
- error.message,
2007
- ),
2008
- ),
2009
- );
2010
-
2011
- return ReservedSettlement.make({
2012
- submissionId: validated.submissionId,
2013
- settlementId: validated.settlementId,
2014
- outcome: validated.outcome,
2015
- record,
2016
- recordDigest: validated.recordDigest,
2017
- replayed: true,
2018
- });
2019
- }
2020
-
2021
- const submission = yield* requireSubmission(operation, validated.submissionId);
1690
+ yield* journal.prepareAppend(prepared.raw).pipe(Effect.mapError(mapAppendError));
2022
1691
 
2023
- if (submission.state === "settled") {
2024
- if (submission.settled_outcome === null) {
2025
- return yield* corruptionFailure(
2026
- operation,
2027
- "effect_agent_submissions",
2028
- validated.submissionId,
2029
- "A settled Submission carries no terminal outcome.",
1692
+ return yield* owner.withProgress(
1693
+ request.append.threadId,
1694
+ owner.withMutation(
1695
+ Effect.gen(function* () {
1696
+ yield* hitFailpoint("append:before", operation);
1697
+
1698
+ const result = yield* journal
1699
+ .withWriteTransaction(operation)(
1700
+ Effect.gen(function* () {
1701
+ const submission = yield* requireSubmission(operation, request.submissionId);
1702
+
1703
+ if (submission.thread_id !== prepared.request.threadId)
1704
+ return yield* LedgerError.make({
1705
+ operation,
1706
+ message: "Settlement publication belongs to another Thread.",
1707
+ });
1708
+ const snapshot = yield* decodeSubmissionSnapshot(operation, submission);
1709
+
1710
+ yield* validateCanonicalSettlement(record, snapshot);
1711
+ const existing = yield* readCanonicalSettlement(operation, submission);
1712
+ const thread = (yield* journal.getThread(prepared.request.threadId))[0];
1713
+
1714
+ if (thread === undefined)
1715
+ return yield* ThreadNotMaterialized.make({ threadId: prepared.request.threadId });
1716
+
1717
+ // Replay still requires authority: finalization releases Owned tokens, while
1718
+ // retained host linkage or abort intent can authorize tokenless settled replay.
1719
+ switch (request.authority._tag) {
1720
+ case "Owned":
1721
+ yield* requireOwner(operation, submission);
1722
+ yield* validateCanonicalSettlement(record, {
1723
+ submissionId: request.submissionId,
1724
+ receiptId: snapshot.receiptId,
1725
+ ...(settlement.outcome === "aborted" && settlement.runId === undefined
1726
+ ? {}
1727
+ : { runId: runIdForSubmission(request.submissionId) }),
1728
+ });
1729
+ break;
1730
+ case "Joined": {
1731
+ if (
1732
+ (submission.state !== "joined" &&
1733
+ !(submission.state === "settled" && existing !== undefined)) ||
1734
+ submission.joined_host_submission_id !== request.authority.hostSubmissionId
1735
+ )
1736
+ return yield* LedgerError.make({
1737
+ operation,
1738
+ message: "Submission is not joined to the named host.",
1739
+ });
1740
+
1741
+ const host = yield* requireSubmission(
1742
+ operation,
1743
+ request.authority.hostSubmissionId,
1744
+ );
1745
+
1746
+ if (host.thread_id !== submission.thread_id)
1747
+ return yield* LedgerError.make({
1748
+ operation,
1749
+ message: "Joined host belongs to another Thread.",
1750
+ });
1751
+ const canonicalHost = yield* readCanonicalSettlement(operation, host);
1752
+
1753
+ if (canonicalHost === undefined)
1754
+ return yield* LedgerError.make({
1755
+ operation,
1756
+ message: "Joined host has no canonical settlement.",
1757
+ });
1758
+ yield* validateJoinedSettlement(settlement, canonicalHost.settlement);
1759
+ break;
1760
+ }
1761
+ case "QueuedAbort":
1762
+ if (
1763
+ (submission.state !== "ready" &&
1764
+ !(submission.state === "settled" && existing !== undefined)) ||
1765
+ settlement.outcome !== "aborted" ||
1766
+ Option.isNone(yield* readAbortIntent(operation, request.submissionId)) ||
1767
+ owner.hasOwner(submission.thread_id, request.submissionId)
1768
+ )
1769
+ return yield* LedgerError.make({
1770
+ operation,
1771
+ message: "Queued abort is no longer authorized.",
1772
+ });
1773
+ yield* validateCanonicalSettlement(record, {
1774
+ submissionId: request.submissionId,
1775
+ receiptId: snapshot.receiptId,
1776
+ runId: undefined,
1777
+ });
1778
+ }
1779
+ if (existing !== undefined) {
1780
+ yield* journal.checkCommitIntent(prepared.raw);
1781
+
1782
+ return SettlementPublicationResult.make({
1783
+ record: existing.record,
1784
+ tailSequence: thread.tail_sequence,
1785
+ replayed: true,
1786
+ });
1787
+ }
1788
+ if (submission.state === "settled")
1789
+ return yield* LedgerError.make({
1790
+ operation,
1791
+ message: "Finalized Submission has no canonical settlement.",
1792
+ });
1793
+ const appended = yield* journal.appendPrepared(prepared.raw);
1794
+
1795
+ return SettlementPublicationResult.make({
1796
+ record,
1797
+ tailSequence: appended.lastSequence,
1798
+ replayed: appended.replayed,
1799
+ });
1800
+ }),
1801
+ )
1802
+ .pipe(
1803
+ Effect.mapError((error) => {
1804
+ if (error._tag === "DoAppendConflict")
1805
+ return AppendConflict.make({
1806
+ threadId: prepared.request.threadId,
1807
+ batchId: prepared.request.batch.batchId,
1808
+ reason: error.reason,
1809
+ ...(error.actualTailSequence !== undefined
1810
+ ? {
1811
+ actualTailSequence: error.actualTailSequence,
1812
+ }
1813
+ : {}),
1814
+ });
1815
+
1816
+ return isDoStorageError(error) ||
1817
+ error._tag === "DoStorageCorruptionError" ||
1818
+ error._tag === "DoStorageFailpointError" ||
1819
+ error._tag === "DoValueBoundExceeded"
1820
+ ? mapAppendError(error)
1821
+ : error;
1822
+ }),
2030
1823
  );
2031
- }
2032
-
2033
- return yield* SettlementConflict.make({
2034
- submissionId: validated.submissionId,
2035
- existingOutcome: submission.settled_outcome,
2036
- });
2037
- }
2038
- // A `joined` Submission settles WITH its host (plan §2.5) and its lane is never
2039
- // worker-claimable, so no ownership token can exist for it: the recorded host linkage
2040
- // authorizes the reservation and the presented token is not consulted.
2041
- if (!(submission.state === "joined" && submission.joined_host_submission_id !== null)) {
2042
- // P7 §7(c): an aborted, never-claimed, still-queued Submission likewise has no live
2043
- // ownership to fence against — its durable abort intent authorizes exactly its
2044
- // ABORTED settlement (`terminalizing` is the same pass's crash replay). Every other
2045
- // reservation stays fenced by the target lane's live ownership.
2046
- let queuedAbortSettlement = false;
2047
-
2048
- if (
2049
- validated.outcome === "aborted" &&
2050
- (submission.state === "ready" || submission.state === "terminalizing")
2051
- ) {
2052
- const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
2053
-
2054
- if (Option.isSome(abortIntent)) {
2055
- const ownership = yield* readOwnership(operation, validated.submissionId);
2056
-
2057
- queuedAbortSettlement = Option.isNone(ownership);
2058
- }
2059
- }
2060
- if (!queuedAbortSettlement) {
2061
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2062
- }
2063
- }
2064
- const now = yield* currentInstant;
2065
1824
 
2066
- yield* sql`
2067
- INSERT INTO effect_agent_settlement_reservations (
2068
- submission_id,
2069
- settlement_id,
2070
- outcome,
2071
- record_id,
2072
- record_json,
2073
- record_digest,
2074
- reserved_at
2075
- ) VALUES (
2076
- ${validated.submissionId},
2077
- ${validated.settlementId},
2078
- ${validated.outcome},
2079
- ${validated.record.recordId},
2080
- ${recordJson},
2081
- ${validated.recordDigest},
2082
- ${now.iso}
2083
- )
2084
- RETURNING *`.pipe(rows.reservations.write, Effect.mapError(internalFailure(operation)));
2085
- yield* sql`
2086
- UPDATE effect_agent_submissions
2087
- SET state = 'terminalizing'
2088
- WHERE submission_id = ${validated.submissionId}
2089
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
1825
+ yield* hitFailpoint("append:after", operation);
2090
1826
 
2091
- return ReservedSettlement.make({
2092
- submissionId: validated.submissionId,
2093
- settlementId: validated.settlementId,
2094
- outcome: validated.outcome,
2095
- record: validated.record,
2096
- recordDigest: validated.recordDigest,
2097
- replayed: false,
2098
- });
2099
- }),
1827
+ return result;
1828
+ }),
1829
+ ),
2100
1830
  );
2101
-
2102
- yield* hitFailpoint("ledger:reserve-settlement:after", operation);
2103
-
2104
- return reserved;
2105
1831
  });
2106
1832
 
2107
- const validateFinalization = Effect.fn("DoSubmissionLedger.validateFinalization")(function* (
1833
+ const validateFinalization = Effect.fnUntraced(function* (
2108
1834
  validated: SettlementFinalization,
2109
- reservation: Option.Option<ReservationRow>,
1835
+ submission: SubmissionRow,
2110
1836
  ) {
2111
1837
  const operation = "ledger finalize settlement";
1838
+ const canonical = yield* readCanonicalSettlement(operation, submission);
2112
1839
 
2113
- if (Option.isNone(reservation)) {
1840
+ if (canonical === undefined)
2114
1841
  return yield* LedgerError.make({
2115
1842
  operation,
2116
- message: `No settlement reservation exists for submission ${validated.submissionId}.`,
1843
+ message: `No canonical settlement exists for submission ${validated.submissionId}.`,
2117
1844
  });
2118
- }
2119
- if (reservation.value.settlement_id !== validated.settlementId) {
1845
+ if (canonical.settlement.settlementId !== validated.settlementId)
2120
1846
  return yield* SettlementConflict.make({
2121
1847
  submissionId: validated.submissionId,
2122
- existingOutcome: reservation.value.outcome,
1848
+ existingOutcome: canonical.settlement.outcome,
2123
1849
  });
2124
- }
2125
-
2126
- const reservationRecord = yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(
2127
- Effect.mapError((error) =>
2128
- corruptionFailure(
2129
- operation,
2130
- "effect_agent_settlement_reservations",
2131
- validated.submissionId,
2132
- error.message,
2133
- ),
2134
- ),
2135
- );
2136
1850
 
2137
- const settlementFailure = settlementFailureFromRecord(reservationRecord);
2138
-
2139
- if ((reservation.value.outcome === "failed") !== (settlementFailure !== undefined)) {
2140
- return yield* corruptionFailure(
2141
- operation,
2142
- "effect_agent_settlement_reservations",
2143
- validated.submissionId,
2144
- "The reserved outcome and canonical failure diagnostic disagree.",
2145
- );
2146
- }
2147
-
2148
- return { reservation: reservation.value, reservationRecord, settlementFailure };
1851
+ return { ...canonical, settlementFailure: settlementFailureFromRecord(canonical.record) };
2149
1852
  });
2150
1853
 
2151
- const replayFinalization = Effect.fn("DoSubmissionLedger.replayFinalization")(function* (
1854
+ const replayFinalization = Effect.fnUntraced(function* (
2152
1855
  validated: SettlementFinalization,
2153
1856
  submission: SubmissionRow,
2154
- { reservation, settlementFailure }: Effect.Success<ReturnType<typeof validateFinalization>>,
1857
+ {
1858
+ record,
1859
+ settlement,
1860
+ settlementFailure,
1861
+ }: Effect.Success<ReturnType<typeof validateFinalization>>,
2155
1862
  ) {
2156
1863
  const operation = "ledger finalize settlement";
2157
1864
 
2158
- if (reservation.finalized_at === null) {
1865
+ if (
1866
+ submission.finalized_at === null ||
1867
+ submission.settled_record_id !== record.recordId ||
1868
+ submission.settled_outcome !== settlement.outcome
1869
+ )
2159
1870
  return yield* corruptionFailure(
2160
1871
  operation,
2161
- "effect_agent_settlement_reservations",
1872
+ "effect_agent_submissions",
2162
1873
  validated.submissionId,
2163
- "A settled Submission's reservation carries no finalization timestamp.",
1874
+ "Settled projection disagrees with its canonical settlement.",
2164
1875
  );
2165
- }
2166
1876
 
2167
1877
  return yield* decodeSettlement({
2168
1878
  submissionId: validated.submissionId,
2169
1879
  settlementId: validated.settlementId,
2170
1880
  receiptId: submission.receipt_id,
2171
- outcome: reservation.outcome,
1881
+ outcome: settlement.outcome,
2172
1882
  ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
2173
- settledAt: reservation.finalized_at,
1883
+ settledAt: submission.finalized_at,
2174
1884
  }).pipe(Effect.mapError(internalFailure(operation)));
2175
1885
  });
2176
1886
 
@@ -2192,34 +1902,39 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2192
1902
 
2193
1903
  if (Option.isNone(submission) || submission.value.state !== "settled") return Option.none();
2194
1904
 
2195
- const finalization = yield* validateFinalization(
2196
- validated,
2197
- yield* readReservation(operation, validated.submissionId),
2198
- );
1905
+ const finalization = yield* validateFinalization(validated, submission.value);
2199
1906
 
2200
- return Option.some(yield* replayFinalization(validated, submission.value, finalization));
1907
+ return Option.some({
1908
+ settlement: yield* replayFinalization(validated, submission.value, finalization),
1909
+ threadId: submission.value.thread_id,
1910
+ });
2201
1911
  }),
2202
1912
  );
2203
1913
 
2204
1914
  if (Option.isSome(replay)) {
1915
+ owner.closeSubmission(replay.value.threadId, validated.submissionId);
2205
1916
  yield* hitFailpoint("ledger:finalize-settlement:after", operation);
2206
1917
 
2207
- return replay.value;
1918
+ return replay.value.settlement;
2208
1919
  }
2209
1920
 
2210
1921
  const settlement = yield* inWriteTransaction(
2211
1922
  operation,
2212
1923
  Effect.gen(function* () {
2213
- const state = yield* validateFinalization(
2214
- validated,
2215
- yield* readReservation(operation, validated.submissionId),
2216
- );
2217
-
2218
- const { reservation, reservationRecord, settlementFailure } = state;
2219
1924
  const submission = yield* requireSubmission(operation, validated.submissionId);
1925
+ const state = yield* validateFinalization(validated, submission);
1926
+
1927
+ const {
1928
+ settlement: canonicalSettlement,
1929
+ record: canonicalRecord,
1930
+ settlementFailure,
1931
+ } = state;
2220
1932
 
2221
1933
  if (submission.state === "settled")
2222
- return yield* replayFinalization(validated, submission, state);
1934
+ return {
1935
+ settlement: yield* replayFinalization(validated, submission, state),
1936
+ threadId: submission.thread_id,
1937
+ };
2223
1938
  const now = yield* currentInstant;
2224
1939
 
2225
1940
  const terminal =
@@ -2227,7 +1942,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2227
1942
  ? undefined
2228
1943
  : workerTerminalFromRecord(
2229
1944
  yield* decodeSubmissionSnapshot(operation, submission),
2230
- reservationRecord,
1945
+ canonicalRecord,
2231
1946
  );
2232
1947
 
2233
1948
  let sealedTerminal: typeof terminal;
@@ -2269,55 +1984,51 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2269
1984
 
2270
1985
  yield* sql`
2271
1986
  UPDATE effect_agent_submissions
2272
- SET state = 'settled', settled_outcome = ${reservation.outcome}
2273
- WHERE submission_id = ${validated.submissionId}
2274
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
2275
- yield* sql`
2276
- UPDATE effect_agent_settlement_reservations
2277
- SET finalized_at = ${now.iso}
1987
+ SET state = 'settled', settled_outcome = ${canonicalSettlement.outcome},
1988
+ settled_record_id = ${canonicalRecord.recordId}, finalized_at = ${now.iso}
2278
1989
  WHERE submission_id = ${validated.submissionId}
2279
- RETURNING *`.pipe(rows.reservations.write, Effect.mapError(internalFailure(operation)));
2280
- yield* sql`
2281
- DELETE FROM effect_agent_submission_ownership
2282
- WHERE submission_id = ${validated.submissionId}
2283
- RETURNING *`.pipe(rows.ownership.remove, Effect.mapError(internalFailure(operation)));
1990
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
1991
+ rows.submissions.write,
1992
+ Effect.mapError(internalFailure(operation)),
1993
+ );
2284
1994
 
2285
1995
  yield* recordProgress("submission", operation);
2286
1996
 
2287
1997
  if (sealedTerminal !== undefined)
2288
1998
  yield* retainWorkerSeal(submission.thread_id, sealedTerminal);
2289
1999
 
2290
- return yield* decodeSettlement({
2291
- submissionId: validated.submissionId,
2292
- settlementId: validated.settlementId,
2293
- receiptId: submission.receipt_id,
2294
- outcome: reservation.outcome,
2295
- ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
2296
- settledAt: now.iso,
2297
- }).pipe(Effect.mapError(internalFailure(operation)));
2000
+ return {
2001
+ threadId: submission.thread_id,
2002
+ settlement: yield* decodeSettlement({
2003
+ submissionId: validated.submissionId,
2004
+ settlementId: validated.settlementId,
2005
+ receiptId: submission.receipt_id,
2006
+ outcome: canonicalSettlement.outcome,
2007
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
2008
+ settledAt: now.iso,
2009
+ }).pipe(Effect.mapError(internalFailure(operation))),
2010
+ };
2298
2011
  }),
2299
2012
  );
2300
2013
 
2014
+ owner.closeSubmission(settlement.threadId, validated.submissionId);
2301
2015
  yield* hitFailpoint("ledger:finalize-settlement:after", operation);
2302
2016
 
2303
- return settlement;
2017
+ return settlement.settlement;
2304
2018
  });
2305
2019
 
2306
- const inspectWorker = Effect.fn("DoSubmissionLedger.inspectWorker")(function* (
2307
- threadId: SubmissionSnapshot["threadId"],
2308
- ) {
2020
+ const inspectWorker = Effect.fnUntraced(function* (threadId: SubmissionSnapshot["threadId"]) {
2309
2021
  const operation = "inspect worker";
2310
2022
 
2311
2023
  return yield* state
2312
2024
  .transaction(
2313
2025
  Effect.gen(function* () {
2314
2026
  const read = Effect.fnUntraced(function* (active: boolean) {
2315
- const rows =
2316
- yield* sql`SELECT ${sql.literal(SUBMISSION_COLUMNS)} FROM effect_agent_submissions
2027
+ const rows = yield* sql`SELECT ${submissionColumns} FROM effect_agent_submissions
2317
2028
  WHERE thread_id = ${threadId} ${active ? sql`AND state <> 'settled'` : sql``}
2318
2029
  ORDER BY queue_sequence ${active ? sql`ASC` : sql`DESC`} LIMIT 1`.pipe(
2319
- Effect.mapError(sqlFailure(operation)),
2320
- );
2030
+ Effect.mapError(sqlFailure(operation)),
2031
+ );
2321
2032
 
2322
2033
  const decoded = yield* decodeSubmissionRows(operation, threadId, rows);
2323
2034
 
@@ -2347,9 +2058,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2347
2058
  );
2348
2059
  });
2349
2060
 
2350
- const stopWorker = Effect.fn("DoSubmissionLedger.stopWorker")(function* (
2351
- request: WorkerStopCommand,
2352
- ) {
2061
+ const stopWorker = Effect.fnUntraced(function* (request: WorkerStopCommand) {
2353
2062
  const operation = "ledger stop worker";
2354
2063
 
2355
2064
  const validated = yield* Schema.decodeEffect(WorkerStopCommand)(request).pipe(
@@ -2375,23 +2084,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2375
2084
  Effect.mapError(internalFailure(operation)),
2376
2085
  );
2377
2086
 
2378
- const owners = yield* sql`SELECT o.submission_id FROM effect_agent_submission_ownership o
2379
- JOIN effect_agent_submissions s ON s.submission_id = o.submission_id
2380
- WHERE s.thread_id = ${validated.threadId} AND s.state <> 'settled'`.pipe(
2381
- Effect.mapError(sqlFailure(operation)),
2382
- );
2383
-
2384
2087
  if (sealed.length > 0 || aborted.length > 0) yield* recordProgress("control", operation);
2385
2088
  if (sealed.length > 0) yield* retainWorkerSeal(validated.threadId, null);
2386
2089
 
2387
- return owners.length;
2090
+ return owner.hasOwner(validated.threadId) ? 1 : 0;
2388
2091
  }),
2389
2092
  );
2390
2093
  });
2391
2094
 
2392
- const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fn(
2393
- "DoSubmissionLedger.requestAbort",
2394
- )(function* (request: AbortCommand) {
2095
+ const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fnUntraced(function* (
2096
+ request: AbortCommand,
2097
+ ) {
2395
2098
  const operation = "ledger request abort";
2396
2099
 
2397
2100
  const validated = yield* Schema.decodeEffect(Schema.toType(AbortCommand))(request).pipe(
@@ -2495,9 +2198,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2495
2198
  return intent;
2496
2199
  });
2497
2200
 
2498
- const claimJoining: SubmissionLedger["Service"]["claimJoining"] = Effect.fn(
2499
- "DoSubmissionLedger.claimJoining",
2500
- )(function* (request: ClaimJoiningRequest) {
2201
+ const claimJoining: SubmissionLedger["Service"]["claimJoining"] = Effect.fnUntraced(function* (
2202
+ request: ClaimJoiningRequest,
2203
+ ) {
2501
2204
  const operation = "ledger claim joining";
2502
2205
 
2503
2206
  const validated = yield* Schema.decodeEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(
@@ -2517,8 +2220,8 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2517
2220
  message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
2518
2221
  });
2519
2222
  }
2520
- // The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
2521
- yield* requireOwnership(operation, host, validated.ownershipToken);
2223
+ // The host Attempt retains its local ownership while the ready prefix joins it.
2224
+ yield* requireOwner(operation, host);
2522
2225
 
2523
2226
  const stopped = yield* cached(
2524
2227
  rows.workerStops.by("thread_id", validated.threadId),
@@ -2527,21 +2230,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2527
2230
 
2528
2231
  if (stopped.length > 0) return [];
2529
2232
 
2530
- const lane = yield* laneWork(validated.threadId, operation);
2531
-
2532
- const laterRows =
2533
- lane === undefined
2534
- ? yield* sql<Record<string, unknown>>`
2535
- SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2233
+ const laterRows = yield* sql<Record<string, unknown>>`
2234
+ SELECT ${submissionColumns}
2536
2235
  FROM effect_agent_submissions
2537
2236
  WHERE thread_id = ${validated.threadId}
2538
2237
  AND queue_sequence > ${host.queue_sequence}
2539
2238
  ORDER BY queue_sequence ASC
2540
- `.pipe(Effect.mapError(sqlFailure(operation)))
2541
- : yield* Effect.forEach(
2542
- lane.filter((item) => item.queue_sequence > host.queue_sequence),
2543
- (item) => requireSubmission(operation, item.submission_id),
2544
- );
2239
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2545
2240
 
2546
2241
  const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
2547
2242
  const claimed: Array<JoiningClaim> = [];
@@ -2563,11 +2258,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2563
2258
  // Any other non-ready row — an admitted-not-ready gap in particular — breaks the
2564
2259
  // contiguous ready prefix (plan §2.5); later ready work stays queued (DUR-004).
2565
2260
  if (row.state !== "ready") break;
2261
+ const canonical = yield* readCanonicalSettlement(operation, row);
2262
+
2263
+ if (canonical?.settlement.outcome === "aborted") continue;
2264
+ if (canonical !== undefined) break;
2566
2265
  yield* sql`
2567
2266
  UPDATE effect_agent_submissions
2568
2267
  SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
2569
2268
  WHERE submission_id = ${row.submission_id}
2570
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
2269
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
2270
+ rows.submissions.write,
2271
+ Effect.mapError(internalFailure(operation)),
2272
+ );
2571
2273
 
2572
2274
  const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
2573
2275
  Effect.mapError((error) =>
@@ -2598,9 +2300,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2598
2300
  return claims;
2599
2301
  });
2600
2302
 
2601
- const markJoined: SubmissionLedger["Service"]["markJoined"] = Effect.fn(
2602
- "DoSubmissionLedger.markJoined",
2603
- )(function* (request: MarkJoinedRequest) {
2303
+ const markJoined: SubmissionLedger["Service"]["markJoined"] = Effect.fnUntraced(function* (
2304
+ request: MarkJoinedRequest,
2305
+ ) {
2604
2306
  const operation = "ledger mark joined";
2605
2307
 
2606
2308
  const validated = yield* Schema.decodeEffect(Schema.toType(MarkJoinedRequest))(request).pipe(
@@ -2623,7 +2325,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2623
2325
 
2624
2326
  // The lane is host-owned: the presented token must own the HOST's ownership period,
2625
2327
  // which also lets a later host Attempt repair a lost marker from history (DUR-016).
2626
- yield* requireOwnership(operation, host, validated.ownershipToken);
2328
+ yield* requireOwner(operation, host);
2627
2329
  if (submission.input_applied_record_id !== null) {
2628
2330
  if (
2629
2331
  submission.input_applied_record_id === validated.recordId &&
@@ -2652,15 +2354,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2652
2354
  input_applied_sequence = ${validated.sequence},
2653
2355
  state = 'joined'
2654
2356
  WHERE submission_id = ${validated.submissionId}
2655
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
2357
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
2358
+ rows.submissions.write,
2359
+ Effect.mapError(internalFailure(operation)),
2360
+ );
2656
2361
  }),
2657
2362
  );
2658
2363
  yield* hitFailpoint("ledger:mark-joined:after", operation);
2659
2364
  });
2660
2365
 
2661
- const revertJoining: SubmissionLedger["Service"]["revertJoining"] = Effect.fn(
2662
- "DoSubmissionLedger.revertJoining",
2663
- )(function* (request: RevertJoiningRequest) {
2366
+ const revertJoining: SubmissionLedger["Service"]["revertJoining"] = Effect.fnUntraced(function* (
2367
+ request: RevertJoiningRequest,
2368
+ ) {
2664
2369
  const operation = "ledger revert joining";
2665
2370
 
2666
2371
  const validated = yield* Schema.decodeEffect(Schema.toType(RevertJoiningRequest))(request).pipe(
@@ -2676,135 +2381,169 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2676
2381
  // Idempotent and recovery-only: only a still-`joining` Submission reverts; an
2677
2382
  // already-joined (or already-reverted) Submission is a no-op (DUR-016).
2678
2383
  if (submission.state !== "joining") return;
2384
+ const guard = validated.guard;
2385
+
2386
+ if (guard !== undefined) {
2387
+ if (submission.joined_host_submission_id !== guard.hostSubmissionId) return;
2388
+ const host = yield* requireSubmission(operation, guard.hostSubmissionId);
2389
+
2390
+ if (host.thread_id !== submission.thread_id)
2391
+ return yield* corruptionFailure(
2392
+ operation,
2393
+ "effect_agent_submissions",
2394
+ validated.submissionId,
2395
+ "The joined host belongs to another Thread.",
2396
+ );
2397
+ if (host.state !== "settled") {
2398
+ yield* requireOwner(operation, host).pipe(
2399
+ Effect.mapError((cause) =>
2400
+ cause._tag === "OwnershipLost"
2401
+ ? LedgerError.make({
2402
+ operation,
2403
+ message: "Joining recovery no longer owns the linked host.",
2404
+ cause,
2405
+ })
2406
+ : cause,
2407
+ ),
2408
+ );
2409
+ }
2410
+ }
2679
2411
  yield* sql`
2680
2412
  UPDATE effect_agent_submissions
2681
2413
  SET state = 'ready', joined_host_submission_id = NULL
2682
2414
  WHERE submission_id = ${validated.submissionId}
2683
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
2415
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
2416
+ rows.submissions.write,
2417
+ Effect.mapError(internalFailure(operation)),
2418
+ );
2684
2419
  }),
2685
2420
  );
2686
2421
  yield* hitFailpoint("ledger:revert-joining:after", operation);
2687
2422
  });
2688
2423
 
2689
- const suspend: SubmissionLedger["Service"]["suspend"] = Effect.fn("DoSubmissionLedger.suspend")(
2690
- function* (request: SuspendRequest) {
2691
- const operation = "ledger suspend";
2692
-
2693
- const validated = yield* Schema.decodeEffect(Schema.toType(SuspendRequest))(request).pipe(
2694
- Effect.mapError(internalFailure(operation)),
2695
- );
2424
+ const suspend: SubmissionLedger["Service"]["suspend"] = Effect.fnUntraced(function* (
2425
+ request: SuspendRequest,
2426
+ ) {
2427
+ const operation = "ledger suspend";
2696
2428
 
2697
- const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
2698
- Effect.mapError(internalFailure(operation)),
2699
- );
2429
+ const validated = yield* Schema.decodeEffect(Schema.toType(SuspendRequest))(request).pipe(
2430
+ Effect.mapError(internalFailure(operation)),
2431
+ );
2700
2432
 
2701
- yield* hitFailpoint("ledger:suspend:before", operation);
2433
+ const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
2434
+ Effect.mapError(internalFailure(operation)),
2435
+ );
2702
2436
 
2703
- const outcome = yield* inWriteTransaction(
2704
- operation,
2705
- Effect.gen(function* () {
2706
- const submission = yield* requireSubmission(operation, validated.submissionId);
2437
+ yield* hitFailpoint("ledger:suspend:before", operation);
2707
2438
 
2708
- if (submission.state === "settled") {
2709
- if (submission.settled_outcome === null) {
2710
- return yield* corruptionFailure(
2711
- operation,
2712
- "effect_agent_submissions",
2713
- validated.submissionId,
2714
- "A settled Submission carries no terminal outcome.",
2715
- );
2716
- }
2439
+ const outcome = yield* inWriteTransaction(
2440
+ operation,
2441
+ Effect.gen(function* () {
2442
+ const submission = yield* requireSubmission(operation, validated.submissionId);
2717
2443
 
2718
- return yield* SettlementConflict.make({
2719
- submissionId: validated.submissionId,
2720
- existingOutcome: submission.settled_outcome,
2721
- });
2444
+ if (submission.state === "settled") {
2445
+ if (submission.settled_outcome === null) {
2446
+ return yield* corruptionFailure(
2447
+ operation,
2448
+ "effect_agent_submissions",
2449
+ validated.submissionId,
2450
+ "A settled Submission carries no terminal outcome.",
2451
+ );
2722
2452
  }
2723
- // An exact terminal outcome is already reserved (DUR-011); suspension would
2724
- // contradict it, so the reservation wins.
2725
- const reservation = yield* readReservation(operation, validated.submissionId);
2726
2453
 
2727
- if (Option.isSome(reservation)) {
2728
- return yield* SettlementConflict.make({
2729
- submissionId: validated.submissionId,
2730
- existingOutcome: reservation.value.outcome,
2731
- });
2454
+ return yield* SettlementConflict.make({
2455
+ submissionId: validated.submissionId,
2456
+ existingOutcome: submission.settled_outcome,
2457
+ });
2458
+ }
2459
+ // Canonical terminal intent wins over a later suspension.
2460
+ const canonical = yield* readCanonicalSettlement(operation, submission);
2461
+
2462
+ if (canonical !== undefined) {
2463
+ return yield* SettlementConflict.make({
2464
+ submissionId: validated.submissionId,
2465
+ existingOutcome: canonical.settlement.outcome,
2466
+ });
2467
+ }
2468
+ yield* requireOwner(operation, submission);
2469
+ // A covering event that raced ahead of the suspend transaction resumes the caller
2470
+ // immediately WITHOUT releasing the lane (plan §2.6, §12). For WaitingForChild the
2471
+ // covering evidence is EITHER a locally settled child row OR a durable cross-store
2472
+ // notification marker: parent and child Threads live in different Durable
2473
+ // Objects, and the port contract requires that a child settlement reported (via
2474
+ // `recordChildSettled` → marker) before this suspend commits is observed here.
2475
+ if (validated.reason._tag === "ApprovalPending") {
2476
+ const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
2477
+ const decided = new Map(decisions.map((row) => [row.tool_call_id, row.decision]));
2478
+
2479
+ if (
2480
+ approvalDecisionsCover(validated.reason.toolCallIds, (toolCallId) =>
2481
+ decided.get(toolCallId),
2482
+ )
2483
+ ) {
2484
+ return { outcome: RESUME_IMMEDIATELY, threadId: submission.thread_id };
2732
2485
  }
2733
- yield* requireOwnership(operation, submission, validated.ownershipToken);
2734
- // A covering event that raced ahead of the suspend transaction resumes the caller
2735
- // immediately WITHOUT releasing the lane (plan §2.6, §12). For WaitingForChild the
2736
- // covering evidence is EITHER a locally settled child row OR a durable cross-store
2737
- // notification marker: parent and child Threads live in different Durable
2738
- // Objects, and the port contract requires that a child settlement reported (via
2739
- // `recordChildSettled` → marker) before this suspend commits is observed here.
2740
- if (validated.reason._tag === "ApprovalPending") {
2741
- const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
2742
- const decided = new Set(decisions.map((row) => row.tool_call_id));
2743
-
2744
- if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
2745
- return RESUME_IMMEDIATELY;
2746
- }
2747
- } else {
2748
- const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
2749
- const markerChildren = new Set(markers.map((row) => row.child_submission_id));
2750
- let allSettled = true;
2486
+ } else {
2487
+ const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
2488
+ const markerChildren = new Set(markers.map((row) => row.child_submission_id));
2489
+ let allSettled = true;
2751
2490
 
2752
- for (const child of validated.reason.children) {
2753
- const settled = yield* childProvablySettled(
2754
- operation,
2755
- markerChildren,
2756
- child.childSubmissionId,
2757
- );
2491
+ for (const child of validated.reason.children) {
2492
+ const settled = yield* childProvablySettled(
2493
+ operation,
2494
+ markerChildren,
2495
+ child.childSubmissionId,
2496
+ );
2758
2497
 
2759
- if (!settled) {
2760
- allSettled = false;
2761
- break;
2762
- }
2763
- }
2764
- if (allSettled) {
2765
- return RESUME_IMMEDIATELY;
2498
+ if (!settled) {
2499
+ allSettled = false;
2500
+ break;
2766
2501
  }
2767
2502
  }
2768
- const now = yield* currentInstant;
2503
+ if (allSettled) {
2504
+ return { outcome: RESUME_IMMEDIATELY, threadId: submission.thread_id };
2505
+ }
2506
+ }
2507
+ const now = yield* currentInstant;
2769
2508
 
2770
- yield* sql`
2509
+ yield* sql`
2771
2510
  UPDATE effect_agent_submissions
2772
2511
  SET
2773
2512
  state = 'suspended',
2774
2513
  suspended_reason_json = ${reasonJson},
2775
2514
  suspended_at = ${now.iso}
2776
2515
  WHERE submission_id = ${validated.submissionId}
2777
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
2778
- // Suspension ends the ownership period WITHOUT settling: the accepted-work
2779
- // obligation stays owed while the lane consumes no worker permit (plan §2.6).
2780
- yield* sql`
2781
- DELETE FROM effect_agent_submission_ownership
2782
- WHERE submission_id = ${validated.submissionId}
2783
- RETURNING *`.pipe(rows.ownership.remove, Effect.mapError(internalFailure(operation)));
2516
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
2517
+ rows.submissions.write,
2518
+ Effect.mapError(internalFailure(operation)),
2519
+ );
2520
+ // Suspension ends the ownership period WITHOUT settling: the accepted-work
2521
+ // obligation stays owed while the lane consumes no worker permit (plan §2.6).
2784
2522
 
2785
- yield* retainLifecycle(submission, {
2786
- _tag: "SubmissionSuspended",
2787
- submissionId: validated.submissionId,
2788
- reason: validated.reason,
2789
- });
2523
+ yield* retainLifecycle(submission, {
2524
+ _tag: "SubmissionSuspended",
2525
+ submissionId: validated.submissionId,
2526
+ reason: validated.reason,
2527
+ });
2790
2528
 
2791
- return SUSPENDED;
2792
- }),
2793
- );
2529
+ return { outcome: SUSPENDED, threadId: submission.thread_id };
2530
+ }),
2531
+ );
2794
2532
 
2795
- yield* hitFailpoint("ledger:suspend:after", operation);
2533
+ if (outcome.outcome === SUSPENDED)
2534
+ owner.closeSubmission(outcome.threadId, validated.submissionId);
2535
+ yield* hitFailpoint("ledger:suspend:after", operation);
2796
2536
 
2797
- return outcome;
2798
- },
2799
- );
2537
+ return outcome.outcome;
2538
+ });
2800
2539
 
2801
2540
  /**
2802
- * Once every pending call of a recorded ApprovalPending suspension has a decision intent,
2541
+ * Once one pending call is denied or every pending call has a decision intent,
2803
2542
  * the lane wakes: suspended → input-applied, suspension cleared (plan §2.6). A
2804
2543
  * WaitingForChild suspension wakes only through recordChildSettled. Runs inside the caller's
2805
2544
  * write transaction.
2806
2545
  */
2807
- const wakeSuspendedIfCovered = Effect.fn("DoSubmissionLedger.wakeSuspendedIfCovered")(function* (
2546
+ const wakeSuspendedIfCovered = Effect.fnUntraced(function* (
2808
2547
  operation: string,
2809
2548
  submission: SubmissionRow,
2810
2549
  ): Effect.fn.Return<void, LedgerError> {
@@ -2825,9 +2564,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2825
2564
 
2826
2565
  if (reason._tag !== "ApprovalPending") return;
2827
2566
  const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
2828
- const decided = new Set(decisions.map((row) => row.tool_call_id));
2567
+ const decided = new Map(decisions.map((row) => [row.tool_call_id, row.decision]));
2829
2568
 
2830
- if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
2569
+ if (!approvalDecisionsCover(reason.toolCallIds, (toolCallId) => decided.get(toolCallId)))
2570
+ return;
2831
2571
  yield* sql`
2832
2572
  UPDATE effect_agent_submissions
2833
2573
  SET
@@ -2835,7 +2575,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2835
2575
  suspended_reason_json = NULL,
2836
2576
  suspended_at = NULL
2837
2577
  WHERE submission_id = ${submission.submission_id}
2838
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
2578
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
2579
+ rows.submissions.write,
2580
+ Effect.mapError(internalFailure(operation)),
2581
+ );
2839
2582
  yield* retainLifecycle(submission, {
2840
2583
  _tag: "SubmissionResumed",
2841
2584
  submissionId: yield* decodeSubmissionId(submission.submission_id).pipe(
@@ -2844,56 +2587,55 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2844
2587
  });
2845
2588
  });
2846
2589
 
2847
- const recordApprovalDecision: SubmissionLedger["Service"]["recordApprovalDecision"] = Effect.fn(
2848
- "DoSubmissionLedger.recordApprovalDecision",
2849
- )(function* (command: ApprovalDecisionCommand) {
2850
- const operation = "ledger record approval decision";
2590
+ const recordApprovalDecision: SubmissionLedger["Service"]["recordApprovalDecision"] =
2591
+ Effect.fnUntraced(function* (command: ApprovalDecisionCommand) {
2592
+ const operation = "ledger record approval decision";
2851
2593
 
2852
- const validated = yield* Schema.decodeEffect(Schema.toType(ApprovalDecisionCommand))(
2853
- command,
2854
- ).pipe(Effect.mapError(internalFailure(operation)));
2594
+ const validated = yield* Schema.decodeEffect(Schema.toType(ApprovalDecisionCommand))(
2595
+ command,
2596
+ ).pipe(Effect.mapError(internalFailure(operation)));
2855
2597
 
2856
- yield* hitFailpoint("ledger:approval-decision:before", operation);
2598
+ yield* hitFailpoint("ledger:approval-decision:before", operation);
2857
2599
 
2858
- const intent = yield* inWriteTransaction(
2859
- operation,
2860
- Effect.gen(function* () {
2861
- const submission = yield* requireSubmission(operation, validated.submissionId);
2600
+ const intent = yield* inWriteTransaction(
2601
+ operation,
2602
+ Effect.gen(function* () {
2603
+ const submission = yield* requireSubmission(operation, validated.submissionId);
2862
2604
 
2863
- if (submission.state === "settled") {
2864
- if (submission.settled_outcome === null) {
2865
- return yield* corruptionFailure(
2866
- operation,
2867
- "effect_agent_submissions",
2868
- validated.submissionId,
2869
- "A settled Submission carries no terminal outcome.",
2870
- );
2871
- }
2605
+ if (submission.state === "settled") {
2606
+ if (submission.settled_outcome === null) {
2607
+ return yield* corruptionFailure(
2608
+ operation,
2609
+ "effect_agent_submissions",
2610
+ validated.submissionId,
2611
+ "A settled Submission carries no terminal outcome.",
2612
+ );
2613
+ }
2872
2614
 
2873
- return yield* SettlementConflict.make({
2874
- submissionId: validated.submissionId,
2875
- existingOutcome: submission.settled_outcome,
2876
- });
2877
- }
2878
- const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
2879
- const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
2880
-
2881
- if (existing !== undefined) {
2882
- // Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
2883
- // the recorded intent unchanged; a divergent re-decision conflicts.
2884
- if (existing.decision !== validated.decision) {
2885
- return yield* ApprovalConflict.make({
2615
+ return yield* SettlementConflict.make({
2886
2616
  submissionId: validated.submissionId,
2887
- toolCallId: validated.toolCallId,
2888
- existingDecision: existing.decision,
2617
+ existingOutcome: submission.settled_outcome,
2889
2618
  });
2890
2619
  }
2620
+ const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
2621
+ const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
2622
+
2623
+ if (existing !== undefined) {
2624
+ // Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
2625
+ // the recorded intent unchanged; a divergent re-decision conflicts.
2626
+ if (existing.decision !== validated.decision) {
2627
+ return yield* ApprovalConflict.make({
2628
+ submissionId: validated.submissionId,
2629
+ toolCallId: validated.toolCallId,
2630
+ existingDecision: existing.decision,
2631
+ });
2632
+ }
2891
2633
 
2892
- return yield* approvalIntentFromRow(operation, existing);
2893
- }
2894
- const now = yield* currentInstant;
2634
+ return yield* approvalIntentFromRow(operation, existing);
2635
+ }
2636
+ const now = yield* currentInstant;
2895
2637
 
2896
- yield* sql`
2638
+ yield* sql`
2897
2639
  INSERT INTO effect_agent_approval_decisions (
2898
2640
  submission_id,
2899
2641
  tool_call_id,
@@ -2910,28 +2652,28 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2910
2652
  ${now.iso}
2911
2653
  )
2912
2654
  RETURNING *`.pipe(rows.approvals.write, Effect.mapError(internalFailure(operation)));
2913
- yield* recordProgress("control", operation);
2914
- yield* wakeSuspendedIfCovered(operation, submission);
2655
+ yield* recordProgress("control", operation);
2656
+ yield* wakeSuspendedIfCovered(operation, submission);
2915
2657
 
2916
- return yield* decodeApprovalDecisionIntent({
2917
- submissionId: validated.submissionId,
2918
- toolCallId: validated.toolCallId,
2919
- decision: validated.decision,
2920
- resolver: validated.resolver,
2921
- reason: validated.reason,
2922
- decidedAt: now.iso,
2923
- }).pipe(Effect.mapError(internalFailure(operation)));
2924
- }),
2925
- );
2658
+ return yield* decodeApprovalDecisionIntent({
2659
+ submissionId: validated.submissionId,
2660
+ toolCallId: validated.toolCallId,
2661
+ decision: validated.decision,
2662
+ resolver: validated.resolver,
2663
+ reason: validated.reason,
2664
+ decidedAt: now.iso,
2665
+ }).pipe(Effect.mapError(internalFailure(operation)));
2666
+ }),
2667
+ );
2926
2668
 
2927
- yield* hitFailpoint("ledger:approval-decision:after", operation);
2669
+ yield* hitFailpoint("ledger:approval-decision:after", operation);
2928
2670
 
2929
- return intent;
2930
- });
2671
+ return intent;
2672
+ });
2931
2673
 
2932
- const markUnknown: SubmissionLedger["Service"]["markUnknown"] = Effect.fn(
2933
- "DoSubmissionLedger.markUnknown",
2934
- )(function* (request: MarkUnknownRequest) {
2674
+ const markUnknown: SubmissionLedger["Service"]["markUnknown"] = Effect.fnUntraced(function* (
2675
+ request: MarkUnknownRequest,
2676
+ ) {
2935
2677
  const operation = "ledger mark unknown";
2936
2678
 
2937
2679
  const validated = yield* Schema.decodeEffect(Schema.toType(MarkUnknownRequest))(request).pipe(
@@ -2959,14 +2701,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2959
2701
  existingOutcome: submission.settled_outcome,
2960
2702
  });
2961
2703
  }
2962
- // A reserved exact outcome wins over a late Unknown marking (DUR-011); the recovery
2963
- // classifier orders reservation ahead of MarkUnknown for the same reason.
2964
- const reservation = yield* readReservation(operation, validated.submissionId);
2704
+ // Canonical terminal intent wins over a late Unknown marking.
2705
+ const canonical = yield* readCanonicalSettlement(operation, submission);
2965
2706
 
2966
- if (Option.isSome(reservation)) {
2707
+ if (canonical !== undefined) {
2967
2708
  return yield* SettlementConflict.make({
2968
2709
  submissionId: validated.submissionId,
2969
- existingOutcome: reservation.value.outcome,
2710
+ existingOutcome: canonical.settlement.outcome,
2970
2711
  });
2971
2712
  }
2972
2713
  // Idempotent merge: repeating is a no-op; additional open calls extend the marked
@@ -2990,7 +2731,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
2990
2731
  unknown_reason = ${submission.unknown_reason ?? validated.reason},
2991
2732
  unknown_tool_call_ids_json = ${idsJson}
2992
2733
  WHERE submission_id = ${validated.submissionId}
2993
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
2734
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
2735
+ rows.submissions.write,
2736
+ Effect.mapError(internalFailure(operation)),
2737
+ );
2994
2738
  if (submission.state !== "unknown")
2995
2739
  yield* retainLifecycle(submission, {
2996
2740
  _tag: "SubmissionUnknown",
@@ -3001,71 +2745,136 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
3001
2745
  yield* hitFailpoint("ledger:mark-unknown:after", operation);
3002
2746
  });
3003
2747
 
3004
- const recordUnknownResolution: SubmissionLedger["Service"]["recordUnknownResolution"] = Effect.fn(
3005
- "DoSubmissionLedger.recordUnknownResolution",
3006
- )(function* (command: UnknownResolutionCommand) {
3007
- const operation = "ledger record unknown resolution";
2748
+ const recordUnknownResolution: SubmissionLedger["Service"]["recordUnknownResolution"] =
2749
+ Effect.fnUntraced(function* (command: UnknownResolutionCommand) {
2750
+ const operation = "ledger record unknown resolution";
3008
2751
 
3009
- const validated = yield* Schema.decodeEffect(Schema.toType(UnknownResolutionCommand))(
3010
- command,
3011
- ).pipe(Effect.mapError(internalFailure(operation)));
2752
+ const validated = yield* Schema.decodeEffect(Schema.toType(UnknownResolutionCommand))(
2753
+ command,
2754
+ ).pipe(Effect.mapError(internalFailure(operation)));
3012
2755
 
3013
- const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
3014
- Effect.mapError(internalFailure(operation)),
3015
- );
2756
+ const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
2757
+ Effect.mapError(internalFailure(operation)),
2758
+ );
3016
2759
 
3017
- yield* hitFailpoint("ledger:unknown-resolution:before", operation);
2760
+ yield* hitFailpoint("ledger:unknown-resolution:before", operation);
3018
2761
 
3019
- const intent = yield* inWriteTransaction(
3020
- operation,
3021
- Effect.gen(function* () {
3022
- const submission = yield* requireSubmission(operation, validated.submissionId);
2762
+ const intent = yield* inWriteTransaction(
2763
+ operation,
2764
+ Effect.gen(function* () {
2765
+ const submission = yield* requireSubmission(operation, validated.submissionId);
3023
2766
 
3024
- if (submission.state === "settled") {
3025
- if (submission.settled_outcome === null) {
3026
- return yield* corruptionFailure(
3027
- operation,
3028
- "effect_agent_submissions",
3029
- validated.submissionId,
3030
- "A settled Submission carries no terminal outcome.",
3031
- );
2767
+ if (submission.state === "settled") {
2768
+ if (submission.settled_outcome === null) {
2769
+ return yield* corruptionFailure(
2770
+ operation,
2771
+ "effect_agent_submissions",
2772
+ validated.submissionId,
2773
+ "A settled Submission carries no terminal outcome.",
2774
+ );
2775
+ }
2776
+
2777
+ // Factual closure may outlive settlement; it never restores execution authority.
2778
+ if (
2779
+ validated.resolution._tag !== "CompletedWithResult" &&
2780
+ validated.resolution._tag !== "NeverHappened"
2781
+ )
2782
+ return yield* SettlementConflict.make({
2783
+ submissionId: validated.submissionId,
2784
+ existingOutcome: submission.settled_outcome,
2785
+ });
3032
2786
  }
2787
+ const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
2788
+ const kind = unknownResolutionKind(validated.resolution);
3033
2789
 
3034
- return yield* SettlementConflict.make({
3035
- submissionId: validated.submissionId,
3036
- existingOutcome: submission.settled_outcome,
3037
- });
3038
- }
3039
- const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
3040
- const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
2790
+ const existing = resolutions.find(
2791
+ (row) => row.tool_call_id === validated.toolCallId && row.resolution_kind === kind,
2792
+ );
3041
2793
 
3042
- const existingIntent =
3043
- existing === undefined
3044
- ? undefined
3045
- : yield* unknownResolutionIntentFromRow(operation, existing);
2794
+ const existingIntent =
2795
+ existing === undefined
2796
+ ? undefined
2797
+ : yield* unknownResolutionIntentFromRow(operation, existing);
3046
2798
 
3047
- if (
3048
- existingIntent !== undefined &&
3049
- !equivalentUnknownResolution(existingIntent.resolution, validated.resolution)
3050
- ) {
3051
- return yield* UnknownResolutionConflict.make({
3052
- submissionId: validated.submissionId,
3053
- toolCallId: validated.toolCallId,
3054
- });
3055
- }
3056
- let resolved: UnknownResolutionIntent;
2799
+ if (
2800
+ (existingIntent !== undefined &&
2801
+ !equivalentUnknownResolution(existingIntent.resolution, validated.resolution)) ||
2802
+ (existing === undefined &&
2803
+ kind === "execution" &&
2804
+ resolutions.some(
2805
+ (row) =>
2806
+ row.tool_call_id === validated.toolCallId && row.resolution_kind === "factual",
2807
+ ))
2808
+ ) {
2809
+ return yield* UnknownResolutionConflict.make({
2810
+ submissionId: validated.submissionId,
2811
+ toolCallId: validated.toolCallId,
2812
+ });
2813
+ }
3057
2814
 
3058
- if (existingIntent !== undefined) {
3059
- // Idempotent replay of the recorded intent (author/reason may differ; the stored
3060
- // audit fields win, exactly like requestAbort).
3061
- resolved = existingIntent;
3062
- } else {
3063
- const now = yield* currentInstant;
2815
+ if (existingIntent === undefined && kind === "factual") {
2816
+ const runId = runIdForSubmission(validated.submissionId);
3064
2817
 
3065
- yield* sql`
2818
+ const found = yield* sql`
2819
+ SELECT ${canonicalRecordJson(sql)} AS record_json FROM effect_agent_canonical_records
2820
+ WHERE thread_id = ${submission.thread_id}
2821
+ AND record_tag = 27
2822
+ AND run_id = ${runKey(sql, sql`${submission.thread_id}`, sql`${JSON.stringify(runId)}`)}
2823
+ AND tool_call_id = ${JSON.stringify(validated.toolCallId)}
2824
+ LIMIT 2
2825
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2826
+
2827
+ const outcomes = yield* decodeRows(
2828
+ Schema.Array(Schema.Struct({ record_json: BoundedStoredText })),
2829
+ "effect_agent_canonical_records",
2830
+ validated.toolCallId,
2831
+ found,
2832
+ ).pipe(Effect.mapError(internalFailure(operation)));
2833
+
2834
+ const result =
2835
+ outcomes[0] === undefined
2836
+ ? undefined
2837
+ : (yield* decodeRecordEnvelopeText(outcomes[0].record_json).pipe(
2838
+ Effect.mapError(internalFailure(operation)),
2839
+ )).payload;
2840
+
2841
+ if (
2842
+ outcomes.length > 1 ||
2843
+ (result !== undefined &&
2844
+ (result._tag !== "ToolCallSettled" ||
2845
+ result.runId !== runId ||
2846
+ result.toolCallId !== validated.toolCallId))
2847
+ )
2848
+ return yield* corruptionFailure(
2849
+ operation,
2850
+ "effect_agent_canonical_records",
2851
+ validated.toolCallId,
2852
+ "Canonical Tool result identity is inconsistent.",
2853
+ );
2854
+
2855
+ if (
2856
+ result?._tag === "ToolCallSettled" &&
2857
+ !unknownResolutionMatchesResult(validated.resolution, result)
2858
+ )
2859
+ return yield* UnknownResolutionConflict.make({
2860
+ submissionId: validated.submissionId,
2861
+ toolCallId: validated.toolCallId,
2862
+ });
2863
+ }
2864
+ let resolved: UnknownResolutionIntent;
2865
+
2866
+ if (existingIntent !== undefined) {
2867
+ // Idempotent replay of the recorded intent (author/reason may differ; the stored
2868
+ // audit fields win, exactly like requestAbort).
2869
+ resolved = existingIntent;
2870
+ } else {
2871
+ const now = yield* currentInstant;
2872
+
2873
+ yield* sql`
3066
2874
  INSERT INTO effect_agent_unknown_resolutions (
3067
2875
  submission_id,
3068
2876
  tool_call_id,
2877
+ resolution_kind,
3069
2878
  author,
3070
2879
  reason,
3071
2880
  resolution_json,
@@ -3073,101 +2882,103 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
3073
2882
  ) VALUES (
3074
2883
  ${validated.submissionId},
3075
2884
  ${validated.toolCallId},
2885
+ ${kind},
3076
2886
  ${validated.author},
3077
2887
  ${validated.reason},
3078
2888
  ${resolutionJson},
3079
2889
  ${now.iso}
3080
2890
  )
3081
2891
  RETURNING *`.pipe(rows.resolutions.write, Effect.mapError(internalFailure(operation)));
3082
- yield* recordProgress("control", operation);
2892
+ yield* recordProgress("control", operation);
3083
2893
 
3084
- const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
3085
- Effect.mapError(internalFailure(operation)),
3086
- );
2894
+ const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
2895
+ Effect.mapError(internalFailure(operation)),
2896
+ );
3087
2897
 
3088
- resolved = yield* decodeUnknownResolutionIntent({
3089
- submissionId: validated.submissionId,
3090
- toolCallId: validated.toolCallId,
3091
- author: validated.author,
3092
- reason: validated.reason,
3093
- resolution,
3094
- resolvedAt: now.iso,
3095
- }).pipe(Effect.mapError(internalFailure(operation)));
3096
- }
3097
- // The lane reopens only when EVERY marked open call has a durable resolution intent:
3098
- // unknown → input-applied (DUR-017). Replays re-run the coverage check so a
3099
- // recovering caller can wake the lane idempotently.
3100
- if (submission.state === "unknown" && submission.unknown_tool_call_ids_json !== null) {
3101
- const markedIds = yield* storedUnknownToolCallIds(operation, submission);
3102
- const covering = yield* readUnknownResolutions(operation, validated.submissionId);
3103
- const coveredIds = new Set(covering.map((row) => row.tool_call_id));
3104
-
3105
- if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) {
3106
- yield* sql`
2898
+ resolved = yield* decodeUnknownResolutionIntent({
2899
+ submissionId: validated.submissionId,
2900
+ toolCallId: validated.toolCallId,
2901
+ author: validated.author,
2902
+ reason: validated.reason,
2903
+ resolution,
2904
+ resolvedAt: now.iso,
2905
+ }).pipe(Effect.mapError(internalFailure(operation)));
2906
+ }
2907
+ // The lane reopens only when EVERY marked open call has a durable resolution intent:
2908
+ // unknown → input-applied (DUR-017). Replays re-run the coverage check so a
2909
+ // recovering caller can wake the lane idempotently.
2910
+ if (submission.state === "unknown" && submission.unknown_tool_call_ids_json !== null) {
2911
+ const markedIds = yield* storedUnknownToolCallIds(operation, submission);
2912
+ const covering = yield* readUnknownResolutions(operation, validated.submissionId);
2913
+ const coveredIds = new Set(covering.map((row) => row.tool_call_id));
2914
+
2915
+ if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) {
2916
+ yield* sql`
3107
2917
  UPDATE effect_agent_submissions
3108
2918
  SET
3109
2919
  state = 'input-applied',
3110
2920
  unknown_reason = NULL,
3111
2921
  unknown_tool_call_ids_json = NULL
3112
2922
  WHERE submission_id = ${validated.submissionId}
3113
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
3114
- yield* retainLifecycle(submission, {
3115
- _tag: "SubmissionResumed",
3116
- submissionId: validated.submissionId,
3117
- });
2923
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
2924
+ rows.submissions.write,
2925
+ Effect.mapError(internalFailure(operation)),
2926
+ );
2927
+ yield* retainLifecycle(submission, {
2928
+ _tag: "SubmissionResumed",
2929
+ submissionId: validated.submissionId,
2930
+ });
2931
+ }
3118
2932
  }
3119
- }
3120
2933
 
3121
- return resolved;
3122
- }),
3123
- );
2934
+ return resolved;
2935
+ }),
2936
+ );
3124
2937
 
3125
- yield* hitFailpoint("ledger:unknown-resolution:after", operation);
2938
+ yield* hitFailpoint("ledger:unknown-resolution:after", operation);
3126
2939
 
3127
- return intent;
3128
- });
2940
+ return intent;
2941
+ });
3129
2942
 
3130
- const recordChildSettled: SubmissionLedger["Service"]["recordChildSettled"] = Effect.fn(
3131
- "DoSubmissionLedger.recordChildSettled",
3132
- )(function* (request: ChildSettledNotification) {
3133
- const operation = "ledger record child settled";
2943
+ const recordChildSettled: SubmissionLedger["Service"]["recordChildSettled"] = Effect.fnUntraced(
2944
+ function* (request: ChildSettledNotification) {
2945
+ const operation = "ledger record child settled";
3134
2946
 
3135
- const validated = yield* Schema.decodeEffect(Schema.toType(ChildSettledNotification))(
3136
- request,
3137
- ).pipe(Effect.mapError(internalFailure(operation)));
3138
-
3139
- yield* hitFailpoint("ledger:child-settled:before", operation);
2947
+ const validated = yield* Schema.decodeEffect(Schema.toType(ChildSettledNotification))(
2948
+ request,
2949
+ ).pipe(Effect.mapError(internalFailure(operation)));
3140
2950
 
3141
- const outcome = yield* inWriteTransaction(
3142
- operation,
3143
- Effect.gen(function* () {
3144
- const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
3145
- // The child's canonical Settlement is the authority for this wake. When the child's
3146
- // row lives in THIS store (single-store latitude, and every conformance lane), either a
3147
- // finalized row or an exact terminalizing reservation admits the notification: the
3148
- // runtime calls only after the canonical append and before ledger finalization. When the
3149
- // row does not live here — the normal cross-DO case — the routed notification from the
3150
- // child's owning Durable Object is the settlement evidence this store records durably.
3151
- const child = yield* readSubmission(operation, validated.childSubmissionId);
3152
- const childReservation = yield* readReservation(operation, validated.childSubmissionId);
2951
+ yield* hitFailpoint("ledger:child-settled:before", operation);
3153
2952
 
3154
- if (
3155
- Option.isSome(child) &&
3156
- child.value.state !== "settled" &&
3157
- !(child.value.state === "terminalizing" && Option.isSome(childReservation))
3158
- ) {
3159
- return yield* LedgerError.make({
3160
- operation,
3161
- message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`,
3162
- });
3163
- }
3164
- // Record the durable notification marker FIRST and unconditionally (idempotent):
3165
- // the port's cross-store race guarantee requires that a notification committed
3166
- // before the parent's suspend transaction is observed by that suspend's covering
3167
- // check, even when the parent is not (or not yet) suspended.
3168
- const now = yield* currentInstant;
2953
+ const outcome = yield* inWriteTransaction(
2954
+ operation,
2955
+ Effect.gen(function* () {
2956
+ const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2957
+ // The child's canonical Settlement is the authority for this wake. When the child's
2958
+ // row lives in THIS store (single-store latitude, and every conformance lane), either a
2959
+ // canonical settlement admits the notification: the
2960
+ // runtime calls only after the canonical append and before ledger finalization. When the
2961
+ // row does not live here — the normal cross-DO case — the routed notification from the
2962
+ // child's owning Durable Object is the settlement evidence this store records durably.
2963
+ const child = yield* readSubmission(operation, validated.childSubmissionId);
2964
+
2965
+ const childCanonical = Option.isSome(child)
2966
+ ? yield* readCanonicalSettlement(operation, child.value)
2967
+ : undefined;
2968
+
2969
+ if (Option.isSome(child) && childCanonical === undefined) {
2970
+ return yield* LedgerError.make({
2971
+ operation,
2972
+ message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`,
2973
+ });
2974
+ }
2975
+ // Record the durable notification marker FIRST and unconditionally (idempotent):
2976
+ // the port's cross-store race guarantee requires that a notification committed
2977
+ // before the parent's suspend transaction is observed by that suspend's covering
2978
+ // check, even when the parent is not (or not yet) suspended.
2979
+ const now = yield* currentInstant;
3169
2980
 
3170
- const added = yield* sql`
2981
+ const added = yield* sql`
3171
2982
  INSERT INTO effect_agent_child_settlements (
3172
2983
  parent_submission_id,
3173
2984
  child_submission_id,
@@ -3176,158 +2987,162 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
3176
2987
  ) VALUES (
3177
2988
  ${validated.parentSubmissionId},
3178
2989
  ${validated.childSubmissionId},
3179
- ${
3180
- Option.isSome(child) && child.value.state === "settled"
3181
- ? child.value.settled_outcome
3182
- : Option.isSome(childReservation)
3183
- ? childReservation.value.outcome
3184
- : null
3185
- },
2990
+ ${childCanonical?.settlement.outcome ?? null},
3186
2991
  ${now.iso}
3187
2992
  )
3188
2993
  ON CONFLICT (parent_submission_id, child_submission_id) DO NOTHING
3189
2994
  RETURNING *`.pipe(
3190
- rows.childSettlements.write,
3191
- Effect.mapError(internalFailure(operation)),
3192
- );
2995
+ rows.childSettlements.write,
2996
+ Effect.mapError(internalFailure(operation)),
2997
+ );
3193
2998
 
3194
- if (added.length > 0) yield* recordProgress("control", operation);
2999
+ if (added.length > 0) yield* recordProgress("control", operation);
3195
3000
 
3196
- if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
3197
- return NOT_WAITING;
3198
- }
3001
+ if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
3002
+ return NOT_WAITING;
3003
+ }
3199
3004
 
3200
- const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
3201
- parent.suspended_reason_json,
3202
- ).pipe(
3203
- Effect.mapError((error) =>
3204
- corruptionFailure(
3205
- operation,
3206
- "effect_agent_submissions",
3207
- parent.submission_id,
3208
- error.message,
3005
+ const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
3006
+ parent.suspended_reason_json,
3007
+ ).pipe(
3008
+ Effect.mapError((error) =>
3009
+ corruptionFailure(
3010
+ operation,
3011
+ "effect_agent_submissions",
3012
+ parent.submission_id,
3013
+ error.message,
3014
+ ),
3209
3015
  ),
3210
- ),
3211
- );
3016
+ );
3212
3017
 
3213
- if (reason._tag !== "WaitingForChild") {
3214
- return NOT_WAITING;
3215
- }
3216
- if (
3217
- !reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
3218
- ) {
3219
- return NOT_WAITING;
3220
- }
3221
- // The parent wakes exactly when EVERY listed child is provably settled — from its
3222
- // local row or a recorded marker (spec §12 step 10); replays re-run the coverage
3223
- // check so a recovering caller wakes the lane idempotently.
3224
- const markers = yield* readChildSettlementMarkers(operation, validated.parentSubmissionId);
3225
- const markerChildren = new Set(markers.map((row) => row.child_submission_id));
3226
-
3227
- for (const entry of reason.children) {
3228
- const settled = yield* childProvablySettled(
3018
+ if (reason._tag !== "WaitingForChild") {
3019
+ return NOT_WAITING;
3020
+ }
3021
+ if (
3022
+ !reason.children.some(
3023
+ (entry) => entry.childSubmissionId === validated.childSubmissionId,
3024
+ )
3025
+ ) {
3026
+ return NOT_WAITING;
3027
+ }
3028
+
3029
+ // The parent wakes exactly when EVERY listed child is provably settled — from its
3030
+ // local row or a recorded marker (spec §12 step 10); replays re-run the coverage
3031
+ // check so a recovering caller wakes the lane idempotently.
3032
+ const markers = yield* readChildSettlementMarkers(
3229
3033
  operation,
3230
- markerChildren,
3231
- entry.childSubmissionId,
3034
+ validated.parentSubmissionId,
3232
3035
  );
3233
3036
 
3234
- if (!settled) {
3235
- return STILL_WAITING;
3037
+ const markerChildren = new Set(markers.map((row) => row.child_submission_id));
3038
+
3039
+ for (const entry of reason.children) {
3040
+ const settled = yield* childProvablySettled(
3041
+ operation,
3042
+ markerChildren,
3043
+ entry.childSubmissionId,
3044
+ );
3045
+
3046
+ if (!settled) {
3047
+ return STILL_WAITING;
3048
+ }
3236
3049
  }
3237
- }
3238
- yield* sql`
3050
+ yield* sql`
3239
3051
  UPDATE effect_agent_submissions
3240
3052
  SET
3241
3053
  state = 'input-applied',
3242
3054
  suspended_reason_json = NULL,
3243
3055
  suspended_at = NULL
3244
3056
  WHERE submission_id = ${validated.parentSubmissionId}
3245
- RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
3057
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
3058
+ rows.submissions.write,
3059
+ Effect.mapError(internalFailure(operation)),
3060
+ );
3246
3061
 
3247
- yield* retainLifecycle(parent, {
3248
- _tag: "SubmissionResumed",
3249
- submissionId: validated.parentSubmissionId,
3250
- });
3062
+ yield* retainLifecycle(parent, {
3063
+ _tag: "SubmissionResumed",
3064
+ submissionId: validated.parentSubmissionId,
3065
+ });
3251
3066
 
3252
- return WOKEN;
3253
- }),
3254
- );
3067
+ return WOKEN;
3068
+ }),
3069
+ );
3255
3070
 
3256
- yield* hitFailpoint("ledger:child-settled:after", operation);
3071
+ yield* hitFailpoint("ledger:child-settled:after", operation);
3257
3072
 
3258
- return outcome;
3259
- });
3073
+ return outcome;
3074
+ },
3075
+ );
3260
3076
 
3261
- const reserveChildBudget: SubmissionLedger["Service"]["reserveChildBudget"] = Effect.fn(
3262
- "DoSubmissionLedger.reserveChildBudget",
3263
- )(function* (request: ChildBudgetReservationRequest) {
3264
- const operation = "ledger reserve child budget";
3077
+ const reserveChildBudget: SubmissionLedger["Service"]["reserveChildBudget"] = Effect.fnUntraced(
3078
+ function* (request: ChildBudgetReservationRequest) {
3079
+ const operation = "ledger reserve child budget";
3265
3080
 
3266
- const validated = yield* Schema.decodeEffect(Schema.toType(ChildBudgetReservationRequest))(
3267
- request,
3268
- ).pipe(Effect.mapError(internalFailure(operation)));
3081
+ const validated = yield* Schema.decodeEffect(Schema.toType(ChildBudgetReservationRequest))(
3082
+ request,
3083
+ ).pipe(Effect.mapError(internalFailure(operation)));
3269
3084
 
3270
- const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
3271
- Effect.mapError(internalFailure(operation)),
3272
- );
3085
+ const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
3086
+ Effect.mapError(internalFailure(operation)),
3087
+ );
3273
3088
 
3274
- yield* hitFailpoint("ledger:child-reservation:before", operation);
3089
+ yield* hitFailpoint("ledger:child-reservation:before", operation);
3275
3090
 
3276
- const reserved = yield* inWriteTransaction(
3277
- operation,
3278
- Effect.gen(function* () {
3279
- const existing = yield* readChildReservation(operation, validated.reservationId);
3091
+ const reserved = yield* inWriteTransaction(
3092
+ operation,
3093
+ Effect.gen(function* () {
3094
+ const existing = yield* readChildReservation(operation, validated.reservationId);
3280
3095
 
3281
- if (Option.isSome(existing)) {
3282
- const existingSnapshot = yield* childReservationSnapshotFromRow(
3096
+ if (Option.isSome(existing)) {
3097
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
3098
+ operation,
3099
+ existing.value,
3100
+ );
3101
+
3102
+ // Identical replays short-circuit before the fence, retaining its first committed allocation: a
3103
+ // replay creates nothing, so a recovering caller resumes rather than duplicates.
3104
+ const identical =
3105
+ existing.value.parent_submission_id === validated.parentSubmissionId &&
3106
+ existing.value.parent_tool_call_id === validated.parentToolCallId &&
3107
+ existing.value.allocation_digest === validated.allocationDigest &&
3108
+ equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
3109
+
3110
+ if (!identical) {
3111
+ return yield* ChildReservationConflict.make({
3112
+ reservationId: validated.reservationId,
3113
+ status: existing.value.status,
3114
+ message:
3115
+ "A reservation with this identity exists with a different parent Tool Call or allocation.",
3116
+ });
3117
+ }
3118
+
3119
+ return ReservedChildBudget.make({
3120
+ reservation: existingSnapshot,
3121
+ replayed: true,
3122
+ });
3123
+ }
3124
+
3125
+ const collision = yield* readChildReservationForCall(
3283
3126
  operation,
3284
- existing.value,
3127
+ validated.parentSubmissionId,
3128
+ validated.parentToolCallId,
3285
3129
  );
3286
3130
 
3287
- // Identical replays short-circuit before the fence, mirroring reserveSettlement: a
3288
- // replay creates nothing, so a recovering caller resumes rather than duplicates.
3289
- const identical =
3290
- existing.value.parent_submission_id === validated.parentSubmissionId &&
3291
- existing.value.parent_tool_call_id === validated.parentToolCallId &&
3292
- existing.value.allocation_digest === validated.allocationDigest &&
3293
- equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
3294
-
3295
- if (!identical) {
3131
+ if (Option.isSome(collision)) {
3296
3132
  return yield* ChildReservationConflict.make({
3297
3133
  reservationId: validated.reservationId,
3298
- status: existing.value.status,
3299
- message:
3300
- "A reservation with this identity exists with a different parent Tool Call or allocation.",
3134
+ status: collision.value.status,
3135
+ message: `Parent Tool Call ${validated.parentToolCallId} already owns reservation ${collision.value.reservation_id}.`,
3301
3136
  });
3302
3137
  }
3138
+ const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
3303
3139
 
3304
- return ReservedChildBudget.make({
3305
- reservation: existingSnapshot,
3306
- replayed: true,
3307
- });
3308
- }
3309
-
3310
- const collision = yield* readChildReservationForCall(
3311
- operation,
3312
- validated.parentSubmissionId,
3313
- validated.parentToolCallId,
3314
- );
3315
-
3316
- if (Option.isSome(collision)) {
3317
- return yield* ChildReservationConflict.make({
3318
- reservationId: validated.reservationId,
3319
- status: collision.value.status,
3320
- message: `Parent Tool Call ${validated.parentToolCallId} already owns reservation ${collision.value.reservation_id}.`,
3321
- });
3322
- }
3323
- const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
3324
-
3325
- // Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
3326
- // parent Attempt can never create new reservation state.
3327
- yield* requireOwnership(operation, parent, validated.ownershipToken);
3328
- const now = yield* currentInstant;
3140
+ // Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
3141
+ // parent Attempt can never create new reservation state.
3142
+ yield* requireOwner(operation, parent);
3143
+ const now = yield* currentInstant;
3329
3144
 
3330
- yield* sql`
3145
+ yield* sql`
3331
3146
  INSERT INTO effect_agent_child_reservations (
3332
3147
  reservation_id,
3333
3148
  parent_submission_id,
@@ -3346,45 +3161,198 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
3346
3161
  ${now.iso}
3347
3162
  )
3348
3163
  RETURNING *`.pipe(
3349
- rows.childReservations.write,
3350
- Effect.mapError(internalFailure(operation)),
3351
- );
3352
- const inserted = yield* readChildReservation(operation, validated.reservationId);
3353
-
3354
- if (Option.isNone(inserted)) {
3355
- return yield* corruptionFailure(
3356
- operation,
3357
- "effect_agent_child_reservations",
3358
- validated.reservationId,
3359
- "An inserted child reservation row is missing inside its own transaction.",
3164
+ rows.childReservations.write,
3165
+ Effect.mapError(internalFailure(operation)),
3360
3166
  );
3361
- }
3167
+ const inserted = yield* readChildReservation(operation, validated.reservationId);
3362
3168
 
3363
- return ReservedChildBudget.make({
3364
- reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
3365
- replayed: false,
3366
- });
3367
- }),
3368
- );
3169
+ if (Option.isNone(inserted)) {
3170
+ return yield* corruptionFailure(
3171
+ operation,
3172
+ "effect_agent_child_reservations",
3173
+ validated.reservationId,
3174
+ "An inserted child reservation row is missing inside its own transaction.",
3175
+ );
3176
+ }
3177
+
3178
+ return ReservedChildBudget.make({
3179
+ reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
3180
+ replayed: false,
3181
+ });
3182
+ }),
3183
+ );
3369
3184
 
3370
- yield* hitFailpoint("ledger:child-reservation:after", operation);
3185
+ yield* hitFailpoint("ledger:child-reservation:after", operation);
3371
3186
 
3372
- return reserved;
3373
- });
3187
+ return reserved;
3188
+ },
3189
+ );
3374
3190
 
3375
3191
  const attachChildToReservation: SubmissionLedger["Service"]["attachChildToReservation"] =
3376
- Effect.fn("DoSubmissionLedger.attachChildToReservation")(function* (
3377
- request: AttachChildToReservationRequest,
3378
- ) {
3192
+ Effect.fnUntraced(function* (request: AttachChildToReservationRequest) {
3379
3193
  const operation = "ledger attach child to reservation";
3380
3194
 
3381
- const validated = yield* Schema.decodeEffect(Schema.toType(AttachChildToReservationRequest))(
3195
+ const validated = yield* Schema.decodeEffect(Schema.toType(AttachChildToReservationRequest))(
3196
+ request,
3197
+ ).pipe(Effect.mapError(internalFailure(operation)));
3198
+
3199
+ yield* hitFailpoint("ledger:child-attach:before", operation);
3200
+
3201
+ const attached = yield* inWriteTransaction(
3202
+ operation,
3203
+ Effect.gen(function* () {
3204
+ const existing = yield* readChildReservation(operation, validated.reservationId);
3205
+
3206
+ if (Option.isNone(existing)) {
3207
+ return yield* LedgerError.make({
3208
+ operation,
3209
+ message: `Unknown child reservation ${validated.reservationId}.`,
3210
+ });
3211
+ }
3212
+ if (existing.value.child_submission_id !== null) {
3213
+ // Idempotent replay of the recorded attachment (unfenced — it mutates nothing).
3214
+ if (existing.value.child_submission_id === validated.childSubmissionId) {
3215
+ return yield* childReservationSnapshotFromRow(operation, existing.value);
3216
+ }
3217
+
3218
+ return yield* ChildReservationConflict.make({
3219
+ reservationId: validated.reservationId,
3220
+ status: existing.value.status,
3221
+ message: `Reservation ${validated.reservationId} already records child ${existing.value.child_submission_id}.`,
3222
+ });
3223
+ }
3224
+ const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
3225
+
3226
+ yield* requireOwner(operation, parent);
3227
+ if (existing.value.status !== "reserved") {
3228
+ return yield* ChildReservationConflict.make({
3229
+ reservationId: validated.reservationId,
3230
+ status: existing.value.status,
3231
+ message: `Cannot attach a child to a ${existing.value.status} reservation.`,
3232
+ });
3233
+ }
3234
+ // Unlike the single-store Node adapter, the admitted child's row lives in ANOTHER
3235
+ // Durable Object, so no local existence check is possible here. The canonical
3236
+ // `SubagentStarted` record remains the attachment's repair authority (DUR-015),
3237
+ // and the coordinator only attaches after the child's admission committed.
3238
+ yield* sql`
3239
+ UPDATE effect_agent_child_reservations
3240
+ SET child_submission_id = ${validated.childSubmissionId}
3241
+ WHERE reservation_id = ${validated.reservationId}
3242
+ RETURNING *`.pipe(
3243
+ rows.childReservations.write,
3244
+ Effect.mapError(internalFailure(operation)),
3245
+ );
3246
+ const updated = yield* readChildReservation(operation, validated.reservationId);
3247
+
3248
+ if (Option.isNone(updated)) {
3249
+ return yield* corruptionFailure(
3250
+ operation,
3251
+ "effect_agent_child_reservations",
3252
+ validated.reservationId,
3253
+ "An updated child reservation row is missing inside its own transaction.",
3254
+ );
3255
+ }
3256
+
3257
+ return yield* childReservationSnapshotFromRow(operation, updated.value);
3258
+ }),
3259
+ );
3260
+
3261
+ yield* hitFailpoint("ledger:child-attach:after", operation);
3262
+
3263
+ return attached;
3264
+ });
3265
+
3266
+ const beginChildBudgetRelease: SubmissionLedger["Service"]["beginChildBudgetRelease"] =
3267
+ Effect.fnUntraced(function* (request: BeginChildBudgetReleaseRequest) {
3268
+ const operation = "ledger begin child budget release";
3269
+
3270
+ const validated = yield* Schema.decodeEffect(Schema.toType(BeginChildBudgetReleaseRequest))(
3271
+ request,
3272
+ ).pipe(Effect.mapError(internalFailure(operation)));
3273
+
3274
+ const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
3275
+ Effect.mapError(internalFailure(operation)),
3276
+ );
3277
+
3278
+ yield* hitFailpoint("ledger:child-release-pending:before", operation);
3279
+
3280
+ const frozen = yield* inWriteTransaction(
3281
+ operation,
3282
+ Effect.gen(function* () {
3283
+ const existing = yield* readChildReservation(operation, validated.reservationId);
3284
+
3285
+ if (Option.isNone(existing)) {
3286
+ return yield* LedgerError.make({
3287
+ operation,
3288
+ message: `Unknown child reservation ${validated.reservationId}.`,
3289
+ });
3290
+ }
3291
+ if (existing.value.status !== "reserved") {
3292
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
3293
+ operation,
3294
+ existing.value,
3295
+ );
3296
+
3297
+ // The accounting decision was already frozen exactly once; an identical replay is a
3298
+ // no-op and a divergent decision conflicts (spec §12 join step 6).
3299
+ if (
3300
+ existingSnapshot.accounting !== undefined &&
3301
+ equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
3302
+ ) {
3303
+ return existingSnapshot;
3304
+ }
3305
+
3306
+ return yield* ChildReservationConflict.make({
3307
+ reservationId: validated.reservationId,
3308
+ status: existing.value.status,
3309
+ message: "A different accounting decision is already frozen for this reservation.",
3310
+ });
3311
+ }
3312
+ const now = yield* currentInstant;
3313
+
3314
+ yield* sql`
3315
+ UPDATE effect_agent_child_reservations
3316
+ SET
3317
+ status = 'releasePending',
3318
+ accounting_json = ${accountingJson},
3319
+ release_began_at = ${now.iso}
3320
+ WHERE reservation_id = ${validated.reservationId}
3321
+ RETURNING *`.pipe(
3322
+ rows.childReservations.write,
3323
+ Effect.mapError(internalFailure(operation)),
3324
+ );
3325
+ const updated = yield* readChildReservation(operation, validated.reservationId);
3326
+
3327
+ if (Option.isNone(updated)) {
3328
+ return yield* corruptionFailure(
3329
+ operation,
3330
+ "effect_agent_child_reservations",
3331
+ validated.reservationId,
3332
+ "An updated child reservation row is missing inside its own transaction.",
3333
+ );
3334
+ }
3335
+
3336
+ return yield* childReservationSnapshotFromRow(operation, updated.value);
3337
+ }),
3338
+ );
3339
+
3340
+ yield* hitFailpoint("ledger:child-release-pending:after", operation);
3341
+
3342
+ return frozen;
3343
+ });
3344
+
3345
+ const releaseChildBudget: SubmissionLedger["Service"]["releaseChildBudget"] = Effect.fnUntraced(
3346
+ function* (request: ReleaseChildBudgetRequest) {
3347
+ const operation = "ledger release child budget";
3348
+
3349
+ const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseChildBudgetRequest))(
3382
3350
  request,
3383
3351
  ).pipe(Effect.mapError(internalFailure(operation)));
3384
3352
 
3385
- yield* hitFailpoint("ledger:child-attach:before", operation);
3353
+ yield* hitFailpoint("ledger:child-release:before", operation);
3386
3354
 
3387
- const attached = yield* inWriteTransaction(
3355
+ const released = yield* inWriteTransaction(
3388
3356
  operation,
3389
3357
  Effect.gen(function* () {
3390
3358
  const existing = yield* readChildReservation(operation, validated.reservationId);
@@ -3395,37 +3363,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
3395
3363
  message: `Unknown child reservation ${validated.reservationId}.`,
3396
3364
  });
3397
3365
  }
3398
- if (existing.value.child_submission_id !== null) {
3399
- // Idempotent replay of the recorded attachment (unfenced — it mutates nothing).
3400
- if (existing.value.child_submission_id === validated.childSubmissionId) {
3401
- return yield* childReservationSnapshotFromRow(operation, existing.value);
3402
- }
3403
-
3404
- return yield* ChildReservationConflict.make({
3405
- reservationId: validated.reservationId,
3406
- status: existing.value.status,
3407
- message: `Reservation ${validated.reservationId} already records child ${existing.value.child_submission_id}.`,
3408
- });
3366
+ // Applied exactly once: replaying a released reservation returns the stored row
3367
+ // unchanged (spec §12: "never available twice").
3368
+ if (existing.value.status === "released") {
3369
+ return yield* childReservationSnapshotFromRow(operation, existing.value);
3409
3370
  }
3410
- const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
3411
-
3412
- yield* requireOwnership(operation, parent, validated.ownershipToken);
3413
- if (existing.value.status !== "reserved") {
3371
+ if (existing.value.status !== "releasePending") {
3414
3372
  return yield* ChildReservationConflict.make({
3415
3373
  reservationId: validated.reservationId,
3416
3374
  status: existing.value.status,
3417
- message: `Cannot attach a child to a ${existing.value.status} reservation.`,
3375
+ message: "Cannot release a reservation whose accounting decision is not frozen.",
3418
3376
  });
3419
3377
  }
3420
- // Unlike the single-store Node adapter, the admitted child's row lives in ANOTHER
3421
- // Durable Object, so no local existence check is possible here. The canonical
3422
- // `SubagentStarted` record remains the attachment's repair authority (DUR-015),
3423
- // and the coordinator only attaches after the child's admission committed.
3378
+ const now = yield* currentInstant;
3379
+
3424
3380
  yield* sql`
3425
- UPDATE effect_agent_child_reservations
3426
- SET child_submission_id = ${validated.childSubmissionId}
3427
- WHERE reservation_id = ${validated.reservationId}
3428
- RETURNING *`.pipe(
3381
+ UPDATE effect_agent_child_reservations
3382
+ SET status = 'released', released_at = ${now.iso}
3383
+ WHERE reservation_id = ${validated.reservationId}
3384
+ RETURNING *`.pipe(
3429
3385
  rows.childReservations.write,
3430
3386
  Effect.mapError(internalFailure(operation)),
3431
3387
  );
@@ -3444,161 +3400,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
3444
3400
  }),
3445
3401
  );
3446
3402
 
3447
- yield* hitFailpoint("ledger:child-attach:after", operation);
3448
-
3449
- return attached;
3450
- });
3451
-
3452
- const beginChildBudgetRelease: SubmissionLedger["Service"]["beginChildBudgetRelease"] = Effect.fn(
3453
- "DoSubmissionLedger.beginChildBudgetRelease",
3454
- )(function* (request: BeginChildBudgetReleaseRequest) {
3455
- const operation = "ledger begin child budget release";
3456
-
3457
- const validated = yield* Schema.decodeEffect(Schema.toType(BeginChildBudgetReleaseRequest))(
3458
- request,
3459
- ).pipe(Effect.mapError(internalFailure(operation)));
3460
-
3461
- const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
3462
- Effect.mapError(internalFailure(operation)),
3463
- );
3464
-
3465
- yield* hitFailpoint("ledger:child-release-pending:before", operation);
3466
-
3467
- const frozen = yield* inWriteTransaction(
3468
- operation,
3469
- Effect.gen(function* () {
3470
- const existing = yield* readChildReservation(operation, validated.reservationId);
3471
-
3472
- if (Option.isNone(existing)) {
3473
- return yield* LedgerError.make({
3474
- operation,
3475
- message: `Unknown child reservation ${validated.reservationId}.`,
3476
- });
3477
- }
3478
- if (existing.value.status !== "reserved") {
3479
- const existingSnapshot = yield* childReservationSnapshotFromRow(
3480
- operation,
3481
- existing.value,
3482
- );
3483
-
3484
- // The accounting decision was already frozen exactly once; an identical replay is a
3485
- // no-op and a divergent decision conflicts (spec §12 join step 6).
3486
- if (
3487
- existingSnapshot.accounting !== undefined &&
3488
- equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
3489
- ) {
3490
- return existingSnapshot;
3491
- }
3492
-
3493
- return yield* ChildReservationConflict.make({
3494
- reservationId: validated.reservationId,
3495
- status: existing.value.status,
3496
- message: "A different accounting decision is already frozen for this reservation.",
3497
- });
3498
- }
3499
- const now = yield* currentInstant;
3500
-
3501
- yield* sql`
3502
- UPDATE effect_agent_child_reservations
3503
- SET
3504
- status = 'releasePending',
3505
- accounting_json = ${accountingJson},
3506
- release_began_at = ${now.iso}
3507
- WHERE reservation_id = ${validated.reservationId}
3508
- RETURNING *`.pipe(
3509
- rows.childReservations.write,
3510
- Effect.mapError(internalFailure(operation)),
3511
- );
3512
- const updated = yield* readChildReservation(operation, validated.reservationId);
3513
-
3514
- if (Option.isNone(updated)) {
3515
- return yield* corruptionFailure(
3516
- operation,
3517
- "effect_agent_child_reservations",
3518
- validated.reservationId,
3519
- "An updated child reservation row is missing inside its own transaction.",
3520
- );
3521
- }
3522
-
3523
- return yield* childReservationSnapshotFromRow(operation, updated.value);
3524
- }),
3525
- );
3526
-
3527
- yield* hitFailpoint("ledger:child-release-pending:after", operation);
3528
-
3529
- return frozen;
3530
- });
3531
-
3532
- const releaseChildBudget: SubmissionLedger["Service"]["releaseChildBudget"] = Effect.fn(
3533
- "DoSubmissionLedger.releaseChildBudget",
3534
- )(function* (request: ReleaseChildBudgetRequest) {
3535
- const operation = "ledger release child budget";
3536
-
3537
- const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseChildBudgetRequest))(
3538
- request,
3539
- ).pipe(Effect.mapError(internalFailure(operation)));
3540
-
3541
- yield* hitFailpoint("ledger:child-release:before", operation);
3542
-
3543
- const released = yield* inWriteTransaction(
3544
- operation,
3545
- Effect.gen(function* () {
3546
- const existing = yield* readChildReservation(operation, validated.reservationId);
3547
-
3548
- if (Option.isNone(existing)) {
3549
- return yield* LedgerError.make({
3550
- operation,
3551
- message: `Unknown child reservation ${validated.reservationId}.`,
3552
- });
3553
- }
3554
- // Applied exactly once: replaying a released reservation returns the stored row
3555
- // unchanged (spec §12: "never available twice").
3556
- if (existing.value.status === "released") {
3557
- return yield* childReservationSnapshotFromRow(operation, existing.value);
3558
- }
3559
- if (existing.value.status !== "releasePending") {
3560
- return yield* ChildReservationConflict.make({
3561
- reservationId: validated.reservationId,
3562
- status: existing.value.status,
3563
- message: "Cannot release a reservation whose accounting decision is not frozen.",
3564
- });
3565
- }
3566
- const now = yield* currentInstant;
3567
-
3568
- yield* sql`
3569
- UPDATE effect_agent_child_reservations
3570
- SET status = 'released', released_at = ${now.iso}
3571
- WHERE reservation_id = ${validated.reservationId}
3572
- RETURNING *`.pipe(
3573
- rows.childReservations.write,
3574
- Effect.mapError(internalFailure(operation)),
3575
- );
3576
- const updated = yield* readChildReservation(operation, validated.reservationId);
3403
+ yield* hitFailpoint("ledger:child-release:after", operation);
3577
3404
 
3578
- if (Option.isNone(updated)) {
3579
- return yield* corruptionFailure(
3580
- operation,
3581
- "effect_agent_child_reservations",
3582
- validated.reservationId,
3583
- "An updated child reservation row is missing inside its own transaction.",
3584
- );
3585
- }
3586
-
3587
- return yield* childReservationSnapshotFromRow(operation, updated.value);
3588
- }),
3589
- );
3590
-
3591
- yield* hitFailpoint("ledger:child-release:after", operation);
3592
-
3593
- return released;
3594
- });
3405
+ return released;
3406
+ },
3407
+ );
3595
3408
 
3596
3409
  interface ScanCursor {
3597
3410
  readonly threadId: string;
3598
3411
  readonly queueSequence: number;
3599
3412
  }
3600
3413
 
3601
- const scanPage = Effect.fn("DoSubmissionLedger.scanPage")(function* (
3414
+ const scanPage = Effect.fnUntraced(function* (
3602
3415
  cursor: ScanCursor | undefined,
3603
3416
  ): Effect.fn.Return<
3604
3417
  readonly [ReadonlyArray<SubmissionWorkItem>, Option.Option<ScanCursor | undefined>],
@@ -3698,264 +3511,226 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
3698
3511
  ),
3699
3512
  );
3700
3513
 
3701
- const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
3702
- "DoSubmissionLedger.readAbortIntentForSubmission",
3703
- )(function* (request) {
3704
- const operation = "ledger read abort intent";
3705
-
3706
- const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
3707
- Effect.mapError(internalFailure(operation)),
3708
- );
3709
-
3710
- return yield* state.read(
3711
- Effect.gen(function* () {
3712
- const submission = yield* requireSubmission(operation, validated.submissionId);
3713
- const intent = yield* readAbortIntent(operation, validated.submissionId);
3514
+ const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] =
3515
+ Effect.fnUntraced(function* (request) {
3516
+ const operation = "ledger read abort intent";
3714
3517
 
3715
- return Option.isNone(intent)
3716
- ? undefined
3717
- : yield* abortIntentFromRow(operation, submission, validated.submissionId, intent.value);
3718
- }),
3719
- );
3720
- });
3518
+ const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
3519
+ Effect.mapError(internalFailure(operation)),
3520
+ );
3721
3521
 
3722
- const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
3723
- "DoSubmissionLedger.loadRecoverySnapshot",
3724
- )(function* (request: RecoverySnapshotRequest) {
3725
- const operation = "ledger load recovery snapshot";
3522
+ return yield* state.read(
3523
+ Effect.gen(function* () {
3524
+ const submission = yield* requireSubmission(operation, validated.submissionId);
3525
+ const intent = yield* readAbortIntent(operation, validated.submissionId);
3726
3526
 
3727
- const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
3728
- request,
3729
- ).pipe(Effect.mapError(internalFailure(operation)));
3527
+ return Option.isNone(intent)
3528
+ ? undefined
3529
+ : yield* abortIntentFromRow(
3530
+ operation,
3531
+ submission,
3532
+ validated.submissionId,
3533
+ intent.value,
3534
+ );
3535
+ }),
3536
+ );
3537
+ });
3730
3538
 
3731
- return yield* state.read(
3732
- Effect.gen(function* () {
3733
- const submissionRow = yield* requireSubmission(operation, validated.submissionId);
3734
- const submission = yield* decodeSubmissionSnapshot(operation, submissionRow);
3735
-
3736
- let ownership: OwnershipSnapshot | undefined;
3737
- const ownershipRow = yield* readOwnership(operation, validated.submissionId);
3738
-
3739
- if (Option.isSome(ownershipRow)) {
3740
- ownership = yield* decodeOwnershipSnapshot({
3741
- attemptId: ownershipRow.value.attempt_id,
3742
- ownerProducerId: ownershipRow.value.owner_producer_id,
3743
- producerEpoch: ownershipRow.value.producer_epoch,
3744
- leaseExpiresAt: ownershipRow.value.lease_expires_at,
3745
- }).pipe(Effect.mapError(internalFailure(operation)));
3746
- }
3539
+ const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] =
3540
+ Effect.fnUntraced(function* (request: RecoverySnapshotRequest) {
3541
+ const operation = "ledger load recovery snapshot";
3747
3542
 
3748
- let inputApplied: InputAppliedMarker | undefined;
3543
+ const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
3544
+ request,
3545
+ ).pipe(Effect.mapError(internalFailure(operation)));
3749
3546
 
3750
- if (
3751
- submissionRow.input_applied_record_id !== null &&
3752
- submissionRow.input_applied_sequence !== null
3753
- ) {
3754
- inputApplied = yield* decodeInputAppliedMarker({
3755
- recordId: submissionRow.input_applied_record_id,
3756
- sequence: submissionRow.input_applied_sequence,
3757
- }).pipe(Effect.mapError(internalFailure(operation)));
3758
- }
3547
+ return yield* state.read(
3548
+ Effect.gen(function* () {
3549
+ const submissionRow = yield* requireSubmission(operation, validated.submissionId);
3550
+ const submission = yield* decodeSubmissionSnapshot(operation, submissionRow);
3759
3551
 
3760
- let reservation: SettlementReservationSnapshot | undefined;
3761
- const reservationRow = yield* readReservation(operation, validated.submissionId);
3552
+ let inputApplied: InputAppliedMarker | undefined;
3762
3553
 
3763
- if (Option.isSome(reservationRow)) {
3764
- const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(
3765
- Effect.mapError((error) =>
3766
- corruptionFailure(
3767
- operation,
3768
- "effect_agent_settlement_reservations",
3769
- validated.submissionId,
3770
- error.message,
3771
- ),
3772
- ),
3773
- );
3554
+ if (
3555
+ submissionRow.input_applied_record_id !== null &&
3556
+ submissionRow.input_applied_sequence !== null
3557
+ ) {
3558
+ inputApplied = yield* decodeInputAppliedMarker({
3559
+ recordId: submissionRow.input_applied_record_id,
3560
+ sequence: submissionRow.input_applied_sequence,
3561
+ }).pipe(Effect.mapError(internalFailure(operation)));
3562
+ }
3774
3563
 
3775
- const settlementId = yield* Schema.decodeEffect(
3776
- SettlementReservationSnapshot.fields.settlementId,
3777
- )(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
3564
+ let abortIntent: AbortIntent | undefined;
3565
+ const abortRow = yield* readAbortIntent(operation, validated.submissionId);
3778
3566
 
3779
- reservation = SettlementReservationSnapshot.make({
3780
- settlementId,
3781
- outcome: reservationRow.value.outcome,
3782
- record,
3783
- recordDigest: reservationRow.value.record_digest,
3784
- finalized: reservationRow.value.finalized_at !== null,
3785
- });
3786
- }
3567
+ if (Option.isSome(abortRow)) {
3568
+ abortIntent = yield* abortIntentFromRow(
3569
+ operation,
3570
+ submissionRow,
3571
+ validated.submissionId,
3572
+ abortRow.value,
3573
+ );
3574
+ }
3787
3575
 
3788
- let abortIntent: AbortIntent | undefined;
3789
- const abortRow = yield* readAbortIntent(operation, validated.submissionId);
3576
+ // Host-side view: every Submission whose host linkage points here, in queue order
3577
+ // (the terminalize loop settles them with the host outcome, DUR-002).
3578
+ const joinRows = (yield* cached(
3579
+ rows.submissions.by("joined_host_submission_id", validated.submissionId),
3580
+ operation,
3581
+ ))
3582
+ .filter((row) => row.joined_host_submission_id === validated.submissionId)
3583
+ .sort((a, b) => a.queue_sequence - b.queue_sequence);
3790
3584
 
3791
- if (Option.isSome(abortRow)) {
3792
- abortIntent = yield* abortIntentFromRow(
3585
+ const joinSubmissions = yield* decodeSubmissionRows(
3793
3586
  operation,
3794
- submissionRow,
3795
3587
  validated.submissionId,
3796
- abortRow.value,
3588
+ joinRows,
3797
3589
  );
3798
- }
3799
3590
 
3800
- // Host-side view: every Submission whose host linkage points here, in queue order
3801
- // (the terminalize loop settles them with the host outcome, DUR-002).
3802
- const joinRows = (yield* cached(
3803
- rows.submissions.by("joined_host_submission_id", validated.submissionId),
3804
- operation,
3805
- ))
3806
- .filter((row) => row.joined_host_submission_id === validated.submissionId)
3807
- .sort((a, b) => a.queue_sequence - b.queue_sequence);
3808
-
3809
- const joinSubmissions = yield* decodeSubmissionRows(
3810
- operation,
3811
- validated.submissionId,
3812
- joinRows,
3813
- );
3814
-
3815
- const joins = yield* Effect.forEach(joinSubmissions, (row) =>
3816
- decodeJoinSnapshot({
3817
- submissionId: row.submission_id,
3818
- state: row.state,
3819
- hostSubmissionId: validated.submissionId,
3820
- }).pipe(Effect.mapError(internalFailure(operation))),
3821
- );
3822
-
3823
- let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
3591
+ const joins = yield* Effect.forEach(joinSubmissions, (row) =>
3592
+ decodeJoinSnapshot({
3593
+ submissionId: row.submission_id,
3594
+ state: row.state,
3595
+ hostSubmissionId: validated.submissionId,
3596
+ }).pipe(Effect.mapError(internalFailure(operation))),
3597
+ );
3824
3598
 
3825
- if (submissionRow.joined_host_submission_id !== null) {
3826
- hostSubmissionId = yield* decodeSubmissionId(
3827
- submissionRow.joined_host_submission_id,
3828
- ).pipe(Effect.mapError(internalFailure(operation)));
3829
- }
3599
+ let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
3830
3600
 
3831
- let suspension: SuspensionSnapshot | undefined;
3601
+ if (submissionRow.joined_host_submission_id !== null) {
3602
+ hostSubmissionId = yield* decodeSubmissionId(
3603
+ submissionRow.joined_host_submission_id,
3604
+ ).pipe(Effect.mapError(internalFailure(operation)));
3605
+ }
3832
3606
 
3833
- if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
3834
- const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(
3835
- Effect.mapError((error) =>
3836
- corruptionFailure(
3837
- operation,
3838
- "effect_agent_submissions",
3839
- validated.submissionId,
3840
- error.message,
3607
+ let suspension: SuspensionSnapshot | undefined;
3608
+
3609
+ if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
3610
+ const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(
3611
+ Effect.mapError((error) =>
3612
+ corruptionFailure(
3613
+ operation,
3614
+ "effect_agent_submissions",
3615
+ validated.submissionId,
3616
+ error.message,
3617
+ ),
3841
3618
  ),
3842
- ),
3843
- );
3619
+ );
3844
3620
 
3845
- suspension = yield* decodeSuspensionSnapshot({
3846
- reason,
3847
- suspendedAt: submissionRow.suspended_at,
3848
- }).pipe(
3849
- Effect.mapError((error) =>
3850
- corruptionFailure(
3851
- operation,
3852
- "effect_agent_submissions",
3853
- validated.submissionId,
3854
- error.message,
3621
+ suspension = yield* decodeSuspensionSnapshot({
3622
+ reason,
3623
+ suspendedAt: submissionRow.suspended_at,
3624
+ }).pipe(
3625
+ Effect.mapError((error) =>
3626
+ corruptionFailure(
3627
+ operation,
3628
+ "effect_agent_submissions",
3629
+ validated.submissionId,
3630
+ error.message,
3631
+ ),
3855
3632
  ),
3856
- ),
3857
- );
3858
- }
3859
-
3860
- const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
3633
+ );
3634
+ }
3861
3635
 
3862
- const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
3863
- approvalIntentFromRow(operation, row),
3864
- );
3636
+ const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
3865
3637
 
3866
- const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
3638
+ const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
3639
+ approvalIntentFromRow(operation, row),
3640
+ );
3867
3641
 
3868
- const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
3869
- unknownResolutionIntentFromRow(operation, row),
3870
- );
3642
+ const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
3871
3643
 
3872
- // Parent-side subagent view: this Submission's child budget reservations in parent
3873
- // Tool Call order, plus each attached child's current lane state (a disposable
3874
- // derived view; canonical records stay the recovery truth, DUR-015). The child's
3875
- // state comes from its local row when this store holds it, and otherwise from the
3876
- // durable cross-store settlement marker; a child that is neither local nor marked
3877
- // settled is enriched by the routed per-child lookup one layer out (plan §1.3).
3878
- const childReservationRows = (yield* cached(
3879
- rows.childReservations.by("parent_submission_id", validated.submissionId),
3880
- operation,
3881
- ))
3882
- .filter((row) => row.parent_submission_id === validated.submissionId)
3883
- .sort((a, b) => a.parent_tool_call_id.localeCompare(b.parent_tool_call_id));
3644
+ const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
3645
+ unknownResolutionIntentFromRow(operation, row),
3646
+ );
3884
3647
 
3885
- const decodedChildReservations = yield* decodeChildReservationRows(
3886
- operation,
3887
- validated.submissionId,
3888
- childReservationRows,
3889
- );
3648
+ // Parent-side subagent view: this Submission's child budget reservations in parent
3649
+ // Tool Call order, plus each attached child's current lane state (a disposable
3650
+ // derived view; canonical records stay the recovery truth, DUR-015). The child's
3651
+ // state comes from its local row when this store holds it, and otherwise from the
3652
+ // durable cross-store settlement marker; a child that is neither local nor marked
3653
+ // settled is enriched by the routed per-child lookup one layer out (plan §1.3).
3654
+ const childReservationRows = (yield* cached(
3655
+ rows.childReservations.by("parent_submission_id", validated.submissionId),
3656
+ operation,
3657
+ ))
3658
+ .filter((row) => row.parent_submission_id === validated.submissionId)
3659
+ .sort((a, b) => a.parent_tool_call_id.localeCompare(b.parent_tool_call_id));
3890
3660
 
3891
- const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
3892
- childReservationSnapshotFromRow(operation, row),
3893
- );
3661
+ const decodedChildReservations = yield* decodeChildReservationRows(
3662
+ operation,
3663
+ validated.submissionId,
3664
+ childReservationRows,
3665
+ );
3894
3666
 
3895
- const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
3896
- const markersByChild = new Map(markers.map((row) => [row.child_submission_id, row]));
3897
- const childAttachments: Array<ChildAttachmentSnapshot> = [];
3667
+ const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
3668
+ childReservationSnapshotFromRow(operation, row),
3669
+ );
3898
3670
 
3899
- for (const row of decodedChildReservations) {
3900
- if (row.child_submission_id === null) continue;
3901
- const child = yield* readSubmission(operation, row.child_submission_id);
3671
+ const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
3672
+ const markersByChild = new Map(markers.map((row) => [row.child_submission_id, row]));
3673
+ const childAttachments: Array<ChildAttachmentSnapshot> = [];
3674
+
3675
+ for (const row of decodedChildReservations) {
3676
+ if (row.child_submission_id === null) continue;
3677
+ const child = yield* readSubmission(operation, row.child_submission_id);
3678
+
3679
+ if (Option.isSome(child)) {
3680
+ childAttachments.push(
3681
+ yield* decodeChildAttachmentSnapshot({
3682
+ toolCallId: row.parent_tool_call_id,
3683
+ childSubmissionId: row.child_submission_id,
3684
+ childState: child.value.state,
3685
+ ...(child.value.settled_outcome === null
3686
+ ? {}
3687
+ : { childOutcome: child.value.settled_outcome }),
3688
+ }).pipe(Effect.mapError(internalFailure(operation))),
3689
+ );
3690
+ continue;
3691
+ }
3692
+ const marker = markersByChild.get(row.child_submission_id);
3902
3693
 
3903
- if (Option.isSome(child)) {
3694
+ if (marker === undefined) continue;
3904
3695
  childAttachments.push(
3905
3696
  yield* decodeChildAttachmentSnapshot({
3906
3697
  toolCallId: row.parent_tool_call_id,
3907
3698
  childSubmissionId: row.child_submission_id,
3908
- childState: child.value.state,
3909
- ...(child.value.settled_outcome === null
3910
- ? {}
3911
- : { childOutcome: child.value.settled_outcome }),
3699
+ childState: "settled",
3700
+ ...(marker.child_outcome === null ? {} : { childOutcome: marker.child_outcome }),
3912
3701
  }).pipe(Effect.mapError(internalFailure(operation))),
3913
3702
  );
3914
- continue;
3915
3703
  }
3916
- const marker = markersByChild.get(row.child_submission_id);
3917
-
3918
- if (marker === undefined) continue;
3919
- childAttachments.push(
3920
- yield* decodeChildAttachmentSnapshot({
3921
- toolCallId: row.parent_tool_call_id,
3922
- childSubmissionId: row.child_submission_id,
3923
- childState: "settled",
3924
- ...(marker.child_outcome === null ? {} : { childOutcome: marker.child_outcome }),
3925
- }).pipe(Effect.mapError(internalFailure(operation))),
3926
- );
3927
- }
3928
3704
 
3929
- let parentLinkage: ParentLinkage | undefined;
3705
+ let parentLinkage: ParentLinkage | undefined;
3930
3706
 
3931
- if (
3932
- submissionRow.parent_submission_id !== null &&
3933
- submissionRow.parent_tool_call_id !== null
3934
- ) {
3935
- parentLinkage = yield* decodeParentLinkage({
3936
- parentSubmissionId: submissionRow.parent_submission_id,
3937
- parentToolCallId: submissionRow.parent_tool_call_id,
3938
- }).pipe(Effect.mapError(internalFailure(operation)));
3939
- }
3707
+ if (
3708
+ submissionRow.parent_submission_id !== null &&
3709
+ submissionRow.parent_tool_call_id !== null
3710
+ ) {
3711
+ parentLinkage = yield* decodeParentLinkage({
3712
+ parentSubmissionId: submissionRow.parent_submission_id,
3713
+ parentToolCallId: submissionRow.parent_tool_call_id,
3714
+ }).pipe(Effect.mapError(internalFailure(operation)));
3715
+ }
3940
3716
 
3941
- return RecoverySnapshot.make({
3942
- submission,
3943
- joins,
3944
- approvalDecisions,
3945
- unknownResolutions,
3946
- childReservations,
3947
- childAttachments,
3948
- ...(parentLinkage === undefined ? {} : { parentLinkage }),
3949
- ...(hostSubmissionId === undefined ? {} : { hostSubmissionId }),
3950
- ...(suspension === undefined ? {} : { suspension }),
3951
- ...(ownership === undefined ? {} : { ownership }),
3952
- ...(inputApplied === undefined ? {} : { inputApplied }),
3953
- ...(reservation === undefined ? {} : { reservation }),
3954
- ...(abortIntent === undefined ? {} : { abortIntent }),
3955
- });
3956
- }),
3957
- );
3958
- });
3717
+ return RecoverySnapshot.make({
3718
+ submission,
3719
+ joins,
3720
+ approvalDecisions,
3721
+ unknownResolutions,
3722
+ childReservations,
3723
+ childAttachments,
3724
+ ...(parentLinkage === undefined ? {} : { parentLinkage }),
3725
+ ...(hostSubmissionId === undefined ? {} : { hostSubmissionId }),
3726
+ ...(suspension === undefined ? {} : { suspension }),
3727
+ hasOwner: owner.hasOwner(submissionRow.thread_id, submissionRow.submission_id),
3728
+ ...(inputApplied === undefined ? {} : { inputApplied }),
3729
+ ...(abortIntent === undefined ? {} : { abortIntent }),
3730
+ });
3731
+ }),
3732
+ );
3733
+ });
3959
3734
 
3960
3735
  return Context.make(
3961
3736
  SubmissionLedger,
@@ -3964,20 +3739,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
3964
3739
  admit,
3965
3740
  markReady,
3966
3741
  lookup,
3742
+ resolveFundingOwner,
3967
3743
  resolveAdmission,
3968
- claim,
3969
- renewOwnership,
3970
- releaseOwnership,
3744
+ selectHead,
3971
3745
  markInputApplied,
3972
- reserveSettlement,
3973
- finalizeSettlement,
3746
+ finalizeSettlement: (request) => owner.withMutation(finalizeSettlement(request)),
3974
3747
  requestAbort,
3975
3748
  stopWorker,
3976
3749
  inspectWorker,
3977
3750
  claimJoining,
3978
3751
  markJoined,
3979
3752
  revertJoining,
3980
- suspend,
3753
+ suspend: (request) => owner.withMutation(suspend(request)),
3981
3754
  recordApprovalDecision,
3982
3755
  markUnknown,
3983
3756
  recordUnknownResolution,
@@ -3990,27 +3763,30 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
3990
3763
  loadRecoverySnapshot,
3991
3764
  readAbortIntent: readAbortIntentForSubmission,
3992
3765
  }),
3993
- );
3766
+ ).pipe(Context.add(StorageOwner, owner), Context.add(SettlementPublisher, { publish }));
3994
3767
  });
3995
3768
 
3996
3769
  /**
3997
3770
  * Durable Object SubmissionLedger implementation sharing the journal's private SQLite
3998
- * database, storage-backed transaction discipline, and producer-epoch fencing substrate.
3771
+ * database, storage-backed transactions, and scoped local owner.
3999
3772
  * Configuration, failpoint, SQL, and Crypto authority stay visible in the input channel.
4000
3773
  */
4001
3774
  export const submissionLedgerLayer: Layer.Layer<
4002
- SubmissionLedger,
3775
+ SubmissionLedger | SettlementPublisher | StorageOwner,
4003
3776
  DoStorageInitializationError,
4004
3777
  DoStorageConfig | DoStorageFailpoint | SqlClientService.SqlClient | Crypto.Crypto
4005
3778
  > = Layer.effectContext(makeServices());
4006
3779
 
4007
3780
  /**
4008
3781
  * A composition-root convenience Layer for the durable Submission Ledger. Point it at the
4009
- * same `ctx.storage` as the ThreadStore so claims fence the same producer epochs.
3782
+ * same `ctx.storage` as the ThreadStore so both share the same scoped storage owner.
4010
3783
  */
4011
3784
  export const ledgerLayer = (
4012
3785
  options: DoStorageOptions,
4013
- ): Layer.Layer<SubmissionLedger, DoStorageInitializationError> =>
3786
+ ): Layer.Layer<
3787
+ SubmissionLedger | SettlementPublisher | StorageOwner,
3788
+ DoStorageInitializationError
3789
+ > =>
4014
3790
  Layer.unwrap(
4015
3791
  Effect.map(DoStorageConfig, (config) =>
4016
3792
  submissionLedgerLayer.pipe(