@yielded/agent-storage-cloudflare 0.1.0-beta.166

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/dist/DoMemoryStore.d.mts +30 -0
  3. package/dist/DoMemoryStore.mjs +66 -0
  4. package/dist/DoMemoryStore.mjs.map +1 -0
  5. package/dist/DoMessageDeliveryStore.d.mts +17 -0
  6. package/dist/DoMessageDeliveryStore.mjs +30 -0
  7. package/dist/DoMessageDeliveryStore.mjs.map +1 -0
  8. package/dist/DoScheduleStore.d.mts +36 -0
  9. package/dist/DoScheduleStore.mjs +388 -0
  10. package/dist/DoScheduleStore.mjs.map +1 -0
  11. package/dist/DoStorageConfig.d.mts +45 -0
  12. package/dist/DoStorageConfig.mjs +52 -0
  13. package/dist/DoStorageConfig.mjs.map +1 -0
  14. package/dist/DoStorageError-CGuAfSwF.d.mts +101 -0
  15. package/dist/DoStorageError.d.mts +2 -0
  16. package/dist/DoStorageError.mjs +167 -0
  17. package/dist/DoStorageError.mjs.map +1 -0
  18. package/dist/DoStorageFailpoint.d.mts +17 -0
  19. package/dist/DoStorageFailpoint.mjs +14 -0
  20. package/dist/DoStorageFailpoint.mjs.map +1 -0
  21. package/dist/DoStorageFailpointTesting.d.mts +24 -0
  22. package/dist/DoStorageFailpointTesting.mjs +22 -0
  23. package/dist/DoStorageFailpointTesting.mjs.map +1 -0
  24. package/dist/DoStorageVersion-iNSyi3cJ.d.mts +12 -0
  25. package/dist/DoStorageVersion.d.mts +2 -0
  26. package/dist/DoStorageVersion.mjs +8 -0
  27. package/dist/DoStorageVersion.mjs.map +1 -0
  28. package/dist/DoSubmissionLedger.d.mts +23 -0
  29. package/dist/DoSubmissionLedger.mjs +1979 -0
  30. package/dist/DoSubmissionLedger.mjs.map +1 -0
  31. package/dist/DoSubscriptionStore.d.mts +25 -0
  32. package/dist/DoSubscriptionStore.mjs +186 -0
  33. package/dist/DoSubscriptionStore.mjs.map +1 -0
  34. package/dist/DoThreadStore-D_83OjCH.d.mts +72 -0
  35. package/dist/DoThreadStore.d.mts +2 -0
  36. package/dist/DoThreadStore.mjs +529 -0
  37. package/dist/DoThreadStore.mjs.map +1 -0
  38. package/dist/MemoryProtocol.d.mts +38160 -0
  39. package/dist/MemoryProtocol.mjs +219 -0
  40. package/dist/MemoryProtocol.mjs.map +1 -0
  41. package/dist/PortProtocol.d.mts +7726 -0
  42. package/dist/PortProtocol.mjs +234 -0
  43. package/dist/PortProtocol.mjs.map +1 -0
  44. package/dist/PortRouting.d.mts +97 -0
  45. package/dist/PortRouting.mjs +544 -0
  46. package/dist/PortRouting.mjs.map +1 -0
  47. package/dist/do-journal-DUhjZkOf.mjs +1352 -0
  48. package/dist/do-journal-DUhjZkOf.mjs.map +1 -0
  49. package/dist/index.d.mts +14 -0
  50. package/dist/index.mjs +14 -0
  51. package/dist/migrations-DtKz_Gxr.mjs +306 -0
  52. package/dist/migrations-DtKz_Gxr.mjs.map +1 -0
  53. package/dist/owned-state-DHm2aRXx.mjs +119 -0
  54. package/dist/owned-state-DHm2aRXx.mjs.map +1 -0
  55. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  56. package/package.json +1 -0
  57. package/src/DoMemoryStore.ts +73 -0
  58. package/src/DoMessageDeliveryStore.ts +39 -0
  59. package/src/DoScheduleStore.ts +622 -0
  60. package/src/DoStorageConfig.ts +53 -0
  61. package/src/DoStorageError.ts +180 -0
  62. package/src/DoStorageFailpoint.ts +20 -0
  63. package/src/DoStorageFailpointTesting.ts +41 -0
  64. package/src/DoStorageVersion.ts +2 -0
  65. package/src/DoSubmissionLedger.ts +4027 -0
  66. package/src/DoSubscriptionStore.ts +327 -0
  67. package/src/DoThreadStore.ts +1121 -0
  68. package/src/MemoryProtocol.ts +355 -0
  69. package/src/PortProtocol.ts +436 -0
  70. package/src/PortRouting.ts +1464 -0
  71. package/src/index.ts +13 -0
  72. package/src/internal/do-journal.ts +2282 -0
  73. package/src/internal/message-delivery-schema.ts +24 -0
  74. package/src/internal/migrations.ts +304 -0
  75. package/src/internal/owned-state.ts +228 -0
  76. package/src/internal/recovery-checkpoint-schema.ts +17 -0
  77. package/src/internal/storage-span.ts +44 -0
