@effect-agent/storage-cloudflare 0.0.1-beta.0

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.
@@ -0,0 +1,2940 @@
1
+ import {
2
+ AbortCommand,
3
+ AbortIntent,
4
+ AdmissionAdmitted,
5
+ AdmissionConflict,
6
+ AdmissionNotAdmitted,
7
+ AdmissionRequest,
8
+ AdmissionResult,
9
+ ApprovalConflict,
10
+ ApprovalDecision,
11
+ ApprovalDecisionCommand,
12
+ ApprovalDecisionIntent,
13
+ AttachChildToReservationRequest,
14
+ BeginChildBudgetReleaseRequest,
15
+ CanonicalSequence,
16
+ ChildAttachmentSnapshot,
17
+ ChildBudgetReservationRequest,
18
+ ChildBudgetReservationSnapshot,
19
+ ChildReservationConflict,
20
+ ChildReservationStatus,
21
+ ChildSettledNotification,
22
+ Claim,
23
+ ClaimJoiningRequest,
24
+ ClaimRequest,
25
+ DefinitionDigests,
26
+ Digest,
27
+ EMPTY_TAIL_DIGEST,
28
+ InputAppliedMarker,
29
+ JoinSnapshot,
30
+ JoinedToHost,
31
+ JoiningClaim,
32
+ LedgerCapabilities,
33
+ LedgerError,
34
+ MarkInputAppliedRequest,
35
+ MarkJoinedRequest,
36
+ MarkReadyRequest,
37
+ MarkUnknownRequest,
38
+ OwnershipLost,
39
+ OwnershipRenewal,
40
+ OwnershipSnapshot,
41
+ ParentLinkage,
42
+ PersistedJson,
43
+ ProducerEpoch,
44
+ QueueSequence,
45
+ RecordEnvelope,
46
+ RecoverySnapshot,
47
+ RecoverySnapshotRequest,
48
+ ReleaseChildBudgetRequest,
49
+ ReleaseOwnershipRequest,
50
+ RenewOwnershipRequest,
51
+ ReservedChildBudget,
52
+ ReservedSettlement,
53
+ RevertJoiningRequest,
54
+ Settlement,
55
+ SettlementConflict,
56
+ SettlementFinalization,
57
+ SettlementOutcome,
58
+ SettlementReservation,
59
+ SubmissionLedger,
60
+ SubmissionLookup,
61
+ SubmissionLookupByKey,
62
+ SubmissionSnapshot,
63
+ SubmissionState,
64
+ SettlementReservationSnapshot,
65
+ SuspendRequest,
66
+ SuspensionReason,
67
+ SuspensionSnapshot,
68
+ UnknownResolution,
69
+ UnknownResolutionCommand,
70
+ UnknownResolutionConflict,
71
+ UnknownResolutionIntent,
72
+ submissionAbortRecordId,
73
+ type ChildSettledOutcome,
74
+ type SuspensionOutcome,
75
+ } from "@effect-agent/session";
76
+ import { BrowserCrypto } from "@effect/platform-browser";
77
+ import { SqliteClient } from "@effect/sql-sqlite-do";
78
+ import { Clock, Context, Crypto, DateTime, Effect, Layer, Option, Schema, Stream } from "effect";
79
+ import * as SqlClientService from "effect/unstable/sql/SqlClient";
80
+ import type { SqlError } from "effect/unstable/sql/SqlError";
81
+
82
+ import {
83
+ storageConfigLayer,
84
+ storageFailpointLayer,
85
+ type DoStorageInitializationError,
86
+ type DoStorageOptions,
87
+ } from "./do-conversation-store.ts";
88
+ import { decodeRows, initializeDoJournal } from "./do-journal.ts";
89
+ import { DoStorageConfig } from "./do-storage-config.ts";
90
+ import { DoStorageFailpoint } from "./do-storage-failpoint.ts";
91
+ import {
92
+ DoLedgerError,
93
+ DoStorageCorruptionError,
94
+ DoStorageError,
95
+ type DoStorageFailpointLocation,
96
+ } from "./errors.ts";
97
+
98
+ type SubmissionId = SubmissionSnapshot["submissionId"];
99
+
100
+ /**
101
+ * Static decode-side ceiling; writes are bounded in bytes by the configured
102
+ * `maxStoredValueBytes` (see do-journal.ts).
103
+ */
104
+ const BoundedStoredText = Schema.String.check(Schema.isMaxLength(2_000_000));
105
+ const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
106
+ const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
107
+
108
+ const SCAN_PAGE_SIZE = 256;
109
+ const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
110
+ const MAX_IDENTIFIER_LENGTH = 1_024;
111
+
112
+ class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
113
+ submission_id: BoundedIdentifier,
114
+ conversation_id: BoundedIdentifier,
115
+ queue_sequence: QueueSequence,
116
+ principal: BoundedIdentifier,
117
+ idempotency_key: BoundedIdentifier,
118
+ agent_id: BoundedIdentifier,
119
+ agent_digests_json: BoundedStoredText,
120
+ deployment_id: BoundedIdentifier,
121
+ input_json: BoundedStoredText,
122
+ input_digest: Digest,
123
+ receipt_id: BoundedIdentifier,
124
+ state: SubmissionState,
125
+ settled_outcome: Schema.NullOr(SettlementOutcome),
126
+ created_at: BoundedTimestamp,
127
+ ready_at: Schema.NullOr(BoundedTimestamp),
128
+ input_applied_record_id: Schema.NullOr(BoundedIdentifier),
129
+ input_applied_sequence: Schema.NullOr(CanonicalSequence),
130
+ joined_host_submission_id: Schema.NullOr(BoundedIdentifier),
131
+ suspended_reason_json: Schema.NullOr(BoundedStoredText),
132
+ suspended_at: Schema.NullOr(BoundedTimestamp),
133
+ unknown_reason: Schema.NullOr(BoundedStoredText),
134
+ unknown_tool_call_ids_json: Schema.NullOr(BoundedStoredText),
135
+ parent_submission_id: Schema.NullOr(BoundedIdentifier),
136
+ parent_tool_call_id: Schema.NullOr(BoundedIdentifier),
137
+ }) {}
138
+
139
+ class ChildReservationRow extends Schema.Class<ChildReservationRow>("ChildReservationRow")({
140
+ reservation_id: BoundedIdentifier,
141
+ parent_submission_id: BoundedIdentifier,
142
+ parent_tool_call_id: BoundedIdentifier,
143
+ child_submission_id: Schema.NullOr(BoundedIdentifier),
144
+ status: ChildReservationStatus,
145
+ allocation_json: BoundedStoredText,
146
+ allocation_digest: Digest,
147
+ accounting_json: Schema.NullOr(BoundedStoredText),
148
+ reserved_at: BoundedTimestamp,
149
+ release_began_at: Schema.NullOr(BoundedTimestamp),
150
+ released_at: Schema.NullOr(BoundedTimestamp),
151
+ }) {}
152
+
153
+ class ChildSettlementMarkerRow extends Schema.Class<ChildSettlementMarkerRow>(
154
+ "ChildSettlementMarkerRow",
155
+ )({
156
+ parent_submission_id: BoundedIdentifier,
157
+ child_submission_id: BoundedIdentifier,
158
+ child_outcome: Schema.NullOr(SettlementOutcome),
159
+ recorded_at: BoundedTimestamp,
160
+ }) {}
161
+
162
+ class ApprovalDecisionRow extends Schema.Class<ApprovalDecisionRow>("ApprovalDecisionRow")({
163
+ submission_id: BoundedIdentifier,
164
+ tool_call_id: BoundedIdentifier,
165
+ decision: ApprovalDecision,
166
+ resolver: BoundedIdentifier,
167
+ reason: BoundedStoredText,
168
+ decided_at: BoundedTimestamp,
169
+ }) {}
170
+
171
+ class UnknownResolutionRow extends Schema.Class<UnknownResolutionRow>("UnknownResolutionRow")({
172
+ submission_id: BoundedIdentifier,
173
+ tool_call_id: BoundedIdentifier,
174
+ author: BoundedIdentifier,
175
+ reason: BoundedStoredText,
176
+ resolution_json: BoundedStoredText,
177
+ resolved_at: BoundedTimestamp,
178
+ }) {}
179
+
180
+ class OwnershipRow extends Schema.Class<OwnershipRow>("OwnershipRow")({
181
+ submission_id: BoundedIdentifier,
182
+ attempt_id: BoundedIdentifier,
183
+ ownership_token: BoundedIdentifier,
184
+ producer_epoch: ProducerEpoch,
185
+ owner_producer_id: BoundedIdentifier,
186
+ lease_expires_at: BoundedTimestamp,
187
+ }) {}
188
+
189
+ class ReservationRow extends Schema.Class<ReservationRow>("ReservationRow")({
190
+ submission_id: BoundedIdentifier,
191
+ settlement_id: BoundedIdentifier,
192
+ outcome: SettlementOutcome,
193
+ record_id: BoundedIdentifier,
194
+ record_json: BoundedStoredText,
195
+ record_digest: Digest,
196
+ reserved_at: BoundedTimestamp,
197
+ finalized_at: Schema.NullOr(BoundedTimestamp),
198
+ }) {}
199
+
200
+ class AbortIntentRow extends Schema.Class<AbortIntentRow>("AbortIntentRow")({
201
+ submission_id: BoundedIdentifier,
202
+ author: BoundedIdentifier,
203
+ reason: BoundedStoredText,
204
+ requested_at: BoundedTimestamp,
205
+ canonical_record_id: Schema.NullOr(BoundedIdentifier),
206
+ }) {}
207
+
208
+ class MaxQueueSequenceRow extends Schema.Class<MaxQueueSequenceRow>("MaxQueueSequenceRow")({
209
+ max_queue_sequence: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
210
+ }) {}
211
+
212
+ class CanonicalRecordIdRow extends Schema.Class<CanonicalRecordIdRow>("CanonicalRecordIdRow")({
213
+ record_id: BoundedIdentifier,
214
+ }) {}
215
+
216
+ const SUBMISSION_COLUMNS = `
217
+ submission_id,
218
+ conversation_id,
219
+ queue_sequence,
220
+ principal,
221
+ idempotency_key,
222
+ agent_id,
223
+ agent_digests_json,
224
+ deployment_id,
225
+ input_json,
226
+ input_digest,
227
+ receipt_id,
228
+ state,
229
+ settled_outcome,
230
+ created_at,
231
+ ready_at,
232
+ input_applied_record_id,
233
+ input_applied_sequence,
234
+ joined_host_submission_id,
235
+ suspended_reason_json,
236
+ suspended_at,
237
+ unknown_reason,
238
+ unknown_tool_call_ids_json,
239
+ parent_submission_id,
240
+ parent_tool_call_id
241
+ `;
242
+
243
+ const CHILD_RESERVATION_COLUMNS = `
244
+ reservation_id,
245
+ parent_submission_id,
246
+ parent_tool_call_id,
247
+ child_submission_id,
248
+ status,
249
+ allocation_json,
250
+ allocation_digest,
251
+ accounting_json,
252
+ reserved_at,
253
+ release_began_at,
254
+ released_at
255
+ `;
256
+
257
+ /** The branded ToolCallId schema, reached through the session port so no core import is needed. */
258
+ const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
259
+ const ToolCallIdList = Schema.Array(ToolCallIdSchema);
260
+
261
+ const encodePersistedJsonText = Schema.encodeEffect(Schema.fromJsonString(PersistedJson));
262
+ const encodeDefinitionDigestsText = Schema.encodeEffect(Schema.fromJsonString(DefinitionDigests));
263
+ const encodeRecordEnvelopeText = Schema.encodeEffect(Schema.fromJsonString(RecordEnvelope));
264
+ const decodeRecordEnvelopeText = Schema.decodeEffect(Schema.fromJsonString(RecordEnvelope));
265
+ const encodeSuspensionReasonText = Schema.encodeEffect(Schema.fromJsonString(SuspensionReason));
266
+ const encodeUnknownResolutionText = Schema.encodeEffect(Schema.fromJsonString(UnknownResolution));
267
+ const encodeToolCallIdsText = Schema.encodeEffect(Schema.fromJsonString(ToolCallIdList));
268
+ const decodeToolCallIdsText = Schema.decodeEffect(Schema.fromJsonString(ToolCallIdList));
269
+ const parseStoredJsonText = Schema.decodeEffect(Schema.fromJsonString(Schema.Json));
270
+ const decodeAdmissionResult = Schema.decodeUnknownEffect(AdmissionResult);
271
+ const decodeClaim = Schema.decodeUnknownEffect(Claim);
272
+ const decodeOwnershipRenewal = Schema.decodeUnknownEffect(OwnershipRenewal);
273
+ const decodeSettlement = Schema.decodeUnknownEffect(Settlement);
274
+ const decodeAbortIntent = Schema.decodeUnknownEffect(AbortIntent);
275
+ const decodeOwnershipSnapshot = Schema.decodeUnknownEffect(OwnershipSnapshot);
276
+ const decodeInputAppliedMarker = Schema.decodeUnknownEffect(InputAppliedMarker);
277
+ const decodeSubmissionSnapshotUnknown = Schema.decodeUnknownEffect(SubmissionSnapshot);
278
+ const decodeSubmissionId = Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId);
279
+ const decodeQueueSequence = Schema.decodeUnknownEffect(QueueSequence);
280
+ const decodeUtcInstant = Schema.decodeUnknownEffect(Schema.DateTimeUtcFromString);
281
+ const decodeJoiningClaim = Schema.decodeUnknownEffect(JoiningClaim);
282
+ const decodeJoinSnapshot = Schema.decodeUnknownEffect(JoinSnapshot);
283
+ const decodeSuspensionSnapshot = Schema.decodeUnknownEffect(SuspensionSnapshot);
284
+ const decodeApprovalDecisionIntent = Schema.decodeUnknownEffect(ApprovalDecisionIntent);
285
+ const decodeUnknownResolutionIntent = Schema.decodeUnknownEffect(UnknownResolutionIntent);
286
+ const decodeParentLinkage = Schema.decodeUnknownEffect(ParentLinkage);
287
+ const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
288
+ ChildBudgetReservationSnapshot,
289
+ );
290
+ const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
291
+
292
+ /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
293
+ const internalFailure =
294
+ (operation: string) =>
295
+ (error: { readonly message: string }): LedgerError =>
296
+ LedgerError.make({ operation, message: error.message, cause: error });
297
+
298
+ /**
299
+ * Classify raw SQL failures. Within one Durable Object there is exactly one writer, so the
300
+ * Node adapter's retryable `SqliteWriteContention` classification has no analogue: every raw
301
+ * failure is a `DoLedgerError` preserved as the LedgerError's cause.
302
+ */
303
+ const sqlFailure =
304
+ (operation: string) =>
305
+ (error: SqlError): LedgerError =>
306
+ internalFailure(operation)(
307
+ DoLedgerError.make({
308
+ cause: error,
309
+ operation,
310
+ message: error.message,
311
+ }),
312
+ );
313
+
314
+ const corruptionFailure = (operation: string, table: string, rowKey: string, message: string) =>
315
+ internalFailure(operation)(DoStorageCorruptionError.make({ table, rowKey, message }));
316
+
317
+ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
318
+ const config = yield* DoStorageConfig;
319
+ const failpoint = yield* DoStorageFailpoint;
320
+ const sql = yield* SqlClientService.SqlClient;
321
+ const crypto = yield* Crypto.Crypto;
322
+ const journal = yield* initializeDoJournal(sql, failpoint.hit, config.maxStoredValueBytes);
323
+
324
+ const hitFailpoint = (
325
+ location: DoStorageFailpointLocation,
326
+ operation: string,
327
+ ): Effect.Effect<void, LedgerError> =>
328
+ failpoint.hit(location).pipe(Effect.mapError((error) => internalFailure(operation)(error)));
329
+
330
+ /**
331
+ * Run one ledger mutation under the journal's Durable Object storage-backed transaction so
332
+ * ownership-token and epoch checks are atomic with their writes (DUR-006). Transaction
333
+ * failures surface as LedgerError carrying the typed `DoStorageError` as cause.
334
+ */
335
+ const inWriteTransaction = <
336
+ A,
337
+ E extends
338
+ | AdmissionConflict
339
+ | ApprovalConflict
340
+ | ChildReservationConflict
341
+ | JoinedToHost
342
+ | OwnershipLost
343
+ | SettlementConflict
344
+ | UnknownResolutionConflict
345
+ | LedgerError,
346
+ >(
347
+ operation: string,
348
+ effect: Effect.Effect<A, E>,
349
+ ): Effect.Effect<A, E | LedgerError> =>
350
+ journal
351
+ .withWriteTransaction(operation)(effect)
352
+ .pipe(
353
+ Effect.mapError((error) =>
354
+ error instanceof DoStorageError ? internalFailure(operation)(error) : error,
355
+ ),
356
+ );
357
+
358
+ const mintUuid = (operation: string): Effect.Effect<string, LedgerError> =>
359
+ crypto.randomUUIDv7.pipe(Effect.mapError((error) => internalFailure(operation)(error)));
360
+
361
+ const currentInstant = Effect.map(Clock.currentTimeMillis, (millis) => ({
362
+ millis,
363
+ iso: new Date(millis).toISOString(),
364
+ }));
365
+
366
+ const timestampMillis = (operation: string, rowKey: string) => (timestamp: string) =>
367
+ decodeUtcInstant(timestamp).pipe(
368
+ Effect.map(DateTime.toEpochMillis),
369
+ Effect.mapError((error) =>
370
+ corruptionFailure(operation, "effect_agent_submission_ownership", rowKey, error.message),
371
+ ),
372
+ );
373
+
374
+ const decodeSubmissionRows = (operation: string, rowKey: string, rows: unknown) =>
375
+ decodeRows(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(
376
+ Effect.mapError(internalFailure(operation)),
377
+ );
378
+
379
+ const readSubmission = Effect.fn("DoSubmissionLedger.readSubmission")(function* (
380
+ operation: string,
381
+ submissionId: string,
382
+ ): Effect.fn.Return<Option.Option<SubmissionRow>, LedgerError> {
383
+ const rows = yield* sql<Record<string, unknown>>`
384
+ SELECT ${sql.literal(SUBMISSION_COLUMNS)}
385
+ FROM effect_agent_submissions
386
+ WHERE submission_id = ${submissionId}
387
+ `.pipe(Effect.mapError(sqlFailure(operation)));
388
+ const decoded = yield* decodeSubmissionRows(operation, submissionId, rows);
389
+ if (decoded.length > 1) {
390
+ return yield* corruptionFailure(
391
+ operation,
392
+ "effect_agent_submissions",
393
+ submissionId,
394
+ "A submission primary key returned more than one row.",
395
+ );
396
+ }
397
+ return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
398
+ });
399
+
400
+ const requireSubmission = Effect.fn("DoSubmissionLedger.requireSubmission")(function* (
401
+ operation: string,
402
+ submissionId: string,
403
+ ): Effect.fn.Return<SubmissionRow, LedgerError> {
404
+ const submission = yield* readSubmission(operation, submissionId);
405
+ if (Option.isNone(submission)) {
406
+ return yield* LedgerError.make({
407
+ operation,
408
+ message: `Unknown submission ${submissionId}.`,
409
+ });
410
+ }
411
+ return submission.value;
412
+ });
413
+
414
+ const readOwnership = Effect.fn("DoSubmissionLedger.readOwnership")(function* (
415
+ operation: string,
416
+ submissionId: string,
417
+ ): Effect.fn.Return<Option.Option<OwnershipRow>, LedgerError> {
418
+ const rows = yield* sql<Record<string, unknown>>`
419
+ SELECT
420
+ submission_id,
421
+ attempt_id,
422
+ ownership_token,
423
+ producer_epoch,
424
+ owner_producer_id,
425
+ lease_expires_at
426
+ FROM effect_agent_submission_ownership
427
+ WHERE submission_id = ${submissionId}
428
+ `.pipe(Effect.mapError(sqlFailure(operation)));
429
+ const decoded = yield* decodeRows(
430
+ Schema.Array(OwnershipRow),
431
+ "effect_agent_submission_ownership",
432
+ submissionId,
433
+ rows,
434
+ ).pipe(Effect.mapError(internalFailure(operation)));
435
+ if (decoded.length > 1) {
436
+ return yield* corruptionFailure(
437
+ operation,
438
+ "effect_agent_submission_ownership",
439
+ submissionId,
440
+ "An ownership primary key returned more than one row.",
441
+ );
442
+ }
443
+ return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
444
+ });
445
+
446
+ const conversationEpoch = Effect.fn("DoSubmissionLedger.conversationEpoch")(function* (
447
+ operation: string,
448
+ conversationId: string,
449
+ ): Effect.fn.Return<ProducerEpoch, LedgerError> {
450
+ const conversations = yield* journal
451
+ .getConversation(conversationId)
452
+ .pipe(Effect.mapError(internalFailure(operation)));
453
+ return conversations.length === 0 ? EPOCH_ZERO : conversations[0].producer_epoch;
454
+ });
455
+
456
+ /**
457
+ * Verify inside the surrounding write transaction that the presented token still owns the
458
+ * Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
459
+ * Conversation's current producer epoch (DUR-006).
460
+ */
461
+ const requireOwnership = Effect.fn("DoSubmissionLedger.requireOwnership")(function* (
462
+ operation: string,
463
+ submission: SubmissionRow,
464
+ ownershipToken: string,
465
+ ): Effect.fn.Return<OwnershipRow, OwnershipLost | LedgerError> {
466
+ const ownership = yield* readOwnership(operation, submission.submission_id);
467
+ if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
468
+ const actualEpoch = yield* conversationEpoch(operation, submission.conversation_id);
469
+ const submissionId = yield* Schema.decodeUnknownEffect(
470
+ SubmissionSnapshot.fields.submissionId,
471
+ )(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
472
+ return yield* OwnershipLost.make({ submissionId, actualEpoch });
473
+ }
474
+ return ownership.value;
475
+ });
476
+
477
+ const decodeSubmissionSnapshot = Effect.fn("DoSubmissionLedger.decodeSubmissionSnapshot")(
478
+ function* (
479
+ operation: string,
480
+ row: SubmissionRow,
481
+ ): Effect.fn.Return<SubmissionSnapshot, LedgerError> {
482
+ const agentDigests = yield* parseStoredJsonText(row.agent_digests_json).pipe(
483
+ Effect.mapError((error) =>
484
+ corruptionFailure(
485
+ operation,
486
+ "effect_agent_submissions",
487
+ row.submission_id,
488
+ error.message,
489
+ ),
490
+ ),
491
+ );
492
+ const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
493
+ Effect.mapError((error) =>
494
+ corruptionFailure(
495
+ operation,
496
+ "effect_agent_submissions",
497
+ row.submission_id,
498
+ error.message,
499
+ ),
500
+ ),
501
+ );
502
+ if ((row.parent_submission_id === null) !== (row.parent_tool_call_id === null)) {
503
+ return yield* corruptionFailure(
504
+ operation,
505
+ "effect_agent_submissions",
506
+ row.submission_id,
507
+ "A parent linkage must record both the parent Submission and the parent Tool Call.",
508
+ );
509
+ }
510
+ return yield* decodeSubmissionSnapshotUnknown({
511
+ submissionId: row.submission_id,
512
+ conversationId: row.conversation_id,
513
+ queueSequence: row.queue_sequence,
514
+ principal: row.principal,
515
+ idempotencyKey: row.idempotency_key,
516
+ agentId: row.agent_id,
517
+ agentDigests,
518
+ deploymentId: row.deployment_id,
519
+ inputPayload,
520
+ inputDigest: row.input_digest,
521
+ receiptId: row.receipt_id,
522
+ state: row.state,
523
+ createdAt: row.created_at,
524
+ ...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
525
+ ...(row.ready_at === null ? {} : { readyAt: row.ready_at }),
526
+ ...(row.parent_submission_id === null || row.parent_tool_call_id === null
527
+ ? {}
528
+ : {
529
+ parentLinkage: {
530
+ parentSubmissionId: row.parent_submission_id,
531
+ parentToolCallId: row.parent_tool_call_id,
532
+ },
533
+ }),
534
+ }).pipe(
535
+ Effect.mapError((error) =>
536
+ corruptionFailure(
537
+ operation,
538
+ "effect_agent_submissions",
539
+ row.submission_id,
540
+ error.message,
541
+ ),
542
+ ),
543
+ );
544
+ },
545
+ );
546
+
547
+ const readReservation = Effect.fn("DoSubmissionLedger.readReservation")(function* (
548
+ operation: string,
549
+ submissionId: string,
550
+ ): Effect.fn.Return<Option.Option<ReservationRow>, LedgerError> {
551
+ const rows = yield* sql<Record<string, unknown>>`
552
+ SELECT
553
+ submission_id,
554
+ settlement_id,
555
+ outcome,
556
+ record_id,
557
+ record_json,
558
+ record_digest,
559
+ reserved_at,
560
+ finalized_at
561
+ FROM effect_agent_settlement_reservations
562
+ WHERE submission_id = ${submissionId}
563
+ `.pipe(Effect.mapError(sqlFailure(operation)));
564
+ const decoded = yield* decodeRows(
565
+ Schema.Array(ReservationRow),
566
+ "effect_agent_settlement_reservations",
567
+ submissionId,
568
+ rows,
569
+ ).pipe(Effect.mapError(internalFailure(operation)));
570
+ if (decoded.length > 1) {
571
+ return yield* corruptionFailure(
572
+ operation,
573
+ "effect_agent_settlement_reservations",
574
+ submissionId,
575
+ "A settlement reservation primary key returned more than one row.",
576
+ );
577
+ }
578
+ return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
579
+ });
580
+
581
+ const readAbortIntent = Effect.fn("DoSubmissionLedger.readAbortIntent")(function* (
582
+ operation: string,
583
+ submissionId: string,
584
+ ): Effect.fn.Return<Option.Option<AbortIntentRow>, LedgerError> {
585
+ const rows = yield* sql<Record<string, unknown>>`
586
+ SELECT
587
+ submission_id,
588
+ author,
589
+ reason,
590
+ requested_at,
591
+ canonical_record_id
592
+ FROM effect_agent_abort_intents
593
+ WHERE submission_id = ${submissionId}
594
+ `.pipe(Effect.mapError(sqlFailure(operation)));
595
+ const decoded = yield* decodeRows(
596
+ Schema.Array(AbortIntentRow),
597
+ "effect_agent_abort_intents",
598
+ submissionId,
599
+ rows,
600
+ ).pipe(Effect.mapError(internalFailure(operation)));
601
+ if (decoded.length > 1) {
602
+ return yield* corruptionFailure(
603
+ operation,
604
+ "effect_agent_abort_intents",
605
+ submissionId,
606
+ "An abort intent primary key returned more than one row.",
607
+ );
608
+ }
609
+ return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
610
+ });
611
+
612
+ /**
613
+ * Read the durable cross-store child-settlement markers recorded against one parent
614
+ * Submission (the DC realization of the port's "cross-store adapters record a durable
615
+ * notification marker" contract).
616
+ */
617
+ const readChildSettlementMarkers = Effect.fn("DoSubmissionLedger.readChildSettlementMarkers")(
618
+ function* (
619
+ operation: string,
620
+ parentSubmissionId: string,
621
+ ): Effect.fn.Return<ReadonlyArray<ChildSettlementMarkerRow>, LedgerError> {
622
+ const rows = yield* sql<Record<string, unknown>>`
623
+ SELECT
624
+ parent_submission_id,
625
+ child_submission_id,
626
+ child_outcome,
627
+ recorded_at
628
+ FROM effect_agent_child_settlements
629
+ WHERE parent_submission_id = ${parentSubmissionId}
630
+ ORDER BY child_submission_id ASC
631
+ `.pipe(Effect.mapError(sqlFailure(operation)));
632
+ return yield* decodeRows(
633
+ Schema.Array(ChildSettlementMarkerRow),
634
+ "effect_agent_child_settlements",
635
+ parentSubmissionId,
636
+ rows,
637
+ ).pipe(Effect.mapError(internalFailure(operation)));
638
+ },
639
+ );
640
+
641
+ /**
642
+ * Whether one listed child is provably settled from THIS store: either its own row lives
643
+ * here and is settled (single-store evidence, identical to the Node adapter), or a durable
644
+ * cross-store notification marker was recorded for it (the child's row lives in another
645
+ * Durable Object and its owner reported the settlement through `recordChildSettled`).
646
+ */
647
+ const childProvablySettled = Effect.fn("DoSubmissionLedger.childProvablySettled")(function* (
648
+ operation: string,
649
+ markerChildren: ReadonlySet<string>,
650
+ childSubmissionId: string,
651
+ ): Effect.fn.Return<boolean, LedgerError> {
652
+ if (markerChildren.has(childSubmissionId)) return true;
653
+ const childRow = yield* readSubmission(operation, childSubmissionId);
654
+ return Option.isSome(childRow) && childRow.value.state === "settled";
655
+ });
656
+
657
+ const decodeChildReservationRows = (operation: string, rowKey: string, rows: unknown) =>
658
+ decodeRows(
659
+ Schema.Array(ChildReservationRow),
660
+ "effect_agent_child_reservations",
661
+ rowKey,
662
+ rows,
663
+ ).pipe(Effect.mapError(internalFailure(operation)));
664
+
665
+ const readChildReservation = Effect.fn("DoSubmissionLedger.readChildReservation")(function* (
666
+ operation: string,
667
+ reservationId: string,
668
+ ): Effect.fn.Return<Option.Option<ChildReservationRow>, LedgerError> {
669
+ const rows = yield* sql<Record<string, unknown>>`
670
+ SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
671
+ FROM effect_agent_child_reservations
672
+ WHERE reservation_id = ${reservationId}
673
+ `.pipe(Effect.mapError(sqlFailure(operation)));
674
+ const decoded = yield* decodeChildReservationRows(operation, reservationId, rows);
675
+ if (decoded.length > 1) {
676
+ return yield* corruptionFailure(
677
+ operation,
678
+ "effect_agent_child_reservations",
679
+ reservationId,
680
+ "A child reservation primary key returned more than one row.",
681
+ );
682
+ }
683
+ return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
684
+ });
685
+
686
+ const readChildReservationForCall = Effect.fn("DoSubmissionLedger.readChildReservationForCall")(
687
+ function* (
688
+ operation: string,
689
+ parentSubmissionId: string,
690
+ parentToolCallId: string,
691
+ ): Effect.fn.Return<Option.Option<ChildReservationRow>, LedgerError> {
692
+ const rows = yield* sql<Record<string, unknown>>`
693
+ SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
694
+ FROM effect_agent_child_reservations
695
+ WHERE parent_submission_id = ${parentSubmissionId}
696
+ AND parent_tool_call_id = ${parentToolCallId}
697
+ `.pipe(Effect.mapError(sqlFailure(operation)));
698
+ const decoded = yield* decodeChildReservationRows(
699
+ operation,
700
+ `${parentSubmissionId}/${parentToolCallId}`,
701
+ rows,
702
+ );
703
+ if (decoded.length > 1) {
704
+ return yield* corruptionFailure(
705
+ operation,
706
+ "effect_agent_child_reservations",
707
+ `${parentSubmissionId}/${parentToolCallId}`,
708
+ "A parent Tool Call returned more than one child reservation.",
709
+ );
710
+ }
711
+ return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
712
+ },
713
+ );
714
+
715
+ const childReservationSnapshotFromRow = Effect.fn(
716
+ "DoSubmissionLedger.childReservationSnapshotFromRow",
717
+ )(function* (
718
+ operation: string,
719
+ row: ChildReservationRow,
720
+ ): Effect.fn.Return<ChildBudgetReservationSnapshot, LedgerError> {
721
+ const rowFailure = (error: { readonly message: string }) =>
722
+ corruptionFailure(
723
+ operation,
724
+ "effect_agent_child_reservations",
725
+ row.reservation_id,
726
+ error.message,
727
+ );
728
+ const allocation = yield* parseStoredJsonText(row.allocation_json).pipe(
729
+ Effect.mapError(rowFailure),
730
+ );
731
+ const accounting =
732
+ row.accounting_json === null
733
+ ? undefined
734
+ : yield* parseStoredJsonText(row.accounting_json).pipe(Effect.mapError(rowFailure));
735
+ return yield* decodeChildReservationSnapshotUnknown({
736
+ reservationId: row.reservation_id,
737
+ parentSubmissionId: row.parent_submission_id,
738
+ parentToolCallId: row.parent_tool_call_id,
739
+ status: row.status,
740
+ allocation,
741
+ allocationDigest: row.allocation_digest,
742
+ reservedAt: row.reserved_at,
743
+ ...(row.child_submission_id === null ? {} : { childSubmissionId: row.child_submission_id }),
744
+ ...(accounting === undefined ? {} : { accounting }),
745
+ ...(row.release_began_at === null ? {} : { releaseBeganAt: row.release_began_at }),
746
+ ...(row.released_at === null ? {} : { releasedAt: row.released_at }),
747
+ }).pipe(Effect.mapError(rowFailure));
748
+ });
749
+
750
+ const readApprovalDecisions = Effect.fn("DoSubmissionLedger.readApprovalDecisions")(function* (
751
+ operation: string,
752
+ submissionId: string,
753
+ ): Effect.fn.Return<ReadonlyArray<ApprovalDecisionRow>, LedgerError> {
754
+ const rows = yield* sql<Record<string, unknown>>`
755
+ SELECT
756
+ submission_id,
757
+ tool_call_id,
758
+ decision,
759
+ resolver,
760
+ reason,
761
+ decided_at
762
+ FROM effect_agent_approval_decisions
763
+ WHERE submission_id = ${submissionId}
764
+ ORDER BY tool_call_id ASC
765
+ `.pipe(Effect.mapError(sqlFailure(operation)));
766
+ return yield* decodeRows(
767
+ Schema.Array(ApprovalDecisionRow),
768
+ "effect_agent_approval_decisions",
769
+ submissionId,
770
+ rows,
771
+ ).pipe(Effect.mapError(internalFailure(operation)));
772
+ });
773
+
774
+ const approvalIntentFromRow = Effect.fn("DoSubmissionLedger.approvalIntentFromRow")(function* (
775
+ operation: string,
776
+ row: ApprovalDecisionRow,
777
+ ): Effect.fn.Return<ApprovalDecisionIntent, LedgerError> {
778
+ return yield* decodeApprovalDecisionIntent({
779
+ submissionId: row.submission_id,
780
+ toolCallId: row.tool_call_id,
781
+ decision: row.decision,
782
+ resolver: row.resolver,
783
+ reason: row.reason,
784
+ decidedAt: row.decided_at,
785
+ }).pipe(
786
+ Effect.mapError((error) =>
787
+ corruptionFailure(
788
+ operation,
789
+ "effect_agent_approval_decisions",
790
+ `${row.submission_id}/${row.tool_call_id}`,
791
+ error.message,
792
+ ),
793
+ ),
794
+ );
795
+ });
796
+
797
+ const readUnknownResolutions = Effect.fn("DoSubmissionLedger.readUnknownResolutions")(function* (
798
+ operation: string,
799
+ submissionId: string,
800
+ ): Effect.fn.Return<ReadonlyArray<UnknownResolutionRow>, LedgerError> {
801
+ const rows = yield* sql<Record<string, unknown>>`
802
+ SELECT
803
+ submission_id,
804
+ tool_call_id,
805
+ author,
806
+ reason,
807
+ resolution_json,
808
+ resolved_at
809
+ FROM effect_agent_unknown_resolutions
810
+ WHERE submission_id = ${submissionId}
811
+ ORDER BY tool_call_id ASC
812
+ `.pipe(Effect.mapError(sqlFailure(operation)));
813
+ return yield* decodeRows(
814
+ Schema.Array(UnknownResolutionRow),
815
+ "effect_agent_unknown_resolutions",
816
+ submissionId,
817
+ rows,
818
+ ).pipe(Effect.mapError(internalFailure(operation)));
819
+ });
820
+
821
+ const unknownResolutionIntentFromRow = Effect.fn(
822
+ "DoSubmissionLedger.unknownResolutionIntentFromRow",
823
+ )(function* (
824
+ operation: string,
825
+ row: UnknownResolutionRow,
826
+ ): Effect.fn.Return<UnknownResolutionIntent, LedgerError> {
827
+ const resolution = yield* parseStoredJsonText(row.resolution_json).pipe(
828
+ Effect.mapError((error) =>
829
+ corruptionFailure(
830
+ operation,
831
+ "effect_agent_unknown_resolutions",
832
+ `${row.submission_id}/${row.tool_call_id}`,
833
+ error.message,
834
+ ),
835
+ ),
836
+ );
837
+ return yield* decodeUnknownResolutionIntent({
838
+ submissionId: row.submission_id,
839
+ toolCallId: row.tool_call_id,
840
+ author: row.author,
841
+ reason: row.reason,
842
+ resolution,
843
+ resolvedAt: row.resolved_at,
844
+ }).pipe(
845
+ Effect.mapError((error) =>
846
+ corruptionFailure(
847
+ operation,
848
+ "effect_agent_unknown_resolutions",
849
+ `${row.submission_id}/${row.tool_call_id}`,
850
+ error.message,
851
+ ),
852
+ ),
853
+ );
854
+ });
855
+
856
+ /** The Submission's marked-unknown open Tool Call identities, empty when never marked. */
857
+ const storedUnknownToolCallIds = Effect.fn("DoSubmissionLedger.storedUnknownToolCallIds")(
858
+ function* (
859
+ operation: string,
860
+ submission: SubmissionRow,
861
+ ): Effect.fn.Return<ReadonlyArray<typeof ToolCallIdSchema.Type>, LedgerError> {
862
+ if (submission.unknown_tool_call_ids_json === null) return [];
863
+ return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(
864
+ Effect.mapError((error) =>
865
+ corruptionFailure(
866
+ operation,
867
+ "effect_agent_submissions",
868
+ submission.submission_id,
869
+ error.message,
870
+ ),
871
+ ),
872
+ );
873
+ },
874
+ );
875
+
876
+ /**
877
+ * Canonical history is the abort authority (DUR-015): the intent's canonicalRecordId is
878
+ * derived from the shared canonical-records table using the deterministic abort record
879
+ * identity, never from a cached ledger marker.
880
+ */
881
+ const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (
882
+ operation: string,
883
+ conversationId: string,
884
+ submissionId: SubmissionId,
885
+ ): Effect.fn.Return<string | undefined, LedgerError> {
886
+ const recordId = submissionAbortRecordId(submissionId);
887
+ const rows = yield* sql<Record<string, unknown>>`
888
+ SELECT record_id
889
+ FROM effect_agent_canonical_records
890
+ WHERE conversation_id = ${conversationId}
891
+ AND record_id = ${recordId}
892
+ `.pipe(Effect.mapError(sqlFailure(operation)));
893
+ const decoded = yield* decodeRows(
894
+ Schema.Array(CanonicalRecordIdRow),
895
+ "effect_agent_canonical_records",
896
+ `${conversationId}/${recordId}`,
897
+ rows,
898
+ ).pipe(Effect.mapError(internalFailure(operation)));
899
+ return decoded.length === 0 ? undefined : recordId;
900
+ });
901
+
902
+ const abortIntentFromRow = Effect.fn("DoSubmissionLedger.abortIntentFromRow")(function* (
903
+ operation: string,
904
+ submission: SubmissionRow,
905
+ submissionId: SubmissionId,
906
+ row: AbortIntentRow,
907
+ ): Effect.fn.Return<AbortIntent, LedgerError> {
908
+ const canonicalRecordId = yield* canonicalAbortRecordId(
909
+ operation,
910
+ submission.conversation_id,
911
+ submissionId,
912
+ );
913
+ return yield* decodeAbortIntent({
914
+ submissionId: row.submission_id,
915
+ author: row.author,
916
+ reason: row.reason,
917
+ requestedAt: row.requested_at,
918
+ ...(canonicalRecordId === undefined ? {} : { canonicalRecordId }),
919
+ }).pipe(
920
+ Effect.mapError((error) =>
921
+ corruptionFailure(
922
+ operation,
923
+ "effect_agent_abort_intents",
924
+ row.submission_id,
925
+ error.message,
926
+ ),
927
+ ),
928
+ );
929
+ });
930
+
931
+ // Durable Object storage is the single serialized owner: writes confirm through output
932
+ // gates before any response is observable, which is exactly the single-owner crash
933
+ // durability this adapter claims — under its own honest label (P7 WP0).
934
+ const capabilities = Effect.succeed(
935
+ LedgerCapabilities.make({ durability: "durable-cloudflare" }),
936
+ );
937
+
938
+ const admit: SubmissionLedger["Service"]["admit"] = Effect.fn("DoSubmissionLedger.admit")(
939
+ function* (request: AdmissionRequest) {
940
+ const operation = "ledger admit";
941
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AdmissionRequest))(
942
+ request,
943
+ ).pipe(Effect.mapError(internalFailure(operation)));
944
+ const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(
945
+ Effect.mapError(internalFailure(operation)),
946
+ );
947
+ // The platform's ~2 MB per-value bound, refused typed BEFORE any durable mutation
948
+ // (resource-limits gate; oversized payloads are the designed R2 overflow path).
949
+ yield* journal
950
+ .checkValueBound(operation, inputJson)
951
+ .pipe(Effect.mapError(internalFailure(operation)));
952
+ const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
953
+ Effect.mapError(internalFailure(operation)),
954
+ );
955
+ // Routable Submission identity (D-P6-5): `{uuidv7}:{conversationId}`. The cross-DO
956
+ // routing layer parses ITS OWN minted format (split at the first ":") to address
957
+ // submissionId-only operations to the owning Conversation Object; the id stays opaque
958
+ // to every other component, exactly like DN's `submission-{uuid}` prefix.
959
+ const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.conversationId}`;
960
+ if (mintedSubmissionId.length > MAX_IDENTIFIER_LENGTH) {
961
+ return yield* LedgerError.make({
962
+ operation,
963
+ message:
964
+ `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds ` +
965
+ `the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Conversation identity.`,
966
+ });
967
+ }
968
+ const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
969
+ yield* hitFailpoint("ledger:admit:before", operation);
970
+ const result = yield* inWriteTransaction(
971
+ operation,
972
+ Effect.gen(function* () {
973
+ const keyRowKey = `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`;
974
+ const existingRows = yield* sql<Record<string, unknown>>`
975
+ SELECT ${sql.literal(SUBMISSION_COLUMNS)}
976
+ FROM effect_agent_submissions
977
+ WHERE conversation_id = ${validated.conversationId}
978
+ AND principal = ${validated.principal}
979
+ AND idempotency_key = ${validated.idempotencyKey}
980
+ `.pipe(Effect.mapError(sqlFailure(operation)));
981
+ const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
982
+ if (existing.length > 1) {
983
+ return yield* corruptionFailure(
984
+ operation,
985
+ "effect_agent_submissions",
986
+ keyRowKey,
987
+ "An admission idempotency key returned more than one row.",
988
+ );
989
+ }
990
+ if (existing.length === 1) {
991
+ // A replay must repeat the exact canonical input AND the exact parent linkage (or
992
+ // its absence): linkage is immutable child lineage (spec §12 step 5, SUB-016).
993
+ const sameLinkage =
994
+ validated.parentLinkage === undefined
995
+ ? existing[0].parent_submission_id === null &&
996
+ existing[0].parent_tool_call_id === null
997
+ : existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId &&
998
+ existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
999
+ if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
1000
+ return yield* AdmissionConflict.make({
1001
+ conversationId: validated.conversationId,
1002
+ principal: validated.principal,
1003
+ idempotencyKey: validated.idempotencyKey,
1004
+ existingInputDigest: existing[0].input_digest,
1005
+ attemptedInputDigest: validated.inputDigest,
1006
+ });
1007
+ }
1008
+ return yield* decodeAdmissionResult({
1009
+ submissionId: existing[0].submission_id,
1010
+ receiptId: existing[0].receipt_id,
1011
+ queueSequence: existing[0].queue_sequence,
1012
+ state: existing[0].state,
1013
+ replayed: true,
1014
+ }).pipe(Effect.mapError(internalFailure(operation)));
1015
+ }
1016
+
1017
+ const maxRows = yield* sql<Record<string, unknown>>`
1018
+ SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
1019
+ FROM effect_agent_submissions
1020
+ WHERE conversation_id = ${validated.conversationId}
1021
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1022
+ const decodedMax = yield* decodeRows(
1023
+ Schema.Array(MaxQueueSequenceRow),
1024
+ "effect_agent_submissions",
1025
+ validated.conversationId,
1026
+ maxRows,
1027
+ ).pipe(Effect.mapError(internalFailure(operation)));
1028
+ const queueSequence = yield* decodeQueueSequence(
1029
+ (decodedMax[0]?.max_queue_sequence ?? 0) + 1,
1030
+ ).pipe(Effect.mapError(internalFailure(operation)));
1031
+ const now = yield* currentInstant;
1032
+
1033
+ yield* sql`
1034
+ INSERT INTO effect_agent_submissions (
1035
+ submission_id,
1036
+ conversation_id,
1037
+ queue_sequence,
1038
+ principal,
1039
+ idempotency_key,
1040
+ agent_id,
1041
+ agent_digests_json,
1042
+ deployment_id,
1043
+ input_json,
1044
+ input_digest,
1045
+ receipt_id,
1046
+ state,
1047
+ created_at,
1048
+ parent_submission_id,
1049
+ parent_tool_call_id
1050
+ ) VALUES (
1051
+ ${mintedSubmissionId},
1052
+ ${validated.conversationId},
1053
+ ${queueSequence},
1054
+ ${validated.principal},
1055
+ ${validated.idempotencyKey},
1056
+ ${validated.agentId},
1057
+ ${agentDigestsJson},
1058
+ ${validated.deploymentId},
1059
+ ${inputJson},
1060
+ ${validated.inputDigest},
1061
+ ${mintedReceiptId},
1062
+ 'admitted',
1063
+ ${now.iso},
1064
+ ${validated.parentLinkage?.parentSubmissionId ?? null},
1065
+ ${validated.parentLinkage?.parentToolCallId ?? null}
1066
+ )
1067
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1068
+
1069
+ return yield* decodeAdmissionResult({
1070
+ submissionId: mintedSubmissionId,
1071
+ receiptId: mintedReceiptId,
1072
+ queueSequence,
1073
+ state: "admitted",
1074
+ replayed: false,
1075
+ }).pipe(Effect.mapError(internalFailure(operation)));
1076
+ }),
1077
+ );
1078
+ yield* hitFailpoint("ledger:admit:after", operation);
1079
+ return result;
1080
+ },
1081
+ );
1082
+
1083
+ const markReady: SubmissionLedger["Service"]["markReady"] = Effect.fn(
1084
+ "DoSubmissionLedger.markReady",
1085
+ )(function* (request: MarkReadyRequest) {
1086
+ const operation = "ledger mark ready";
1087
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkReadyRequest))(
1088
+ request,
1089
+ ).pipe(Effect.mapError(internalFailure(operation)));
1090
+ yield* hitFailpoint("ledger:mark-ready:before", operation);
1091
+ yield* inWriteTransaction(
1092
+ operation,
1093
+ Effect.gen(function* () {
1094
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1095
+ if (submission.state !== "admitted") return;
1096
+ const now = yield* currentInstant;
1097
+ yield* sql`
1098
+ UPDATE effect_agent_submissions
1099
+ SET state = 'ready', ready_at = ${now.iso}
1100
+ WHERE submission_id = ${validated.submissionId}
1101
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1102
+ }),
1103
+ );
1104
+ yield* hitFailpoint("ledger:mark-ready:after", operation);
1105
+ });
1106
+
1107
+ const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn("DoSubmissionLedger.lookup")(
1108
+ function* (request: SubmissionLookup) {
1109
+ const operation = "ledger lookup";
1110
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookup))(
1111
+ request,
1112
+ ).pipe(Effect.mapError(internalFailure(operation)));
1113
+ if (validated._tag === "SubmissionLookupById") {
1114
+ const row = yield* readSubmission(operation, validated.submissionId);
1115
+ if (Option.isNone(row)) return Option.none();
1116
+ return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
1117
+ }
1118
+ const rows = yield* sql<Record<string, unknown>>`
1119
+ SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1120
+ FROM effect_agent_submissions
1121
+ WHERE conversation_id = ${validated.conversationId}
1122
+ AND principal = ${validated.principal}
1123
+ AND idempotency_key = ${validated.idempotencyKey}
1124
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1125
+ const decoded = yield* decodeSubmissionRows(
1126
+ operation,
1127
+ `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1128
+ rows,
1129
+ );
1130
+ if (decoded.length > 1) {
1131
+ return yield* corruptionFailure(
1132
+ operation,
1133
+ "effect_agent_submissions",
1134
+ `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1135
+ "An admission idempotency key returned more than one row.",
1136
+ );
1137
+ }
1138
+ if (decoded.length === 0) return Option.none();
1139
+ return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
1140
+ },
1141
+ );
1142
+
1143
+ // This LOCAL facet is the authoritative owner of every Conversation stored in this Durable
1144
+ // Object, so the key-scoped read IS the admission truth and the tri-state degenerates to
1145
+ // NotAdmitted or Admitted (SUB-031). `AdmissionIndeterminate` becomes real one layer out:
1146
+ // the WP2 routed decorator answers it when the OWNING Durable Object is unreachable.
1147
+ const resolveAdmission: SubmissionLedger["Service"]["resolveAdmission"] = Effect.fn(
1148
+ "DoSubmissionLedger.resolveAdmission",
1149
+ )(function* (request: SubmissionLookupByKey) {
1150
+ const operation = "ledger resolve admission";
1151
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SubmissionLookupByKey))(
1152
+ request,
1153
+ ).pipe(Effect.mapError(internalFailure(operation)));
1154
+ const rows = yield* sql<Record<string, unknown>>`
1155
+ SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1156
+ FROM effect_agent_submissions
1157
+ WHERE conversation_id = ${validated.conversationId}
1158
+ AND principal = ${validated.principal}
1159
+ AND idempotency_key = ${validated.idempotencyKey}
1160
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1161
+ const decoded = yield* decodeSubmissionRows(
1162
+ operation,
1163
+ `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1164
+ rows,
1165
+ );
1166
+ if (decoded.length > 1) {
1167
+ return yield* corruptionFailure(
1168
+ operation,
1169
+ "effect_agent_submissions",
1170
+ `${validated.conversationId}/${validated.principal}/${validated.idempotencyKey}`,
1171
+ "An admission idempotency key returned more than one row.",
1172
+ );
1173
+ }
1174
+ if (decoded.length === 0) return AdmissionNotAdmitted.make();
1175
+ return AdmissionAdmitted.make({
1176
+ submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
1177
+ });
1178
+ });
1179
+
1180
+ const claim: SubmissionLedger["Service"]["claim"] = Effect.fn("DoSubmissionLedger.claim")(
1181
+ function* (request: ClaimRequest) {
1182
+ const operation = "ledger claim";
1183
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimRequest))(
1184
+ request,
1185
+ ).pipe(Effect.mapError(internalFailure(operation)));
1186
+ const attemptId = `attempt-${yield* mintUuid(operation)}`;
1187
+ const ownershipToken = `owner-${yield* mintUuid(operation)}`;
1188
+ yield* hitFailpoint("ledger:claim:before", operation);
1189
+ const claimed = yield* inWriteTransaction(
1190
+ operation,
1191
+ Effect.gen(function* () {
1192
+ const headRows = yield* sql<Record<string, unknown>>`
1193
+ SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1194
+ FROM effect_agent_submissions
1195
+ WHERE conversation_id = ${validated.conversationId}
1196
+ AND state <> 'settled'
1197
+ ORDER BY queue_sequence ASC
1198
+ LIMIT 1
1199
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1200
+ const heads = yield* decodeSubmissionRows(operation, validated.conversationId, headRows);
1201
+ if (heads.length === 0) return Option.none<Claim>();
1202
+ const head = heads[0];
1203
+
1204
+ // A joining/joined head is host-owned and a suspended/unknown head is durably
1205
+ // blocked (DUR-017); the lane produces no claim and later ready work is never
1206
+ // skipped past the blocked head (DUR-004).
1207
+ if (
1208
+ head.state === "joining" ||
1209
+ head.state === "joined" ||
1210
+ head.state === "suspended" ||
1211
+ head.state === "unknown"
1212
+ ) {
1213
+ return Option.none<Claim>();
1214
+ }
1215
+
1216
+ const now = yield* currentInstant;
1217
+ const ownership = yield* readOwnership(operation, head.submission_id);
1218
+ if (Option.isSome(ownership)) {
1219
+ const expiresAt = yield* timestampMillis(
1220
+ operation,
1221
+ head.submission_id,
1222
+ )(ownership.value.lease_expires_at);
1223
+ // A live lease blocks every new claim; expiry alone only revokes the liveness
1224
+ // assumption — correctness stays with producer-epoch fencing (D5). In DC a live
1225
+ // lease under another token can only come from an evicted incarnation.
1226
+ if (expiresAt > now.millis) return Option.none<Claim>();
1227
+ }
1228
+
1229
+ // Bump the Conversation's producer epoch atomically with the claim so every stale
1230
+ // Attempt is fenced out of canonical appends (DUR-006). A Conversation that was
1231
+ // never materialized (eviction between admission and materialization) is created
1232
+ // here so recovery can claim first and re-materialize idempotently at this epoch.
1233
+ const conversations = yield* journal
1234
+ .getConversation(head.conversation_id)
1235
+ .pipe(Effect.mapError(internalFailure(operation)));
1236
+ let producerEpoch: number;
1237
+ if (conversations.length === 0) {
1238
+ producerEpoch = 1;
1239
+ yield* sql`
1240
+ INSERT INTO effect_agent_conversations (
1241
+ conversation_id,
1242
+ created_at,
1243
+ tail_sequence,
1244
+ tail_digest,
1245
+ producer_epoch
1246
+ ) VALUES (
1247
+ ${head.conversation_id},
1248
+ ${now.iso},
1249
+ 0,
1250
+ ${EMPTY_TAIL_DIGEST},
1251
+ ${producerEpoch}
1252
+ )
1253
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1254
+ } else {
1255
+ producerEpoch = conversations[0].producer_epoch + 1;
1256
+ yield* sql`
1257
+ UPDATE effect_agent_conversations
1258
+ SET producer_epoch = ${producerEpoch}
1259
+ WHERE conversation_id = ${head.conversation_id}
1260
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1261
+ }
1262
+
1263
+ const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1264
+ yield* sql`
1265
+ INSERT INTO effect_agent_submission_ownership (
1266
+ submission_id,
1267
+ attempt_id,
1268
+ ownership_token,
1269
+ producer_epoch,
1270
+ owner_producer_id,
1271
+ lease_expires_at
1272
+ ) VALUES (
1273
+ ${head.submission_id},
1274
+ ${attemptId},
1275
+ ${ownershipToken},
1276
+ ${producerEpoch},
1277
+ ${validated.producerId},
1278
+ ${leaseExpiresAt}
1279
+ )
1280
+ ON CONFLICT (submission_id) DO UPDATE SET
1281
+ attempt_id = excluded.attempt_id,
1282
+ ownership_token = excluded.ownership_token,
1283
+ producer_epoch = excluded.producer_epoch,
1284
+ owner_producer_id = excluded.owner_producer_id,
1285
+ lease_expires_at = excluded.lease_expires_at
1286
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1287
+
1288
+ yield* sql`
1289
+ INSERT INTO effect_agent_attempts (
1290
+ attempt_id,
1291
+ submission_id,
1292
+ conversation_id,
1293
+ owner_producer_id,
1294
+ producer_epoch,
1295
+ claimed_at
1296
+ ) VALUES (
1297
+ ${attemptId},
1298
+ ${head.submission_id},
1299
+ ${head.conversation_id},
1300
+ ${validated.producerId},
1301
+ ${producerEpoch},
1302
+ ${now.iso}
1303
+ )
1304
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1305
+
1306
+ if (head.state === "ready") {
1307
+ yield* sql`
1308
+ UPDATE effect_agent_submissions
1309
+ SET state = 'running'
1310
+ WHERE submission_id = ${head.submission_id}
1311
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1312
+ }
1313
+
1314
+ const inputPayload = yield* parseStoredJsonText(head.input_json).pipe(
1315
+ Effect.mapError((error) =>
1316
+ corruptionFailure(
1317
+ operation,
1318
+ "effect_agent_submissions",
1319
+ head.submission_id,
1320
+ error.message,
1321
+ ),
1322
+ ),
1323
+ );
1324
+ return Option.some(
1325
+ yield* decodeClaim({
1326
+ submissionId: head.submission_id,
1327
+ attemptId,
1328
+ ownershipToken,
1329
+ producerEpoch,
1330
+ leaseExpiresAt,
1331
+ inputPayload,
1332
+ }).pipe(Effect.mapError(internalFailure(operation))),
1333
+ );
1334
+ }),
1335
+ );
1336
+ yield* hitFailpoint("ledger:claim:after", operation);
1337
+ return claimed;
1338
+ },
1339
+ );
1340
+
1341
+ const renewOwnership: SubmissionLedger["Service"]["renewOwnership"] = Effect.fn(
1342
+ "DoSubmissionLedger.renewOwnership",
1343
+ )(function* (request: RenewOwnershipRequest) {
1344
+ const operation = "ledger renew ownership";
1345
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RenewOwnershipRequest))(
1346
+ request,
1347
+ ).pipe(Effect.mapError(internalFailure(operation)));
1348
+ yield* hitFailpoint("ledger:renew:before", operation);
1349
+ const renewal = yield* inWriteTransaction(
1350
+ operation,
1351
+ Effect.gen(function* () {
1352
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1353
+ yield* requireOwnership(operation, submission, validated.ownershipToken);
1354
+ const now = yield* currentInstant;
1355
+ const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
1356
+ yield* sql`
1357
+ UPDATE effect_agent_submission_ownership
1358
+ SET lease_expires_at = ${leaseExpiresAt}
1359
+ WHERE submission_id = ${validated.submissionId}
1360
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1361
+ return yield* decodeOwnershipRenewal({
1362
+ ownershipToken: validated.ownershipToken,
1363
+ leaseExpiresAt,
1364
+ }).pipe(Effect.mapError(internalFailure(operation)));
1365
+ }),
1366
+ );
1367
+ yield* hitFailpoint("ledger:renew:after", operation);
1368
+ return renewal;
1369
+ });
1370
+
1371
+ const releaseOwnership: SubmissionLedger["Service"]["releaseOwnership"] = Effect.fn(
1372
+ "DoSubmissionLedger.releaseOwnership",
1373
+ )(function* (request: ReleaseOwnershipRequest) {
1374
+ const operation = "ledger release ownership";
1375
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseOwnershipRequest))(
1376
+ request,
1377
+ ).pipe(Effect.mapError(internalFailure(operation)));
1378
+ yield* hitFailpoint("ledger:release:before", operation);
1379
+ yield* inWriteTransaction(
1380
+ operation,
1381
+ Effect.gen(function* () {
1382
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1383
+ yield* requireOwnership(operation, submission, validated.ownershipToken);
1384
+ yield* sql`
1385
+ DELETE FROM effect_agent_submission_ownership
1386
+ WHERE submission_id = ${validated.submissionId}
1387
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1388
+ if (submission.state === "running") {
1389
+ yield* sql`
1390
+ UPDATE effect_agent_submissions
1391
+ SET state = 'ready'
1392
+ WHERE submission_id = ${validated.submissionId}
1393
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1394
+ }
1395
+ }),
1396
+ );
1397
+ yield* hitFailpoint("ledger:release:after", operation);
1398
+ });
1399
+
1400
+ const markInputApplied: SubmissionLedger["Service"]["markInputApplied"] = Effect.fn(
1401
+ "DoSubmissionLedger.markInputApplied",
1402
+ )(function* (request: MarkInputAppliedRequest) {
1403
+ const operation = "ledger mark input applied";
1404
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkInputAppliedRequest))(
1405
+ request,
1406
+ ).pipe(Effect.mapError(internalFailure(operation)));
1407
+ yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1408
+ yield* inWriteTransaction(
1409
+ operation,
1410
+ Effect.gen(function* () {
1411
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1412
+ yield* requireOwnership(operation, submission, validated.ownershipToken);
1413
+ if (submission.input_applied_record_id !== null) {
1414
+ if (
1415
+ submission.input_applied_record_id === validated.recordId &&
1416
+ submission.input_applied_sequence === validated.sequence
1417
+ ) {
1418
+ return;
1419
+ }
1420
+ return yield* corruptionFailure(
1421
+ operation,
1422
+ "effect_agent_submissions",
1423
+ validated.submissionId,
1424
+ "A different canonical input marker is already recorded for this Submission.",
1425
+ );
1426
+ }
1427
+ yield* sql`
1428
+ UPDATE effect_agent_submissions
1429
+ SET
1430
+ input_applied_record_id = ${validated.recordId},
1431
+ input_applied_sequence = ${validated.sequence},
1432
+ state = CASE
1433
+ WHEN state IN ('admitted', 'ready', 'running') THEN 'input-applied'
1434
+ ELSE state
1435
+ END
1436
+ WHERE submission_id = ${validated.submissionId}
1437
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1438
+ }),
1439
+ );
1440
+ yield* hitFailpoint("ledger:mark-input-applied:after", operation);
1441
+ });
1442
+
1443
+ const reserveSettlement: SubmissionLedger["Service"]["reserveSettlement"] = Effect.fn(
1444
+ "DoSubmissionLedger.reserveSettlement",
1445
+ )(function* (request: SettlementReservation) {
1446
+ const operation = "ledger reserve settlement";
1447
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementReservation))(
1448
+ request,
1449
+ ).pipe(Effect.mapError(internalFailure(operation)));
1450
+ const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(
1451
+ Effect.mapError(internalFailure(operation)),
1452
+ );
1453
+ // The reserved record is appended canonically later; refuse over-bound payloads typed
1454
+ // before the reservation row exists.
1455
+ yield* journal
1456
+ .checkValueBound(operation, recordJson)
1457
+ .pipe(Effect.mapError(internalFailure(operation)));
1458
+ yield* hitFailpoint("ledger:reserve-settlement:before", operation);
1459
+ const reserved = yield* inWriteTransaction(
1460
+ operation,
1461
+ Effect.gen(function* () {
1462
+ const existing = yield* readReservation(operation, validated.submissionId);
1463
+ if (Option.isSome(existing)) {
1464
+ const identical =
1465
+ existing.value.settlement_id === validated.settlementId &&
1466
+ existing.value.outcome === validated.outcome &&
1467
+ existing.value.record_digest === validated.recordDigest &&
1468
+ existing.value.record_json === recordJson;
1469
+ if (!identical) {
1470
+ return yield* SettlementConflict.make({
1471
+ submissionId: validated.submissionId,
1472
+ existingOutcome: existing.value.outcome,
1473
+ });
1474
+ }
1475
+ const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(
1476
+ Effect.mapError((error) =>
1477
+ corruptionFailure(
1478
+ operation,
1479
+ "effect_agent_settlement_reservations",
1480
+ validated.submissionId,
1481
+ error.message,
1482
+ ),
1483
+ ),
1484
+ );
1485
+ return ReservedSettlement.make({
1486
+ submissionId: validated.submissionId,
1487
+ settlementId: validated.settlementId,
1488
+ outcome: validated.outcome,
1489
+ record,
1490
+ recordDigest: validated.recordDigest,
1491
+ replayed: true,
1492
+ });
1493
+ }
1494
+
1495
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1496
+ if (submission.state === "settled") {
1497
+ if (submission.settled_outcome === null) {
1498
+ return yield* corruptionFailure(
1499
+ operation,
1500
+ "effect_agent_submissions",
1501
+ validated.submissionId,
1502
+ "A settled Submission carries no terminal outcome.",
1503
+ );
1504
+ }
1505
+ return yield* SettlementConflict.make({
1506
+ submissionId: validated.submissionId,
1507
+ existingOutcome: submission.settled_outcome,
1508
+ });
1509
+ }
1510
+ // A `joined` Submission settles WITH its host (plan §2.5) and its lane is never
1511
+ // worker-claimable, so no ownership token can exist for it: the recorded host linkage
1512
+ // authorizes the reservation and the presented token is not consulted.
1513
+ if (!(submission.state === "joined" && submission.joined_host_submission_id !== null)) {
1514
+ // P7 §7(c): an aborted, never-claimed, still-queued Submission likewise has no live
1515
+ // ownership to fence against — its durable abort intent authorizes exactly its
1516
+ // ABORTED settlement (`terminalizing` is the same pass's crash replay). Every other
1517
+ // reservation stays fenced by the target lane's live ownership.
1518
+ let queuedAbortSettlement = false;
1519
+ if (
1520
+ validated.outcome === "aborted" &&
1521
+ (submission.state === "ready" || submission.state === "terminalizing")
1522
+ ) {
1523
+ const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
1524
+ if (Option.isSome(abortIntent)) {
1525
+ const ownership = yield* readOwnership(operation, validated.submissionId);
1526
+ queuedAbortSettlement = Option.isNone(ownership);
1527
+ }
1528
+ }
1529
+ if (!queuedAbortSettlement) {
1530
+ yield* requireOwnership(operation, submission, validated.ownershipToken);
1531
+ }
1532
+ }
1533
+ const now = yield* currentInstant;
1534
+ yield* sql`
1535
+ INSERT INTO effect_agent_settlement_reservations (
1536
+ submission_id,
1537
+ settlement_id,
1538
+ outcome,
1539
+ record_id,
1540
+ record_json,
1541
+ record_digest,
1542
+ reserved_at
1543
+ ) VALUES (
1544
+ ${validated.submissionId},
1545
+ ${validated.settlementId},
1546
+ ${validated.outcome},
1547
+ ${validated.record.recordId},
1548
+ ${recordJson},
1549
+ ${validated.recordDigest},
1550
+ ${now.iso}
1551
+ )
1552
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1553
+ yield* sql`
1554
+ UPDATE effect_agent_submissions
1555
+ SET state = 'terminalizing'
1556
+ WHERE submission_id = ${validated.submissionId}
1557
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1558
+ return ReservedSettlement.make({
1559
+ submissionId: validated.submissionId,
1560
+ settlementId: validated.settlementId,
1561
+ outcome: validated.outcome,
1562
+ record: validated.record,
1563
+ recordDigest: validated.recordDigest,
1564
+ replayed: false,
1565
+ });
1566
+ }),
1567
+ );
1568
+ yield* hitFailpoint("ledger:reserve-settlement:after", operation);
1569
+ return reserved;
1570
+ });
1571
+
1572
+ const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
1573
+ "DoSubmissionLedger.finalizeSettlement",
1574
+ )(function* (request: SettlementFinalization) {
1575
+ const operation = "ledger finalize settlement";
1576
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SettlementFinalization))(
1577
+ request,
1578
+ ).pipe(Effect.mapError(internalFailure(operation)));
1579
+ yield* hitFailpoint("ledger:finalize-settlement:before", operation);
1580
+ const settlement = yield* inWriteTransaction(
1581
+ operation,
1582
+ Effect.gen(function* () {
1583
+ const reservation = yield* readReservation(operation, validated.submissionId);
1584
+ if (Option.isNone(reservation)) {
1585
+ return yield* LedgerError.make({
1586
+ operation,
1587
+ message: `No settlement reservation exists for submission ${validated.submissionId}.`,
1588
+ });
1589
+ }
1590
+ if (reservation.value.settlement_id !== validated.settlementId) {
1591
+ return yield* SettlementConflict.make({
1592
+ submissionId: validated.submissionId,
1593
+ existingOutcome: reservation.value.outcome,
1594
+ });
1595
+ }
1596
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1597
+ if (submission.state === "settled") {
1598
+ if (reservation.value.finalized_at === null) {
1599
+ return yield* corruptionFailure(
1600
+ operation,
1601
+ "effect_agent_settlement_reservations",
1602
+ validated.submissionId,
1603
+ "A settled Submission's reservation carries no finalization timestamp.",
1604
+ );
1605
+ }
1606
+ return yield* decodeSettlement({
1607
+ submissionId: validated.submissionId,
1608
+ settlementId: validated.settlementId,
1609
+ receiptId: submission.receipt_id,
1610
+ outcome: reservation.value.outcome,
1611
+ settledAt: reservation.value.finalized_at,
1612
+ }).pipe(Effect.mapError(internalFailure(operation)));
1613
+ }
1614
+ const now = yield* currentInstant;
1615
+ yield* sql`
1616
+ UPDATE effect_agent_submissions
1617
+ SET state = 'settled', settled_outcome = ${reservation.value.outcome}
1618
+ WHERE submission_id = ${validated.submissionId}
1619
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1620
+ yield* sql`
1621
+ UPDATE effect_agent_settlement_reservations
1622
+ SET finalized_at = ${now.iso}
1623
+ WHERE submission_id = ${validated.submissionId}
1624
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1625
+ yield* sql`
1626
+ DELETE FROM effect_agent_submission_ownership
1627
+ WHERE submission_id = ${validated.submissionId}
1628
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1629
+ return yield* decodeSettlement({
1630
+ submissionId: validated.submissionId,
1631
+ settlementId: validated.settlementId,
1632
+ receiptId: submission.receipt_id,
1633
+ outcome: reservation.value.outcome,
1634
+ settledAt: now.iso,
1635
+ }).pipe(Effect.mapError(internalFailure(operation)));
1636
+ }),
1637
+ );
1638
+ yield* hitFailpoint("ledger:finalize-settlement:after", operation);
1639
+ return settlement;
1640
+ });
1641
+
1642
+ const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fn(
1643
+ "DoSubmissionLedger.requestAbort",
1644
+ )(function* (request: AbortCommand) {
1645
+ const operation = "ledger request abort";
1646
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(AbortCommand))(request).pipe(
1647
+ Effect.mapError(internalFailure(operation)),
1648
+ );
1649
+ yield* hitFailpoint("ledger:request-abort:before", operation);
1650
+ const intent = yield* inWriteTransaction(
1651
+ operation,
1652
+ Effect.gen(function* () {
1653
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1654
+ if (submission.state === "settled") {
1655
+ if (submission.settled_outcome === null) {
1656
+ return yield* corruptionFailure(
1657
+ operation,
1658
+ "effect_agent_submissions",
1659
+ validated.submissionId,
1660
+ "A settled Submission carries no terminal outcome.",
1661
+ );
1662
+ }
1663
+ return yield* SettlementConflict.make({
1664
+ submissionId: validated.submissionId,
1665
+ existingOutcome: submission.settled_outcome,
1666
+ });
1667
+ }
1668
+ // A joined Submission settles WITH its host; the abort target is the host (plan
1669
+ // §2.5). A joining Submission still records the intent: it is honored only if the
1670
+ // host has not consumed the input (revert-then-abort).
1671
+ if (submission.state === "joined") {
1672
+ if (submission.joined_host_submission_id === null) {
1673
+ return yield* corruptionFailure(
1674
+ operation,
1675
+ "effect_agent_submissions",
1676
+ validated.submissionId,
1677
+ "A joined Submission carries no host linkage.",
1678
+ );
1679
+ }
1680
+ const hostSubmissionId = yield* decodeSubmissionId(
1681
+ submission.joined_host_submission_id,
1682
+ ).pipe(Effect.mapError(internalFailure(operation)));
1683
+ return yield* JoinedToHost.make({
1684
+ submissionId: validated.submissionId,
1685
+ hostSubmissionId,
1686
+ });
1687
+ }
1688
+ const existing = yield* readAbortIntent(operation, validated.submissionId);
1689
+ if (Option.isSome(existing)) {
1690
+ return yield* abortIntentFromRow(
1691
+ operation,
1692
+ submission,
1693
+ validated.submissionId,
1694
+ existing.value,
1695
+ );
1696
+ }
1697
+ const now = yield* currentInstant;
1698
+ yield* sql`
1699
+ INSERT INTO effect_agent_abort_intents (
1700
+ submission_id,
1701
+ author,
1702
+ reason,
1703
+ requested_at
1704
+ ) VALUES (
1705
+ ${validated.submissionId},
1706
+ ${validated.author},
1707
+ ${validated.reason},
1708
+ ${now.iso}
1709
+ )
1710
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1711
+ const canonicalRecordId = yield* canonicalAbortRecordId(
1712
+ operation,
1713
+ submission.conversation_id,
1714
+ validated.submissionId,
1715
+ );
1716
+ return yield* decodeAbortIntent({
1717
+ submissionId: validated.submissionId,
1718
+ author: validated.author,
1719
+ reason: validated.reason,
1720
+ requestedAt: now.iso,
1721
+ ...(canonicalRecordId === undefined ? {} : { canonicalRecordId }),
1722
+ }).pipe(Effect.mapError(internalFailure(operation)));
1723
+ }),
1724
+ );
1725
+ yield* hitFailpoint("ledger:request-abort:after", operation);
1726
+ return intent;
1727
+ });
1728
+
1729
+ const claimJoining: SubmissionLedger["Service"]["claimJoining"] = Effect.fn(
1730
+ "DoSubmissionLedger.claimJoining",
1731
+ )(function* (request: ClaimJoiningRequest) {
1732
+ const operation = "ledger claim joining";
1733
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ClaimJoiningRequest))(
1734
+ request,
1735
+ ).pipe(Effect.mapError(internalFailure(operation)));
1736
+ yield* hitFailpoint("ledger:claim-joining:before", operation);
1737
+ const claims = yield* inWriteTransaction(
1738
+ operation,
1739
+ Effect.gen(function* () {
1740
+ const host = yield* requireSubmission(operation, validated.hostSubmissionId);
1741
+ if (host.conversation_id !== validated.conversationId) {
1742
+ return yield* LedgerError.make({
1743
+ operation,
1744
+ message: `Host submission ${validated.hostSubmissionId} does not belong to conversation ${validated.conversationId}.`,
1745
+ });
1746
+ }
1747
+ // The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
1748
+ yield* requireOwnership(operation, host, validated.ownershipToken);
1749
+ const laterRows = yield* sql<Record<string, unknown>>`
1750
+ SELECT ${sql.literal(SUBMISSION_COLUMNS)}
1751
+ FROM effect_agent_submissions
1752
+ WHERE conversation_id = ${validated.conversationId}
1753
+ AND queue_sequence > ${host.queue_sequence}
1754
+ ORDER BY queue_sequence ASC
1755
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1756
+ const later = yield* decodeSubmissionRows(operation, validated.conversationId, laterRows);
1757
+ const claimed: Array<JoiningClaim> = [];
1758
+ for (const row of later) {
1759
+ if (claimed.length >= validated.maxCount) break;
1760
+ // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
1761
+ // the coordinator re-delivers already-joined input through the coverage rule.
1762
+ if (
1763
+ (row.state === "joining" || row.state === "joined") &&
1764
+ row.joined_host_submission_id === validated.hostSubmissionId
1765
+ ) {
1766
+ continue;
1767
+ }
1768
+ // P7 §7(c): an aborted-settled row is a CLOSED obligation, not a gap — recovery
1769
+ // settles aborted never-claimed queued work immediately, and settlement order of
1770
+ // never-run work is not execution order (DUR-004 bounds execution).
1771
+ if (row.state === "settled" && row.settled_outcome === "aborted") continue;
1772
+ // Any other non-ready row — an admitted-not-ready gap in particular — breaks the
1773
+ // contiguous ready prefix (plan §2.5); later ready work stays queued (DUR-004).
1774
+ if (row.state !== "ready") break;
1775
+ yield* sql`
1776
+ UPDATE effect_agent_submissions
1777
+ SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
1778
+ WHERE submission_id = ${row.submission_id}
1779
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1780
+ const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
1781
+ Effect.mapError((error) =>
1782
+ corruptionFailure(
1783
+ operation,
1784
+ "effect_agent_submissions",
1785
+ row.submission_id,
1786
+ error.message,
1787
+ ),
1788
+ ),
1789
+ );
1790
+ claimed.push(
1791
+ yield* decodeJoiningClaim({
1792
+ submissionId: row.submission_id,
1793
+ queueSequence: row.queue_sequence,
1794
+ inputPayload,
1795
+ }).pipe(Effect.mapError(internalFailure(operation))),
1796
+ );
1797
+ }
1798
+ return claimed as ReadonlyArray<JoiningClaim>;
1799
+ }),
1800
+ );
1801
+ yield* hitFailpoint("ledger:claim-joining:after", operation);
1802
+ return claims;
1803
+ });
1804
+
1805
+ const markJoined: SubmissionLedger["Service"]["markJoined"] = Effect.fn(
1806
+ "DoSubmissionLedger.markJoined",
1807
+ )(function* (request: MarkJoinedRequest) {
1808
+ const operation = "ledger mark joined";
1809
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkJoinedRequest))(
1810
+ request,
1811
+ ).pipe(Effect.mapError(internalFailure(operation)));
1812
+ yield* hitFailpoint("ledger:mark-joined:before", operation);
1813
+ yield* inWriteTransaction(
1814
+ operation,
1815
+ Effect.gen(function* () {
1816
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1817
+ if (submission.joined_host_submission_id === null) {
1818
+ return yield* LedgerError.make({
1819
+ operation,
1820
+ message: `Submission ${validated.submissionId} was never claimed for joining.`,
1821
+ });
1822
+ }
1823
+ const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
1824
+ // The lane is host-owned: the presented token must own the HOST's ownership period,
1825
+ // which also lets a later host Attempt repair a lost marker from history (DUR-016).
1826
+ yield* requireOwnership(operation, host, validated.ownershipToken);
1827
+ if (submission.input_applied_record_id !== null) {
1828
+ if (
1829
+ submission.input_applied_record_id === validated.recordId &&
1830
+ submission.input_applied_sequence === validated.sequence
1831
+ ) {
1832
+ return;
1833
+ }
1834
+ return yield* corruptionFailure(
1835
+ operation,
1836
+ "effect_agent_submissions",
1837
+ validated.submissionId,
1838
+ "A different join marker is already recorded for this Submission.",
1839
+ );
1840
+ }
1841
+ if (submission.state !== "joining" && submission.state !== "joined") {
1842
+ return yield* LedgerError.make({
1843
+ operation,
1844
+ message: `Cannot mark submission ${validated.submissionId} joined from state ${submission.state}.`,
1845
+ });
1846
+ }
1847
+ yield* sql`
1848
+ UPDATE effect_agent_submissions
1849
+ SET
1850
+ input_applied_record_id = ${validated.recordId},
1851
+ input_applied_sequence = ${validated.sequence},
1852
+ state = 'joined'
1853
+ WHERE submission_id = ${validated.submissionId}
1854
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1855
+ }),
1856
+ );
1857
+ yield* hitFailpoint("ledger:mark-joined:after", operation);
1858
+ });
1859
+
1860
+ const revertJoining: SubmissionLedger["Service"]["revertJoining"] = Effect.fn(
1861
+ "DoSubmissionLedger.revertJoining",
1862
+ )(function* (request: RevertJoiningRequest) {
1863
+ const operation = "ledger revert joining";
1864
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RevertJoiningRequest))(
1865
+ request,
1866
+ ).pipe(Effect.mapError(internalFailure(operation)));
1867
+ yield* hitFailpoint("ledger:revert-joining:before", operation);
1868
+ yield* inWriteTransaction(
1869
+ operation,
1870
+ Effect.gen(function* () {
1871
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1872
+ // Idempotent and recovery-only: only a still-`joining` Submission reverts; an
1873
+ // already-joined (or already-reverted) Submission is a no-op (DUR-016).
1874
+ if (submission.state !== "joining") return;
1875
+ yield* sql`
1876
+ UPDATE effect_agent_submissions
1877
+ SET state = 'ready', joined_host_submission_id = NULL
1878
+ WHERE submission_id = ${validated.submissionId}
1879
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1880
+ }),
1881
+ );
1882
+ yield* hitFailpoint("ledger:revert-joining:after", operation);
1883
+ });
1884
+
1885
+ const suspend: SubmissionLedger["Service"]["suspend"] = Effect.fn("DoSubmissionLedger.suspend")(
1886
+ function* (request: SuspendRequest) {
1887
+ const operation = "ledger suspend";
1888
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SuspendRequest))(
1889
+ request,
1890
+ ).pipe(Effect.mapError(internalFailure(operation)));
1891
+ const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
1892
+ Effect.mapError(internalFailure(operation)),
1893
+ );
1894
+ yield* hitFailpoint("ledger:suspend:before", operation);
1895
+ const outcome = yield* inWriteTransaction(
1896
+ operation,
1897
+ Effect.gen(function* () {
1898
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1899
+ if (submission.state === "settled") {
1900
+ if (submission.settled_outcome === null) {
1901
+ return yield* corruptionFailure(
1902
+ operation,
1903
+ "effect_agent_submissions",
1904
+ validated.submissionId,
1905
+ "A settled Submission carries no terminal outcome.",
1906
+ );
1907
+ }
1908
+ return yield* SettlementConflict.make({
1909
+ submissionId: validated.submissionId,
1910
+ existingOutcome: submission.settled_outcome,
1911
+ });
1912
+ }
1913
+ // An exact terminal outcome is already reserved (DUR-011); suspension would
1914
+ // contradict it, so the reservation wins.
1915
+ const reservation = yield* readReservation(operation, validated.submissionId);
1916
+ if (Option.isSome(reservation)) {
1917
+ return yield* SettlementConflict.make({
1918
+ submissionId: validated.submissionId,
1919
+ existingOutcome: reservation.value.outcome,
1920
+ });
1921
+ }
1922
+ yield* requireOwnership(operation, submission, validated.ownershipToken);
1923
+ // A covering event that raced ahead of the suspend transaction resumes the caller
1924
+ // immediately WITHOUT releasing the lane (plan §2.6, §12). For WaitingForChild the
1925
+ // covering evidence is EITHER a locally settled child row OR a durable cross-store
1926
+ // notification marker: parent and child Conversations live in different Durable
1927
+ // Objects, and the port contract requires that a child settlement reported (via
1928
+ // `recordChildSettled` → marker) before this suspend commits is observed here.
1929
+ if (validated.reason._tag === "ApprovalPending") {
1930
+ const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
1931
+ const decided = new Set(decisions.map((row) => row.tool_call_id));
1932
+ if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
1933
+ return "resume-immediately" as SuspensionOutcome;
1934
+ }
1935
+ } else {
1936
+ const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
1937
+ const markerChildren = new Set(markers.map((row) => row.child_submission_id));
1938
+ let allSettled = true;
1939
+ for (const child of validated.reason.children) {
1940
+ const settled = yield* childProvablySettled(
1941
+ operation,
1942
+ markerChildren,
1943
+ child.childSubmissionId,
1944
+ );
1945
+ if (!settled) {
1946
+ allSettled = false;
1947
+ break;
1948
+ }
1949
+ }
1950
+ if (allSettled) {
1951
+ return "resume-immediately" as SuspensionOutcome;
1952
+ }
1953
+ }
1954
+ const now = yield* currentInstant;
1955
+ yield* sql`
1956
+ UPDATE effect_agent_submissions
1957
+ SET
1958
+ state = 'suspended',
1959
+ suspended_reason_json = ${reasonJson},
1960
+ suspended_at = ${now.iso}
1961
+ WHERE submission_id = ${validated.submissionId}
1962
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1963
+ // Suspension ends the ownership period WITHOUT settling: the accepted-work
1964
+ // obligation stays owed while the lane consumes no worker permit (plan §2.6).
1965
+ yield* sql`
1966
+ DELETE FROM effect_agent_submission_ownership
1967
+ WHERE submission_id = ${validated.submissionId}
1968
+ `.pipe(Effect.mapError(sqlFailure(operation)));
1969
+ return "suspended" as SuspensionOutcome;
1970
+ }),
1971
+ );
1972
+ yield* hitFailpoint("ledger:suspend:after", operation);
1973
+ return outcome;
1974
+ },
1975
+ );
1976
+
1977
+ /**
1978
+ * Once every pending call of a recorded ApprovalPending suspension has a decision intent,
1979
+ * the lane wakes: suspended → input-applied, suspension cleared (plan §2.6). A
1980
+ * WaitingForChild suspension wakes only through recordChildSettled. Runs inside the caller's
1981
+ * write transaction.
1982
+ */
1983
+ const wakeSuspendedIfCovered = Effect.fn("DoSubmissionLedger.wakeSuspendedIfCovered")(function* (
1984
+ operation: string,
1985
+ submission: SubmissionRow,
1986
+ ): Effect.fn.Return<void, LedgerError> {
1987
+ if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
1988
+ const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
1989
+ submission.suspended_reason_json,
1990
+ ).pipe(
1991
+ Effect.mapError((error) =>
1992
+ corruptionFailure(
1993
+ operation,
1994
+ "effect_agent_submissions",
1995
+ submission.submission_id,
1996
+ error.message,
1997
+ ),
1998
+ ),
1999
+ );
2000
+ if (reason._tag !== "ApprovalPending") return;
2001
+ const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
2002
+ const decided = new Set(decisions.map((row) => row.tool_call_id));
2003
+ if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
2004
+ yield* sql`
2005
+ UPDATE effect_agent_submissions
2006
+ SET
2007
+ state = 'input-applied',
2008
+ suspended_reason_json = NULL,
2009
+ suspended_at = NULL
2010
+ WHERE submission_id = ${submission.submission_id}
2011
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2012
+ });
2013
+
2014
+ const recordApprovalDecision: SubmissionLedger["Service"]["recordApprovalDecision"] = Effect.fn(
2015
+ "DoSubmissionLedger.recordApprovalDecision",
2016
+ )(function* (command: ApprovalDecisionCommand) {
2017
+ const operation = "ledger record approval decision";
2018
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ApprovalDecisionCommand))(
2019
+ command,
2020
+ ).pipe(Effect.mapError(internalFailure(operation)));
2021
+ yield* hitFailpoint("ledger:approval-decision:before", operation);
2022
+ const intent = yield* inWriteTransaction(
2023
+ operation,
2024
+ Effect.gen(function* () {
2025
+ const submission = yield* requireSubmission(operation, validated.submissionId);
2026
+ if (submission.state === "settled") {
2027
+ if (submission.settled_outcome === null) {
2028
+ return yield* corruptionFailure(
2029
+ operation,
2030
+ "effect_agent_submissions",
2031
+ validated.submissionId,
2032
+ "A settled Submission carries no terminal outcome.",
2033
+ );
2034
+ }
2035
+ return yield* SettlementConflict.make({
2036
+ submissionId: validated.submissionId,
2037
+ existingOutcome: submission.settled_outcome,
2038
+ });
2039
+ }
2040
+ const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
2041
+ const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
2042
+ if (existing !== undefined) {
2043
+ // Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
2044
+ // the recorded intent unchanged; a divergent re-decision conflicts.
2045
+ if (existing.decision !== validated.decision) {
2046
+ return yield* ApprovalConflict.make({
2047
+ submissionId: validated.submissionId,
2048
+ toolCallId: validated.toolCallId,
2049
+ existingDecision: existing.decision,
2050
+ });
2051
+ }
2052
+ return yield* approvalIntentFromRow(operation, existing);
2053
+ }
2054
+ const now = yield* currentInstant;
2055
+ yield* sql`
2056
+ INSERT INTO effect_agent_approval_decisions (
2057
+ submission_id,
2058
+ tool_call_id,
2059
+ decision,
2060
+ resolver,
2061
+ reason,
2062
+ decided_at
2063
+ ) VALUES (
2064
+ ${validated.submissionId},
2065
+ ${validated.toolCallId},
2066
+ ${validated.decision},
2067
+ ${validated.resolver},
2068
+ ${validated.reason},
2069
+ ${now.iso}
2070
+ )
2071
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2072
+ yield* wakeSuspendedIfCovered(operation, submission);
2073
+ return yield* decodeApprovalDecisionIntent({
2074
+ submissionId: validated.submissionId,
2075
+ toolCallId: validated.toolCallId,
2076
+ decision: validated.decision,
2077
+ resolver: validated.resolver,
2078
+ reason: validated.reason,
2079
+ decidedAt: now.iso,
2080
+ }).pipe(Effect.mapError(internalFailure(operation)));
2081
+ }),
2082
+ );
2083
+ yield* hitFailpoint("ledger:approval-decision:after", operation);
2084
+ return intent;
2085
+ });
2086
+
2087
+ const markUnknown: SubmissionLedger["Service"]["markUnknown"] = Effect.fn(
2088
+ "DoSubmissionLedger.markUnknown",
2089
+ )(function* (request: MarkUnknownRequest) {
2090
+ const operation = "ledger mark unknown";
2091
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(MarkUnknownRequest))(
2092
+ request,
2093
+ ).pipe(Effect.mapError(internalFailure(operation)));
2094
+ yield* hitFailpoint("ledger:mark-unknown:before", operation);
2095
+ yield* inWriteTransaction(
2096
+ operation,
2097
+ Effect.gen(function* () {
2098
+ const submission = yield* requireSubmission(operation, validated.submissionId);
2099
+ if (submission.state === "settled") {
2100
+ if (submission.settled_outcome === null) {
2101
+ return yield* corruptionFailure(
2102
+ operation,
2103
+ "effect_agent_submissions",
2104
+ validated.submissionId,
2105
+ "A settled Submission carries no terminal outcome.",
2106
+ );
2107
+ }
2108
+ return yield* SettlementConflict.make({
2109
+ submissionId: validated.submissionId,
2110
+ existingOutcome: submission.settled_outcome,
2111
+ });
2112
+ }
2113
+ // A reserved exact outcome wins over a late Unknown marking (DUR-011); the recovery
2114
+ // classifier orders reservation ahead of MarkUnknown for the same reason.
2115
+ const reservation = yield* readReservation(operation, validated.submissionId);
2116
+ if (Option.isSome(reservation)) {
2117
+ return yield* SettlementConflict.make({
2118
+ submissionId: validated.submissionId,
2119
+ existingOutcome: reservation.value.outcome,
2120
+ });
2121
+ }
2122
+ // Idempotent merge: repeating is a no-op; additional open calls extend the marked
2123
+ // set while the first recorded reason is kept.
2124
+ const existingIds = yield* storedUnknownToolCallIds(operation, submission);
2125
+ const known = new Set(existingIds);
2126
+ const merged = [
2127
+ ...existingIds,
2128
+ ...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
2129
+ ];
2130
+ const idsJson = yield* encodeToolCallIdsText(merged).pipe(
2131
+ Effect.mapError(internalFailure(operation)),
2132
+ );
2133
+ yield* sql`
2134
+ UPDATE effect_agent_submissions
2135
+ SET
2136
+ state = 'unknown',
2137
+ unknown_reason = ${submission.unknown_reason ?? validated.reason},
2138
+ unknown_tool_call_ids_json = ${idsJson}
2139
+ WHERE submission_id = ${validated.submissionId}
2140
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2141
+ }),
2142
+ );
2143
+ yield* hitFailpoint("ledger:mark-unknown:after", operation);
2144
+ });
2145
+
2146
+ const recordUnknownResolution: SubmissionLedger["Service"]["recordUnknownResolution"] = Effect.fn(
2147
+ "DoSubmissionLedger.recordUnknownResolution",
2148
+ )(function* (command: UnknownResolutionCommand) {
2149
+ const operation = "ledger record unknown resolution";
2150
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(UnknownResolutionCommand))(
2151
+ command,
2152
+ ).pipe(Effect.mapError(internalFailure(operation)));
2153
+ const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
2154
+ Effect.mapError(internalFailure(operation)),
2155
+ );
2156
+ yield* hitFailpoint("ledger:unknown-resolution:before", operation);
2157
+ const intent = yield* inWriteTransaction(
2158
+ operation,
2159
+ Effect.gen(function* () {
2160
+ const submission = yield* requireSubmission(operation, validated.submissionId);
2161
+ if (submission.state === "settled") {
2162
+ if (submission.settled_outcome === null) {
2163
+ return yield* corruptionFailure(
2164
+ operation,
2165
+ "effect_agent_submissions",
2166
+ validated.submissionId,
2167
+ "A settled Submission carries no terminal outcome.",
2168
+ );
2169
+ }
2170
+ return yield* SettlementConflict.make({
2171
+ submissionId: validated.submissionId,
2172
+ existingOutcome: submission.settled_outcome,
2173
+ });
2174
+ }
2175
+ const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
2176
+ const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
2177
+ if (existing !== undefined && existing.resolution_json !== resolutionJson) {
2178
+ return yield* UnknownResolutionConflict.make({
2179
+ submissionId: validated.submissionId,
2180
+ toolCallId: validated.toolCallId,
2181
+ });
2182
+ }
2183
+ let resolved: UnknownResolutionIntent;
2184
+ if (existing !== undefined) {
2185
+ // Idempotent replay of the recorded intent (author/reason may differ; the stored
2186
+ // audit fields win, exactly like requestAbort).
2187
+ resolved = yield* unknownResolutionIntentFromRow(operation, existing);
2188
+ } else {
2189
+ const now = yield* currentInstant;
2190
+ yield* sql`
2191
+ INSERT INTO effect_agent_unknown_resolutions (
2192
+ submission_id,
2193
+ tool_call_id,
2194
+ author,
2195
+ reason,
2196
+ resolution_json,
2197
+ resolved_at
2198
+ ) VALUES (
2199
+ ${validated.submissionId},
2200
+ ${validated.toolCallId},
2201
+ ${validated.author},
2202
+ ${validated.reason},
2203
+ ${resolutionJson},
2204
+ ${now.iso}
2205
+ )
2206
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2207
+ const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
2208
+ Effect.mapError(internalFailure(operation)),
2209
+ );
2210
+ resolved = yield* decodeUnknownResolutionIntent({
2211
+ submissionId: validated.submissionId,
2212
+ toolCallId: validated.toolCallId,
2213
+ author: validated.author,
2214
+ reason: validated.reason,
2215
+ resolution,
2216
+ resolvedAt: now.iso,
2217
+ }).pipe(Effect.mapError(internalFailure(operation)));
2218
+ }
2219
+ // The lane reopens only when EVERY marked open call has a durable resolution intent:
2220
+ // unknown → input-applied (DUR-017). Replays re-run the coverage check so a
2221
+ // recovering caller can wake the lane idempotently.
2222
+ if (submission.state === "unknown" && submission.unknown_tool_call_ids_json !== null) {
2223
+ const markedIds = yield* storedUnknownToolCallIds(operation, submission);
2224
+ const covering = yield* readUnknownResolutions(operation, validated.submissionId);
2225
+ const coveredIds = new Set(covering.map((row) => row.tool_call_id));
2226
+ if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) {
2227
+ yield* sql`
2228
+ UPDATE effect_agent_submissions
2229
+ SET
2230
+ state = 'input-applied',
2231
+ unknown_reason = NULL,
2232
+ unknown_tool_call_ids_json = NULL
2233
+ WHERE submission_id = ${validated.submissionId}
2234
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2235
+ }
2236
+ }
2237
+ return resolved;
2238
+ }),
2239
+ );
2240
+ yield* hitFailpoint("ledger:unknown-resolution:after", operation);
2241
+ return intent;
2242
+ });
2243
+
2244
+ const recordChildSettled: SubmissionLedger["Service"]["recordChildSettled"] = Effect.fn(
2245
+ "DoSubmissionLedger.recordChildSettled",
2246
+ )(function* (request: ChildSettledNotification) {
2247
+ const operation = "ledger record child settled";
2248
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ChildSettledNotification))(
2249
+ request,
2250
+ ).pipe(Effect.mapError(internalFailure(operation)));
2251
+ yield* hitFailpoint("ledger:child-settled:before", operation);
2252
+ const outcome = yield* inWriteTransaction(
2253
+ operation,
2254
+ Effect.gen(function* () {
2255
+ const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2256
+ // The child's canonical Settlement is the authority for this wake. When the child's
2257
+ // row lives in THIS store (single-store latitude, and every conformance lane), an
2258
+ // unsettled child makes the notification a caller error exactly as on Node. When it
2259
+ // does not — the normal cross-DO case — the routed notification from the child's
2260
+ // owning Durable Object is the settlement evidence this store records durably.
2261
+ const child = yield* readSubmission(operation, validated.childSubmissionId);
2262
+ if (Option.isSome(child) && child.value.state !== "settled") {
2263
+ return yield* LedgerError.make({
2264
+ operation,
2265
+ message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`,
2266
+ });
2267
+ }
2268
+ // Record the durable notification marker FIRST and unconditionally (idempotent):
2269
+ // the port's cross-store race guarantee requires that a notification committed
2270
+ // before the parent's suspend transaction is observed by that suspend's covering
2271
+ // check, even when the parent is not (or not yet) suspended.
2272
+ const now = yield* currentInstant;
2273
+ yield* sql`
2274
+ INSERT INTO effect_agent_child_settlements (
2275
+ parent_submission_id,
2276
+ child_submission_id,
2277
+ child_outcome,
2278
+ recorded_at
2279
+ ) VALUES (
2280
+ ${validated.parentSubmissionId},
2281
+ ${validated.childSubmissionId},
2282
+ ${Option.isSome(child) ? child.value.settled_outcome : null},
2283
+ ${now.iso}
2284
+ )
2285
+ ON CONFLICT (parent_submission_id, child_submission_id) DO NOTHING
2286
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2287
+
2288
+ if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
2289
+ return "not-waiting" as ChildSettledOutcome;
2290
+ }
2291
+ const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
2292
+ parent.suspended_reason_json,
2293
+ ).pipe(
2294
+ Effect.mapError((error) =>
2295
+ corruptionFailure(
2296
+ operation,
2297
+ "effect_agent_submissions",
2298
+ parent.submission_id,
2299
+ error.message,
2300
+ ),
2301
+ ),
2302
+ );
2303
+ if (reason._tag !== "WaitingForChild") {
2304
+ return "not-waiting" as ChildSettledOutcome;
2305
+ }
2306
+ if (
2307
+ !reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
2308
+ ) {
2309
+ return "not-waiting" as ChildSettledOutcome;
2310
+ }
2311
+ // The parent wakes exactly when EVERY listed child is provably settled — from its
2312
+ // local row or a recorded marker (spec §12 step 10); replays re-run the coverage
2313
+ // check so a recovering caller wakes the lane idempotently.
2314
+ const markers = yield* readChildSettlementMarkers(operation, validated.parentSubmissionId);
2315
+ const markerChildren = new Set(markers.map((row) => row.child_submission_id));
2316
+ for (const entry of reason.children) {
2317
+ const settled = yield* childProvablySettled(
2318
+ operation,
2319
+ markerChildren,
2320
+ entry.childSubmissionId,
2321
+ );
2322
+ if (!settled) {
2323
+ return "still-waiting" as ChildSettledOutcome;
2324
+ }
2325
+ }
2326
+ yield* sql`
2327
+ UPDATE effect_agent_submissions
2328
+ SET
2329
+ state = 'input-applied',
2330
+ suspended_reason_json = NULL,
2331
+ suspended_at = NULL
2332
+ WHERE submission_id = ${validated.parentSubmissionId}
2333
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2334
+ return "woken" as ChildSettledOutcome;
2335
+ }),
2336
+ );
2337
+ yield* hitFailpoint("ledger:child-settled:after", operation);
2338
+ return outcome;
2339
+ });
2340
+
2341
+ const reserveChildBudget: SubmissionLedger["Service"]["reserveChildBudget"] = Effect.fn(
2342
+ "DoSubmissionLedger.reserveChildBudget",
2343
+ )(function* (request: ChildBudgetReservationRequest) {
2344
+ const operation = "ledger reserve child budget";
2345
+ const validated = yield* Schema.decodeUnknownEffect(
2346
+ Schema.toType(ChildBudgetReservationRequest),
2347
+ )(request).pipe(Effect.mapError(internalFailure(operation)));
2348
+ const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
2349
+ Effect.mapError(internalFailure(operation)),
2350
+ );
2351
+ yield* hitFailpoint("ledger:child-reservation:before", operation);
2352
+ const reserved = yield* inWriteTransaction(
2353
+ operation,
2354
+ Effect.gen(function* () {
2355
+ const existing = yield* readChildReservation(operation, validated.reservationId);
2356
+ if (Option.isSome(existing)) {
2357
+ // Identical replays short-circuit before the fence, mirroring reserveSettlement: a
2358
+ // replay creates nothing, so a recovering caller resumes rather than duplicates.
2359
+ const identical =
2360
+ existing.value.parent_submission_id === validated.parentSubmissionId &&
2361
+ existing.value.parent_tool_call_id === validated.parentToolCallId &&
2362
+ existing.value.allocation_digest === validated.allocationDigest &&
2363
+ existing.value.allocation_json === allocationJson;
2364
+ if (!identical) {
2365
+ return yield* ChildReservationConflict.make({
2366
+ reservationId: validated.reservationId,
2367
+ status: existing.value.status,
2368
+ message:
2369
+ "A reservation with this identity exists with a different parent Tool Call or allocation.",
2370
+ });
2371
+ }
2372
+ return ReservedChildBudget.make({
2373
+ reservation: yield* childReservationSnapshotFromRow(operation, existing.value),
2374
+ replayed: true,
2375
+ });
2376
+ }
2377
+ const collision = yield* readChildReservationForCall(
2378
+ operation,
2379
+ validated.parentSubmissionId,
2380
+ validated.parentToolCallId,
2381
+ );
2382
+ if (Option.isSome(collision)) {
2383
+ return yield* ChildReservationConflict.make({
2384
+ reservationId: validated.reservationId,
2385
+ status: collision.value.status,
2386
+ message: `Parent Tool Call ${validated.parentToolCallId} already owns reservation ${collision.value.reservation_id}.`,
2387
+ });
2388
+ }
2389
+ const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
2390
+ // Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
2391
+ // parent Attempt can never create new reservation state.
2392
+ yield* requireOwnership(operation, parent, validated.ownershipToken);
2393
+ const now = yield* currentInstant;
2394
+ yield* sql`
2395
+ INSERT INTO effect_agent_child_reservations (
2396
+ reservation_id,
2397
+ parent_submission_id,
2398
+ parent_tool_call_id,
2399
+ status,
2400
+ allocation_json,
2401
+ allocation_digest,
2402
+ reserved_at
2403
+ ) VALUES (
2404
+ ${validated.reservationId},
2405
+ ${validated.parentSubmissionId},
2406
+ ${validated.parentToolCallId},
2407
+ 'reserved',
2408
+ ${allocationJson},
2409
+ ${validated.allocationDigest},
2410
+ ${now.iso}
2411
+ )
2412
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2413
+ const inserted = yield* readChildReservation(operation, validated.reservationId);
2414
+ if (Option.isNone(inserted)) {
2415
+ return yield* corruptionFailure(
2416
+ operation,
2417
+ "effect_agent_child_reservations",
2418
+ validated.reservationId,
2419
+ "An inserted child reservation row is missing inside its own transaction.",
2420
+ );
2421
+ }
2422
+ return ReservedChildBudget.make({
2423
+ reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
2424
+ replayed: false,
2425
+ });
2426
+ }),
2427
+ );
2428
+ yield* hitFailpoint("ledger:child-reservation:after", operation);
2429
+ return reserved;
2430
+ });
2431
+
2432
+ const attachChildToReservation: SubmissionLedger["Service"]["attachChildToReservation"] =
2433
+ Effect.fn("DoSubmissionLedger.attachChildToReservation")(function* (
2434
+ request: AttachChildToReservationRequest,
2435
+ ) {
2436
+ const operation = "ledger attach child to reservation";
2437
+ const validated = yield* Schema.decodeUnknownEffect(
2438
+ Schema.toType(AttachChildToReservationRequest),
2439
+ )(request).pipe(Effect.mapError(internalFailure(operation)));
2440
+ yield* hitFailpoint("ledger:child-attach:before", operation);
2441
+ const attached = yield* inWriteTransaction(
2442
+ operation,
2443
+ Effect.gen(function* () {
2444
+ const existing = yield* readChildReservation(operation, validated.reservationId);
2445
+ if (Option.isNone(existing)) {
2446
+ return yield* LedgerError.make({
2447
+ operation,
2448
+ message: `Unknown child reservation ${validated.reservationId}.`,
2449
+ });
2450
+ }
2451
+ if (existing.value.child_submission_id !== null) {
2452
+ // Idempotent replay of the recorded attachment (unfenced — it mutates nothing).
2453
+ if (existing.value.child_submission_id === validated.childSubmissionId) {
2454
+ return yield* childReservationSnapshotFromRow(operation, existing.value);
2455
+ }
2456
+ return yield* ChildReservationConflict.make({
2457
+ reservationId: validated.reservationId,
2458
+ status: existing.value.status,
2459
+ message: `Reservation ${validated.reservationId} already records child ${existing.value.child_submission_id}.`,
2460
+ });
2461
+ }
2462
+ const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
2463
+ yield* requireOwnership(operation, parent, validated.ownershipToken);
2464
+ if (existing.value.status !== "reserved") {
2465
+ return yield* ChildReservationConflict.make({
2466
+ reservationId: validated.reservationId,
2467
+ status: existing.value.status,
2468
+ message: `Cannot attach a child to a ${existing.value.status} reservation.`,
2469
+ });
2470
+ }
2471
+ // Unlike the single-store Node adapter, the admitted child's row lives in ANOTHER
2472
+ // Durable Object, so no local existence check is possible here. The canonical
2473
+ // `SubagentStarted` record remains the attachment's repair authority (DUR-015),
2474
+ // and the coordinator only attaches after the child's admission committed.
2475
+ yield* sql`
2476
+ UPDATE effect_agent_child_reservations
2477
+ SET child_submission_id = ${validated.childSubmissionId}
2478
+ WHERE reservation_id = ${validated.reservationId}
2479
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2480
+ const updated = yield* readChildReservation(operation, validated.reservationId);
2481
+ if (Option.isNone(updated)) {
2482
+ return yield* corruptionFailure(
2483
+ operation,
2484
+ "effect_agent_child_reservations",
2485
+ validated.reservationId,
2486
+ "An updated child reservation row is missing inside its own transaction.",
2487
+ );
2488
+ }
2489
+ return yield* childReservationSnapshotFromRow(operation, updated.value);
2490
+ }),
2491
+ );
2492
+ yield* hitFailpoint("ledger:child-attach:after", operation);
2493
+ return attached;
2494
+ });
2495
+
2496
+ const beginChildBudgetRelease: SubmissionLedger["Service"]["beginChildBudgetRelease"] = Effect.fn(
2497
+ "DoSubmissionLedger.beginChildBudgetRelease",
2498
+ )(function* (request: BeginChildBudgetReleaseRequest) {
2499
+ const operation = "ledger begin child budget release";
2500
+ const validated = yield* Schema.decodeUnknownEffect(
2501
+ Schema.toType(BeginChildBudgetReleaseRequest),
2502
+ )(request).pipe(Effect.mapError(internalFailure(operation)));
2503
+ const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
2504
+ Effect.mapError(internalFailure(operation)),
2505
+ );
2506
+ yield* hitFailpoint("ledger:child-release-pending:before", operation);
2507
+ const frozen = yield* inWriteTransaction(
2508
+ operation,
2509
+ Effect.gen(function* () {
2510
+ const existing = yield* readChildReservation(operation, validated.reservationId);
2511
+ if (Option.isNone(existing)) {
2512
+ return yield* LedgerError.make({
2513
+ operation,
2514
+ message: `Unknown child reservation ${validated.reservationId}.`,
2515
+ });
2516
+ }
2517
+ if (existing.value.status !== "reserved") {
2518
+ // The accounting decision was already frozen exactly once; an identical replay is a
2519
+ // no-op and a divergent decision conflicts (spec §12 join step 6).
2520
+ if (existing.value.accounting_json === accountingJson) {
2521
+ return yield* childReservationSnapshotFromRow(operation, existing.value);
2522
+ }
2523
+ return yield* ChildReservationConflict.make({
2524
+ reservationId: validated.reservationId,
2525
+ status: existing.value.status,
2526
+ message: "A different accounting decision is already frozen for this reservation.",
2527
+ });
2528
+ }
2529
+ const now = yield* currentInstant;
2530
+ yield* sql`
2531
+ UPDATE effect_agent_child_reservations
2532
+ SET
2533
+ status = 'releasePending',
2534
+ accounting_json = ${accountingJson},
2535
+ release_began_at = ${now.iso}
2536
+ WHERE reservation_id = ${validated.reservationId}
2537
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2538
+ const updated = yield* readChildReservation(operation, validated.reservationId);
2539
+ if (Option.isNone(updated)) {
2540
+ return yield* corruptionFailure(
2541
+ operation,
2542
+ "effect_agent_child_reservations",
2543
+ validated.reservationId,
2544
+ "An updated child reservation row is missing inside its own transaction.",
2545
+ );
2546
+ }
2547
+ return yield* childReservationSnapshotFromRow(operation, updated.value);
2548
+ }),
2549
+ );
2550
+ yield* hitFailpoint("ledger:child-release-pending:after", operation);
2551
+ return frozen;
2552
+ });
2553
+
2554
+ const releaseChildBudget: SubmissionLedger["Service"]["releaseChildBudget"] = Effect.fn(
2555
+ "DoSubmissionLedger.releaseChildBudget",
2556
+ )(function* (request: ReleaseChildBudgetRequest) {
2557
+ const operation = "ledger release child budget";
2558
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ReleaseChildBudgetRequest))(
2559
+ request,
2560
+ ).pipe(Effect.mapError(internalFailure(operation)));
2561
+ yield* hitFailpoint("ledger:child-release:before", operation);
2562
+ const released = yield* inWriteTransaction(
2563
+ operation,
2564
+ Effect.gen(function* () {
2565
+ const existing = yield* readChildReservation(operation, validated.reservationId);
2566
+ if (Option.isNone(existing)) {
2567
+ return yield* LedgerError.make({
2568
+ operation,
2569
+ message: `Unknown child reservation ${validated.reservationId}.`,
2570
+ });
2571
+ }
2572
+ // Applied exactly once: replaying a released reservation returns the stored row
2573
+ // unchanged (spec §12: "never available twice").
2574
+ if (existing.value.status === "released") {
2575
+ return yield* childReservationSnapshotFromRow(operation, existing.value);
2576
+ }
2577
+ if (existing.value.status !== "releasePending") {
2578
+ return yield* ChildReservationConflict.make({
2579
+ reservationId: validated.reservationId,
2580
+ status: existing.value.status,
2581
+ message: "Cannot release a reservation whose accounting decision is not frozen.",
2582
+ });
2583
+ }
2584
+ const now = yield* currentInstant;
2585
+ yield* sql`
2586
+ UPDATE effect_agent_child_reservations
2587
+ SET status = 'released', released_at = ${now.iso}
2588
+ WHERE reservation_id = ${validated.reservationId}
2589
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2590
+ const updated = yield* readChildReservation(operation, validated.reservationId);
2591
+ if (Option.isNone(updated)) {
2592
+ return yield* corruptionFailure(
2593
+ operation,
2594
+ "effect_agent_child_reservations",
2595
+ validated.reservationId,
2596
+ "An updated child reservation row is missing inside its own transaction.",
2597
+ );
2598
+ }
2599
+ return yield* childReservationSnapshotFromRow(operation, updated.value);
2600
+ }),
2601
+ );
2602
+ yield* hitFailpoint("ledger:child-release:after", operation);
2603
+ return released;
2604
+ });
2605
+
2606
+ interface ScanCursor {
2607
+ readonly conversationId: string;
2608
+ readonly queueSequence: number;
2609
+ }
2610
+
2611
+ const scanPage = Effect.fn("DoSubmissionLedger.scanPage")(function* (
2612
+ cursor: ScanCursor | undefined,
2613
+ ): Effect.fn.Return<
2614
+ readonly [ReadonlyArray<SubmissionSnapshot>, Option.Option<ScanCursor | undefined>],
2615
+ LedgerError
2616
+ > {
2617
+ const operation = "ledger scan nonterminal";
2618
+ const rows = yield* (
2619
+ cursor === undefined
2620
+ ? sql<Record<string, unknown>>`
2621
+ SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2622
+ FROM effect_agent_submissions
2623
+ WHERE state <> 'settled'
2624
+ ORDER BY conversation_id ASC, queue_sequence ASC
2625
+ LIMIT ${SCAN_PAGE_SIZE}
2626
+ `
2627
+ : sql<Record<string, unknown>>`
2628
+ SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2629
+ FROM effect_agent_submissions
2630
+ WHERE state <> 'settled'
2631
+ AND (
2632
+ conversation_id > ${cursor.conversationId}
2633
+ OR (
2634
+ conversation_id = ${cursor.conversationId}
2635
+ AND queue_sequence > ${cursor.queueSequence}
2636
+ )
2637
+ )
2638
+ ORDER BY conversation_id ASC, queue_sequence ASC
2639
+ LIMIT ${SCAN_PAGE_SIZE}
2640
+ `
2641
+ ).pipe(Effect.mapError(sqlFailure(operation)));
2642
+ const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
2643
+ const snapshots = yield* Effect.forEach(decoded, (row) =>
2644
+ decodeSubmissionSnapshot(operation, row),
2645
+ );
2646
+ const last = decoded[decoded.length - 1];
2647
+ const next: Option.Option<ScanCursor | undefined> =
2648
+ last === undefined || decoded.length < SCAN_PAGE_SIZE
2649
+ ? Option.none()
2650
+ : Option.some({
2651
+ conversationId: last.conversation_id,
2652
+ queueSequence: last.queue_sequence,
2653
+ });
2654
+ return [snapshots, next] as const;
2655
+ });
2656
+
2657
+ const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate(
2658
+ undefined as ScanCursor | undefined,
2659
+ scanPage,
2660
+ );
2661
+
2662
+ const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
2663
+ "DoSubmissionLedger.loadRecoverySnapshot",
2664
+ )(function* (request: RecoverySnapshotRequest) {
2665
+ const operation = "ledger load recovery snapshot";
2666
+ const validated = yield* Schema.decodeUnknownEffect(Schema.toType(RecoverySnapshotRequest))(
2667
+ request,
2668
+ ).pipe(Effect.mapError(internalFailure(operation)));
2669
+ return yield* sql
2670
+ .withTransaction(
2671
+ Effect.gen(function* () {
2672
+ const submissionRow = yield* requireSubmission(operation, validated.submissionId);
2673
+ const submission = yield* decodeSubmissionSnapshot(operation, submissionRow);
2674
+
2675
+ let ownership: OwnershipSnapshot | undefined;
2676
+ const ownershipRow = yield* readOwnership(operation, validated.submissionId);
2677
+ if (Option.isSome(ownershipRow)) {
2678
+ ownership = yield* decodeOwnershipSnapshot({
2679
+ attemptId: ownershipRow.value.attempt_id,
2680
+ ownerProducerId: ownershipRow.value.owner_producer_id,
2681
+ producerEpoch: ownershipRow.value.producer_epoch,
2682
+ leaseExpiresAt: ownershipRow.value.lease_expires_at,
2683
+ }).pipe(Effect.mapError(internalFailure(operation)));
2684
+ }
2685
+
2686
+ let inputApplied: InputAppliedMarker | undefined;
2687
+ if (
2688
+ submissionRow.input_applied_record_id !== null &&
2689
+ submissionRow.input_applied_sequence !== null
2690
+ ) {
2691
+ inputApplied = yield* decodeInputAppliedMarker({
2692
+ recordId: submissionRow.input_applied_record_id,
2693
+ sequence: submissionRow.input_applied_sequence,
2694
+ }).pipe(Effect.mapError(internalFailure(operation)));
2695
+ }
2696
+
2697
+ let reservation: SettlementReservationSnapshot | undefined;
2698
+ const reservationRow = yield* readReservation(operation, validated.submissionId);
2699
+ if (Option.isSome(reservationRow)) {
2700
+ const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(
2701
+ Effect.mapError((error) =>
2702
+ corruptionFailure(
2703
+ operation,
2704
+ "effect_agent_settlement_reservations",
2705
+ validated.submissionId,
2706
+ error.message,
2707
+ ),
2708
+ ),
2709
+ );
2710
+ const settlementId = yield* Schema.decodeUnknownEffect(
2711
+ SettlementReservationSnapshot.fields.settlementId,
2712
+ )(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
2713
+ reservation = SettlementReservationSnapshot.make({
2714
+ settlementId,
2715
+ outcome: reservationRow.value.outcome,
2716
+ record,
2717
+ recordDigest: reservationRow.value.record_digest,
2718
+ finalized: reservationRow.value.finalized_at !== null,
2719
+ });
2720
+ }
2721
+
2722
+ let abortIntent: AbortIntent | undefined;
2723
+ const abortRow = yield* readAbortIntent(operation, validated.submissionId);
2724
+ if (Option.isSome(abortRow)) {
2725
+ abortIntent = yield* abortIntentFromRow(
2726
+ operation,
2727
+ submissionRow,
2728
+ validated.submissionId,
2729
+ abortRow.value,
2730
+ );
2731
+ }
2732
+
2733
+ // Host-side view: every Submission whose host linkage points here, in queue order
2734
+ // (the terminalize loop settles them with the host outcome, DUR-002).
2735
+ const joinRows = yield* sql<Record<string, unknown>>`
2736
+ SELECT ${sql.literal(SUBMISSION_COLUMNS)}
2737
+ FROM effect_agent_submissions
2738
+ WHERE joined_host_submission_id = ${validated.submissionId}
2739
+ ORDER BY queue_sequence ASC
2740
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2741
+ const joinSubmissions = yield* decodeSubmissionRows(
2742
+ operation,
2743
+ validated.submissionId,
2744
+ joinRows,
2745
+ );
2746
+ const joins = yield* Effect.forEach(joinSubmissions, (row) =>
2747
+ decodeJoinSnapshot({
2748
+ submissionId: row.submission_id,
2749
+ state: row.state,
2750
+ hostSubmissionId: validated.submissionId,
2751
+ }).pipe(Effect.mapError(internalFailure(operation))),
2752
+ );
2753
+
2754
+ let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
2755
+ if (submissionRow.joined_host_submission_id !== null) {
2756
+ hostSubmissionId = yield* decodeSubmissionId(
2757
+ submissionRow.joined_host_submission_id,
2758
+ ).pipe(Effect.mapError(internalFailure(operation)));
2759
+ }
2760
+
2761
+ let suspension: SuspensionSnapshot | undefined;
2762
+ if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
2763
+ const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(
2764
+ Effect.mapError((error) =>
2765
+ corruptionFailure(
2766
+ operation,
2767
+ "effect_agent_submissions",
2768
+ validated.submissionId,
2769
+ error.message,
2770
+ ),
2771
+ ),
2772
+ );
2773
+ suspension = yield* decodeSuspensionSnapshot({
2774
+ reason,
2775
+ suspendedAt: submissionRow.suspended_at,
2776
+ }).pipe(
2777
+ Effect.mapError((error) =>
2778
+ corruptionFailure(
2779
+ operation,
2780
+ "effect_agent_submissions",
2781
+ validated.submissionId,
2782
+ error.message,
2783
+ ),
2784
+ ),
2785
+ );
2786
+ }
2787
+
2788
+ const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
2789
+ const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
2790
+ approvalIntentFromRow(operation, row),
2791
+ );
2792
+
2793
+ const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
2794
+ const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
2795
+ unknownResolutionIntentFromRow(operation, row),
2796
+ );
2797
+
2798
+ // Parent-side subagent view: this Submission's child budget reservations in parent
2799
+ // Tool Call order, plus each attached child's current lane state (a disposable
2800
+ // derived view; canonical records stay the recovery truth, DUR-015). The child's
2801
+ // state comes from its local row when this store holds it, and otherwise from the
2802
+ // durable cross-store settlement marker; a child that is neither local nor marked
2803
+ // settled is enriched by the routed per-child lookup one layer out (plan §1.3).
2804
+ const childReservationRows = yield* sql<Record<string, unknown>>`
2805
+ SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
2806
+ FROM effect_agent_child_reservations
2807
+ WHERE parent_submission_id = ${validated.submissionId}
2808
+ ORDER BY parent_tool_call_id ASC
2809
+ `.pipe(Effect.mapError(sqlFailure(operation)));
2810
+ const decodedChildReservations = yield* decodeChildReservationRows(
2811
+ operation,
2812
+ validated.submissionId,
2813
+ childReservationRows,
2814
+ );
2815
+ const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
2816
+ childReservationSnapshotFromRow(operation, row),
2817
+ );
2818
+ const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
2819
+ const markersByChild = new Map(markers.map((row) => [row.child_submission_id, row]));
2820
+ const childAttachments: Array<ChildAttachmentSnapshot> = [];
2821
+ for (const row of decodedChildReservations) {
2822
+ if (row.child_submission_id === null) continue;
2823
+ const child = yield* readSubmission(operation, row.child_submission_id);
2824
+ if (Option.isSome(child)) {
2825
+ childAttachments.push(
2826
+ yield* decodeChildAttachmentSnapshot({
2827
+ toolCallId: row.parent_tool_call_id,
2828
+ childSubmissionId: row.child_submission_id,
2829
+ childState: child.value.state,
2830
+ ...(child.value.settled_outcome === null
2831
+ ? {}
2832
+ : { childOutcome: child.value.settled_outcome }),
2833
+ }).pipe(Effect.mapError(internalFailure(operation))),
2834
+ );
2835
+ continue;
2836
+ }
2837
+ const marker = markersByChild.get(row.child_submission_id);
2838
+ if (marker === undefined) continue;
2839
+ childAttachments.push(
2840
+ yield* decodeChildAttachmentSnapshot({
2841
+ toolCallId: row.parent_tool_call_id,
2842
+ childSubmissionId: row.child_submission_id,
2843
+ childState: "settled",
2844
+ ...(marker.child_outcome === null ? {} : { childOutcome: marker.child_outcome }),
2845
+ }).pipe(Effect.mapError(internalFailure(operation))),
2846
+ );
2847
+ }
2848
+
2849
+ let parentLinkage: ParentLinkage | undefined;
2850
+ if (
2851
+ submissionRow.parent_submission_id !== null &&
2852
+ submissionRow.parent_tool_call_id !== null
2853
+ ) {
2854
+ parentLinkage = yield* decodeParentLinkage({
2855
+ parentSubmissionId: submissionRow.parent_submission_id,
2856
+ parentToolCallId: submissionRow.parent_tool_call_id,
2857
+ }).pipe(Effect.mapError(internalFailure(operation)));
2858
+ }
2859
+
2860
+ return RecoverySnapshot.make({
2861
+ submission,
2862
+ joins,
2863
+ approvalDecisions,
2864
+ unknownResolutions,
2865
+ childReservations,
2866
+ childAttachments,
2867
+ ...(parentLinkage === undefined ? {} : { parentLinkage }),
2868
+ ...(hostSubmissionId === undefined ? {} : { hostSubmissionId }),
2869
+ ...(suspension === undefined ? {} : { suspension }),
2870
+ ...(ownership === undefined ? {} : { ownership }),
2871
+ ...(inputApplied === undefined ? {} : { inputApplied }),
2872
+ ...(reservation === undefined ? {} : { reservation }),
2873
+ ...(abortIntent === undefined ? {} : { abortIntent }),
2874
+ });
2875
+ }),
2876
+ )
2877
+ .pipe(Effect.catchTag("SqlError", (error) => Effect.fail(sqlFailure(operation)(error))));
2878
+ });
2879
+
2880
+ return Context.make(
2881
+ SubmissionLedger,
2882
+ SubmissionLedger.of({
2883
+ capabilities,
2884
+ admit,
2885
+ markReady,
2886
+ lookup,
2887
+ resolveAdmission,
2888
+ claim,
2889
+ renewOwnership,
2890
+ releaseOwnership,
2891
+ markInputApplied,
2892
+ reserveSettlement,
2893
+ finalizeSettlement,
2894
+ requestAbort,
2895
+ claimJoining,
2896
+ markJoined,
2897
+ revertJoining,
2898
+ suspend,
2899
+ recordApprovalDecision,
2900
+ markUnknown,
2901
+ recordUnknownResolution,
2902
+ recordChildSettled,
2903
+ reserveChildBudget,
2904
+ attachChildToReservation,
2905
+ beginChildBudgetRelease,
2906
+ releaseChildBudget,
2907
+ scanNonterminal,
2908
+ loadRecoverySnapshot,
2909
+ }),
2910
+ );
2911
+ });
2912
+
2913
+ /**
2914
+ * Durable Object SubmissionLedger implementation sharing the journal's private SQLite
2915
+ * database, storage-backed transaction discipline, and producer-epoch fencing substrate.
2916
+ * Configuration, failpoint, SQL, and Crypto authority stay visible in the input channel.
2917
+ */
2918
+ export const submissionLedgerLayer: Layer.Layer<
2919
+ SubmissionLedger,
2920
+ DoStorageInitializationError,
2921
+ DoStorageConfig | DoStorageFailpoint | SqlClientService.SqlClient | Crypto.Crypto
2922
+ > = Layer.effectContext(makeServices());
2923
+
2924
+ /**
2925
+ * A composition-root convenience Layer for the durable Submission Ledger. Point it at the
2926
+ * same `ctx.storage` as the ConversationStore so claims fence the same producer epochs.
2927
+ */
2928
+ export const ledgerLayer = (
2929
+ options: DoStorageOptions,
2930
+ ): Layer.Layer<SubmissionLedger, DoStorageInitializationError> =>
2931
+ submissionLedgerLayer.pipe(
2932
+ Layer.provide(
2933
+ Layer.mergeAll(
2934
+ storageConfigLayer(options),
2935
+ storageFailpointLayer(options),
2936
+ SqliteClient.layer({ storage: options.storage }),
2937
+ BrowserCrypto.layer,
2938
+ ),
2939
+ ),
2940
+ );