@@ -0,0 +1,4027 @@
1
+ import { BrowserCrypto } from "@effect/platform-browser";
2
+ import { SqliteClient } from "@effect/sql-sqlite-do";
3
+ import {
4
+ SqlStorageProgress,
5
+ type SqlStorageProgressKind,
6
+ } from "@yielded/agent-storage-sql/sql-storage-progress";
7
+ import { EMPTY_TAIL_DIGEST } from "@yielded/agent/digest";
8
+ import type { LifecyclePublicationFact } from "@yielded/agent/lifecycle-publication";
9
+ import { InputMessage } from "@yielded/agent/messaging";
10
+ import {
11
+ ApprovalDecision,
12
+ CanonicalSequence,
13
+ DefinitionDigests,
14
+ Digest,
15
+ PersistedJson,
16
+ WorkerAdmission,
17
+ ProducerEpoch,
18
+ RecordEnvelope,
19
+ SettlementOutcome,
20
+ } from "@yielded/agent/records";
21
+ import {
22
+ AbortCommand,
23
+ WorkerStopCommand,
24
+ WorkerLedgerState,
25
+ workerTerminalFromRecord,
26
+ AbortIntent,
27
+ AbortIntentRequest,
28
+ AdmissionAdmitted,
29
+ AdmissionConflict,
30
+ AdmissionFence,
31
+ AdmissionGroup,
32
+ AdmissionPolicyError,
33
+ AdmissionNotAdmitted,
34
+ AdmissionRequest,
35
+ SubmissionAdmissionFence,
36
+ AdmissionResult,
37
+ ApprovalConflict,
38
+ ApprovalDecisionCommand,
39
+ ApprovalDecisionIntent,
40
+ AttachChildToReservationRequest,
41
+ BeginChildBudgetReleaseRequest,
42
+ ChildAttachmentSnapshot,
43
+ ChildBudgetReservationRequest,
44
+ ChildBudgetReservationSnapshot,
45
+ ChildReservationConflict,
46
+ ChildReservationStatus,
47
+ ChildSettledNotification,
48
+ Claim,
49
+ ClaimJoiningRequest,
50
+ ClaimRequest,
51
+ InputAppliedMarker,
52
+ JoinSnapshot,
53
+ JoinedToHost,
54
+ JoiningClaim,
55
+ LedgerCapabilities,
56
+ LedgerError,
57
+ MarkInputAppliedRequest,
58
+ MarkJoinedRequest,
59
+ MarkReadyRequest,
60
+ MarkUnknownRequest,
61
+ OwnershipLost,
62
+ OwnershipRenewal,
63
+ OwnershipSnapshot,
64
+ ParentLinkage,
65
+ QueueSequence,
66
+ RecoverySnapshot,
67
+ RecoverySnapshotRequest,
68
+ ReleaseChildBudgetRequest,
69
+ ReleaseOwnershipRequest,
70
+ RenewOwnershipRequest,
71
+ ReservedChildBudget,
72
+ ReservedSettlement,
73
+ RevertJoiningRequest,
74
+ Settlement,
75
+ SettlementConflict,
76
+ SettlementFinalization,
77
+ SettlementReservation,
78
+ SubmissionLedger,
79
+ SubmissionLookup,
80
+ SubmissionLookupByKey,
81
+ SubmissionSnapshot,
82
+ SubmissionWorkItem,
83
+ SubmissionState,
84
+ SettlementReservationSnapshot,
85
+ settlementFailureFromRecord,
86
+ SuspendRequest,
87
+ SuspensionReason,
88
+ SuspensionSnapshot,
89
+ UnknownResolution,
90
+ UnknownResolutionCommand,
91
+ UnknownResolutionConflict,
92
+ UnknownResolutionIntent,
93
+ submissionAbortRecordId,
94
+ type ChildSettledOutcome,
95
+ type SuspensionOutcome,
96
+ } from "@yielded/agent/submission-ledger";
97
+ import { ThreadStoreDiagnostic } from "@yielded/agent/thread-store";
98
+ import { AssignmentTerminal } from "@yielded/agent/worker";
99
+ import {
100
+ Cause,
101
+ Clock,
102
+ Context,
103
+ Crypto,
104
+ DateTime,
105
+ Effect,
106
+ Layer,
107
+ Option,
108
+ Schema,
109
+ Stream,
110
+ } from "effect";
111
+ import * as SqlClientService from "effect/sql/SqlClient";
112
+ import type { SqlError } from "effect/sql/SqlError";
113
+
114
+ import { DoStorageConfig } from "./DoStorageConfig.ts";
115
+ import {
116
+ DoLedgerError,
117
+ DoStorageCorruptionError,
118
+ DoStorageError,
119
+ type DoStorageFailpointLocation,
120
+ } from "./DoStorageError.ts";
121
+ import { DoStorageFailpoint } from "./DoStorageFailpoint.ts";
122
+ import {
123
+ storageConfigLayer,
124
+ storageFailpointLayer,
125
+ type DoStorageInitializationError,
126
+ type DoStorageOptions,
127
+ } from "./DoThreadStore.ts";
128
+ import { decodeRows, initializeDoJournal } from "./internal/do-journal.ts";
129
+ import { ownedRows } from "./internal/owned-state.ts";
130
+ import { withStorageSpan } from "./internal/storage-span.ts";
131
+
132
+ type SubmissionId = SubmissionSnapshot["submissionId"];
133
+
134
+ /**
135
+ * Static decode-side ceiling; writes are bounded in bytes by the configured
136
+ * `maxStoredValueBytes` (see do-journal.ts).
137
+ */
138
+ const BoundedStoredText = Schema.String.check(Schema.isMaxLength(2_000_000));
139
+ const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
140
+ const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
141
+
142
+ const SCAN_PAGE_SIZE = 256;
143
+ const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
144
+ const RESUME_IMMEDIATELY: SuspensionOutcome = "resume-immediately";
145
+ const SUSPENDED: SuspensionOutcome = "suspended";
146
+ const NOT_WAITING: ChildSettledOutcome = "not-waiting";
147
+ const STILL_WAITING: ChildSettledOutcome = "still-waiting";
148
+ const WOKEN: ChildSettledOutcome = "woken";
149
+ const MAX_IDENTIFIER_LENGTH = 1_024;
150
+
151
+ class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
152
+ submission_id: BoundedIdentifier,
153
+ thread_id: BoundedIdentifier,
154
+ queue_sequence: QueueSequence,
155
+ principal: BoundedIdentifier,
156
+ idempotency_key: BoundedIdentifier,
157
+ agent_id: BoundedIdentifier,
158
+ agent_digests_json: BoundedStoredText,
159
+ deployment_id: BoundedIdentifier,
160
+ input_json: BoundedStoredText,
161
+ input_digest: Digest,
162
+ receipt_id: BoundedIdentifier,
163
+ state: SubmissionState,
164
+ settled_outcome: Schema.NullOr(SettlementOutcome),
165
+ created_at: BoundedTimestamp,
166
+ ready_at: Schema.NullOr(BoundedTimestamp),
167
+ input_applied_record_id: Schema.NullOr(BoundedIdentifier),
168
+ input_applied_sequence: Schema.NullOr(CanonicalSequence),
169
+ joined_host_submission_id: Schema.NullOr(BoundedIdentifier),
170
+ suspended_reason_json: Schema.NullOr(BoundedStoredText),
171
+ suspended_at: Schema.NullOr(BoundedTimestamp),
172
+ unknown_reason: Schema.NullOr(BoundedStoredText),
173
+ unknown_tool_call_ids_json: Schema.NullOr(BoundedStoredText),
174
+ parent_submission_id: Schema.NullOr(BoundedIdentifier),
175
+ parent_tool_call_id: Schema.NullOr(BoundedIdentifier),
176
+ admission_group: Schema.NullOr(AdmissionGroup),
177
+ admission_fence_json: Schema.NullOr(BoundedStoredText),
178
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
179
+ message_admission_json: Schema.NullOr(BoundedStoredText),
180
+ }) {}
181
+
182
+ // Discovery retains identity and state, never every queued input payload.
183
+ const SubmissionWorkRow = Schema.Struct({
184
+ submission_id: SubmissionRow.fields.submission_id,
185
+ thread_id: SubmissionRow.fields.thread_id,
186
+ queue_sequence: SubmissionRow.fields.queue_sequence,
187
+ principal: SubmissionRow.fields.principal,
188
+ idempotency_key: SubmissionRow.fields.idempotency_key,
189
+ deployment_id: SubmissionRow.fields.deployment_id,
190
+ receipt_id: SubmissionRow.fields.receipt_id,
191
+ state: SubmissionRow.fields.state,
192
+ });
193
+
194
+ class ChildReservationRow extends Schema.Class<ChildReservationRow>("ChildReservationRow")({
195
+ reservation_id: BoundedIdentifier,
196
+ parent_submission_id: BoundedIdentifier,
197
+ parent_tool_call_id: BoundedIdentifier,
198
+ child_submission_id: Schema.NullOr(BoundedIdentifier),
199
+ status: ChildReservationStatus,
200
+ allocation_json: BoundedStoredText,
201
+ allocation_digest: Digest,
202
+ accounting_json: Schema.NullOr(BoundedStoredText),
203
+ reserved_at: BoundedTimestamp,
204
+ release_began_at: Schema.NullOr(BoundedTimestamp),
205
+ released_at: Schema.NullOr(BoundedTimestamp),
206
+ }) {}
207
+
208
+ class ChildSettlementMarkerRow extends Schema.Class<ChildSettlementMarkerRow>(
209
+ "ChildSettlementMarkerRow",
210
+ )({
211
+ parent_submission_id: BoundedIdentifier,
212
+ child_submission_id: BoundedIdentifier,
213
+ child_outcome: Schema.NullOr(SettlementOutcome),
214
+ recorded_at: BoundedTimestamp,
215
+ }) {}
216
+
217
+ class ApprovalDecisionRow extends Schema.Class<ApprovalDecisionRow>("ApprovalDecisionRow")({
218
+ submission_id: BoundedIdentifier,
219
+ tool_call_id: BoundedIdentifier,
220
+ decision: ApprovalDecision,
221
+ resolver: BoundedIdentifier,
222
+ reason: BoundedStoredText,
223
+ decided_at: BoundedTimestamp,
224
+ }) {}
225
+
226
+ class UnknownResolutionRow extends Schema.Class<UnknownResolutionRow>("UnknownResolutionRow")({
227
+ submission_id: BoundedIdentifier,
228
+ tool_call_id: BoundedIdentifier,
229
+ author: BoundedIdentifier,
230
+ reason: BoundedStoredText,
231
+ resolution_json: BoundedStoredText,
232
+ resolved_at: BoundedTimestamp,
233
+ }) {}
234
+
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
+ class AbortIntentRow extends Schema.Class<AbortIntentRow>("AbortIntentRow")({
256
+ submission_id: BoundedIdentifier,
257
+ author: BoundedIdentifier,
258
+ reason: BoundedStoredText,
259
+ requested_at: BoundedTimestamp,
260
+ canonical_record_id: Schema.NullOr(BoundedIdentifier),
261
+ }) {}
262
+
263
+ class WorkerStopRow extends Schema.Class<WorkerStopRow>("WorkerStopRow")({
264
+ thread_id: BoundedIdentifier,
265
+ terminal: Schema.NullOr(AssignmentTerminal),
266
+ }) {}
267
+
268
+ class MaxQueueSequenceRow extends Schema.Class<MaxQueueSequenceRow>("MaxQueueSequenceRow")({
269
+ max_queue_sequence: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
270
+ }) {}
271
+
272
+ const SUBMISSION_COLUMNS = `
273
+ submission_id,
274
+ thread_id,
275
+ queue_sequence,
276
+ principal,
277
+ idempotency_key,
278
+ agent_id,
279
+ agent_digests_json,
280
+ deployment_id,
281
+ input_json,
282
+ input_digest,
283
+ receipt_id,
284
+ state,
285
+ settled_outcome,
286
+ created_at,
287
+ ready_at,
288
+ input_applied_record_id,
289
+ input_applied_sequence,
290
+ joined_host_submission_id,
291
+ suspended_reason_json,
292
+ suspended_at,
293
+ unknown_reason,
294
+ unknown_tool_call_ids_json,
295
+ parent_submission_id,
296
+ parent_tool_call_id,
297
+ admission_group,
298
+ admission_fence_json,
299
+ worker_admission_json,
300
+ message_admission_json
301
+ `;
302
+
303
+ const CHILD_RESERVATION_COLUMNS = `
304
+ reservation_id,
305
+ parent_submission_id,
306
+ parent_tool_call_id,
307
+ child_submission_id,
308
+ status,
309
+ allocation_json,
310
+ allocation_digest,
311
+ accounting_json,
312
+ reserved_at,
313
+ release_began_at,
314
+ released_at
315
+ `;
316
+
317
+ /** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
318
+ const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
319
+ const ToolCallIdList = Schema.Array(ToolCallIdSchema);
320
+
321
+ const encodePersistedJsonText = Schema.encodeEffect(Schema.fromJsonString(PersistedJson));
322
+ const encodeDefinitionDigestsText = Schema.encodeEffect(Schema.fromJsonString(DefinitionDigests));
323
+ const encodeRecordEnvelopeText = Schema.encodeEffect(Schema.fromJsonString(RecordEnvelope));
324
+ const decodeRecordEnvelopeText = Schema.decodeEffect(Schema.fromJsonString(RecordEnvelope));
325
+ const encodeSuspensionReasonText = Schema.encodeEffect(Schema.fromJsonString(SuspensionReason));
326
+ const encodeUnknownResolutionText = Schema.encodeEffect(Schema.fromJsonString(UnknownResolution));
327
+ const encodeToolCallIdsText = Schema.encodeEffect(Schema.fromJsonString(ToolCallIdList));
328
+ const decodeToolCallIdsText = Schema.decodeEffect(Schema.fromJsonString(ToolCallIdList));
329
+ const parseStoredJsonText = Schema.decodeEffect(Schema.fromJsonString(Schema.Json));
330
+ const decodeAdmissionResult = Schema.decodeUnknownEffect(AdmissionResult);
331
+ const decodeClaim = Schema.decodeUnknownEffect(Claim);
332
+ const decodeOwnershipRenewal = Schema.decodeUnknownEffect(OwnershipRenewal);
333
+ const decodeSettlement = Schema.decodeUnknownEffect(Settlement);
334
+ const decodeAbortIntent = Schema.decodeUnknownEffect(AbortIntent);
335
+ const decodeOwnershipSnapshot = Schema.decodeUnknownEffect(OwnershipSnapshot);
336
+ const decodeInputAppliedMarker = Schema.decodeUnknownEffect(InputAppliedMarker);
337
+ const decodeSubmissionSnapshotUnknown = Schema.decodeUnknownEffect(SubmissionSnapshot);
338
+ const decodeSubmissionId = Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId);
339
+ const decodeQueueSequence = Schema.decodeUnknownEffect(QueueSequence);
340
+ const decodeUtcInstant = Schema.decodeUnknownEffect(Schema.DateTimeUtcFromString);
341
+ const decodeJoiningClaim = Schema.decodeUnknownEffect(JoiningClaim);
342
+ const decodeJoinSnapshot = Schema.decodeUnknownEffect(JoinSnapshot);
343
+ const decodeSuspensionSnapshot = Schema.decodeUnknownEffect(SuspensionSnapshot);
344
+ const decodeApprovalDecisionIntent = Schema.decodeUnknownEffect(ApprovalDecisionIntent);
345
+ const decodeUnknownResolutionIntent = Schema.decodeUnknownEffect(UnknownResolutionIntent);
346
+ const decodeParentLinkage = Schema.decodeUnknownEffect(ParentLinkage);
347
+
348
+ const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
349
+ ChildBudgetReservationSnapshot,
350
+ );
351
+
352
+ const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
353
+ const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
354
+ const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
355
+ const isDoStorageError = Schema.is(DoStorageError);
356
+
357
+ /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
358
+ const internalFailure =
359
+ (operation: string) =>
360
+ (error: { readonly message: string }): LedgerError =>
361
+ LedgerError.make({ operation, message: error.message, cause: error });
362
+
363
+ /**
364
+ * Classify raw SQL failures. Within one Durable Object there is exactly one writer, so the
365
+ * Node adapter's retryable `SqliteWriteContention` classification has no analogue: every raw
366
+ * failure is a `DoLedgerError` preserved as the LedgerError's cause.
367
+ */
368
+ const sqlFailure =
369
+ (operation: string) =>
370
+ (error: SqlError): LedgerError =>
371
+ internalFailure(operation)(
372
+ DoLedgerError.make({
373
+ cause: error,
374
+ operation,
375
+ message: error.message,
376
+ }),
377
+ );
378
+
379
+ const corruptionFailure = (operation: string, table: string, rowKey: string, message: string) =>
380
+ internalFailure(operation)(DoStorageCorruptionError.make({ table, rowKey, message }));
381
+
382
+ const submissionsRows = ownedRows(
383
+ SubmissionRow,
384
+ "effect_agent_submissions",
385
+ (row) => row.submission_id,
386
+ "submission_id",
387
+ [["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",
402
+ );
403
+
404
+ const abortsRows = ownedRows(
405
+ AbortIntentRow,
406
+ "effect_agent_abort_intents",
407
+ (row) => row.submission_id,
408
+ "submission_id",
409
+ );
410
+
411
+ const approvalsRows = ownedRows(ApprovalDecisionRow, "effect_agent_approval_decisions", (row) =>
412
+ JSON.stringify([row.submission_id, row.tool_call_id]),
413
+ );
414
+
415
+ const resolutionsRows = ownedRows(UnknownResolutionRow, "effect_agent_unknown_resolutions", (row) =>
416
+ JSON.stringify([row.submission_id, row.tool_call_id]),
417
+ );
418
+
419
+ const childSettlementsRows = ownedRows(
420
+ ChildSettlementMarkerRow,
421
+ "effect_agent_child_settlements",
422
+ (row) => JSON.stringify([row.parent_submission_id, row.child_submission_id]),
423
+ );
424
+
425
+ const childReservationsRows = ownedRows(
426
+ ChildReservationRow,
427
+ "effect_agent_child_reservations",
428
+ (row) => row.reservation_id,
429
+ "reservation_id",
430
+ );
431
+
432
+ const workerStopsRows = ownedRows(
433
+ WorkerStopRow,
434
+ "effect_agent_worker_stops",
435
+ (row) => row.thread_id,
436
+ "thread_id",
437
+ );
438
+
439
+ const submissionWorkRows = ownedRows(
440
+ SubmissionWorkRow,
441
+ "effect_agent_submissions",
442
+ (row) => row.submission_id,
443
+ );
444
+
445
+ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
446
+ const progress = yield* SqlStorageProgress;
447
+
448
+ const recordProgress = (kind: SqlStorageProgressKind, operation: string) =>
449
+ progress
450
+ .committed(kind)
451
+ .pipe(
452
+ Effect.catchCause((cause) =>
453
+ Effect.failCause(Cause.map(cause, internalFailure(operation))),
454
+ ),
455
+ );
456
+
457
+ const config = yield* DoStorageConfig;
458
+ const failpoint = yield* DoStorageFailpoint;
459
+ const sql = yield* SqlClientService.SqlClient;
460
+ const crypto = yield* Crypto.Crypto;
461
+ const admissionFence = yield* SubmissionAdmissionFence;
462
+ const journal = yield* initializeDoJournal(sql, failpoint.hit, config.maxStoredValueBytes);
463
+ const lifecycle = journal.lifecycle;
464
+ const state = journal.state;
465
+ const submissionViews = submissionsRows(state, sql);
466
+ const workRows = submissionWorkRows(state, sql);
467
+
468
+ const rows = {
469
+ submissions: {
470
+ by: submissionViews.by,
471
+ byFields: submissionViews.byFields,
472
+ seed: submissionViews.seed,
473
+ // The same RETURNING rows update the full-row views and the small discovery index.
474
+ write: <E, R>(effect: Effect.Effect<ReadonlyArray<unknown>, E, R>) =>
475
+ submissionViews
476
+ .write(effect)
477
+ .pipe(Effect.tap((changed) => workRows.write(Effect.succeed(changed)))),
478
+ },
479
+ ownership: ownershipRows(state, sql),
480
+ reservations: reservationsRows(state, sql),
481
+ aborts: abortsRows(state, sql),
482
+ approvals: approvalsRows(state, sql),
483
+ resolutions: resolutionsRows(state, sql),
484
+ childSettlements: childSettlementsRows(state, sql),
485
+ childReservations: childReservationsRows(state, sql),
486
+ threads: journal.threads,
487
+ workerStops: workerStopsRows(state, sql),
488
+ };
489
+
490
+ const cached = <A>(
491
+ effect: Effect.Effect<A, SqlError | DoStorageCorruptionError>,
492
+ operation: string,
493
+ ) => effect.pipe(Effect.mapError(internalFailure(operation)));
494
+
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
+ const byKey = Effect.fnUntraced(function* (
514
+ request: Pick<SubmissionLookupByKey, "threadId" | "principal" | "idempotencyKey">,
515
+ operation: string,
516
+ ) {
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,
532
+ );
533
+
534
+ return yield* Effect.forEach(matches, (row) => requireSubmission(operation, row.submission_id));
535
+ }, state.read);
536
+
537
+ const retainLifecycle = (submission: SubmissionRow, fact: LifecyclePublicationFact) =>
538
+ lifecycle === undefined
539
+ ? Effect.void
540
+ : Effect.gen(function* () {
541
+ const ownerThreadId = yield* Schema.decodeEffect(SubmissionSnapshot.fields.threadId)(
542
+ submission.thread_id,
543
+ ).pipe(Effect.mapError(internalFailure("lifecycle owner")));
544
+
545
+ yield* lifecycle
546
+ .retain({ ownerThreadId, createdAt: yield* DateTime.now, fact })
547
+ .pipe(Effect.mapError(internalFailure("retain lifecycle publication")));
548
+ });
549
+
550
+ const retainWorkerSeal = (
551
+ threadId: string,
552
+ terminal: Extract<LifecyclePublicationFact, { readonly _tag: "WorkerInboxSealed" }>["terminal"],
553
+ ) =>
554
+ lifecycle === undefined
555
+ ? Effect.void
556
+ : Effect.gen(function* () {
557
+ const operation = "retain worker inbox seal";
558
+
559
+ const ownerThreadId = yield* Schema.decodeEffect(SubmissionSnapshot.fields.threadId)(
560
+ threadId,
561
+ ).pipe(Effect.mapError(internalFailure(operation)));
562
+
563
+ const active =
564
+ yield* sql`SELECT submission_id FROM effect_agent_submissions WHERE thread_id = ${threadId} AND state <> 'settled' ORDER BY queue_sequence`.pipe(
565
+ Effect.mapError(sqlFailure(operation)),
566
+ );
567
+
568
+ const activeSubmissionIds = yield* Effect.forEach(active, (row) =>
569
+ decodeSubmissionId(row.submission_id).pipe(Effect.mapError(internalFailure(operation))),
570
+ );
571
+
572
+ yield* lifecycle
573
+ .retain({
574
+ id: JSON.stringify([threadId, "inbox-sealed"]),
575
+ ownerThreadId,
576
+ createdAt: yield* DateTime.now,
577
+ fact: {
578
+ _tag: "WorkerInboxSealed",
579
+ threadId: ownerThreadId,
580
+ activeSubmissionIds,
581
+ terminal,
582
+ },
583
+ })
584
+ .pipe(Effect.mapError(internalFailure(operation)));
585
+ });
586
+
587
+ const hitFailpoint = (
588
+ location: DoStorageFailpointLocation,
589
+ operation: string,
590
+ ): Effect.Effect<void, LedgerError> =>
591
+ failpoint.hit(location).pipe(Effect.mapError((error) => internalFailure(operation)(error)));
592
+
593
+ /**
594
+ * Run one ledger mutation under the journal's Durable Object storage-backed transaction so
595
+ * ownership-token and epoch checks are atomic with their writes (DUR-006). Transaction
596
+ * failures surface as LedgerError carrying the typed `DoStorageError` as cause.
597
+ */
598
+ const inWriteTransaction = <
599
+ A,
600
+ E extends
601
+ | AdmissionConflict
602
+ | AdmissionPolicyError
603
+ | ApprovalConflict
604
+ | ChildReservationConflict
605
+ | JoinedToHost
606
+ | OwnershipLost
607
+ | SettlementConflict
608
+ | UnknownResolutionConflict
609
+ | LedgerError,
610
+ >(
611
+ operation: string,
612
+ effect: Effect.Effect<A, E>,
613
+ expected?: (error: { readonly _tag: string }) => boolean,
614
+ ): Effect.Effect<A, E | LedgerError> =>
615
+ journal
616
+ .withWriteTransaction(
617
+ operation,
618
+ expected,
619
+ )(effect)
620
+ .pipe(
621
+ Effect.mapError((error) =>
622
+ isDoStorageError(error) ? internalFailure(operation)(error) : error,
623
+ ),
624
+ );
625
+
626
+ const mintUuid = (operation: string): Effect.Effect<string, LedgerError> =>
627
+ crypto.randomUUIDv7.pipe(Effect.mapError((error) => internalFailure(operation)(error)));
628
+
629
+ const currentInstant = Effect.map(Clock.currentTimeMillis, (millis) => ({
630
+ millis,
631
+ iso: new Date(millis).toISOString(),
632
+ }));
633
+
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
+ const decodeSubmissionRows = (operation: string, rowKey: string, rows: unknown) =>
643
+ decodeRows(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(
644
+ Effect.mapError(internalFailure(operation)),
645
+ );
646
+
647
+ const readSubmission = Effect.fn("DoSubmissionLedger.readSubmission")(function* (
648
+ operation: string,
649
+ submissionId: string,
650
+ ): Effect.fn.Return<Option.Option<SubmissionRow>, LedgerError> {
651
+ const found = yield* cached(rows.submissions.by("submission_id", submissionId), operation);
652
+
653
+ return Option.fromUndefinedOr(found[0]);
654
+ });
655
+
656
+ const requireSubmission = Effect.fn("DoSubmissionLedger.requireSubmission")(function* (
657
+ operation: string,
658
+ submissionId: string,
659
+ ): Effect.fn.Return<SubmissionRow, LedgerError> {
660
+ const submission = yield* readSubmission(operation, submissionId);
661
+
662
+ if (Option.isNone(submission)) {
663
+ return yield* LedgerError.make({
664
+ operation,
665
+ message: `Unknown submission ${submissionId}.`,
666
+ });
667
+ }
668
+
669
+ return submission.value;
670
+ });
671
+
672
+ const readOwnership = Effect.fn("DoSubmissionLedger.readOwnership")(function* (
673
+ 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);
677
+
678
+ return Option.fromUndefinedOr(found[0]);
679
+ });
680
+
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)));
688
+
689
+ return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
690
+ });
691
+
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);
705
+
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)));
714
+
715
+ return yield* OwnershipLost.make({ submissionId, actualEpoch });
716
+ }
717
+
718
+ return ownership.value;
719
+ },
720
+ withStorageSpan(
721
+ "DoSubmissionLedger.requireOwnership",
722
+ (error) => error._tag === "OwnershipLost",
723
+ ),
724
+ );
725
+
726
+ const submissions = new WeakMap<SubmissionRow, SubmissionSnapshot>();
727
+
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
+ );
751
+
752
+ const agentDigests = yield* parseStoredJsonText(row.agent_digests_json).pipe(
753
+ Effect.mapError(decodeFailure),
754
+ );
755
+
756
+ const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
757
+ Effect.mapError(decodeFailure),
758
+ );
759
+
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
+ }
768
+
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));
816
+
817
+ submissions.set(row, snapshot);
818
+
819
+ return snapshot;
820
+ },
821
+ );
822
+
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);
828
+
829
+ return Option.fromUndefinedOr(found[0]);
830
+ });
831
+
832
+ const readAbortIntent = Effect.fn("DoSubmissionLedger.readAbortIntent")(function* (
833
+ operation: string,
834
+ submissionId: string,
835
+ ): Effect.fn.Return<Option.Option<AbortIntentRow>, LedgerError> {
836
+ const found = yield* cached(rows.aborts.by("submission_id", submissionId), operation);
837
+
838
+ return Option.fromUndefinedOr(found[0]);
839
+ });
840
+
841
+ /**
842
+ * Read the durable cross-store child-settlement markers recorded against one parent
843
+ * Submission (the DC realization of the port's "cross-store adapters record a durable
844
+ * notification marker" contract).
845
+ */
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
+ );
855
+
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
+ );
865
+
866
+ /**
867
+ * Whether one listed child is provably settled from THIS store: either its own row lives
868
+ * here and is settled (single-store evidence, identical to the Node adapter), or a durable
869
+ * cross-store notification marker was recorded for it (the child's row lives in another
870
+ * Durable Object and its owner reported the settlement through `recordChildSettled`).
871
+ */
872
+ const childProvablySettled = Effect.fn("DoSubmissionLedger.childProvablySettled")(function* (
873
+ operation: string,
874
+ markerChildren: ReadonlySet<string>,
875
+ childSubmissionId: string,
876
+ ): Effect.fn.Return<boolean, LedgerError> {
877
+ if (markerChildren.has(childSubmissionId)) return true;
878
+ const childRow = yield* readSubmission(operation, childSubmissionId);
879
+
880
+ return Option.isSome(childRow) && childRow.value.state === "settled";
881
+ });
882
+
883
+ const decodeChildReservationRows = (operation: string, rowKey: string, rows: unknown) =>
884
+ decodeRows(
885
+ Schema.Array(ChildReservationRow),
886
+ "effect_agent_child_reservations",
887
+ rowKey,
888
+ rows,
889
+ ).pipe(Effect.mapError(internalFailure(operation)));
890
+
891
+ const readChildReservation = Effect.fn("DoSubmissionLedger.readChildReservation")(function* (
892
+ operation: string,
893
+ reservationId: string,
894
+ ): Effect.fn.Return<Option.Option<ChildReservationRow>, LedgerError> {
895
+ const rows = yield* sql<Record<string, unknown>>`
896
+ SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
897
+ FROM effect_agent_child_reservations
898
+ WHERE reservation_id = ${reservationId}
899
+ `.pipe(Effect.mapError(sqlFailure(operation)));
900
+
901
+ const decoded = yield* decodeChildReservationRows(operation, reservationId, rows);
902
+
903
+ if (decoded.length > 1) {
904
+ return yield* corruptionFailure(
905
+ operation,
906
+ "effect_agent_child_reservations",
907
+ reservationId,
908
+ "A child reservation primary key returned more than one row.",
909
+ );
910
+ }
911
+
912
+ return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
913
+ });
914
+
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>>`
922
+ SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
923
+ FROM effect_agent_child_reservations
924
+ WHERE parent_submission_id = ${parentSubmissionId}
925
+ AND parent_tool_call_id = ${parentToolCallId}
926
+ `.pipe(Effect.mapError(sqlFailure(operation)));
927
+
928
+ const decoded = yield* decodeChildReservationRows(
929
+ operation,
930
+ `${parentSubmissionId}/${parentToolCallId}`,
931
+ rows,
932
+ );
933
+
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
+ );
946
+
947
+ const childReservationSnapshotFromRow = Effect.fn(
948
+ "DoSubmissionLedger.childReservationSnapshotFromRow",
949
+ )(function* (
950
+ operation: string,
951
+ row: ChildReservationRow,
952
+ ): Effect.fn.Return<ChildBudgetReservationSnapshot, LedgerError> {
953
+ const rowFailure = (error: { readonly message: string }) =>
954
+ corruptionFailure(
955
+ operation,
956
+ "effect_agent_child_reservations",
957
+ row.reservation_id,
958
+ error.message,
959
+ );
960
+
961
+ const allocation = yield* parseStoredJsonText(row.allocation_json).pipe(
962
+ Effect.mapError(rowFailure),
963
+ );
964
+
965
+ const accounting =
966
+ row.accounting_json === null
967
+ ? undefined
968
+ : yield* parseStoredJsonText(row.accounting_json).pipe(Effect.mapError(rowFailure));
969
+
970
+ return yield* decodeChildReservationSnapshotUnknown({
971
+ reservationId: row.reservation_id,
972
+ parentSubmissionId: row.parent_submission_id,
973
+ parentToolCallId: row.parent_tool_call_id,
974
+ status: row.status,
975
+ allocation,
976
+ allocationDigest: row.allocation_digest,
977
+ reservedAt: row.reserved_at,
978
+ ...(row.child_submission_id === null ? {} : { childSubmissionId: row.child_submission_id }),
979
+ ...(accounting === undefined ? {} : { accounting }),
980
+ ...(row.release_began_at === null ? {} : { releaseBeganAt: row.release_began_at }),
981
+ ...(row.released_at === null ? {} : { releasedAt: row.released_at }),
982
+ }).pipe(Effect.mapError(rowFailure));
983
+ });
984
+
985
+ const readApprovalDecisions = Effect.fn("DoSubmissionLedger.readApprovalDecisions")(function* (
986
+ operation: string,
987
+ submissionId: string,
988
+ ): Effect.fn.Return<ReadonlyArray<ApprovalDecisionRow>, LedgerError> {
989
+ const found = yield* cached(rows.approvals.by("submission_id", submissionId), operation);
990
+
991
+ return found.toSorted((a, b) =>
992
+ a.tool_call_id < b.tool_call_id ? -1 : a.tool_call_id > b.tool_call_id ? 1 : 0,
993
+ );
994
+ });
995
+
996
+ const approvalIntentFromRow = Effect.fn("DoSubmissionLedger.approvalIntentFromRow")(function* (
997
+ operation: string,
998
+ 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(
1008
+ Effect.mapError((error) =>
1009
+ corruptionFailure(
1010
+ operation,
1011
+ "effect_agent_approval_decisions",
1012
+ `${row.submission_id}/${row.tool_call_id}`,
1013
+ error.message,
1014
+ ),
1015
+ ),
1016
+ );
1017
+ });
1018
+
1019
+ const readUnknownResolutions = Effect.fn("DoSubmissionLedger.readUnknownResolutions")(function* (
1020
+ operation: string,
1021
+ submissionId: string,
1022
+ ): Effect.fn.Return<ReadonlyArray<UnknownResolutionRow>, LedgerError> {
1023
+ const found = yield* cached(rows.resolutions.by("submission_id", submissionId), operation);
1024
+
1025
+ return found.toSorted((a, b) =>
1026
+ a.tool_call_id < b.tool_call_id ? -1 : a.tool_call_id > b.tool_call_id ? 1 : 0,
1027
+ );
1028
+ });
1029
+
1030
+ const unknownResolutionIntentFromRow = Effect.fn(
1031
+ "DoSubmissionLedger.unknownResolutionIntentFromRow",
1032
+ )(function* (
1033
+ operation: string,
1034
+ row: UnknownResolutionRow,
1035
+ ): Effect.fn.Return<UnknownResolutionIntent, LedgerError> {
1036
+ const resolution = yield* parseStoredJsonText(row.resolution_json).pipe(
1037
+ Effect.mapError((error) =>
1038
+ corruptionFailure(
1039
+ operation,
1040
+ "effect_agent_unknown_resolutions",
1041
+ `${row.submission_id}/${row.tool_call_id}`,
1042
+ error.message,
1043
+ ),
1044
+ ),
1045
+ );
1046
+
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(
1055
+ Effect.mapError((error) =>
1056
+ corruptionFailure(
1057
+ operation,
1058
+ "effect_agent_unknown_resolutions",
1059
+ `${row.submission_id}/${row.tool_call_id}`,
1060
+ error.message,
1061
+ ),
1062
+ ),
1063
+ );
1064
+ });
1065
+
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
+ /**
1088
+ * Canonical history is the abort authority (DUR-015): the intent's canonicalRecordId is
1089
+ * derived from the shared canonical-records table using the deterministic abort record
1090
+ * identity, never from a cached ledger marker.
1091
+ */
1092
+ const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (
1093
+ operation: string,
1094
+ threadId: string,
1095
+ submissionId: SubmissionId,
1096
+ ): Effect.fn.Return<string | undefined, LedgerError> {
1097
+ const recordId = submissionAbortRecordId(submissionId);
1098
+
1099
+ const present = yield* journal
1100
+ .hasRecord(threadId, recordId)
1101
+ .pipe(Effect.mapError(internalFailure(operation)));
1102
+
1103
+ return present ? recordId : undefined;
1104
+ });
1105
+
1106
+ const abortIntentFromRow = Effect.fn("DoSubmissionLedger.abortIntentFromRow")(function* (
1107
+ operation: string,
1108
+ submission: SubmissionRow,
1109
+ submissionId: SubmissionId,
1110
+ row: AbortIntentRow,
1111
+ ): Effect.fn.Return<AbortIntent, LedgerError> {
1112
+ const canonicalRecordId = yield* canonicalAbortRecordId(
1113
+ operation,
1114
+ submission.thread_id,
1115
+ submissionId,
1116
+ );
1117
+
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(
1125
+ Effect.mapError((error) =>
1126
+ corruptionFailure(
1127
+ operation,
1128
+ "effect_agent_abort_intents",
1129
+ row.submission_id,
1130
+ error.message,
1131
+ ),
1132
+ ),
1133
+ );
1134
+ });
1135
+
1136
+ // Durable Object storage is the single serialized owner: writes confirm through output
1137
+ // gates before any response is observable, which is exactly the single-owner crash
1138
+ // durability this adapter claims — under its own honest label (P7 WP0).
1139
+ const capabilities = Effect.succeed(
1140
+ LedgerCapabilities.make({ durability: "durable-cloudflare" }),
1141
+ );
1142
+
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
+ );
1154
+
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)));
1160
+
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)));
1167
+
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
+ }
1177
+
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)));
1184
+
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
+ }
1194
+
1195
+ const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
1196
+ Effect.mapError(internalFailure(operation)),
1197
+ );
1198
+
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}`;
1204
+
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)}`;
1214
+
1215
+ yield* hitFailpoint("ledger:admit:before", operation);
1216
+
1217
+ const result = yield* inWriteTransaction(
1218
+ operation,
1219
+ Effect.gen(function* () {
1220
+ const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
1221
+
1222
+ const existingRows = yield* byKey(validated, operation);
1223
+
1224
+ const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
1225
+
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
+ }
1253
+
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)));
1260
+
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)));
1267
+
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)));
1274
+
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
+ )
1289
+ )
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
+ });
1297
+
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
+ }
1306
+
1307
+ const stopped = yield* cached(
1308
+ rows.workerStops.by("thread_id", validated.threadId),
1309
+ operation,
1310
+ );
1311
+
1312
+ if (stopped.length > 0)
1313
+ return yield* AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" });
1314
+
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);
1318
+
1319
+ const firstRows =
1320
+ lane === undefined
1321
+ ? yield* sql<Record<string, unknown>>`
1322
+ SELECT worker_admission_json FROM effect_agent_submissions
1323
+ 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)));
1336
+
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)));
1344
+
1345
+ if (
1346
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
1347
+ previous?.origin,
1348
+ validated.workerAdmission?.origin,
1349
+ )
1350
+ )
1351
+ return yield* AdmissionPolicyError.make({
1352
+ reason: "refused",
1353
+ code: "worker-origin-conflict",
1354
+ });
1355
+ }
1356
+
1357
+ yield* admissionFence.check(validated);
1358
+ if (validated.admissionGroup !== undefined) {
1359
+ const occupied = yield* sql<Record<string, unknown>>`
1360
+ SELECT submission_id FROM effect_agent_submissions
1361
+ WHERE thread_id=${validated.threadId} AND admission_group=${validated.admissionGroup} AND state<>'settled' LIMIT 1
1362
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1363
+
1364
+ if (occupied.length > 0)
1365
+ return yield* AdmissionPolicyError.make({
1366
+ reason: "occupied",
1367
+ code: "admission-group",
1368
+ });
1369
+ }
1370
+
1371
+ const maxRows =
1372
+ lane === undefined
1373
+ ? yield* sql<Record<string, unknown>>`
1374
+ SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
1375
+ FROM effect_agent_submissions
1376
+ WHERE thread_id = ${validated.threadId}
1377
+ `.pipe(Effect.mapError(sqlFailure(operation)))
1378
+ : [{ max_queue_sequence: lane.at(-1)?.queue_sequence ?? 0 }];
1379
+
1380
+ const decodedMax = yield* decodeRows(
1381
+ Schema.Array(MaxQueueSequenceRow),
1382
+ "effect_agent_submissions",
1383
+ validated.threadId,
1384
+ maxRows,
1385
+ ).pipe(Effect.mapError(internalFailure(operation)));
1386
+
1387
+ const queueSequence = yield* decodeQueueSequence(
1388
+ (decodedMax[0]?.max_queue_sequence ?? 0) + 1,
1389
+ ).pipe(Effect.mapError(internalFailure(operation)));
1390
+
1391
+ const now = yield* currentInstant;
1392
+
1393
+ yield* sql`
1394
+ INSERT INTO effect_agent_submissions (
1395
+ submission_id,
1396
+ thread_id,
1397
+ queue_sequence,
1398
+ principal,
1399
+ idempotency_key,
1400
+ agent_id,
1401
+ agent_digests_json,
1402
+ deployment_id,
1403
+ input_json,
1404
+ input_digest,
1405
+ receipt_id,
1406
+ state,
1407
+ created_at,
1408
+ parent_submission_id,
1409
+ parent_tool_call_id,
1410
+ admission_group,
1411
+ admission_fence_json,
1412
+ worker_admission_json,
1413
+ message_admission_json
1414
+ ) VALUES (
1415
+ ${mintedSubmissionId},
1416
+ ${validated.threadId},
1417
+ ${queueSequence},
1418
+ ${validated.principal},
1419
+ ${validated.idempotencyKey},
1420
+ ${validated.agentId},
1421
+ ${agentDigestsJson},
1422
+ ${validated.deploymentId},
1423
+ ${inputJson},
1424
+ ${validated.inputDigest},
1425
+ ${mintedReceiptId},
1426
+ 'admitted',
1427
+ ${now.iso},
1428
+ ${validated.parentLinkage?.parentSubmissionId ?? null},
1429
+ ${validated.parentLinkage?.parentToolCallId ?? null},
1430
+ ${validated.admissionGroup ?? null},
1431
+ ${validated.admissionFence === undefined ? null : JSON.stringify(validated.admissionFence)},
1432
+ ${workerAdmissionJson},
1433
+ ${messageAdmissionJson}
1434
+ )
1435
+ RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
1436
+
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, []);
1447
+
1448
+ yield* recordProgress("submission", operation);
1449
+
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
+ );
1459
+
1460
+ yield* hitFailpoint("ledger:admit:after", operation);
1461
+
1462
+ return result;
1463
+ },
1464
+ );
1465
+
1466
+ const markReady: SubmissionLedger["Service"]["markReady"] = Effect.fn(
1467
+ "DoSubmissionLedger.markReady",
1468
+ )(function* (request: MarkReadyRequest) {
1469
+ const operation = "ledger mark ready";
1470
+
1471
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkReadyRequest))(request).pipe(
1472
+ Effect.mapError(internalFailure(operation)),
1473
+ );
1474
+
1475
+ yield* hitFailpoint("ledger:mark-ready:before", operation);
1476
+ yield* inWriteTransaction(
1477
+ operation,
1478
+ Effect.gen(function* () {
1479
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1480
+
1481
+ if (submission.state !== "admitted") return;
1482
+ const now = yield* currentInstant;
1483
+
1484
+ yield* sql`
1485
+ UPDATE effect_agent_submissions
1486
+ SET state = 'ready', ready_at = ${now.iso}
1487
+ WHERE submission_id = ${validated.submissionId}
1488
+ RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
1489
+ yield* recordProgress("submission", operation);
1490
+ yield* retainLifecycle(submission, {
1491
+ _tag: "SubmissionReady",
1492
+ submissionId: validated.submissionId,
1493
+ });
1494
+ }),
1495
+ );
1496
+ yield* hitFailpoint("ledger:mark-ready:after", operation);
1497
+ });
1498
+
1499
+ const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn("DoSubmissionLedger.lookup")(
1500
+ function* (request: SubmissionLookup) {
1501
+ const operation = "ledger lookup";
1502
+
1503
+ const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookup))(request).pipe(
1504
+ Effect.mapError(internalFailure(operation)),
1505
+ );
1506
+
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
+ }
1514
+
1515
+ const foundRows = yield* byKey(validated, operation);
1516
+
1517
+ const decoded = yield* decodeSubmissionRows(
1518
+ operation,
1519
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1520
+ foundRows,
1521
+ );
1522
+
1523
+ if (decoded.length > 1) {
1524
+ return yield* corruptionFailure(
1525
+ operation,
1526
+ "effect_agent_submissions",
1527
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1528
+ "An admission idempotency key returned more than one row.",
1529
+ );
1530
+ }
1531
+ if (decoded.length === 0) return Option.none();
1532
+
1533
+ return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
1534
+ },
1535
+ );
1536
+
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";
1545
+
1546
+ const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookupByKey))(
1547
+ request,
1548
+ ).pipe(Effect.mapError(internalFailure(operation)));
1549
+
1550
+ const foundRows = yield* byKey(validated, operation);
1551
+
1552
+ const decoded = yield* decodeSubmissionRows(
1553
+ operation,
1554
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1555
+ foundRows,
1556
+ );
1557
+
1558
+ if (decoded.length > 1) {
1559
+ return yield* corruptionFailure(
1560
+ operation,
1561
+ "effect_agent_submissions",
1562
+ `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1563
+ "An admission idempotency key returned more than one row.",
1564
+ );
1565
+ }
1566
+ if (decoded.length === 0) return AdmissionNotAdmitted.make();
1567
+
1568
+ return AdmissionAdmitted.make({
1569
+ submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
1570
+ });
1571
+ });
1572
+
1573
+ const claim: SubmissionLedger["Service"]["claim"] = Effect.fn("DoSubmissionLedger.claim")(
1574
+ function* (request: ClaimRequest) {
1575
+ const operation = "ledger claim";
1576
+
1577
+ const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
1578
+ Effect.mapError(internalFailure(operation)),
1579
+ );
1580
+
1581
+ const attemptId = `attempt-${yield* mintUuid(operation)}`;
1582
+ const ownershipToken = `owner-${yield* mintUuid(operation)}`;
1583
+
1584
+ yield* hitFailpoint("ledger:claim:before", operation);
1585
+
1586
+ const claimed = yield* inWriteTransaction(
1587
+ operation,
1588
+ Effect.gen(function* () {
1589
+ const now = yield* currentInstant;
1590
+
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)));
1622
+
1623
+ if (ownership.length > 0) {
1624
+ const expiresAt = yield* timestampMillis(
1625
+ operation,
1626
+ ownership[0].submission_id,
1627
+ )(ownership[0].lease_expires_at);
1628
+
1629
+ if (expiresAt > now.millis) return Option.none<Claim>();
1630
+ }
1631
+
1632
+ const headRows =
1633
+ lane === undefined
1634
+ ? yield* sql<Record<string, unknown>>`
1635
+ SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1636
+ FROM effect_agent_submissions
1637
+ WHERE thread_id = ${validated.threadId}
1638
+ AND state <> 'settled'
1639
+ AND (
1640
+ state <> 'unknown' OR EXISTS (
1641
+ SELECT 1 FROM effect_agent_abort_intents
1642
+ WHERE submission_id = effect_agent_submissions.submission_id
1643
+ )
1644
+ )
1645
+ ORDER BY queue_sequence ASC
1646
+ 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
+ });
1670
+
1671
+ const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
1672
+
1673
+ if (heads.length === 0) return Option.none<Claim>();
1674
+ let head = heads[0];
1675
+
1676
+ if (validated.handoff !== undefined) {
1677
+ const handoff = validated.handoff;
1678
+
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>();
1690
+ }
1691
+ if (
1692
+ head.submission_id !== handoff.submissionId ||
1693
+ (head.state !== "ready" && head.state !== "running" && head.state !== "input-applied")
1694
+ )
1695
+ return Option.none<Claim>();
1696
+ }
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
+ if (
1719
+ validated.handoff !== undefined &&
1720
+ threads[0]?.producer_epoch !== validated.handoff.producerEpoch
1721
+ )
1722
+ return Option.none<Claim>();
1723
+
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
+ }
1803
+
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
+ ),
1812
+ ),
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)));
1859
+
1860
+ return yield* decodeOwnershipRenewal({
1861
+ ownershipToken: validated.ownershipToken,
1862
+ leaseExpiresAt,
1863
+ }).pipe(Effect.mapError(internalFailure(operation)));
1864
+ }),
1865
+ );
1866
+
1867
+ yield* hitFailpoint("ledger:renew:after", operation);
1868
+
1869
+ return renewal;
1870
+ });
1871
+
1872
+ const releaseOwnership: SubmissionLedger["Service"]["releaseOwnership"] = Effect.fnUntraced(
1873
+ function* (request: ReleaseOwnershipRequest) {
1874
+ const operation = "ledger release ownership";
1875
+
1876
+ const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseOwnershipRequest))(
1877
+ request,
1878
+ ).pipe(Effect.mapError(internalFailure(operation)));
1879
+
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);
1886
+ yield* inWriteTransaction(
1887
+ operation,
1888
+ Effect.gen(function* () {
1889
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1890
+
1891
+ yield* requireOwnership(operation, submission, validated.ownershipToken);
1892
+ yield* sql`
1893
+ DELETE FROM effect_agent_submission_ownership
1894
+ 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
+ }
1903
+ }),
1904
+ (error) => error._tag === "OwnershipLost",
1905
+ );
1906
+ yield* hitFailpoint("ledger:release:after", operation);
1907
+ },
1908
+ withStorageSpan(
1909
+ "DoSubmissionLedger.releaseOwnership",
1910
+ (error) => error._tag === "OwnershipLost",
1911
+ ),
1912
+ );
1913
+
1914
+ const markInputApplied: SubmissionLedger["Service"]["markInputApplied"] = Effect.fn(
1915
+ "DoSubmissionLedger.markInputApplied",
1916
+ )(function* (request: MarkInputAppliedRequest) {
1917
+ const operation = "ledger mark input applied";
1918
+
1919
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
1920
+ request,
1921
+ ).pipe(Effect.mapError(internalFailure(operation)));
1922
+
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)));
1969
+
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);
2022
+
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.",
2030
+ );
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
+
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)));
2090
+
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
+ }),
2100
+ );
2101
+
2102
+ yield* hitFailpoint("ledger:reserve-settlement:after", operation);
2103
+
2104
+ return reserved;
2105
+ });
2106
+
2107
+ const validateFinalization = Effect.fn("DoSubmissionLedger.validateFinalization")(function* (
2108
+ validated: SettlementFinalization,
2109
+ reservation: Option.Option<ReservationRow>,
2110
+ ) {
2111
+ const operation = "ledger finalize settlement";
2112
+
2113
+ if (Option.isNone(reservation)) {
2114
+ return yield* LedgerError.make({
2115
+ operation,
2116
+ message: `No settlement reservation exists for submission ${validated.submissionId}.`,
2117
+ });
2118
+ }
2119
+ if (reservation.value.settlement_id !== validated.settlementId) {
2120
+ return yield* SettlementConflict.make({
2121
+ submissionId: validated.submissionId,
2122
+ existingOutcome: reservation.value.outcome,
2123
+ });
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
+
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 };
2149
+ });
2150
+
2151
+ const replayFinalization = Effect.fn("DoSubmissionLedger.replayFinalization")(function* (
2152
+ validated: SettlementFinalization,
2153
+ submission: SubmissionRow,
2154
+ { reservation, settlementFailure }: Effect.Success<ReturnType<typeof validateFinalization>>,
2155
+ ) {
2156
+ const operation = "ledger finalize settlement";
2157
+
2158
+ if (reservation.finalized_at === null) {
2159
+ return yield* corruptionFailure(
2160
+ operation,
2161
+ "effect_agent_settlement_reservations",
2162
+ validated.submissionId,
2163
+ "A settled Submission's reservation carries no finalization timestamp.",
2164
+ );
2165
+ }
2166
+
2167
+ return yield* decodeSettlement({
2168
+ submissionId: validated.submissionId,
2169
+ settlementId: validated.settlementId,
2170
+ receiptId: submission.receipt_id,
2171
+ outcome: reservation.outcome,
2172
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
2173
+ settledAt: reservation.finalized_at,
2174
+ }).pipe(Effect.mapError(internalFailure(operation)));
2175
+ });
2176
+
2177
+ const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
2178
+ "DoSubmissionLedger.finalizeSettlement",
2179
+ )(function* (request: SettlementFinalization) {
2180
+ const operation = "ledger finalize settlement";
2181
+
2182
+ const validated = yield* Schema.decodeEffect(Schema.toType(SettlementFinalization))(
2183
+ request,
2184
+ ).pipe(Effect.mapError(internalFailure(operation)));
2185
+
2186
+ yield* hitFailpoint("ledger:finalize-settlement:before", operation);
2187
+
2188
+ // Read the immutable receipt under the same gate as every local write.
2189
+ const replay = yield* state.read(
2190
+ Effect.gen(function* () {
2191
+ const submission = yield* readSubmission(operation, validated.submissionId);
2192
+
2193
+ if (Option.isNone(submission) || submission.value.state !== "settled") return Option.none();
2194
+
2195
+ const finalization = yield* validateFinalization(
2196
+ validated,
2197
+ yield* readReservation(operation, validated.submissionId),
2198
+ );
2199
+
2200
+ return Option.some(yield* replayFinalization(validated, submission.value, finalization));
2201
+ }),
2202
+ );
2203
+
2204
+ if (Option.isSome(replay)) {
2205
+ yield* hitFailpoint("ledger:finalize-settlement:after", operation);
2206
+
2207
+ return replay.value;
2208
+ }
2209
+
2210
+ const settlement = yield* inWriteTransaction(
2211
+ operation,
2212
+ 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
+ const submission = yield* requireSubmission(operation, validated.submissionId);
2220
+
2221
+ if (submission.state === "settled")
2222
+ return yield* replayFinalization(validated, submission, state);
2223
+ const now = yield* currentInstant;
2224
+
2225
+ const terminal =
2226
+ submission.worker_admission_json === null
2227
+ ? undefined
2228
+ : workerTerminalFromRecord(
2229
+ yield* decodeSubmissionSnapshot(operation, submission),
2230
+ reservationRecord,
2231
+ );
2232
+
2233
+ let sealedTerminal: typeof terminal;
2234
+
2235
+ if (terminal !== undefined) {
2236
+ // Admission and finalization serialize here. An accepted correction that this Run
2237
+ // has not applied vetoes its completion, including admission after RunCompleted.
2238
+ const pending =
2239
+ terminal === "completed"
2240
+ ? yield* sql`SELECT submission_id FROM effect_agent_submissions
2241
+ WHERE thread_id = ${submission.thread_id} AND queue_sequence > ${submission.queue_sequence}
2242
+ AND queue_sequence = (SELECT MAX(queue_sequence) FROM effect_agent_submissions WHERE thread_id = ${submission.thread_id})
2243
+ AND (joined_host_submission_id IS NULL OR joined_host_submission_id <> ${submission.submission_id}
2244
+ OR input_applied_record_id IS NULL) LIMIT 1`.pipe(
2245
+ Effect.mapError(sqlFailure(operation)),
2246
+ )
2247
+ : [];
2248
+
2249
+ if (pending.length === 0) {
2250
+ const sealed =
2251
+ yield* sql`INSERT OR IGNORE INTO effect_agent_worker_stops (thread_id, terminal)
2252
+ VALUES (${submission.thread_id}, ${terminal}) RETURNING *`.pipe(
2253
+ rows.workerStops.write,
2254
+ Effect.mapError(internalFailure(operation)),
2255
+ );
2256
+
2257
+ yield* sql`INSERT OR IGNORE INTO effect_agent_abort_intents (submission_id, author, reason, requested_at)
2258
+ SELECT submission_id, ${submission.principal}, ${`Worker assignment ${terminal}`}, ${now.iso}
2259
+ FROM effect_agent_submissions WHERE thread_id = ${submission.thread_id} AND state <> 'settled'
2260
+ AND submission_id <> ${submission.submission_id}
2261
+ AND (joined_host_submission_id IS NULL OR joined_host_submission_id <> ${submission.submission_id}
2262
+ OR input_applied_record_id IS NULL) RETURNING *`.pipe(
2263
+ rows.aborts.write,
2264
+ Effect.mapError(internalFailure(operation)),
2265
+ );
2266
+ if (sealed.length > 0) sealedTerminal = terminal;
2267
+ }
2268
+ }
2269
+
2270
+ yield* sql`
2271
+ 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}
2278
+ 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)));
2284
+
2285
+ yield* recordProgress("submission", operation);
2286
+
2287
+ if (sealedTerminal !== undefined)
2288
+ yield* retainWorkerSeal(submission.thread_id, sealedTerminal);
2289
+
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)));
2298
+ }),
2299
+ );
2300
+
2301
+ yield* hitFailpoint("ledger:finalize-settlement:after", operation);
2302
+
2303
+ return settlement;
2304
+ });
2305
+
2306
+ const inspectWorker = Effect.fn("DoSubmissionLedger.inspectWorker")(function* (
2307
+ threadId: SubmissionSnapshot["threadId"],
2308
+ ) {
2309
+ const operation = "inspect worker";
2310
+
2311
+ return yield* state
2312
+ .transaction(
2313
+ Effect.gen(function* () {
2314
+ const read = Effect.fnUntraced(function* (active: boolean) {
2315
+ const rows =
2316
+ yield* sql`SELECT ${sql.literal(SUBMISSION_COLUMNS)} FROM effect_agent_submissions
2317
+ WHERE thread_id = ${threadId} ${active ? sql`AND state <> 'settled'` : sql``}
2318
+ ORDER BY queue_sequence ${active ? sql`ASC` : sql`DESC`} LIMIT 1`.pipe(
2319
+ Effect.mapError(sqlFailure(operation)),
2320
+ );
2321
+
2322
+ const decoded = yield* decodeSubmissionRows(operation, threadId, rows);
2323
+
2324
+ return decoded[0] === undefined
2325
+ ? null
2326
+ : yield* decodeSubmissionSnapshot(operation, decoded[0]);
2327
+ });
2328
+
2329
+ const latest = yield* read(false);
2330
+ const active = yield* read(true);
2331
+
2332
+ const stops = yield* cached(rows.workerStops.by("thread_id", threadId), operation);
2333
+
2334
+ return yield* Schema.decodeEffect(Schema.toType(WorkerLedgerState))({
2335
+ latest,
2336
+ active,
2337
+ stopped: stops.length > 0,
2338
+ ...(stops[0] === undefined || stops[0].terminal === null
2339
+ ? {}
2340
+ : { terminal: stops[0].terminal }),
2341
+ }).pipe(Effect.mapError(internalFailure(operation)));
2342
+ }),
2343
+ )
2344
+ .pipe(
2345
+ Effect.provideService(SqlClientService.SqlClient, sql),
2346
+ Effect.catchTag("SqlError", (cause) => sqlFailure(operation)(cause)),
2347
+ );
2348
+ });
2349
+
2350
+ const stopWorker = Effect.fn("DoSubmissionLedger.stopWorker")(function* (
2351
+ request: WorkerStopCommand,
2352
+ ) {
2353
+ const operation = "ledger stop worker";
2354
+
2355
+ const validated = yield* Schema.decodeEffect(WorkerStopCommand)(request).pipe(
2356
+ Effect.mapError(internalFailure(operation)),
2357
+ );
2358
+
2359
+ return yield* inWriteTransaction(
2360
+ operation,
2361
+ Effect.gen(function* () {
2362
+ const now = yield* currentInstant;
2363
+
2364
+ const sealed =
2365
+ yield* sql`INSERT OR IGNORE INTO effect_agent_worker_stops (thread_id) VALUES (${validated.threadId}) RETURNING *`.pipe(
2366
+ rows.workerStops.write,
2367
+ Effect.mapError(internalFailure(operation)),
2368
+ );
2369
+
2370
+ const aborted =
2371
+ yield* sql`INSERT OR IGNORE INTO effect_agent_abort_intents (submission_id, author, reason, requested_at)
2372
+ SELECT submission_id, ${validated.author}, 'Worker owner stopped the worker', ${now.iso}
2373
+ FROM effect_agent_submissions WHERE thread_id = ${validated.threadId} AND state <> 'settled' RETURNING *`.pipe(
2374
+ rows.aborts.write,
2375
+ Effect.mapError(internalFailure(operation)),
2376
+ );
2377
+
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
+ if (sealed.length > 0 || aborted.length > 0) yield* recordProgress("control", operation);
2385
+ if (sealed.length > 0) yield* retainWorkerSeal(validated.threadId, null);
2386
+
2387
+ return owners.length;
2388
+ }),
2389
+ );
2390
+ });
2391
+
2392
+ const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fn(
2393
+ "DoSubmissionLedger.requestAbort",
2394
+ )(function* (request: AbortCommand) {
2395
+ const operation = "ledger request abort";
2396
+
2397
+ const validated = yield* Schema.decodeEffect(Schema.toType(AbortCommand))(request).pipe(
2398
+ Effect.mapError(internalFailure(operation)),
2399
+ );
2400
+
2401
+ yield* hitFailpoint("ledger:request-abort:before", operation);
2402
+
2403
+ const intent = yield* inWriteTransaction(
2404
+ operation,
2405
+ Effect.gen(function* () {
2406
+ const submission = yield* requireSubmission(operation, validated.submissionId);
2407
+
2408
+ if (submission.state === "settled") {
2409
+ if (submission.settled_outcome === null) {
2410
+ return yield* corruptionFailure(
2411
+ operation,
2412
+ "effect_agent_submissions",
2413
+ validated.submissionId,
2414
+ "A settled Submission carries no terminal outcome.",
2415
+ );
2416
+ }
2417
+
2418
+ return yield* SettlementConflict.make({
2419
+ submissionId: validated.submissionId,
2420
+ existingOutcome: submission.settled_outcome,
2421
+ });
2422
+ }
2423
+ // A joined Submission settles WITH its host; the abort target is the host (plan
2424
+ // §2.5). A joining Submission still records the intent: it is honored only if the
2425
+ // host has not consumed the input (revert-then-abort).
2426
+ if (submission.state === "joined") {
2427
+ if (submission.joined_host_submission_id === null) {
2428
+ return yield* corruptionFailure(
2429
+ operation,
2430
+ "effect_agent_submissions",
2431
+ validated.submissionId,
2432
+ "A joined Submission carries no host linkage.",
2433
+ );
2434
+ }
2435
+
2436
+ const hostSubmissionId = yield* decodeSubmissionId(
2437
+ submission.joined_host_submission_id,
2438
+ ).pipe(Effect.mapError(internalFailure(operation)));
2439
+
2440
+ return yield* JoinedToHost.make({
2441
+ submissionId: validated.submissionId,
2442
+ hostSubmissionId,
2443
+ });
2444
+ }
2445
+ const existing = yield* readAbortIntent(operation, validated.submissionId);
2446
+
2447
+ if (Option.isSome(existing)) {
2448
+ return yield* abortIntentFromRow(
2449
+ operation,
2450
+ submission,
2451
+ validated.submissionId,
2452
+ existing.value,
2453
+ );
2454
+ }
2455
+ const now = yield* currentInstant;
2456
+
2457
+ yield* sql`
2458
+ INSERT INTO effect_agent_abort_intents (
2459
+ submission_id,
2460
+ author,
2461
+ reason,
2462
+ requested_at
2463
+ ) VALUES (
2464
+ ${validated.submissionId},
2465
+ ${validated.author},
2466
+ ${validated.reason},
2467
+ ${now.iso}
2468
+ )
2469
+ RETURNING *`.pipe(rows.aborts.write, Effect.mapError(internalFailure(operation)));
2470
+
2471
+ yield* recordProgress("control", operation);
2472
+
2473
+ const canonicalRecordId = yield* canonicalAbortRecordId(
2474
+ operation,
2475
+ submission.thread_id,
2476
+ validated.submissionId,
2477
+ );
2478
+
2479
+ const intent = yield* decodeAbortIntent({
2480
+ submissionId: validated.submissionId,
2481
+ author: validated.author,
2482
+ reason: validated.reason,
2483
+ requestedAt: now.iso,
2484
+ ...(canonicalRecordId === undefined ? {} : { canonicalRecordId }),
2485
+ }).pipe(Effect.mapError(internalFailure(operation)));
2486
+
2487
+ yield* retainLifecycle(submission, { _tag: "AbortIntentRecorded", intent });
2488
+
2489
+ return intent;
2490
+ }),
2491
+ );
2492
+
2493
+ yield* hitFailpoint("ledger:request-abort:after", operation);
2494
+
2495
+ return intent;
2496
+ });
2497
+
2498
+ const claimJoining: SubmissionLedger["Service"]["claimJoining"] = Effect.fn(
2499
+ "DoSubmissionLedger.claimJoining",
2500
+ )(function* (request: ClaimJoiningRequest) {
2501
+ const operation = "ledger claim joining";
2502
+
2503
+ const validated = yield* Schema.decodeEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(
2504
+ Effect.mapError(internalFailure(operation)),
2505
+ );
2506
+
2507
+ yield* hitFailpoint("ledger:claim-joining:before", operation);
2508
+
2509
+ const claims = yield* inWriteTransaction(
2510
+ operation,
2511
+ Effect.gen(function* () {
2512
+ const host = yield* requireSubmission(operation, validated.hostSubmissionId);
2513
+
2514
+ if (host.thread_id !== validated.threadId) {
2515
+ return yield* LedgerError.make({
2516
+ operation,
2517
+ message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
2518
+ });
2519
+ }
2520
+ // The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
2521
+ yield* requireOwnership(operation, host, validated.ownershipToken);
2522
+
2523
+ const stopped = yield* cached(
2524
+ rows.workerStops.by("thread_id", validated.threadId),
2525
+ operation,
2526
+ );
2527
+
2528
+ if (stopped.length > 0) return [];
2529
+
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)}
2536
+ FROM effect_agent_submissions
2537
+ WHERE thread_id = ${validated.threadId}
2538
+ AND queue_sequence > ${host.queue_sequence}
2539
+ 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
+ );
2545
+
2546
+ const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
2547
+ const claimed: Array<JoiningClaim> = [];
2548
+
2549
+ for (const row of later) {
2550
+ if (claimed.length >= validated.maxCount) break;
2551
+ // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
2552
+ // the coordinator re-delivers already-joined input through the coverage rule.
2553
+ if (
2554
+ (row.state === "joining" || row.state === "joined") &&
2555
+ row.joined_host_submission_id === validated.hostSubmissionId
2556
+ ) {
2557
+ continue;
2558
+ }
2559
+ // P7 §7(c): an aborted-settled row is a CLOSED obligation, not a gap — recovery
2560
+ // settles aborted never-claimed queued work immediately, and settlement order of
2561
+ // never-run work is not execution order (DUR-004 bounds execution).
2562
+ if (row.state === "settled" && row.settled_outcome === "aborted") continue;
2563
+ // Any other non-ready row — an admitted-not-ready gap in particular — breaks the
2564
+ // contiguous ready prefix (plan §2.5); later ready work stays queued (DUR-004).
2565
+ if (row.state !== "ready") break;
2566
+ yield* sql`
2567
+ UPDATE effect_agent_submissions
2568
+ SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
2569
+ WHERE submission_id = ${row.submission_id}
2570
+ RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
2571
+
2572
+ const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
2573
+ Effect.mapError((error) =>
2574
+ corruptionFailure(
2575
+ operation,
2576
+ "effect_agent_submissions",
2577
+ row.submission_id,
2578
+ error.message,
2579
+ ),
2580
+ ),
2581
+ );
2582
+
2583
+ claimed.push(
2584
+ yield* decodeJoiningClaim({
2585
+ submissionId: row.submission_id,
2586
+ queueSequence: row.queue_sequence,
2587
+ inputPayload,
2588
+ }).pipe(Effect.mapError(internalFailure(operation))),
2589
+ );
2590
+ }
2591
+
2592
+ return claimed;
2593
+ }),
2594
+ );
2595
+
2596
+ yield* hitFailpoint("ledger:claim-joining:after", operation);
2597
+
2598
+ return claims;
2599
+ });
2600
+
2601
+ const markJoined: SubmissionLedger["Service"]["markJoined"] = Effect.fn(
2602
+ "DoSubmissionLedger.markJoined",
2603
+ )(function* (request: MarkJoinedRequest) {
2604
+ const operation = "ledger mark joined";
2605
+
2606
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkJoinedRequest))(request).pipe(
2607
+ Effect.mapError(internalFailure(operation)),
2608
+ );
2609
+
2610
+ yield* hitFailpoint("ledger:mark-joined:before", operation);
2611
+ yield* inWriteTransaction(
2612
+ operation,
2613
+ Effect.gen(function* () {
2614
+ const submission = yield* requireSubmission(operation, validated.submissionId);
2615
+
2616
+ if (submission.joined_host_submission_id === null) {
2617
+ return yield* LedgerError.make({
2618
+ operation,
2619
+ message: `Submission ${validated.submissionId} was never claimed for joining.`,
2620
+ });
2621
+ }
2622
+ const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
2623
+
2624
+ // The lane is host-owned: the presented token must own the HOST's ownership period,
2625
+ // which also lets a later host Attempt repair a lost marker from history (DUR-016).
2626
+ yield* requireOwnership(operation, host, validated.ownershipToken);
2627
+ if (submission.input_applied_record_id !== null) {
2628
+ if (
2629
+ submission.input_applied_record_id === validated.recordId &&
2630
+ submission.input_applied_sequence === validated.sequence
2631
+ ) {
2632
+ return;
2633
+ }
2634
+
2635
+ return yield* corruptionFailure(
2636
+ operation,
2637
+ "effect_agent_submissions",
2638
+ validated.submissionId,
2639
+ "A different join marker is already recorded for this Submission.",
2640
+ );
2641
+ }
2642
+ if (submission.state !== "joining" && submission.state !== "joined") {
2643
+ return yield* LedgerError.make({
2644
+ operation,
2645
+ message: `Cannot mark submission ${validated.submissionId} joined from state ${submission.state}.`,
2646
+ });
2647
+ }
2648
+ yield* sql`
2649
+ UPDATE effect_agent_submissions
2650
+ SET
2651
+ input_applied_record_id = ${validated.recordId},
2652
+ input_applied_sequence = ${validated.sequence},
2653
+ state = 'joined'
2654
+ WHERE submission_id = ${validated.submissionId}
2655
+ RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
2656
+ }),
2657
+ );
2658
+ yield* hitFailpoint("ledger:mark-joined:after", operation);
2659
+ });
2660
+
2661
+ const revertJoining: SubmissionLedger["Service"]["revertJoining"] = Effect.fn(
2662
+ "DoSubmissionLedger.revertJoining",
2663
+ )(function* (request: RevertJoiningRequest) {
2664
+ const operation = "ledger revert joining";
2665
+
2666
+ const validated = yield* Schema.decodeEffect(Schema.toType(RevertJoiningRequest))(request).pipe(
2667
+ Effect.mapError(internalFailure(operation)),
2668
+ );
2669
+
2670
+ yield* hitFailpoint("ledger:revert-joining:before", operation);
2671
+ yield* inWriteTransaction(
2672
+ operation,
2673
+ Effect.gen(function* () {
2674
+ const submission = yield* requireSubmission(operation, validated.submissionId);
2675
+
2676
+ // Idempotent and recovery-only: only a still-`joining` Submission reverts; an
2677
+ // already-joined (or already-reverted) Submission is a no-op (DUR-016).
2678
+ if (submission.state !== "joining") return;
2679
+ yield* sql`
2680
+ UPDATE effect_agent_submissions
2681
+ SET state = 'ready', joined_host_submission_id = NULL
2682
+ WHERE submission_id = ${validated.submissionId}
2683
+ RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
2684
+ }),
2685
+ );
2686
+ yield* hitFailpoint("ledger:revert-joining:after", operation);
2687
+ });
2688
+
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
+ );
2696
+
2697
+ const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
2698
+ Effect.mapError(internalFailure(operation)),
2699
+ );
2700
+
2701
+ yield* hitFailpoint("ledger:suspend:before", operation);
2702
+
2703
+ const outcome = yield* inWriteTransaction(
2704
+ operation,
2705
+ Effect.gen(function* () {
2706
+ const submission = yield* requireSubmission(operation, validated.submissionId);
2707
+
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
+ }
2717
+
2718
+ return yield* SettlementConflict.make({
2719
+ submissionId: validated.submissionId,
2720
+ existingOutcome: submission.settled_outcome,
2721
+ });
2722
+ }
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
+
2727
+ if (Option.isSome(reservation)) {
2728
+ return yield* SettlementConflict.make({
2729
+ submissionId: validated.submissionId,
2730
+ existingOutcome: reservation.value.outcome,
2731
+ });
2732
+ }
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;
2751
+
2752
+ for (const child of validated.reason.children) {
2753
+ const settled = yield* childProvablySettled(
2754
+ operation,
2755
+ markerChildren,
2756
+ child.childSubmissionId,
2757
+ );
2758
+
2759
+ if (!settled) {
2760
+ allSettled = false;
2761
+ break;
2762
+ }
2763
+ }
2764
+ if (allSettled) {
2765
+ return RESUME_IMMEDIATELY;
2766
+ }
2767
+ }
2768
+ const now = yield* currentInstant;
2769
+
2770
+ yield* sql`
2771
+ UPDATE effect_agent_submissions
2772
+ SET
2773
+ state = 'suspended',
2774
+ suspended_reason_json = ${reasonJson},
2775
+ suspended_at = ${now.iso}
2776
+ 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)));
2784
+
2785
+ yield* retainLifecycle(submission, {
2786
+ _tag: "SubmissionSuspended",
2787
+ submissionId: validated.submissionId,
2788
+ reason: validated.reason,
2789
+ });
2790
+
2791
+ return SUSPENDED;
2792
+ }),
2793
+ );
2794
+
2795
+ yield* hitFailpoint("ledger:suspend:after", operation);
2796
+
2797
+ return outcome;
2798
+ },
2799
+ );
2800
+
2801
+ /**
2802
+ * Once every pending call of a recorded ApprovalPending suspension has a decision intent,
2803
+ * the lane wakes: suspended → input-applied, suspension cleared (plan §2.6). A
2804
+ * WaitingForChild suspension wakes only through recordChildSettled. Runs inside the caller's
2805
+ * write transaction.
2806
+ */
2807
+ const wakeSuspendedIfCovered = Effect.fn("DoSubmissionLedger.wakeSuspendedIfCovered")(function* (
2808
+ operation: string,
2809
+ submission: SubmissionRow,
2810
+ ): Effect.fn.Return<void, LedgerError> {
2811
+ if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
2812
+
2813
+ const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
2814
+ submission.suspended_reason_json,
2815
+ ).pipe(
2816
+ Effect.mapError((error) =>
2817
+ corruptionFailure(
2818
+ operation,
2819
+ "effect_agent_submissions",
2820
+ submission.submission_id,
2821
+ error.message,
2822
+ ),
2823
+ ),
2824
+ );
2825
+
2826
+ if (reason._tag !== "ApprovalPending") return;
2827
+ const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
2828
+ const decided = new Set(decisions.map((row) => row.tool_call_id));
2829
+
2830
+ if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
2831
+ yield* sql`
2832
+ UPDATE effect_agent_submissions
2833
+ SET
2834
+ state = 'input-applied',
2835
+ suspended_reason_json = NULL,
2836
+ suspended_at = NULL
2837
+ WHERE submission_id = ${submission.submission_id}
2838
+ RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
2839
+ yield* retainLifecycle(submission, {
2840
+ _tag: "SubmissionResumed",
2841
+ submissionId: yield* decodeSubmissionId(submission.submission_id).pipe(
2842
+ Effect.mapError(internalFailure(operation)),
2843
+ ),
2844
+ });
2845
+ });
2846
+
2847
+ const recordApprovalDecision: SubmissionLedger["Service"]["recordApprovalDecision"] = Effect.fn(
2848
+ "DoSubmissionLedger.recordApprovalDecision",
2849
+ )(function* (command: ApprovalDecisionCommand) {
2850
+ const operation = "ledger record approval decision";
2851
+
2852
+ const validated = yield* Schema.decodeEffect(Schema.toType(ApprovalDecisionCommand))(
2853
+ command,
2854
+ ).pipe(Effect.mapError(internalFailure(operation)));
2855
+
2856
+ yield* hitFailpoint("ledger:approval-decision:before", operation);
2857
+
2858
+ const intent = yield* inWriteTransaction(
2859
+ operation,
2860
+ Effect.gen(function* () {
2861
+ const submission = yield* requireSubmission(operation, validated.submissionId);
2862
+
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
+ }
2872
+
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({
2886
+ submissionId: validated.submissionId,
2887
+ toolCallId: validated.toolCallId,
2888
+ existingDecision: existing.decision,
2889
+ });
2890
+ }
2891
+
2892
+ return yield* approvalIntentFromRow(operation, existing);
2893
+ }
2894
+ const now = yield* currentInstant;
2895
+
2896
+ yield* sql`
2897
+ INSERT INTO effect_agent_approval_decisions (
2898
+ submission_id,
2899
+ tool_call_id,
2900
+ decision,
2901
+ resolver,
2902
+ reason,
2903
+ decided_at
2904
+ ) VALUES (
2905
+ ${validated.submissionId},
2906
+ ${validated.toolCallId},
2907
+ ${validated.decision},
2908
+ ${validated.resolver},
2909
+ ${validated.reason},
2910
+ ${now.iso}
2911
+ )
2912
+ RETURNING *`.pipe(rows.approvals.write, Effect.mapError(internalFailure(operation)));
2913
+ yield* recordProgress("control", operation);
2914
+ yield* wakeSuspendedIfCovered(operation, submission);
2915
+
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
+ );
2926
+
2927
+ yield* hitFailpoint("ledger:approval-decision:after", operation);
2928
+
2929
+ return intent;
2930
+ });
2931
+
2932
+ const markUnknown: SubmissionLedger["Service"]["markUnknown"] = Effect.fn(
2933
+ "DoSubmissionLedger.markUnknown",
2934
+ )(function* (request: MarkUnknownRequest) {
2935
+ const operation = "ledger mark unknown";
2936
+
2937
+ const validated = yield* Schema.decodeEffect(Schema.toType(MarkUnknownRequest))(request).pipe(
2938
+ Effect.mapError(internalFailure(operation)),
2939
+ );
2940
+
2941
+ yield* hitFailpoint("ledger:mark-unknown:before", operation);
2942
+ yield* inWriteTransaction(
2943
+ operation,
2944
+ Effect.gen(function* () {
2945
+ const submission = yield* requireSubmission(operation, validated.submissionId);
2946
+
2947
+ if (submission.state === "settled") {
2948
+ if (submission.settled_outcome === null) {
2949
+ return yield* corruptionFailure(
2950
+ operation,
2951
+ "effect_agent_submissions",
2952
+ validated.submissionId,
2953
+ "A settled Submission carries no terminal outcome.",
2954
+ );
2955
+ }
2956
+
2957
+ return yield* SettlementConflict.make({
2958
+ submissionId: validated.submissionId,
2959
+ existingOutcome: submission.settled_outcome,
2960
+ });
2961
+ }
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);
2965
+
2966
+ if (Option.isSome(reservation)) {
2967
+ return yield* SettlementConflict.make({
2968
+ submissionId: validated.submissionId,
2969
+ existingOutcome: reservation.value.outcome,
2970
+ });
2971
+ }
2972
+ // Idempotent merge: repeating is a no-op; additional open calls extend the marked
2973
+ // set while the first recorded reason is kept.
2974
+ const existingIds = yield* storedUnknownToolCallIds(operation, submission);
2975
+ const known = new Set(existingIds);
2976
+
2977
+ const merged = [
2978
+ ...existingIds,
2979
+ ...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
2980
+ ];
2981
+
2982
+ const idsJson = yield* encodeToolCallIdsText(merged).pipe(
2983
+ Effect.mapError(internalFailure(operation)),
2984
+ );
2985
+
2986
+ yield* sql`
2987
+ UPDATE effect_agent_submissions
2988
+ SET
2989
+ state = 'unknown',
2990
+ unknown_reason = ${submission.unknown_reason ?? validated.reason},
2991
+ unknown_tool_call_ids_json = ${idsJson}
2992
+ WHERE submission_id = ${validated.submissionId}
2993
+ RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
2994
+ if (submission.state !== "unknown")
2995
+ yield* retainLifecycle(submission, {
2996
+ _tag: "SubmissionUnknown",
2997
+ submissionId: validated.submissionId,
2998
+ });
2999
+ }),
3000
+ );
3001
+ yield* hitFailpoint("ledger:mark-unknown:after", operation);
3002
+ });
3003
+
3004
+ const recordUnknownResolution: SubmissionLedger["Service"]["recordUnknownResolution"] = Effect.fn(
3005
+ "DoSubmissionLedger.recordUnknownResolution",
3006
+ )(function* (command: UnknownResolutionCommand) {
3007
+ const operation = "ledger record unknown resolution";
3008
+
3009
+ const validated = yield* Schema.decodeEffect(Schema.toType(UnknownResolutionCommand))(
3010
+ command,
3011
+ ).pipe(Effect.mapError(internalFailure(operation)));
3012
+
3013
+ const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
3014
+ Effect.mapError(internalFailure(operation)),
3015
+ );
3016
+
3017
+ yield* hitFailpoint("ledger:unknown-resolution:before", operation);
3018
+
3019
+ const intent = yield* inWriteTransaction(
3020
+ operation,
3021
+ Effect.gen(function* () {
3022
+ const submission = yield* requireSubmission(operation, validated.submissionId);
3023
+
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
+ );
3032
+ }
3033
+
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);
3041
+
3042
+ const existingIntent =
3043
+ existing === undefined
3044
+ ? undefined
3045
+ : yield* unknownResolutionIntentFromRow(operation, existing);
3046
+
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;
3057
+
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;
3064
+
3065
+ yield* sql`
3066
+ INSERT INTO effect_agent_unknown_resolutions (
3067
+ submission_id,
3068
+ tool_call_id,
3069
+ author,
3070
+ reason,
3071
+ resolution_json,
3072
+ resolved_at
3073
+ ) VALUES (
3074
+ ${validated.submissionId},
3075
+ ${validated.toolCallId},
3076
+ ${validated.author},
3077
+ ${validated.reason},
3078
+ ${resolutionJson},
3079
+ ${now.iso}
3080
+ )
3081
+ RETURNING *`.pipe(rows.resolutions.write, Effect.mapError(internalFailure(operation)));
3082
+ yield* recordProgress("control", operation);
3083
+
3084
+ const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
3085
+ Effect.mapError(internalFailure(operation)),
3086
+ );
3087
+
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`
3107
+ UPDATE effect_agent_submissions
3108
+ SET
3109
+ state = 'input-applied',
3110
+ unknown_reason = NULL,
3111
+ unknown_tool_call_ids_json = NULL
3112
+ 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
+ });
3118
+ }
3119
+ }
3120
+
3121
+ return resolved;
3122
+ }),
3123
+ );
3124
+
3125
+ yield* hitFailpoint("ledger:unknown-resolution:after", operation);
3126
+
3127
+ return intent;
3128
+ });
3129
+
3130
+ const recordChildSettled: SubmissionLedger["Service"]["recordChildSettled"] = Effect.fn(
3131
+ "DoSubmissionLedger.recordChildSettled",
3132
+ )(function* (request: ChildSettledNotification) {
3133
+ const operation = "ledger record child settled";
3134
+
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);
3140
+
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);
3153
+
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;
3169
+
3170
+ const added = yield* sql`
3171
+ INSERT INTO effect_agent_child_settlements (
3172
+ parent_submission_id,
3173
+ child_submission_id,
3174
+ child_outcome,
3175
+ recorded_at
3176
+ ) VALUES (
3177
+ ${validated.parentSubmissionId},
3178
+ ${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
+ },
3186
+ ${now.iso}
3187
+ )
3188
+ ON CONFLICT (parent_submission_id, child_submission_id) DO NOTHING
3189
+ RETURNING *`.pipe(
3190
+ rows.childSettlements.write,
3191
+ Effect.mapError(internalFailure(operation)),
3192
+ );
3193
+
3194
+ if (added.length > 0) yield* recordProgress("control", operation);
3195
+
3196
+ if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
3197
+ return NOT_WAITING;
3198
+ }
3199
+
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,
3209
+ ),
3210
+ ),
3211
+ );
3212
+
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(
3229
+ operation,
3230
+ markerChildren,
3231
+ entry.childSubmissionId,
3232
+ );
3233
+
3234
+ if (!settled) {
3235
+ return STILL_WAITING;
3236
+ }
3237
+ }
3238
+ yield* sql`
3239
+ UPDATE effect_agent_submissions
3240
+ SET
3241
+ state = 'input-applied',
3242
+ suspended_reason_json = NULL,
3243
+ suspended_at = NULL
3244
+ WHERE submission_id = ${validated.parentSubmissionId}
3245
+ RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
3246
+
3247
+ yield* retainLifecycle(parent, {
3248
+ _tag: "SubmissionResumed",
3249
+ submissionId: validated.parentSubmissionId,
3250
+ });
3251
+
3252
+ return WOKEN;
3253
+ }),
3254
+ );
3255
+
3256
+ yield* hitFailpoint("ledger:child-settled:after", operation);
3257
+
3258
+ return outcome;
3259
+ });
3260
+
3261
+ const reserveChildBudget: SubmissionLedger["Service"]["reserveChildBudget"] = Effect.fn(
3262
+ "DoSubmissionLedger.reserveChildBudget",
3263
+ )(function* (request: ChildBudgetReservationRequest) {
3264
+ const operation = "ledger reserve child budget";
3265
+
3266
+ const validated = yield* Schema.decodeEffect(Schema.toType(ChildBudgetReservationRequest))(
3267
+ request,
3268
+ ).pipe(Effect.mapError(internalFailure(operation)));
3269
+
3270
+ const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
3271
+ Effect.mapError(internalFailure(operation)),
3272
+ );
3273
+
3274
+ yield* hitFailpoint("ledger:child-reservation:before", operation);
3275
+
3276
+ const reserved = yield* inWriteTransaction(
3277
+ operation,
3278
+ Effect.gen(function* () {
3279
+ const existing = yield* readChildReservation(operation, validated.reservationId);
3280
+
3281
+ if (Option.isSome(existing)) {
3282
+ const existingSnapshot = yield* childReservationSnapshotFromRow(
3283
+ operation,
3284
+ existing.value,
3285
+ );
3286
+
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) {
3296
+ return yield* ChildReservationConflict.make({
3297
+ 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.",
3301
+ });
3302
+ }
3303
+
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;
3329
+
3330
+ yield* sql`
3331
+ INSERT INTO effect_agent_child_reservations (
3332
+ reservation_id,
3333
+ parent_submission_id,
3334
+ parent_tool_call_id,
3335
+ status,
3336
+ allocation_json,
3337
+ allocation_digest,
3338
+ reserved_at
3339
+ ) VALUES (
3340
+ ${validated.reservationId},
3341
+ ${validated.parentSubmissionId},
3342
+ ${validated.parentToolCallId},
3343
+ 'reserved',
3344
+ ${allocationJson},
3345
+ ${validated.allocationDigest},
3346
+ ${now.iso}
3347
+ )
3348
+ 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.",
3360
+ );
3361
+ }
3362
+
3363
+ return ReservedChildBudget.make({
3364
+ reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
3365
+ replayed: false,
3366
+ });
3367
+ }),
3368
+ );
3369
+
3370
+ yield* hitFailpoint("ledger:child-reservation:after", operation);
3371
+
3372
+ return reserved;
3373
+ });
3374
+
3375
+ const attachChildToReservation: SubmissionLedger["Service"]["attachChildToReservation"] =
3376
+ Effect.fn("DoSubmissionLedger.attachChildToReservation")(function* (
3377
+ request: AttachChildToReservationRequest,
3378
+ ) {
3379
+ const operation = "ledger attach child to reservation";
3380
+
3381
+ const validated = yield* Schema.decodeEffect(Schema.toType(AttachChildToReservationRequest))(
3382
+ request,
3383
+ ).pipe(Effect.mapError(internalFailure(operation)));
3384
+
3385
+ yield* hitFailpoint("ledger:child-attach:before", operation);
3386
+
3387
+ const attached = yield* inWriteTransaction(
3388
+ operation,
3389
+ Effect.gen(function* () {
3390
+ const existing = yield* readChildReservation(operation, validated.reservationId);
3391
+
3392
+ if (Option.isNone(existing)) {
3393
+ return yield* LedgerError.make({
3394
+ operation,
3395
+ message: `Unknown child reservation ${validated.reservationId}.`,
3396
+ });
3397
+ }
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
+ });
3409
+ }
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") {
3414
+ return yield* ChildReservationConflict.make({
3415
+ reservationId: validated.reservationId,
3416
+ status: existing.value.status,
3417
+ message: `Cannot attach a child to a ${existing.value.status} reservation.`,
3418
+ });
3419
+ }
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.
3424
+ yield* sql`
3425
+ UPDATE effect_agent_child_reservations
3426
+ SET child_submission_id = ${validated.childSubmissionId}
3427
+ WHERE reservation_id = ${validated.reservationId}
3428
+ RETURNING *`.pipe(
3429
+ rows.childReservations.write,
3430
+ Effect.mapError(internalFailure(operation)),
3431
+ );
3432
+ const updated = yield* readChildReservation(operation, validated.reservationId);
3433
+
3434
+ if (Option.isNone(updated)) {
3435
+ return yield* corruptionFailure(
3436
+ operation,
3437
+ "effect_agent_child_reservations",
3438
+ validated.reservationId,
3439
+ "An updated child reservation row is missing inside its own transaction.",
3440
+ );
3441
+ }
3442
+
3443
+ return yield* childReservationSnapshotFromRow(operation, updated.value);
3444
+ }),
3445
+ );
3446
+
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);
3577
+
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
+ });
3595
+
3596
+ interface ScanCursor {
3597
+ readonly threadId: string;
3598
+ readonly queueSequence: number;
3599
+ }
3600
+
3601
+ const scanPage = Effect.fn("DoSubmissionLedger.scanPage")(function* (
3602
+ cursor: ScanCursor | undefined,
3603
+ ): Effect.fn.Return<
3604
+ readonly [ReadonlyArray<SubmissionWorkItem>, Option.Option<ScanCursor | undefined>],
3605
+ LedgerError
3606
+ > {
3607
+ const operation = "ledger scan nonterminal";
3608
+
3609
+ const rows = yield* (
3610
+ cursor === undefined
3611
+ ? sql<Record<string, unknown>>`
3612
+ SELECT submission_id AS "submissionId", thread_id AS "threadId",
3613
+ queue_sequence AS "queueSequence", principal, idempotency_key AS "idempotencyKey",
3614
+ deployment_id AS "deploymentId", receipt_id AS "receiptId", state
3615
+ FROM effect_agent_submissions
3616
+ WHERE state <> 'settled'
3617
+ ORDER BY thread_id ASC, queue_sequence ASC
3618
+ LIMIT ${SCAN_PAGE_SIZE}
3619
+ `
3620
+ : sql<Record<string, unknown>>`
3621
+ SELECT submission_id AS "submissionId", thread_id AS "threadId",
3622
+ queue_sequence AS "queueSequence", principal, idempotency_key AS "idempotencyKey",
3623
+ deployment_id AS "deploymentId", receipt_id AS "receiptId", state
3624
+ FROM effect_agent_submissions
3625
+ WHERE state <> 'settled'
3626
+ AND (thread_id, queue_sequence) > (${cursor.threadId}, ${cursor.queueSequence})
3627
+ ORDER BY thread_id ASC, queue_sequence ASC
3628
+ LIMIT ${SCAN_PAGE_SIZE}
3629
+ `
3630
+ ).pipe(Effect.mapError(sqlFailure(operation)));
3631
+
3632
+ const decoded = yield* decodeRows(
3633
+ Schema.Array(SubmissionWorkItem),
3634
+ "effect_agent_submissions",
3635
+ "nonterminal_scan",
3636
+ rows,
3637
+ "SubmissionWorkItem",
3638
+ ).pipe(Effect.mapError(internalFailure(operation)));
3639
+
3640
+ const last = decoded[decoded.length - 1];
3641
+
3642
+ const next: Option.Option<ScanCursor | undefined> =
3643
+ last === undefined || decoded.length < SCAN_PAGE_SIZE
3644
+ ? Option.none()
3645
+ : Option.some({
3646
+ threadId: last.threadId,
3647
+ queueSequence: last.queueSequence,
3648
+ });
3649
+
3650
+ return [decoded, next] as const;
3651
+ });
3652
+
3653
+ const scanNonterminal: Stream.Stream<SubmissionWorkItem, LedgerError> = Stream.unwrap(
3654
+ state.read(
3655
+ Effect.gen(function* () {
3656
+ // Cache only a complete bounded active set. Large hosts keep the paged scan.
3657
+ const active = yield* cached(
3658
+ workRows.matching(
3659
+ "nonterminal",
3660
+ (row) => row.state !== "settled",
3661
+ sql`SELECT submission_id, thread_id, queue_sequence, principal, idempotency_key,
3662
+ deployment_id, receipt_id, state
3663
+ FROM effect_agent_submissions WHERE state <> 'settled' LIMIT 129`,
3664
+ 128,
3665
+ ),
3666
+ "ledger scan nonterminal",
3667
+ );
3668
+
3669
+ if (active.length > 128)
3670
+ return Stream.paginate<ScanCursor | undefined, SubmissionWorkItem, LedgerError>(
3671
+ undefined,
3672
+ scanPage,
3673
+ );
3674
+
3675
+ const sorted = [...active].sort((left, right) =>
3676
+ left.thread_id < right.thread_id
3677
+ ? -1
3678
+ : left.thread_id > right.thread_id
3679
+ ? 1
3680
+ : left.queue_sequence - right.queue_sequence,
3681
+ );
3682
+
3683
+ const work = yield* Schema.decodeEffect(Schema.Array(SubmissionWorkItem))(
3684
+ sorted.map((row) => ({
3685
+ submissionId: row.submission_id,
3686
+ threadId: row.thread_id,
3687
+ queueSequence: row.queue_sequence,
3688
+ principal: row.principal,
3689
+ idempotencyKey: row.idempotency_key,
3690
+ deploymentId: row.deployment_id,
3691
+ receiptId: row.receipt_id,
3692
+ state: row.state,
3693
+ })),
3694
+ ).pipe(Effect.mapError(internalFailure("ledger scan nonterminal")));
3695
+
3696
+ return Stream.fromIterable(work);
3697
+ }),
3698
+ ),
3699
+ );
3700
+
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);
3714
+
3715
+ return Option.isNone(intent)
3716
+ ? undefined
3717
+ : yield* abortIntentFromRow(operation, submission, validated.submissionId, intent.value);
3718
+ }),
3719
+ );
3720
+ });
3721
+
3722
+ const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
3723
+ "DoSubmissionLedger.loadRecoverySnapshot",
3724
+ )(function* (request: RecoverySnapshotRequest) {
3725
+ const operation = "ledger load recovery snapshot";
3726
+
3727
+ const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
3728
+ request,
3729
+ ).pipe(Effect.mapError(internalFailure(operation)));
3730
+
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
+ }
3747
+
3748
+ let inputApplied: InputAppliedMarker | undefined;
3749
+
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
+ }
3759
+
3760
+ let reservation: SettlementReservationSnapshot | undefined;
3761
+ const reservationRow = yield* readReservation(operation, validated.submissionId);
3762
+
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
+ );
3774
+
3775
+ const settlementId = yield* Schema.decodeEffect(
3776
+ SettlementReservationSnapshot.fields.settlementId,
3777
+ )(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
3778
+
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
+ }
3787
+
3788
+ let abortIntent: AbortIntent | undefined;
3789
+ const abortRow = yield* readAbortIntent(operation, validated.submissionId);
3790
+
3791
+ if (Option.isSome(abortRow)) {
3792
+ abortIntent = yield* abortIntentFromRow(
3793
+ operation,
3794
+ submissionRow,
3795
+ validated.submissionId,
3796
+ abortRow.value,
3797
+ );
3798
+ }
3799
+
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"];
3824
+
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
+ }
3830
+
3831
+ let suspension: SuspensionSnapshot | undefined;
3832
+
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,
3841
+ ),
3842
+ ),
3843
+ );
3844
+
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,
3855
+ ),
3856
+ ),
3857
+ );
3858
+ }
3859
+
3860
+ const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
3861
+
3862
+ const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
3863
+ approvalIntentFromRow(operation, row),
3864
+ );
3865
+
3866
+ const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
3867
+
3868
+ const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
3869
+ unknownResolutionIntentFromRow(operation, row),
3870
+ );
3871
+
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));
3884
+
3885
+ const decodedChildReservations = yield* decodeChildReservationRows(
3886
+ operation,
3887
+ validated.submissionId,
3888
+ childReservationRows,
3889
+ );
3890
+
3891
+ const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
3892
+ childReservationSnapshotFromRow(operation, row),
3893
+ );
3894
+
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> = [];
3898
+
3899
+ for (const row of decodedChildReservations) {
3900
+ if (row.child_submission_id === null) continue;
3901
+ const child = yield* readSubmission(operation, row.child_submission_id);
3902
+
3903
+ if (Option.isSome(child)) {
3904
+ childAttachments.push(
3905
+ yield* decodeChildAttachmentSnapshot({
3906
+ toolCallId: row.parent_tool_call_id,
3907
+ childSubmissionId: row.child_submission_id,
3908
+ childState: child.value.state,
3909
+ ...(child.value.settled_outcome === null
3910
+ ? {}
3911
+ : { childOutcome: child.value.settled_outcome }),
3912
+ }).pipe(Effect.mapError(internalFailure(operation))),
3913
+ );
3914
+ continue;
3915
+ }
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
+
3929
+ let parentLinkage: ParentLinkage | undefined;
3930
+
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
+ }
3940
+
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
+ });
3959
+
3960
+ return Context.make(
3961
+ SubmissionLedger,
3962
+ SubmissionLedger.of({
3963
+ capabilities,
3964
+ admit,
3965
+ markReady,
3966
+ lookup,
3967
+ resolveAdmission,
3968
+ claim,
3969
+ renewOwnership,
3970
+ releaseOwnership,
3971
+ markInputApplied,
3972
+ reserveSettlement,
3973
+ finalizeSettlement,
3974
+ requestAbort,
3975
+ stopWorker,
3976
+ inspectWorker,
3977
+ claimJoining,
3978
+ markJoined,
3979
+ revertJoining,
3980
+ suspend,
3981
+ recordApprovalDecision,
3982
+ markUnknown,
3983
+ recordUnknownResolution,
3984
+ recordChildSettled,
3985
+ reserveChildBudget,
3986
+ attachChildToReservation,
3987
+ beginChildBudgetRelease,
3988
+ releaseChildBudget,
3989
+ scanNonterminal,
3990
+ loadRecoverySnapshot,
3991
+ readAbortIntent: readAbortIntentForSubmission,
3992
+ }),
3993
+ );
3994
+ });
3995
+
3996
+ /**
3997
+ * Durable Object SubmissionLedger implementation sharing the journal's private SQLite
3998
+ * database, storage-backed transaction discipline, and producer-epoch fencing substrate.
3999
+ * Configuration, failpoint, SQL, and Crypto authority stay visible in the input channel.
4000
+ */
4001
+ export const submissionLedgerLayer: Layer.Layer<
4002
+ SubmissionLedger,
4003
+ DoStorageInitializationError,
4004
+ DoStorageConfig | DoStorageFailpoint | SqlClientService.SqlClient | Crypto.Crypto
4005
+ > = Layer.effectContext(makeServices());
4006
+
4007
+ /**
4008
+ * 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.
4010
+ */
4011
+ export const ledgerLayer = (
4012
+ options: DoStorageOptions,
4013
+ ): Layer.Layer<SubmissionLedger, DoStorageInitializationError> =>
4014
+ Layer.unwrap(
4015
+ Effect.map(DoStorageConfig, (config) =>
4016
+ submissionLedgerLayer.pipe(
4017
+ Layer.provide(
4018
+ Layer.mergeAll(
4019
+ Layer.succeed(DoStorageConfig)(config),
4020
+ storageFailpointLayer(options),
4021
+ SqliteClient.layer({ storage: options.storage }),
4022
+ BrowserCrypto.layer,
4023
+ ),
4024
+ ),
4025
+ ),
4026
+ ),
4027
+ ).pipe(Layer.provide(storageConfigLayer(options)));