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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2844 @@
1
+ import {
2
+ type ToolCallId,
3
+ AttemptId,
4
+ ReceiptId,
5
+ SubmissionId,
6
+ type AgentId,
7
+ type ThreadId,
8
+ type SettlementId,
9
+ } from "@yielded/agent/identifiers";
10
+ import { InputMessage } from "@yielded/agent/messaging";
11
+ import {
12
+ PersistedJson,
13
+ WorkerAdmission,
14
+ ProducerEpoch,
15
+ type DefinitionDigests,
16
+ type DeploymentId,
17
+ type Digest,
18
+ type ProducerId,
19
+ type RecordEnvelope,
20
+ type SettlementOutcome,
21
+ } from "@yielded/agent/records";
22
+ import {
23
+ type ParentLinkage,
24
+ AbortCommand,
25
+ WorkerStopCommand,
26
+ WorkerLedgerState,
27
+ workerTerminalFromRecord,
28
+ AdmissionAdmitted,
29
+ AdmissionConflict,
30
+ AdmissionPolicyError,
31
+ AdmissionIndeterminate,
32
+ AdmissionNotAdmitted,
33
+ AdmissionRequest,
34
+ SubmissionAdmissionFence,
35
+ AdmissionResult,
36
+ ApprovalConflict,
37
+ ApprovalDecisionCommand,
38
+ ApprovalDecisionIntent,
39
+ AttachChildToReservationRequest,
40
+ BeginChildBudgetReleaseRequest,
41
+ ChildAttachmentSnapshot,
42
+ ChildBudgetReservationRequest,
43
+ ChildBudgetReservationSnapshot,
44
+ ChildReservationConflict,
45
+ ChildSettledNotification,
46
+ Claim,
47
+ ClaimJoiningRequest,
48
+ ClaimRequest,
49
+ DEFAULT_OWNERSHIP_LEASE_DURATION,
50
+ InputAppliedMarker,
51
+ JoinSnapshot,
52
+ JoinedToHost,
53
+ JoiningClaim,
54
+ LedgerCapabilities,
55
+ LedgerError,
56
+ MarkInputAppliedRequest,
57
+ MarkJoinedRequest,
58
+ MarkReadyRequest,
59
+ MarkUnknownRequest,
60
+ OwnershipLost,
61
+ OwnershipRenewal,
62
+ OwnershipSnapshot,
63
+ OwnershipToken,
64
+ QueueSequence,
65
+ RecoverySnapshot,
66
+ RecoverySnapshotRequest,
67
+ ReleaseChildBudgetRequest,
68
+ ReleaseOwnershipRequest,
69
+ RenewOwnershipRequest,
70
+ ReservedChildBudget,
71
+ ReservedSettlement,
72
+ RevertJoiningRequest,
73
+ Settlement,
74
+ SettlementConflict,
75
+ SettlementFinalization,
76
+ SettlementReservation,
77
+ SettlementReservationSnapshot,
78
+ settlementFailureFromRecord,
79
+ AbortIntent,
80
+ AbortIntentRequest,
81
+ SubmissionLedger,
82
+ SubmissionLookup,
83
+ SubmissionLookupByKey,
84
+ SubmissionSnapshot,
85
+ SubmissionWorkItem,
86
+ SuspendRequest,
87
+ SuspensionSnapshot,
88
+ UnknownResolution,
89
+ UnknownResolutionCommand,
90
+ UnknownResolutionConflict,
91
+ UnknownResolutionIntent,
92
+ type ChildReservationId,
93
+ type ChildReservationStatus,
94
+ type ChildSettledOutcome,
95
+ type IdempotencyKey,
96
+ type Principal,
97
+ type SubmissionState,
98
+ type SuspensionOutcome,
99
+ type SuspensionReason,
100
+ } from "@yielded/agent/submission-ledger";
101
+ import {
102
+ Clock,
103
+ Cause,
104
+ Exit,
105
+ Fiber,
106
+ DateTime,
107
+ Duration,
108
+ Effect,
109
+ Layer,
110
+ Option,
111
+ Ref,
112
+ Schema,
113
+ Stream,
114
+ } from "effect";
115
+
116
+ const MAX_SUBMISSIONS = 65_536;
117
+
118
+ /**
119
+ * Lifecycle ordering used to advance-but-never-regress the operational state marker: a reclaimed
120
+ * Attempt must not erase progress markers (input-applied, terminalizing) that an earlier Attempt
121
+ * already committed.
122
+ */
123
+ const STATE_RANK: Record<SubmissionState, number> = {
124
+ admitted: 0,
125
+ ready: 1,
126
+ joining: 2,
127
+ joined: 3,
128
+ running: 4,
129
+ "input-applied": 5,
130
+ suspended: 6,
131
+ unknown: 7,
132
+ terminalizing: 8,
133
+ settled: 9,
134
+ };
135
+
136
+ interface SubmissionRow {
137
+ readonly submissionId: SubmissionId;
138
+ readonly threadId: ThreadId;
139
+ readonly queueSequence: QueueSequence;
140
+ readonly principal: Principal;
141
+ readonly idempotencyKey: IdempotencyKey;
142
+ readonly agentId: AgentId;
143
+ readonly agentDigests: DefinitionDigests;
144
+ readonly deploymentId: DeploymentId;
145
+ readonly inputPayload: PersistedJson;
146
+ readonly inputDigest: Digest;
147
+ readonly receiptId: ReceiptId;
148
+ readonly state: SubmissionState;
149
+ readonly settledOutcome: SettlementOutcome | undefined;
150
+ readonly createdAtMillis: number;
151
+ readonly readyAtMillis: number | undefined;
152
+ /** Immutable child-side lineage recorded at admission (spec §12 step 5). */
153
+ readonly parentLinkage: ParentLinkage | undefined;
154
+ readonly admissionGroup?: string;
155
+ readonly admissionFence?: AdmissionRequest["admissionFence"];
156
+ readonly workerAdmissionJson?: string;
157
+ readonly messageAdmissionJson?: string;
158
+ }
159
+
160
+ interface StoredOwnership {
161
+ readonly attemptId: AttemptId;
162
+ readonly ownershipToken: OwnershipToken;
163
+ readonly producerEpoch: ProducerEpoch;
164
+ readonly ownerProducerId: ProducerId;
165
+ readonly leaseExpiresAtMillis: number;
166
+ }
167
+
168
+ interface StoredReservation {
169
+ readonly settlementId: SettlementId;
170
+ readonly outcome: SettlementOutcome;
171
+ readonly record: RecordEnvelope;
172
+ readonly recordDigest: Digest;
173
+ readonly finalizedAtMillis: number | undefined;
174
+ }
175
+
176
+ interface StoredSuspension {
177
+ readonly reason: SuspensionReason;
178
+ readonly suspendedAtMillis: number;
179
+ }
180
+
181
+ interface StoredUnknownMark {
182
+ readonly reason: MarkUnknownRequest["reason"];
183
+ readonly toolCallIds: ReadonlyArray<ToolCallId>;
184
+ }
185
+
186
+ interface StoredUnknownResolution {
187
+ readonly intent: UnknownResolutionIntent;
188
+ }
189
+
190
+ interface StoredSubmission {
191
+ readonly row: SubmissionRow;
192
+ readonly ownership: StoredOwnership | undefined;
193
+ readonly inputApplied: InputAppliedMarker | undefined;
194
+ readonly reservation: StoredReservation | undefined;
195
+ readonly abortIntent: AbortIntent | undefined;
196
+ /** Host linkage recorded at `claimJoining` time; cleared by `revertJoining` (DUR-016). */
197
+ readonly joinedHostSubmissionId: SubmissionId | undefined;
198
+ readonly suspension: StoredSuspension | undefined;
199
+ readonly unknownMark: StoredUnknownMark | undefined;
200
+ readonly approvalDecisions: ReadonlyMap<ToolCallId, ApprovalDecisionIntent>;
201
+ readonly unknownResolutions: ReadonlyMap<ToolCallId, StoredUnknownResolution>;
202
+ }
203
+
204
+ /**
205
+ * States in which `claim` never grants the head: the lane is host-owned (`joining`/`joined`)
206
+ * or durably suspended rather than worker-claimable. Unknown heads are checked against abort
207
+ * intent separately: abort authorizes cleanup and settlement, never ordinary Tool replay.
208
+ */
209
+ const BLOCKED_HEAD_STATES: ReadonlySet<SubmissionState> = new Set([
210
+ "joining",
211
+ "joined",
212
+ "suspended",
213
+ ]);
214
+
215
+ interface LaneState {
216
+ readonly nextQueueSequence: number;
217
+ readonly producerEpoch: number;
218
+ }
219
+
220
+ /** One parent-owned child budget reservation row (spec §12 steps 2 and 6). */
221
+ interface StoredChildReservation {
222
+ readonly reservationId: ChildReservationId;
223
+ readonly parentSubmissionId: SubmissionId;
224
+ readonly parentToolCallId: ToolCallId;
225
+ readonly childSubmissionId: SubmissionId | undefined;
226
+ readonly status: ChildReservationStatus;
227
+ readonly allocation: PersistedJson;
228
+ readonly allocationDigest: Digest;
229
+ readonly accounting: PersistedJson | undefined;
230
+ readonly reservedAtMillis: number;
231
+ readonly releaseBeganAtMillis: number | undefined;
232
+ readonly releasedAtMillis: number | undefined;
233
+ }
234
+
235
+ interface LedgerState {
236
+ readonly submissions: ReadonlyMap<SubmissionId, StoredSubmission>;
237
+ readonly admissionIndex: ReadonlyMap<string, SubmissionId>;
238
+ readonly lanes: ReadonlyMap<ThreadId, LaneState>;
239
+ readonly childReservations: ReadonlyMap<ChildReservationId, StoredChildReservation>;
240
+ readonly mintCounter: number;
241
+ readonly latestByThread: ReadonlyMap<ThreadId, SubmissionId>;
242
+ readonly activeByThread: ReadonlyMap<ThreadId, ReadonlySet<SubmissionId>>;
243
+ readonly stoppedWorkers: ReadonlyMap<ThreadId, WorkerLedgerState["terminal"]>;
244
+ }
245
+
246
+ type Decision<A, E> =
247
+ | { readonly _tag: "failure"; readonly error: E }
248
+ | { readonly _tag: "success"; readonly value: A };
249
+
250
+ const failure = <E>(error: E): Decision<never, E> => ({ _tag: "failure", error });
251
+ const success = <A>(value: A): Decision<A, never> => ({ _tag: "success", value });
252
+
253
+ const ledgerError = (operation: string, message: string, cause?: unknown): LedgerError =>
254
+ cause === undefined
255
+ ? LedgerError.make({ operation, message })
256
+ : LedgerError.make({ operation, message, cause });
257
+
258
+ const validate = Effect.fn("MemorySubmissionLedger.validate")(
259
+ <A, I>(
260
+ schema: Schema.Codec<A, I>,
261
+ operation: string,
262
+ value: unknown,
263
+ ): Effect.Effect<A, LedgerError> =>
264
+ Schema.encodeUnknownEffect(schema)(value).pipe(
265
+ Effect.flatMap(Schema.decodeUnknownEffect(schema)),
266
+ Effect.mapError((error) => ledgerError(operation, `Invalid ${operation} request`, error)),
267
+ ),
268
+ );
269
+
270
+ const decodeSubmissionId = Schema.decodeSync(SubmissionId);
271
+ const decodeReceiptId = Schema.decodeSync(ReceiptId);
272
+ const decodeAttemptId = Schema.decodeSync(AttemptId);
273
+ const decodeOwnershipToken = Schema.decodeSync(OwnershipToken);
274
+ const decodeQueueSequence = Schema.decodeSync(QueueSequence);
275
+ const decodeProducerEpoch = Schema.decodeSync(ProducerEpoch);
276
+ const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
277
+ const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
278
+
279
+ const utc = (millis: number): DateTime.Utc => DateTime.toUtc(DateTime.makeUnsafe(millis));
280
+
281
+ const admissionKey = (
282
+ threadId: ThreadId,
283
+ principal: Principal,
284
+ idempotencyKey: IdempotencyKey,
285
+ ): string => JSON.stringify([threadId, principal, idempotencyKey]);
286
+
287
+ const toSnapshot = (row: SubmissionRow): SubmissionSnapshot =>
288
+ SubmissionSnapshot.make({
289
+ submissionId: row.submissionId,
290
+ threadId: row.threadId,
291
+ queueSequence: row.queueSequence,
292
+ principal: row.principal,
293
+ idempotencyKey: row.idempotencyKey,
294
+ agentId: row.agentId,
295
+ agentDigests: row.agentDigests,
296
+ deploymentId: row.deploymentId,
297
+ inputPayload: row.inputPayload,
298
+ inputDigest: row.inputDigest,
299
+ receiptId: row.receiptId,
300
+ state: row.state,
301
+ createdAt: utc(row.createdAtMillis),
302
+ ...(row.admissionGroup === undefined ? {} : { admissionGroup: row.admissionGroup }),
303
+ ...(row.admissionFence === undefined ? {} : { admissionFence: row.admissionFence }),
304
+ ...(row.workerAdmissionJson === undefined
305
+ ? {}
306
+ : {
307
+ workerAdmission: Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
308
+ row.workerAdmissionJson,
309
+ ),
310
+ }),
311
+ ...(row.messageAdmissionJson === undefined
312
+ ? {}
313
+ : {
314
+ messageAdmission: Schema.decodeSync(Schema.fromJsonString(InputMessage))(
315
+ row.messageAdmissionJson,
316
+ ),
317
+ }),
318
+ ...(row.settledOutcome === undefined ? {} : { settledOutcome: row.settledOutcome }),
319
+ ...(row.readyAtMillis === undefined ? {} : { readyAt: utc(row.readyAtMillis) }),
320
+ ...(row.parentLinkage === undefined ? {} : { parentLinkage: row.parentLinkage }),
321
+ });
322
+
323
+ const toReservationSnapshot = (row: StoredChildReservation): ChildBudgetReservationSnapshot =>
324
+ ChildBudgetReservationSnapshot.make({
325
+ reservationId: row.reservationId,
326
+ parentSubmissionId: row.parentSubmissionId,
327
+ parentToolCallId: row.parentToolCallId,
328
+ status: row.status,
329
+ allocation: row.allocation,
330
+ allocationDigest: row.allocationDigest,
331
+ reservedAt: utc(row.reservedAtMillis),
332
+ ...(row.childSubmissionId === undefined ? {} : { childSubmissionId: row.childSubmissionId }),
333
+ ...(row.accounting === undefined ? {} : { accounting: row.accounting }),
334
+ ...(row.releaseBeganAtMillis === undefined
335
+ ? {}
336
+ : { releaseBeganAt: utc(row.releaseBeganAtMillis) }),
337
+ ...(row.releasedAtMillis === undefined ? {} : { releasedAt: utc(row.releasedAtMillis) }),
338
+ });
339
+
340
+ /** Linkage equality: both absent, or both present naming the same parent Tool Call. */
341
+ const sameParentLinkage = (
342
+ left: ParentLinkage | undefined,
343
+ right: ParentLinkage | undefined,
344
+ ): boolean =>
345
+ left === undefined
346
+ ? right === undefined
347
+ : right !== undefined &&
348
+ left.parentSubmissionId === right.parentSubmissionId &&
349
+ left.parentToolCallId === right.parentToolCallId;
350
+
351
+ const laneEpoch = (state: LedgerState, threadId: ThreadId): number =>
352
+ state.lanes.get(threadId)?.producerEpoch ?? 0;
353
+
354
+ const ownershipLost = (state: LedgerState, stored: StoredSubmission): OwnershipLost =>
355
+ OwnershipLost.make({
356
+ submissionId: stored.row.submissionId,
357
+ actualEpoch: decodeProducerEpoch(laneEpoch(state, stored.row.threadId)),
358
+ });
359
+
360
+ /** A retained row token cannot outlive a newer owner of the same Thread. */
361
+ const ownsLane = (
362
+ state: LedgerState,
363
+ stored: StoredSubmission,
364
+ ownershipToken: OwnershipToken,
365
+ ): boolean =>
366
+ stored.ownership !== undefined &&
367
+ stored.ownership.ownershipToken === ownershipToken &&
368
+ stored.ownership.producerEpoch === laneEpoch(state, stored.row.threadId);
369
+
370
+ const withSubmission = (state: LedgerState, stored: StoredSubmission): LedgerState => {
371
+ const active = new Set(state.activeByThread.get(stored.row.threadId));
372
+
373
+ if (stored.row.state === "settled") active.delete(stored.row.submissionId);
374
+ else active.add(stored.row.submissionId);
375
+
376
+ return {
377
+ ...state,
378
+ submissions: new Map(state.submissions).set(stored.row.submissionId, stored),
379
+ activeByThread: new Map(state.activeByThread).set(stored.row.threadId, active),
380
+ };
381
+ };
382
+
383
+ const withChildReservation = (
384
+ state: LedgerState,
385
+ reservation: StoredChildReservation,
386
+ ): LedgerState => ({
387
+ ...state,
388
+ childReservations: new Map(state.childReservations).set(reservation.reservationId, reservation),
389
+ });
390
+
391
+ const findHead = (state: LedgerState, threadId: ThreadId): StoredSubmission | undefined => {
392
+ let head: StoredSubmission | undefined;
393
+
394
+ for (const stored of state.submissions.values()) {
395
+ if (
396
+ stored.row.threadId !== threadId ||
397
+ stored.row.state === "settled" ||
398
+ (stored.row.state === "unknown" && stored.abortIntent === undefined)
399
+ )
400
+ continue;
401
+ if (head === undefined || stored.row.queueSequence < head.row.queueSequence) head = stored;
402
+ }
403
+
404
+ return head;
405
+ };
406
+
407
+ /**
408
+ * Reference in-memory SubmissionLedger. It implements the full port contract — atomic idempotent
409
+ * admission, eligible FIFO claims, producer-epoch fencing, Clock-driven ownership leases, idempotent
410
+ * settlement reservation/finalization, and durable abort intent — with every transition applied
411
+ * as one atomic `Ref.modify`, but its state does not survive the process (`non-durable`).
412
+ *
413
+ * Adapter-specific semantics within the port's latitude:
414
+ *
415
+ * - Time comes exclusively from the Effect `Clock` service, so `TestClock` drives lease expiry
416
+ * deterministically; no wall clock is consulted.
417
+ * - The ownership lease is pinned to `DEFAULT_OWNERSHIP_LEASE_DURATION` (D5); durable adapters
418
+ * own the configuration seam.
419
+ * - Any live lease in the Thread blocks every new claim, including the same `producerId`.
420
+ * Unresolved unknown work is skipped only after ownership is released or expires.
421
+ * - Claiming advances `ready` to `running` and otherwise preserves the recorded state, so
422
+ * progress markers from an earlier Attempt survive a reclaim.
423
+ * - `renewOwnership` keeps the token stable (the port allows rotation); a replayed admission
424
+ * reports the Submission's current state alongside the original identities.
425
+ * - `claimJoining` walks the strictly-later queue: rows already `joining`/`joined` to the
426
+ * SAME host extend the claimed prefix and are skipped, and an aborted-settled row is a
427
+ * closed obligation that is also skipped (P7 §7(c)); any other non-`ready` row (an
428
+ * `admitted` gap, a non-aborted settled row, foreign-host linkage) breaks the prefix
429
+ * conservatively.
430
+ * - `markJoined` verifies the token against the HOST's live ownership (the lane is
431
+ * host-owned), so a later host Attempt can repair a lost marker from history (DUR-016). The
432
+ * join marker reuses the input-applied marker: the joined input IS `input:{sid}`.
433
+ * - `suspend` and `markUnknown` refuse when an exact settlement is already reserved
434
+ * (`SettlementConflict` with the reserved outcome) — DUR-011's reservation wins.
435
+ * - `resolveAdmission` derives its answer from the single strongly consistent store, so it
436
+ * never answers `Indeterminate` on its own; the test-only `resolveAdmissionFault` option
437
+ * injects the `Indeterminate` classification so SUB-031 callers can be conformance-tested.
438
+ * - `recordChildSettled` and `suspend(WaitingForChild)` observe child settlement directly from
439
+ * the child rows (single-store latitude); no separate notification marker is stored.
440
+ */
441
+ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
442
+ Effect.gen(function* () {
443
+ const state = yield* Ref.make<LedgerState>({
444
+ submissions: new Map(),
445
+ admissionIndex: new Map(),
446
+ lanes: new Map(),
447
+ childReservations: new Map(),
448
+ mintCounter: 0,
449
+ stoppedWorkers: new Map(),
450
+ latestByThread: new Map(),
451
+ activeByThread: new Map(),
452
+ });
453
+
454
+ const admissionFence = yield* SubmissionAdmissionFence;
455
+ const leaseMillis = Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION);
456
+
457
+ const capabilities = Effect.succeed(LedgerCapabilities.make({ durability: "non-durable" }));
458
+
459
+ const admit: SubmissionLedger["Service"]["admit"] = Effect.fn("MemorySubmissionLedger.admit")(
460
+ (unvalidated) =>
461
+ Effect.gen(function* () {
462
+ const request = yield* validate(AdmissionRequest, "admit", unvalidated);
463
+
464
+ const workerAdmissionJson =
465
+ request.workerAdmission === undefined
466
+ ? undefined
467
+ : yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
468
+ request.workerAdmission,
469
+ ).pipe(
470
+ Effect.mapError(() => ledgerError("admit", "Invalid worker admission metadata")),
471
+ );
472
+
473
+ const messageAdmissionJson =
474
+ request.messageAdmission === undefined
475
+ ? undefined
476
+ : yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
477
+ request.messageAdmission,
478
+ ).pipe(
479
+ Effect.mapError(() => ledgerError("admit", "Invalid message admission metadata")),
480
+ );
481
+
482
+ const nowMillis = yield* Clock.currentTimeMillis;
483
+ const services = yield* Effect.context<never>();
484
+
485
+ const decision = yield* Ref.modify(
486
+ state,
487
+ (
488
+ current,
489
+ ): readonly [
490
+ (
491
+ | Decision<AdmissionResult, AdmissionConflict | AdmissionPolicyError | LedgerError>
492
+ | { readonly _tag: "cause"; readonly cause: Cause.Cause<AdmissionPolicyError> }
493
+ ),
494
+ LedgerState,
495
+ ] => {
496
+ const key = admissionKey(request.threadId, request.principal, request.idempotencyKey);
497
+ const existingId = current.admissionIndex.get(key);
498
+
499
+ if (existingId !== undefined) {
500
+ const existing = current.submissions.get(existingId);
501
+
502
+ if (existing === undefined) {
503
+ return [
504
+ failure(
505
+ ledgerError("admit", "Admission index references a missing Submission"),
506
+ ),
507
+ current,
508
+ ];
509
+ }
510
+ // A replay must repeat the exact canonical input AND the exact parent linkage
511
+ // (or its absence): linkage is immutable lineage (spec §12 step 5, SUB-016).
512
+ if (
513
+ existing.row.inputDigest !== request.inputDigest ||
514
+ !sameParentLinkage(existing.row.parentLinkage, request.parentLinkage) ||
515
+ existing.row.admissionGroup !== request.admissionGroup ||
516
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission))(
517
+ existing.row.workerAdmissionJson === undefined
518
+ ? undefined
519
+ : Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
520
+ existing.row.workerAdmissionJson,
521
+ ),
522
+ request.workerAdmission,
523
+ ) ||
524
+ !Schema.toEquivalence(Schema.optional(InputMessage))(
525
+ existing.row.messageAdmissionJson === undefined
526
+ ? undefined
527
+ : Schema.decodeSync(Schema.fromJsonString(InputMessage))(
528
+ existing.row.messageAdmissionJson,
529
+ ),
530
+ request.messageAdmission,
531
+ ) ||
532
+ !Schema.toEquivalence(Schema.optional(Schema.Json))(
533
+ existing.row.admissionFence,
534
+ request.admissionFence,
535
+ )
536
+ ) {
537
+ return [
538
+ failure(
539
+ AdmissionConflict.make({
540
+ threadId: request.threadId,
541
+ principal: request.principal,
542
+ idempotencyKey: request.idempotencyKey,
543
+ existingInputDigest: existing.row.inputDigest,
544
+ attemptedInputDigest: request.inputDigest,
545
+ }),
546
+ ),
547
+ current,
548
+ ];
549
+ }
550
+
551
+ return [
552
+ success(
553
+ AdmissionResult.make({
554
+ submissionId: existing.row.submissionId,
555
+ receiptId: existing.row.receiptId,
556
+ queueSequence: existing.row.queueSequence,
557
+ state: existing.row.state,
558
+ replayed: true,
559
+ }),
560
+ ),
561
+ current,
562
+ ];
563
+ }
564
+
565
+ if (current.stoppedWorkers.has(request.threadId))
566
+ return [
567
+ failure(AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" })),
568
+ current,
569
+ ];
570
+
571
+ // A Thread's first admission fixes its worker origin before canonical materialization.
572
+ const first = [...current.submissions.values()].find(
573
+ ({ row }) => row.threadId === request.threadId,
574
+ );
575
+
576
+ if (first !== undefined) {
577
+ const previous =
578
+ first.row.workerAdmissionJson === undefined
579
+ ? undefined
580
+ : Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
581
+ first.row.workerAdmissionJson,
582
+ );
583
+
584
+ if (
585
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
586
+ previous?.origin,
587
+ request.workerAdmission?.origin,
588
+ )
589
+ )
590
+ return [
591
+ failure(
592
+ AdmissionPolicyError.make({
593
+ reason: "refused",
594
+ code: "worker-origin-conflict",
595
+ }),
596
+ ),
597
+ current,
598
+ ];
599
+ }
600
+ // A memory policy and the ledger mutation share one synchronous critical section.
601
+ // An asynchronous policy cannot fence this Ref and therefore fails closed.
602
+ const checked = Effect.runSyncExitWith(services)(admissionFence.check(request));
603
+
604
+ if (Exit.isFailure(checked)) {
605
+ return [{ _tag: "cause", cause: checked.cause }, current];
606
+ }
607
+ if (
608
+ request.admissionGroup !== undefined &&
609
+ [...current.submissions.values()].some(
610
+ ({ row }) =>
611
+ row.threadId === request.threadId &&
612
+ row.admissionGroup === request.admissionGroup &&
613
+ row.state !== "settled",
614
+ )
615
+ )
616
+ return [
617
+ failure(
618
+ AdmissionPolicyError.make({ reason: "occupied", code: "admission-group" }),
619
+ ),
620
+ current,
621
+ ];
622
+ if (current.submissions.size >= MAX_SUBMISSIONS) {
623
+ return [
624
+ failure(
625
+ ledgerError("admit", `In-memory submission limit ${MAX_SUBMISSIONS} exceeded`),
626
+ ),
627
+ current,
628
+ ];
629
+ }
630
+
631
+ const lane = current.lanes.get(request.threadId) ?? {
632
+ nextQueueSequence: 1,
633
+ producerEpoch: 0,
634
+ };
635
+
636
+ const mintCounter = current.mintCounter + 1;
637
+
638
+ const row: SubmissionRow = {
639
+ submissionId: decodeSubmissionId(`submission-memory-${mintCounter}`),
640
+ threadId: request.threadId,
641
+ queueSequence: decodeQueueSequence(lane.nextQueueSequence),
642
+ principal: request.principal,
643
+ idempotencyKey: request.idempotencyKey,
644
+ agentId: request.agentId,
645
+ agentDigests: request.agentDigests,
646
+ deploymentId: request.deploymentId,
647
+ inputPayload: request.inputPayload,
648
+ inputDigest: request.inputDigest,
649
+ receiptId: decodeReceiptId(`receipt-memory-${mintCounter}`),
650
+ state: "admitted",
651
+ settledOutcome: undefined,
652
+ createdAtMillis: nowMillis,
653
+ readyAtMillis: undefined,
654
+ parentLinkage: request.parentLinkage,
655
+ ...(workerAdmissionJson === undefined ? {} : { workerAdmissionJson }),
656
+ ...(messageAdmissionJson === undefined ? {} : { messageAdmissionJson }),
657
+ ...(request.admissionGroup === undefined
658
+ ? {}
659
+ : { admissionGroup: request.admissionGroup }),
660
+ ...(request.admissionFence === undefined
661
+ ? {}
662
+ : { admissionFence: request.admissionFence }),
663
+ };
664
+
665
+ const submissions = new Map(current.submissions).set(row.submissionId, {
666
+ row,
667
+ ownership: undefined,
668
+ inputApplied: undefined,
669
+ reservation: undefined,
670
+ abortIntent: undefined,
671
+ joinedHostSubmissionId: undefined,
672
+ suspension: undefined,
673
+ unknownMark: undefined,
674
+ approvalDecisions: new Map<ToolCallId, ApprovalDecisionIntent>(),
675
+ unknownResolutions: new Map<ToolCallId, StoredUnknownResolution>(),
676
+ });
677
+
678
+ const admissionIndex = new Map(current.admissionIndex).set(key, row.submissionId);
679
+
680
+ const lanes = new Map(current.lanes).set(request.threadId, {
681
+ nextQueueSequence: lane.nextQueueSequence + 1,
682
+ producerEpoch: lane.producerEpoch,
683
+ });
684
+
685
+ return [
686
+ success(
687
+ AdmissionResult.make({
688
+ submissionId: row.submissionId,
689
+ receiptId: row.receiptId,
690
+ queueSequence: row.queueSequence,
691
+ state: row.state,
692
+ replayed: false,
693
+ }),
694
+ ),
695
+ {
696
+ ...current,
697
+ submissions,
698
+ admissionIndex,
699
+ lanes,
700
+ mintCounter,
701
+ latestByThread: new Map(current.latestByThread).set(
702
+ request.threadId,
703
+ row.submissionId,
704
+ ),
705
+ activeByThread: new Map(current.activeByThread).set(
706
+ request.threadId,
707
+ new Set([
708
+ ...(current.activeByThread.get(request.threadId) ?? []),
709
+ row.submissionId,
710
+ ]),
711
+ ),
712
+ },
713
+ ];
714
+ },
715
+ );
716
+
717
+ if (decision._tag === "failure") return yield* decision.error;
718
+ if (decision._tag === "cause") {
719
+ for (const reason of decision.cause.reasons) {
720
+ if (Cause.isDieReason(reason) && Cause.isAsyncFiberError(reason.defect)) {
721
+ yield* Fiber.interrupt(reason.defect.fiber);
722
+
723
+ return yield* AdmissionPolicyError.make({
724
+ reason: "unavailable",
725
+ code: "synchronous-memory-policy-required",
726
+ });
727
+ }
728
+ }
729
+
730
+ return yield* Effect.failCause(decision.cause);
731
+ }
732
+
733
+ return decision.value;
734
+ }),
735
+ Effect.uninterruptible,
736
+ );
737
+
738
+ const markReady: SubmissionLedger["Service"]["markReady"] = Effect.fn(
739
+ "MemorySubmissionLedger.markReady",
740
+ )((unvalidated) =>
741
+ Effect.gen(function* () {
742
+ const request = yield* validate(MarkReadyRequest, "markReady", unvalidated);
743
+ const nowMillis = yield* Clock.currentTimeMillis;
744
+
745
+ const decision = yield* Ref.modify(
746
+ state,
747
+ (current): readonly [Decision<void, LedgerError>, LedgerState] => {
748
+ const stored = current.submissions.get(request.submissionId);
749
+
750
+ if (stored === undefined) {
751
+ return [
752
+ failure(ledgerError("markReady", `Unknown Submission ${request.submissionId}`)),
753
+ current,
754
+ ];
755
+ }
756
+ if (stored.row.state !== "admitted") return [success(undefined), current];
757
+
758
+ return [
759
+ success(undefined),
760
+ withSubmission(current, {
761
+ ...stored,
762
+ row: { ...stored.row, state: "ready", readyAtMillis: nowMillis },
763
+ }),
764
+ ];
765
+ },
766
+ );
767
+
768
+ if (decision._tag === "failure") return yield* decision.error;
769
+ }),
770
+ );
771
+
772
+ const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn(
773
+ "MemorySubmissionLedger.lookup",
774
+ )((unvalidated) =>
775
+ Effect.gen(function* () {
776
+ const request = yield* validate(SubmissionLookup, "lookup", unvalidated);
777
+ const current = yield* Ref.get(state);
778
+
779
+ const submissionId =
780
+ request._tag === "SubmissionLookupById"
781
+ ? request.submissionId
782
+ : current.admissionIndex.get(
783
+ admissionKey(request.threadId, request.principal, request.idempotencyKey),
784
+ );
785
+
786
+ const stored =
787
+ submissionId === undefined ? undefined : current.submissions.get(submissionId);
788
+
789
+ return stored === undefined ? Option.none() : Option.some(toSnapshot(stored.row));
790
+ }),
791
+ );
792
+
793
+ const resolveAdmission: SubmissionLedger["Service"]["resolveAdmission"] = Effect.fn(
794
+ "MemorySubmissionLedger.resolveAdmission",
795
+ )((unvalidated) =>
796
+ Effect.gen(function* () {
797
+ const request = yield* validate(SubmissionLookupByKey, "resolveAdmission", unvalidated);
798
+
799
+ // Test-only fault seam: lets suites exercise the Indeterminate classification that a
800
+ // single strongly consistent store never produces on its own (SUB-031, P6 honesty).
801
+ if (options.resolveAdmissionFault !== undefined) {
802
+ const fault = yield* options.resolveAdmissionFault;
803
+
804
+ if (Option.isSome(fault)) {
805
+ return AdmissionIndeterminate.make({ reason: fault.value });
806
+ }
807
+ }
808
+ const current = yield* Ref.get(state);
809
+
810
+ const submissionId = current.admissionIndex.get(
811
+ admissionKey(request.threadId, request.principal, request.idempotencyKey),
812
+ );
813
+
814
+ const stored =
815
+ submissionId === undefined ? undefined : current.submissions.get(submissionId);
816
+
817
+ return stored === undefined
818
+ ? AdmissionNotAdmitted.make()
819
+ : AdmissionAdmitted.make({ submission: toSnapshot(stored.row) });
820
+ }),
821
+ );
822
+
823
+ const claim: SubmissionLedger["Service"]["claim"] = Effect.fn("MemorySubmissionLedger.claim")(
824
+ (unvalidated) =>
825
+ Effect.gen(function* () {
826
+ const request = yield* validate(ClaimRequest, "claim", unvalidated);
827
+ const nowMillis = yield* Clock.currentTimeMillis;
828
+
829
+ const decision = yield* Ref.modify(
830
+ state,
831
+ (current): readonly [Decision<Option.Option<Claim>, LedgerError>, LedgerState] => {
832
+ for (const stored of current.submissions.values()) {
833
+ if (
834
+ stored.row.threadId === request.threadId &&
835
+ stored.ownership !== undefined &&
836
+ stored.ownership.leaseExpiresAtMillis > nowMillis
837
+ ) {
838
+ return [success(Option.none()), current];
839
+ }
840
+ }
841
+ let head = findHead(current, request.threadId);
842
+
843
+ if (request.handoff !== undefined) {
844
+ const handoff = request.handoff;
845
+
846
+ if (current.lanes.get(request.threadId)?.producerEpoch !== handoff.producerEpoch)
847
+ return [success(Option.none()), current];
848
+
849
+ const candidates = [...current.submissions.values()]
850
+ .filter(
851
+ (entry) =>
852
+ entry.row.threadId === request.threadId &&
853
+ entry.row.state !== "settled" &&
854
+ !(entry.row.state === "unknown" && entry.abortIntent === undefined),
855
+ )
856
+ .sort((a, b) => a.row.queueSequence - b.row.queueSequence);
857
+
858
+ for (const candidate of candidates) {
859
+ if (candidate.row.submissionId === handoff.submissionId) {
860
+ head = candidate;
861
+ break;
862
+ }
863
+ if (
864
+ !handoff.deferredSubmissionIds.includes(candidate.row.submissionId) ||
865
+ candidate.row.state !== "input-applied" ||
866
+ candidate.abortIntent !== undefined
867
+ )
868
+ return [success(Option.none()), current];
869
+ }
870
+ if (
871
+ head?.row.submissionId !== handoff.submissionId ||
872
+ (head.row.state !== "ready" &&
873
+ head.row.state !== "running" &&
874
+ head.row.state !== "input-applied")
875
+ )
876
+ return [success(Option.none()), current];
877
+ }
878
+
879
+ if (head === undefined) return [success(Option.none()), current];
880
+ if (BLOCKED_HEAD_STATES.has(head.row.state)) return [success(Option.none()), current];
881
+ const lane = current.lanes.get(request.threadId);
882
+
883
+ if (lane === undefined) {
884
+ return [
885
+ failure(ledgerError("claim", "Claimable head without a Thread lane")),
886
+ current,
887
+ ];
888
+ }
889
+ const producerEpoch = decodeProducerEpoch(lane.producerEpoch + 1);
890
+ const mintCounter = current.mintCounter + 1;
891
+
892
+ const ownership: StoredOwnership = {
893
+ attemptId: decodeAttemptId(`attempt-memory-${mintCounter}`),
894
+ ownershipToken: decodeOwnershipToken(`ownership-memory-${mintCounter}`),
895
+ producerEpoch,
896
+ ownerProducerId: request.producerId,
897
+ leaseExpiresAtMillis: nowMillis + leaseMillis,
898
+ };
899
+
900
+ const row: SubmissionRow =
901
+ head.row.state === "ready" ? { ...head.row, state: "running" } : head.row;
902
+
903
+ const next = withSubmission(current, { ...head, row, ownership });
904
+
905
+ const lanes = new Map(next.lanes).set(request.threadId, {
906
+ nextQueueSequence: lane.nextQueueSequence,
907
+ producerEpoch: lane.producerEpoch + 1,
908
+ });
909
+
910
+ return [
911
+ success(
912
+ Option.some(
913
+ Claim.make({
914
+ submissionId: row.submissionId,
915
+ attemptId: ownership.attemptId,
916
+ ownershipToken: ownership.ownershipToken,
917
+ producerEpoch,
918
+ leaseExpiresAt: utc(ownership.leaseExpiresAtMillis),
919
+ inputPayload: row.inputPayload,
920
+ }),
921
+ ),
922
+ ),
923
+ { ...next, lanes, mintCounter },
924
+ ];
925
+ },
926
+ );
927
+
928
+ if (decision._tag === "failure") return yield* decision.error;
929
+
930
+ return decision.value;
931
+ }),
932
+ );
933
+
934
+ const renewOwnership: SubmissionLedger["Service"]["renewOwnership"] = Effect.fn(
935
+ "MemorySubmissionLedger.renewOwnership",
936
+ )((unvalidated) =>
937
+ Effect.gen(function* () {
938
+ const request = yield* validate(RenewOwnershipRequest, "renewOwnership", unvalidated);
939
+ const nowMillis = yield* Clock.currentTimeMillis;
940
+
941
+ const decision = yield* Ref.modify(
942
+ state,
943
+ (
944
+ current,
945
+ ): readonly [Decision<OwnershipRenewal, OwnershipLost | LedgerError>, LedgerState] => {
946
+ const stored = current.submissions.get(request.submissionId);
947
+
948
+ if (stored === undefined) {
949
+ return [
950
+ failure(
951
+ ledgerError("renewOwnership", `Unknown Submission ${request.submissionId}`),
952
+ ),
953
+ current,
954
+ ];
955
+ }
956
+ if (
957
+ stored.ownership === undefined ||
958
+ !ownsLane(current, stored, request.ownershipToken)
959
+ ) {
960
+ return [failure(ownershipLost(current, stored)), current];
961
+ }
962
+
963
+ const ownership: StoredOwnership = {
964
+ ...stored.ownership,
965
+ leaseExpiresAtMillis: nowMillis + leaseMillis,
966
+ };
967
+
968
+ return [
969
+ success(
970
+ OwnershipRenewal.make({
971
+ ownershipToken: ownership.ownershipToken,
972
+ leaseExpiresAt: utc(ownership.leaseExpiresAtMillis),
973
+ }),
974
+ ),
975
+ withSubmission(current, { ...stored, ownership }),
976
+ ];
977
+ },
978
+ );
979
+
980
+ if (decision._tag === "failure") return yield* decision.error;
981
+
982
+ return decision.value;
983
+ }),
984
+ );
985
+
986
+ const releaseOwnership: SubmissionLedger["Service"]["releaseOwnership"] = Effect.fn(
987
+ "MemorySubmissionLedger.releaseOwnership",
988
+ )((unvalidated) =>
989
+ Effect.gen(function* () {
990
+ const request = yield* validate(ReleaseOwnershipRequest, "releaseOwnership", unvalidated);
991
+
992
+ const decision = yield* Ref.modify(
993
+ state,
994
+ (current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
995
+ const stored = current.submissions.get(request.submissionId);
996
+
997
+ if (stored === undefined) {
998
+ return [
999
+ failure(
1000
+ ledgerError("releaseOwnership", `Unknown Submission ${request.submissionId}`),
1001
+ ),
1002
+ current,
1003
+ ];
1004
+ }
1005
+ if (!ownsLane(current, stored, request.ownershipToken)) {
1006
+ return [failure(ownershipLost(current, stored)), current];
1007
+ }
1008
+
1009
+ return [
1010
+ success(undefined),
1011
+ withSubmission(current, { ...stored, ownership: undefined }),
1012
+ ];
1013
+ },
1014
+ );
1015
+
1016
+ if (decision._tag === "failure") return yield* decision.error;
1017
+ }),
1018
+ );
1019
+
1020
+ const markInputApplied: SubmissionLedger["Service"]["markInputApplied"] = Effect.fn(
1021
+ "MemorySubmissionLedger.markInputApplied",
1022
+ )((unvalidated) =>
1023
+ Effect.gen(function* () {
1024
+ const request = yield* validate(MarkInputAppliedRequest, "markInputApplied", unvalidated);
1025
+
1026
+ const decision = yield* Ref.modify(
1027
+ state,
1028
+ (current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
1029
+ const stored = current.submissions.get(request.submissionId);
1030
+
1031
+ if (stored === undefined) {
1032
+ return [
1033
+ failure(
1034
+ ledgerError("markInputApplied", `Unknown Submission ${request.submissionId}`),
1035
+ ),
1036
+ current,
1037
+ ];
1038
+ }
1039
+ if (!ownsLane(current, stored, request.ownershipToken)) {
1040
+ return [failure(ownershipLost(current, stored)), current];
1041
+ }
1042
+
1043
+ if (stored.inputApplied !== undefined) {
1044
+ if (
1045
+ stored.inputApplied.recordId === request.recordId &&
1046
+ stored.inputApplied.sequence === request.sequence
1047
+ ) {
1048
+ return [success(undefined), current];
1049
+ }
1050
+
1051
+ return [
1052
+ failure(
1053
+ ledgerError(
1054
+ "markInputApplied",
1055
+ `A different canonical input marker is already recorded for Submission ${request.submissionId}`,
1056
+ ),
1057
+ ),
1058
+ current,
1059
+ ];
1060
+ }
1061
+
1062
+ const marker = InputAppliedMarker.make({
1063
+ recordId: request.recordId,
1064
+ sequence: request.sequence,
1065
+ });
1066
+
1067
+ const row: SubmissionRow =
1068
+ STATE_RANK[stored.row.state] < STATE_RANK["input-applied"]
1069
+ ? { ...stored.row, state: "input-applied" }
1070
+ : stored.row;
1071
+
1072
+ return [
1073
+ success(undefined),
1074
+ withSubmission(current, { ...stored, row, inputApplied: marker }),
1075
+ ];
1076
+ },
1077
+ );
1078
+
1079
+ if (decision._tag === "failure") return yield* decision.error;
1080
+ }),
1081
+ );
1082
+
1083
+ const reserveSettlement: SubmissionLedger["Service"]["reserveSettlement"] = Effect.fn(
1084
+ "MemorySubmissionLedger.reserveSettlement",
1085
+ )((unvalidated) =>
1086
+ Effect.gen(function* () {
1087
+ const request = yield* validate(SettlementReservation, "reserveSettlement", unvalidated);
1088
+
1089
+ const decision = yield* Ref.modify(
1090
+ state,
1091
+ (
1092
+ current,
1093
+ ): readonly [
1094
+ Decision<ReservedSettlement, SettlementConflict | OwnershipLost | LedgerError>,
1095
+ LedgerState,
1096
+ ] => {
1097
+ const stored = current.submissions.get(request.submissionId);
1098
+
1099
+ if (stored === undefined) {
1100
+ return [
1101
+ failure(
1102
+ ledgerError("reserveSettlement", `Unknown Submission ${request.submissionId}`),
1103
+ ),
1104
+ current,
1105
+ ];
1106
+ }
1107
+
1108
+ // A `joined` Submission settles WITH its host (plan §2.5) and its lane is never
1109
+ // worker-claimable, so no ownership token can exist for it: the recorded host
1110
+ // linkage authorizes the reservation and the presented token is not consulted.
1111
+ const joinedSettlement =
1112
+ stored.row.state === "joined" && stored.joinedHostSubmissionId !== undefined;
1113
+
1114
+ // P7 §7(c): an aborted, never-claimed, still-queued Submission likewise has no
1115
+ // live ownership to fence against — its durable abort intent authorizes exactly
1116
+ // its ABORTED settlement (`terminalizing` is the same pass's crash replay). Every
1117
+ // other reservation stays fenced by the target lane's live ownership.
1118
+ const queuedAbortSettlement =
1119
+ request.outcome === "aborted" &&
1120
+ stored.abortIntent !== undefined &&
1121
+ stored.ownership === undefined &&
1122
+ (stored.row.state === "ready" || stored.row.state === "terminalizing");
1123
+
1124
+ if (
1125
+ !joinedSettlement &&
1126
+ !queuedAbortSettlement &&
1127
+ !ownsLane(current, stored, request.ownershipToken)
1128
+ ) {
1129
+ return [failure(ownershipLost(current, stored)), current];
1130
+ }
1131
+ const existing = stored.reservation;
1132
+
1133
+ if (existing !== undefined) {
1134
+ if (
1135
+ existing.settlementId !== request.settlementId ||
1136
+ existing.outcome !== request.outcome ||
1137
+ existing.recordDigest !== request.recordDigest
1138
+ ) {
1139
+ return [
1140
+ failure(
1141
+ SettlementConflict.make({
1142
+ submissionId: request.submissionId,
1143
+ existingOutcome: existing.outcome,
1144
+ }),
1145
+ ),
1146
+ current,
1147
+ ];
1148
+ }
1149
+
1150
+ return [
1151
+ success(
1152
+ ReservedSettlement.make({
1153
+ submissionId: request.submissionId,
1154
+ settlementId: existing.settlementId,
1155
+ outcome: existing.outcome,
1156
+ record: existing.record,
1157
+ recordDigest: existing.recordDigest,
1158
+ replayed: true,
1159
+ }),
1160
+ ),
1161
+ current,
1162
+ ];
1163
+ }
1164
+
1165
+ const reservation: StoredReservation = {
1166
+ settlementId: request.settlementId,
1167
+ outcome: request.outcome,
1168
+ record: request.record,
1169
+ recordDigest: request.recordDigest,
1170
+ finalizedAtMillis: undefined,
1171
+ };
1172
+
1173
+ const row: SubmissionRow =
1174
+ STATE_RANK[stored.row.state] < STATE_RANK.terminalizing
1175
+ ? { ...stored.row, state: "terminalizing" }
1176
+ : stored.row;
1177
+
1178
+ return [
1179
+ success(
1180
+ ReservedSettlement.make({
1181
+ submissionId: request.submissionId,
1182
+ settlementId: reservation.settlementId,
1183
+ outcome: reservation.outcome,
1184
+ record: reservation.record,
1185
+ recordDigest: reservation.recordDigest,
1186
+ replayed: false,
1187
+ }),
1188
+ ),
1189
+ withSubmission(current, { ...stored, row, reservation }),
1190
+ ];
1191
+ },
1192
+ );
1193
+
1194
+ if (decision._tag === "failure") return yield* decision.error;
1195
+
1196
+ return decision.value;
1197
+ }),
1198
+ );
1199
+
1200
+ const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
1201
+ "MemorySubmissionLedger.finalizeSettlement",
1202
+ )((unvalidated) =>
1203
+ Effect.gen(function* () {
1204
+ const request = yield* validate(SettlementFinalization, "finalizeSettlement", unvalidated);
1205
+ const nowMillis = yield* Clock.currentTimeMillis;
1206
+
1207
+ const decision = yield* Ref.modify(
1208
+ state,
1209
+ (
1210
+ current,
1211
+ ): readonly [Decision<Settlement, SettlementConflict | LedgerError>, LedgerState] => {
1212
+ const stored = current.submissions.get(request.submissionId);
1213
+
1214
+ if (stored === undefined) {
1215
+ return [
1216
+ failure(
1217
+ ledgerError("finalizeSettlement", `Unknown Submission ${request.submissionId}`),
1218
+ ),
1219
+ current,
1220
+ ];
1221
+ }
1222
+ const reservation = stored.reservation;
1223
+
1224
+ if (reservation === undefined) {
1225
+ return [
1226
+ failure(
1227
+ ledgerError(
1228
+ "finalizeSettlement",
1229
+ `No settlement reservation for Submission ${request.submissionId}`,
1230
+ ),
1231
+ ),
1232
+ current,
1233
+ ];
1234
+ }
1235
+ const settlementFailure = settlementFailureFromRecord(reservation.record);
1236
+
1237
+ if ((reservation.outcome === "failed") !== (settlementFailure !== undefined)) {
1238
+ return [
1239
+ failure(
1240
+ ledgerError(
1241
+ "finalizeSettlement",
1242
+ `Settlement reservation for Submission ${request.submissionId} has contradictory failure evidence`,
1243
+ ),
1244
+ ),
1245
+ current,
1246
+ ];
1247
+ }
1248
+ if (reservation.settlementId !== request.settlementId) {
1249
+ return [
1250
+ failure(
1251
+ SettlementConflict.make({
1252
+ submissionId: request.submissionId,
1253
+ existingOutcome: reservation.outcome,
1254
+ }),
1255
+ ),
1256
+ current,
1257
+ ];
1258
+ }
1259
+ if (reservation.finalizedAtMillis !== undefined) {
1260
+ return [
1261
+ success(
1262
+ Settlement.make({
1263
+ submissionId: stored.row.submissionId,
1264
+ settlementId: reservation.settlementId,
1265
+ receiptId: stored.row.receiptId,
1266
+ outcome: reservation.outcome,
1267
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1268
+ settledAt: utc(reservation.finalizedAtMillis),
1269
+ }),
1270
+ ),
1271
+ current,
1272
+ ];
1273
+ }
1274
+
1275
+ const terminal =
1276
+ stored.row.workerAdmissionJson === undefined
1277
+ ? undefined
1278
+ : workerTerminalFromRecord(toSnapshot(stored.row), reservation.record);
1279
+
1280
+ const latestId = current.latestByThread.get(stored.row.threadId);
1281
+ const latest = latestId === undefined ? undefined : current.submissions.get(latestId);
1282
+
1283
+ const pendingNewer =
1284
+ terminal === "completed" &&
1285
+ latest !== undefined &&
1286
+ latest.row.queueSequence > stored.row.queueSequence &&
1287
+ !(
1288
+ latest.joinedHostSubmissionId === stored.row.submissionId &&
1289
+ latest.inputApplied !== undefined
1290
+ );
1291
+
1292
+ let sealed = current;
1293
+
1294
+ if (
1295
+ terminal !== undefined &&
1296
+ !pendingNewer &&
1297
+ !current.stoppedWorkers.has(stored.row.threadId)
1298
+ ) {
1299
+ const submissions = new Map(current.submissions);
1300
+
1301
+ for (const id of current.activeByThread.get(stored.row.threadId) ?? []) {
1302
+ const other = submissions.get(id);
1303
+
1304
+ if (other === undefined) continue;
1305
+ if (
1306
+ id !== stored.row.submissionId &&
1307
+ !(
1308
+ other.joinedHostSubmissionId === stored.row.submissionId &&
1309
+ other.inputApplied !== undefined
1310
+ ) &&
1311
+ other.abortIntent === undefined
1312
+ ) {
1313
+ submissions.set(id, {
1314
+ ...other,
1315
+ abortIntent: AbortIntent.make({
1316
+ submissionId: id,
1317
+ author: stored.row.principal,
1318
+ reason: `Worker assignment ${terminal}`,
1319
+ requestedAt: utc(nowMillis),
1320
+ }),
1321
+ });
1322
+ }
1323
+ }
1324
+ sealed = {
1325
+ ...current,
1326
+ submissions,
1327
+ stoppedWorkers: new Map(current.stoppedWorkers).set(stored.row.threadId, terminal),
1328
+ };
1329
+ }
1330
+
1331
+ const next = withSubmission(sealed, {
1332
+ ...stored,
1333
+ row: { ...stored.row, state: "settled", settledOutcome: reservation.outcome },
1334
+ ownership: undefined,
1335
+ reservation: { ...reservation, finalizedAtMillis: nowMillis },
1336
+ });
1337
+
1338
+ return [
1339
+ success(
1340
+ Settlement.make({
1341
+ submissionId: stored.row.submissionId,
1342
+ settlementId: reservation.settlementId,
1343
+ receiptId: stored.row.receiptId,
1344
+ outcome: reservation.outcome,
1345
+ ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1346
+ settledAt: utc(nowMillis),
1347
+ }),
1348
+ ),
1349
+ next,
1350
+ ];
1351
+ },
1352
+ );
1353
+
1354
+ if (decision._tag === "failure") return yield* decision.error;
1355
+
1356
+ return decision.value;
1357
+ }),
1358
+ );
1359
+
1360
+ const inspectWorker = Effect.fn("MemorySubmissionLedger.inspectWorker")(function* (
1361
+ threadId: ThreadId,
1362
+ ) {
1363
+ const current = yield* Ref.get(state);
1364
+ const latestId = current.latestByThread.get(threadId);
1365
+ const latest = latestId === undefined ? undefined : current.submissions.get(latestId);
1366
+
1367
+ const active = [...(current.activeByThread.get(threadId) ?? [])]
1368
+ .flatMap((id) => {
1369
+ const row = current.submissions.get(id);
1370
+
1371
+ return row === undefined ? [] : [row];
1372
+ })
1373
+ .sort((a, b) => a.row.queueSequence - b.row.queueSequence)[0];
1374
+
1375
+ const terminal = current.stoppedWorkers.get(threadId);
1376
+
1377
+ return WorkerLedgerState.make({
1378
+ latest: latest === undefined ? null : toSnapshot(latest.row),
1379
+ active: active === undefined ? null : toSnapshot(active.row),
1380
+ stopped: current.stoppedWorkers.has(threadId),
1381
+ ...(terminal === undefined ? {} : { terminal }),
1382
+ });
1383
+ });
1384
+
1385
+ const stopWorker = Effect.fn("MemorySubmissionLedger.stopWorker")(function* (
1386
+ unvalidated: WorkerStopCommand,
1387
+ ) {
1388
+ const request = yield* validate(WorkerStopCommand, "stopWorker", unvalidated);
1389
+ const now = yield* Clock.currentTimeMillis;
1390
+
1391
+ return yield* Ref.modify(state, (current) => {
1392
+ const submissions = new Map(current.submissions);
1393
+ let owned = 0;
1394
+
1395
+ for (const id of current.activeByThread.get(request.threadId) ?? []) {
1396
+ const stored = submissions.get(id);
1397
+
1398
+ if (stored === undefined) continue;
1399
+ if (stored.ownership !== undefined) owned++;
1400
+ if (stored.abortIntent === undefined)
1401
+ submissions.set(id, {
1402
+ ...stored,
1403
+ abortIntent: AbortIntent.make({
1404
+ submissionId: id,
1405
+ author: request.author,
1406
+ reason: "Worker owner stopped the worker",
1407
+ requestedAt: utc(now),
1408
+ }),
1409
+ });
1410
+ }
1411
+
1412
+ return [
1413
+ owned,
1414
+ {
1415
+ ...current,
1416
+ submissions,
1417
+ stoppedWorkers: current.stoppedWorkers.has(request.threadId)
1418
+ ? current.stoppedWorkers
1419
+ : new Map(current.stoppedWorkers).set(request.threadId, undefined),
1420
+ },
1421
+ ] as const;
1422
+ });
1423
+ });
1424
+
1425
+ const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fn(
1426
+ "MemorySubmissionLedger.requestAbort",
1427
+ )((unvalidated) =>
1428
+ Effect.gen(function* () {
1429
+ const request = yield* validate(AbortCommand, "requestAbort", unvalidated);
1430
+ const nowMillis = yield* Clock.currentTimeMillis;
1431
+
1432
+ const decision = yield* Ref.modify(
1433
+ state,
1434
+ (
1435
+ current,
1436
+ ): readonly [
1437
+ Decision<AbortIntent, SettlementConflict | JoinedToHost | LedgerError>,
1438
+ LedgerState,
1439
+ ] => {
1440
+ const stored = current.submissions.get(request.submissionId);
1441
+
1442
+ if (stored === undefined) {
1443
+ return [
1444
+ failure(ledgerError("requestAbort", `Unknown Submission ${request.submissionId}`)),
1445
+ current,
1446
+ ];
1447
+ }
1448
+ // A joined Submission settles WITH its host; the abort target is the host (plan
1449
+ // §2.5). A joining Submission still records the intent: it is honored only if the
1450
+ // host has not consumed the input (revert-then-abort).
1451
+ if (stored.row.state === "joined") {
1452
+ if (stored.joinedHostSubmissionId === undefined) {
1453
+ return [
1454
+ failure(
1455
+ ledgerError(
1456
+ "requestAbort",
1457
+ `Joined Submission ${request.submissionId} is missing its host linkage`,
1458
+ ),
1459
+ ),
1460
+ current,
1461
+ ];
1462
+ }
1463
+
1464
+ return [
1465
+ failure(
1466
+ JoinedToHost.make({
1467
+ submissionId: request.submissionId,
1468
+ hostSubmissionId: stored.joinedHostSubmissionId,
1469
+ }),
1470
+ ),
1471
+ current,
1472
+ ];
1473
+ }
1474
+ if (stored.row.state === "settled") {
1475
+ if (stored.row.settledOutcome === undefined) {
1476
+ return [
1477
+ failure(
1478
+ ledgerError(
1479
+ "requestAbort",
1480
+ `Settled Submission ${request.submissionId} is missing its outcome`,
1481
+ ),
1482
+ ),
1483
+ current,
1484
+ ];
1485
+ }
1486
+
1487
+ return [
1488
+ failure(
1489
+ SettlementConflict.make({
1490
+ submissionId: request.submissionId,
1491
+ existingOutcome: stored.row.settledOutcome,
1492
+ }),
1493
+ ),
1494
+ current,
1495
+ ];
1496
+ }
1497
+ if (stored.abortIntent !== undefined) return [success(stored.abortIntent), current];
1498
+
1499
+ const intent = AbortIntent.make({
1500
+ submissionId: request.submissionId,
1501
+ author: request.author,
1502
+ reason: request.reason,
1503
+ requestedAt: utc(nowMillis),
1504
+ });
1505
+
1506
+ return [success(intent), withSubmission(current, { ...stored, abortIntent: intent })];
1507
+ },
1508
+ );
1509
+
1510
+ if (decision._tag === "failure") return yield* decision.error;
1511
+
1512
+ return decision.value;
1513
+ }),
1514
+ );
1515
+
1516
+ const claimJoining: SubmissionLedger["Service"]["claimJoining"] = Effect.fn(
1517
+ "MemorySubmissionLedger.claimJoining",
1518
+ )((unvalidated) =>
1519
+ Effect.gen(function* () {
1520
+ const request = yield* validate(ClaimJoiningRequest, "claimJoining", unvalidated);
1521
+
1522
+ const decision = yield* Ref.modify(
1523
+ state,
1524
+ (
1525
+ current,
1526
+ ): readonly [
1527
+ Decision<ReadonlyArray<JoiningClaim>, OwnershipLost | LedgerError>,
1528
+ LedgerState,
1529
+ ] => {
1530
+ if (current.stoppedWorkers.has(request.threadId)) return [success([]), current];
1531
+
1532
+ const host = current.submissions.get(request.hostSubmissionId);
1533
+
1534
+ if (host === undefined) {
1535
+ return [
1536
+ failure(
1537
+ ledgerError("claimJoining", `Unknown Submission ${request.hostSubmissionId}`),
1538
+ ),
1539
+ current,
1540
+ ];
1541
+ }
1542
+ if (host.row.threadId !== request.threadId) {
1543
+ return [
1544
+ failure(
1545
+ ledgerError(
1546
+ "claimJoining",
1547
+ `Host Submission ${request.hostSubmissionId} does not belong to Thread ${request.threadId}`,
1548
+ ),
1549
+ ),
1550
+ current,
1551
+ ];
1552
+ }
1553
+ if (!ownsLane(current, host, request.ownershipToken)) {
1554
+ return [failure(ownershipLost(current, host)), current];
1555
+ }
1556
+
1557
+ const later = [...current.submissions.values()]
1558
+ .filter(
1559
+ (stored) =>
1560
+ stored.row.threadId === request.threadId &&
1561
+ stored.row.queueSequence > host.row.queueSequence,
1562
+ )
1563
+ .sort((left, right) => left.row.queueSequence - right.row.queueSequence);
1564
+
1565
+ const claims: Array<JoiningClaim> = [];
1566
+ const submissions = new Map(current.submissions);
1567
+
1568
+ for (const stored of later) {
1569
+ if (claims.length >= request.maxCount) break;
1570
+ // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
1571
+ // the coordinator re-delivers already-joined input through the coverage rule.
1572
+ if (
1573
+ (stored.row.state === "joining" || stored.row.state === "joined") &&
1574
+ stored.joinedHostSubmissionId === request.hostSubmissionId
1575
+ ) {
1576
+ continue;
1577
+ }
1578
+ // P7 §7(c): an aborted-settled row is a CLOSED obligation, not a gap — recovery
1579
+ // settles aborted never-claimed queued work immediately, and settlement order of
1580
+ // never-run work is not execution order (DUR-004 bounds execution).
1581
+ if (stored.row.state === "settled" && stored.row.settledOutcome === "aborted") {
1582
+ continue;
1583
+ }
1584
+ // Any other non-ready row — an admitted-not-ready gap in particular — breaks the
1585
+ // contiguous ready prefix (plan §2.5); later ready work stays queued (DUR-004).
1586
+ if (stored.row.state !== "ready") break;
1587
+ submissions.set(stored.row.submissionId, {
1588
+ ...stored,
1589
+ row: { ...stored.row, state: "joining" },
1590
+ joinedHostSubmissionId: request.hostSubmissionId,
1591
+ });
1592
+ claims.push(
1593
+ JoiningClaim.make({
1594
+ submissionId: stored.row.submissionId,
1595
+ queueSequence: stored.row.queueSequence,
1596
+ inputPayload: stored.row.inputPayload,
1597
+ }),
1598
+ );
1599
+ }
1600
+
1601
+ return [success(claims), { ...current, submissions }];
1602
+ },
1603
+ );
1604
+
1605
+ if (decision._tag === "failure") return yield* decision.error;
1606
+
1607
+ return decision.value;
1608
+ }),
1609
+ );
1610
+
1611
+ const markJoined: SubmissionLedger["Service"]["markJoined"] = Effect.fn(
1612
+ "MemorySubmissionLedger.markJoined",
1613
+ )((unvalidated) =>
1614
+ Effect.gen(function* () {
1615
+ const request = yield* validate(MarkJoinedRequest, "markJoined", unvalidated);
1616
+
1617
+ const decision = yield* Ref.modify(
1618
+ state,
1619
+ (current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
1620
+ const stored = current.submissions.get(request.submissionId);
1621
+
1622
+ if (stored === undefined) {
1623
+ return [
1624
+ failure(ledgerError("markJoined", `Unknown Submission ${request.submissionId}`)),
1625
+ current,
1626
+ ];
1627
+ }
1628
+ if (stored.joinedHostSubmissionId === undefined) {
1629
+ return [
1630
+ failure(
1631
+ ledgerError(
1632
+ "markJoined",
1633
+ `Submission ${request.submissionId} was never claimed for joining`,
1634
+ ),
1635
+ ),
1636
+ current,
1637
+ ];
1638
+ }
1639
+ const host = current.submissions.get(stored.joinedHostSubmissionId);
1640
+
1641
+ if (host === undefined) {
1642
+ return [
1643
+ failure(
1644
+ ledgerError(
1645
+ "markJoined",
1646
+ `Host Submission ${stored.joinedHostSubmissionId} is missing`,
1647
+ ),
1648
+ ),
1649
+ current,
1650
+ ];
1651
+ }
1652
+ // The lane is host-owned: the presented token must own the HOST's ownership period,
1653
+ // which also lets a later host Attempt repair a lost marker from history (DUR-016).
1654
+ if (!ownsLane(current, host, request.ownershipToken)) {
1655
+ return [failure(ownershipLost(current, host)), current];
1656
+ }
1657
+ if (stored.inputApplied !== undefined) {
1658
+ if (
1659
+ stored.inputApplied.recordId === request.recordId &&
1660
+ stored.inputApplied.sequence === request.sequence
1661
+ ) {
1662
+ return [success(undefined), current];
1663
+ }
1664
+
1665
+ return [
1666
+ failure(
1667
+ ledgerError(
1668
+ "markJoined",
1669
+ `A different join marker is already recorded for Submission ${request.submissionId}`,
1670
+ ),
1671
+ ),
1672
+ current,
1673
+ ];
1674
+ }
1675
+ if (stored.row.state !== "joining" && stored.row.state !== "joined") {
1676
+ return [
1677
+ failure(
1678
+ ledgerError(
1679
+ "markJoined",
1680
+ `Cannot mark Submission ${request.submissionId} joined from state ${stored.row.state}`,
1681
+ ),
1682
+ ),
1683
+ current,
1684
+ ];
1685
+ }
1686
+
1687
+ const marker = InputAppliedMarker.make({
1688
+ recordId: request.recordId,
1689
+ sequence: request.sequence,
1690
+ });
1691
+
1692
+ return [
1693
+ success(undefined),
1694
+ withSubmission(current, {
1695
+ ...stored,
1696
+ row: { ...stored.row, state: "joined" },
1697
+ inputApplied: marker,
1698
+ }),
1699
+ ];
1700
+ },
1701
+ );
1702
+
1703
+ if (decision._tag === "failure") return yield* decision.error;
1704
+ }),
1705
+ );
1706
+
1707
+ const revertJoining: SubmissionLedger["Service"]["revertJoining"] = Effect.fn(
1708
+ "MemorySubmissionLedger.revertJoining",
1709
+ )((unvalidated) =>
1710
+ Effect.gen(function* () {
1711
+ const request = yield* validate(RevertJoiningRequest, "revertJoining", unvalidated);
1712
+
1713
+ const decision = yield* Ref.modify(
1714
+ state,
1715
+ (current): readonly [Decision<void, LedgerError>, LedgerState] => {
1716
+ const stored = current.submissions.get(request.submissionId);
1717
+
1718
+ if (stored === undefined) {
1719
+ return [
1720
+ failure(ledgerError("revertJoining", `Unknown Submission ${request.submissionId}`)),
1721
+ current,
1722
+ ];
1723
+ }
1724
+ // Idempotent and recovery-only: only a still-`joining` Submission reverts; an
1725
+ // already-joined (or already-reverted) Submission is a no-op (DUR-016).
1726
+ if (stored.row.state !== "joining") return [success(undefined), current];
1727
+
1728
+ return [
1729
+ success(undefined),
1730
+ withSubmission(current, {
1731
+ ...stored,
1732
+ row: { ...stored.row, state: "ready" },
1733
+ joinedHostSubmissionId: undefined,
1734
+ }),
1735
+ ];
1736
+ },
1737
+ );
1738
+
1739
+ if (decision._tag === "failure") return yield* decision.error;
1740
+ }),
1741
+ );
1742
+
1743
+ const suspend: SubmissionLedger["Service"]["suspend"] = Effect.fn(
1744
+ "MemorySubmissionLedger.suspend",
1745
+ )((unvalidated) =>
1746
+ Effect.gen(function* () {
1747
+ const request = yield* validate(SuspendRequest, "suspend", unvalidated);
1748
+ const nowMillis = yield* Clock.currentTimeMillis;
1749
+
1750
+ const decision = yield* Ref.modify(
1751
+ state,
1752
+ (
1753
+ current,
1754
+ ): readonly [
1755
+ Decision<SuspensionOutcome, OwnershipLost | SettlementConflict | LedgerError>,
1756
+ LedgerState,
1757
+ ] => {
1758
+ const stored = current.submissions.get(request.submissionId);
1759
+
1760
+ if (stored === undefined) {
1761
+ return [
1762
+ failure(ledgerError("suspend", `Unknown Submission ${request.submissionId}`)),
1763
+ current,
1764
+ ];
1765
+ }
1766
+ if (stored.row.state === "settled") {
1767
+ if (stored.row.settledOutcome === undefined) {
1768
+ return [
1769
+ failure(
1770
+ ledgerError(
1771
+ "suspend",
1772
+ `Settled Submission ${request.submissionId} is missing its outcome`,
1773
+ ),
1774
+ ),
1775
+ current,
1776
+ ];
1777
+ }
1778
+
1779
+ return [
1780
+ failure(
1781
+ SettlementConflict.make({
1782
+ submissionId: request.submissionId,
1783
+ existingOutcome: stored.row.settledOutcome,
1784
+ }),
1785
+ ),
1786
+ current,
1787
+ ];
1788
+ }
1789
+ // An exact terminal outcome is already reserved (DUR-011); suspension would
1790
+ // contradict it, so the reservation wins.
1791
+ if (stored.reservation !== undefined) {
1792
+ return [
1793
+ failure(
1794
+ SettlementConflict.make({
1795
+ submissionId: request.submissionId,
1796
+ existingOutcome: stored.reservation.outcome,
1797
+ }),
1798
+ ),
1799
+ current,
1800
+ ];
1801
+ }
1802
+ if (!ownsLane(current, stored, request.ownershipToken)) {
1803
+ return [failure(ownershipLost(current, stored)), current];
1804
+ }
1805
+
1806
+ // A covering event that raced ahead of the suspend transaction (an approval decision,
1807
+ // or a child settlement observed directly from the child's row in this single store)
1808
+ // resumes the caller immediately WITHOUT releasing the lane (plan §2.6, spec §12).
1809
+ const alreadyCovered =
1810
+ request.reason._tag === "ApprovalPending"
1811
+ ? request.reason.toolCallIds.every((toolCallId) =>
1812
+ stored.approvalDecisions.has(toolCallId),
1813
+ )
1814
+ : request.reason.children.every(
1815
+ (child) =>
1816
+ current.submissions.get(child.childSubmissionId)?.row.state === "settled",
1817
+ );
1818
+
1819
+ if (alreadyCovered) {
1820
+ return [success("resume-immediately" as const), current];
1821
+ }
1822
+
1823
+ return [
1824
+ success("suspended" as const),
1825
+ withSubmission(current, {
1826
+ ...stored,
1827
+ row: { ...stored.row, state: "suspended" },
1828
+ ownership: undefined,
1829
+ suspension: { reason: request.reason, suspendedAtMillis: nowMillis },
1830
+ }),
1831
+ ];
1832
+ },
1833
+ );
1834
+
1835
+ if (decision._tag === "failure") return yield* decision.error;
1836
+
1837
+ return decision.value;
1838
+ }),
1839
+ );
1840
+
1841
+ const recordApprovalDecision: SubmissionLedger["Service"]["recordApprovalDecision"] = Effect.fn(
1842
+ "MemorySubmissionLedger.recordApprovalDecision",
1843
+ )((unvalidated) =>
1844
+ Effect.gen(function* () {
1845
+ const command = yield* validate(
1846
+ ApprovalDecisionCommand,
1847
+ "recordApprovalDecision",
1848
+ unvalidated,
1849
+ );
1850
+
1851
+ const nowMillis = yield* Clock.currentTimeMillis;
1852
+
1853
+ const decision = yield* Ref.modify(
1854
+ state,
1855
+ (
1856
+ current,
1857
+ ): readonly [
1858
+ Decision<ApprovalDecisionIntent, ApprovalConflict | SettlementConflict | LedgerError>,
1859
+ LedgerState,
1860
+ ] => {
1861
+ const stored = current.submissions.get(command.submissionId);
1862
+
1863
+ if (stored === undefined) {
1864
+ return [
1865
+ failure(
1866
+ ledgerError(
1867
+ "recordApprovalDecision",
1868
+ `Unknown Submission ${command.submissionId}`,
1869
+ ),
1870
+ ),
1871
+ current,
1872
+ ];
1873
+ }
1874
+ if (stored.row.state === "settled") {
1875
+ if (stored.row.settledOutcome === undefined) {
1876
+ return [
1877
+ failure(
1878
+ ledgerError(
1879
+ "recordApprovalDecision",
1880
+ `Settled Submission ${command.submissionId} is missing its outcome`,
1881
+ ),
1882
+ ),
1883
+ current,
1884
+ ];
1885
+ }
1886
+
1887
+ return [
1888
+ failure(
1889
+ SettlementConflict.make({
1890
+ submissionId: command.submissionId,
1891
+ existingOutcome: stored.row.settledOutcome,
1892
+ }),
1893
+ ),
1894
+ current,
1895
+ ];
1896
+ }
1897
+ const existing = stored.approvalDecisions.get(command.toolCallId);
1898
+
1899
+ if (existing !== undefined) {
1900
+ if (existing.decision !== command.decision) {
1901
+ return [
1902
+ failure(
1903
+ ApprovalConflict.make({
1904
+ submissionId: command.submissionId,
1905
+ toolCallId: command.toolCallId,
1906
+ existingDecision: existing.decision,
1907
+ }),
1908
+ ),
1909
+ current,
1910
+ ];
1911
+ }
1912
+
1913
+ return [success(existing), current];
1914
+ }
1915
+
1916
+ const intent = ApprovalDecisionIntent.make({
1917
+ submissionId: command.submissionId,
1918
+ toolCallId: command.toolCallId,
1919
+ decision: command.decision,
1920
+ resolver: command.resolver,
1921
+ reason: command.reason,
1922
+ decidedAt: utc(nowMillis),
1923
+ });
1924
+
1925
+ const approvalDecisions = new Map(stored.approvalDecisions).set(
1926
+ command.toolCallId,
1927
+ intent,
1928
+ );
1929
+
1930
+ // Once every pending call of an ApprovalPending suspension is decided, the lane
1931
+ // wakes: suspended → input-applied (plan §2.6). A WaitingForChild suspension wakes
1932
+ // only through recordChildSettled.
1933
+ const wakes =
1934
+ stored.row.state === "suspended" &&
1935
+ stored.suspension !== undefined &&
1936
+ stored.suspension.reason._tag === "ApprovalPending" &&
1937
+ stored.suspension.reason.toolCallIds.every((toolCallId) =>
1938
+ approvalDecisions.has(toolCallId),
1939
+ );
1940
+
1941
+ return [
1942
+ success(intent),
1943
+ withSubmission(current, {
1944
+ ...stored,
1945
+ row: wakes ? { ...stored.row, state: "input-applied" } : stored.row,
1946
+ suspension: wakes ? undefined : stored.suspension,
1947
+ approvalDecisions,
1948
+ }),
1949
+ ];
1950
+ },
1951
+ );
1952
+
1953
+ if (decision._tag === "failure") return yield* decision.error;
1954
+
1955
+ return decision.value;
1956
+ }),
1957
+ );
1958
+
1959
+ const markUnknown: SubmissionLedger["Service"]["markUnknown"] = Effect.fn(
1960
+ "MemorySubmissionLedger.markUnknown",
1961
+ )((unvalidated) =>
1962
+ Effect.gen(function* () {
1963
+ const request = yield* validate(MarkUnknownRequest, "markUnknown", unvalidated);
1964
+
1965
+ const decision = yield* Ref.modify(
1966
+ state,
1967
+ (current): readonly [Decision<void, SettlementConflict | LedgerError>, LedgerState] => {
1968
+ const stored = current.submissions.get(request.submissionId);
1969
+
1970
+ if (stored === undefined) {
1971
+ return [
1972
+ failure(ledgerError("markUnknown", `Unknown Submission ${request.submissionId}`)),
1973
+ current,
1974
+ ];
1975
+ }
1976
+ if (stored.row.state === "settled") {
1977
+ if (stored.row.settledOutcome === undefined) {
1978
+ return [
1979
+ failure(
1980
+ ledgerError(
1981
+ "markUnknown",
1982
+ `Settled Submission ${request.submissionId} is missing its outcome`,
1983
+ ),
1984
+ ),
1985
+ current,
1986
+ ];
1987
+ }
1988
+
1989
+ return [
1990
+ failure(
1991
+ SettlementConflict.make({
1992
+ submissionId: request.submissionId,
1993
+ existingOutcome: stored.row.settledOutcome,
1994
+ }),
1995
+ ),
1996
+ current,
1997
+ ];
1998
+ }
1999
+ // A reserved exact outcome wins over a late Unknown marking (DUR-011); the recovery
2000
+ // classifier orders reservation ahead of MarkUnknown for the same reason.
2001
+ if (stored.reservation !== undefined) {
2002
+ return [
2003
+ failure(
2004
+ SettlementConflict.make({
2005
+ submissionId: request.submissionId,
2006
+ existingOutcome: stored.reservation.outcome,
2007
+ }),
2008
+ ),
2009
+ current,
2010
+ ];
2011
+ }
2012
+ // Idempotent merge: repeating is a no-op; additional open calls extend the marked
2013
+ // set while the first recorded reason is kept.
2014
+ const existing = stored.unknownMark;
2015
+ const known = new Set(existing?.toolCallIds ?? []);
2016
+
2017
+ const merged = [
2018
+ ...(existing?.toolCallIds ?? []),
2019
+ ...request.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
2020
+ ];
2021
+
2022
+ return [
2023
+ success(undefined),
2024
+ withSubmission(current, {
2025
+ ...stored,
2026
+ row:
2027
+ stored.row.state === "unknown" ? stored.row : { ...stored.row, state: "unknown" },
2028
+ unknownMark: { reason: existing?.reason ?? request.reason, toolCallIds: merged },
2029
+ }),
2030
+ ];
2031
+ },
2032
+ );
2033
+
2034
+ if (decision._tag === "failure") return yield* decision.error;
2035
+ }),
2036
+ );
2037
+
2038
+ const recordUnknownResolution: SubmissionLedger["Service"]["recordUnknownResolution"] =
2039
+ Effect.fn("MemorySubmissionLedger.recordUnknownResolution")((unvalidated) =>
2040
+ Effect.gen(function* () {
2041
+ const command = yield* validate(
2042
+ UnknownResolutionCommand,
2043
+ "recordUnknownResolution",
2044
+ unvalidated,
2045
+ );
2046
+
2047
+ const nowMillis = yield* Clock.currentTimeMillis;
2048
+
2049
+ const decision = yield* Ref.modify(
2050
+ state,
2051
+ (
2052
+ current,
2053
+ ): readonly [
2054
+ Decision<
2055
+ UnknownResolutionIntent,
2056
+ UnknownResolutionConflict | SettlementConflict | LedgerError
2057
+ >,
2058
+ LedgerState,
2059
+ ] => {
2060
+ const stored = current.submissions.get(command.submissionId);
2061
+
2062
+ if (stored === undefined) {
2063
+ return [
2064
+ failure(
2065
+ ledgerError(
2066
+ "recordUnknownResolution",
2067
+ `Unknown Submission ${command.submissionId}`,
2068
+ ),
2069
+ ),
2070
+ current,
2071
+ ];
2072
+ }
2073
+ if (stored.row.state === "settled") {
2074
+ if (stored.row.settledOutcome === undefined) {
2075
+ return [
2076
+ failure(
2077
+ ledgerError(
2078
+ "recordUnknownResolution",
2079
+ `Settled Submission ${command.submissionId} is missing its outcome`,
2080
+ ),
2081
+ ),
2082
+ current,
2083
+ ];
2084
+ }
2085
+
2086
+ return [
2087
+ failure(
2088
+ SettlementConflict.make({
2089
+ submissionId: command.submissionId,
2090
+ existingOutcome: stored.row.settledOutcome,
2091
+ }),
2092
+ ),
2093
+ current,
2094
+ ];
2095
+ }
2096
+ const existing = stored.unknownResolutions.get(command.toolCallId);
2097
+
2098
+ if (
2099
+ existing !== undefined &&
2100
+ !equivalentUnknownResolution(existing.intent.resolution, command.resolution)
2101
+ ) {
2102
+ return [
2103
+ failure(
2104
+ UnknownResolutionConflict.make({
2105
+ submissionId: command.submissionId,
2106
+ toolCallId: command.toolCallId,
2107
+ }),
2108
+ ),
2109
+ current,
2110
+ ];
2111
+ }
2112
+
2113
+ const intent =
2114
+ existing?.intent ??
2115
+ UnknownResolutionIntent.make({
2116
+ submissionId: command.submissionId,
2117
+ toolCallId: command.toolCallId,
2118
+ author: command.author,
2119
+ reason: command.reason,
2120
+ resolution: command.resolution,
2121
+ resolvedAt: utc(nowMillis),
2122
+ });
2123
+
2124
+ const unknownResolutions =
2125
+ existing !== undefined
2126
+ ? stored.unknownResolutions
2127
+ : new Map(stored.unknownResolutions).set(command.toolCallId, {
2128
+ intent,
2129
+ });
2130
+
2131
+ // The lane reopens only when EVERY marked open call has a durable resolution
2132
+ // intent: unknown → input-applied (DUR-017). Replays re-run the coverage check so
2133
+ // a recovering caller can wake the lane idempotently.
2134
+ const wakes =
2135
+ stored.row.state === "unknown" &&
2136
+ stored.unknownMark !== undefined &&
2137
+ stored.unknownMark.toolCallIds.every((toolCallId) =>
2138
+ unknownResolutions.has(toolCallId),
2139
+ );
2140
+
2141
+ return [
2142
+ success(intent),
2143
+ withSubmission(current, {
2144
+ ...stored,
2145
+ row: wakes ? { ...stored.row, state: "input-applied" } : stored.row,
2146
+ unknownMark: wakes ? undefined : stored.unknownMark,
2147
+ unknownResolutions,
2148
+ }),
2149
+ ];
2150
+ },
2151
+ );
2152
+
2153
+ if (decision._tag === "failure") return yield* decision.error;
2154
+
2155
+ return decision.value;
2156
+ }),
2157
+ );
2158
+
2159
+ const recordChildSettled: SubmissionLedger["Service"]["recordChildSettled"] = Effect.fn(
2160
+ "MemorySubmissionLedger.recordChildSettled",
2161
+ )((unvalidated) =>
2162
+ Effect.gen(function* () {
2163
+ const request = yield* validate(
2164
+ ChildSettledNotification,
2165
+ "recordChildSettled",
2166
+ unvalidated,
2167
+ );
2168
+
2169
+ const decision = yield* Ref.modify(
2170
+ state,
2171
+ (current): readonly [Decision<ChildSettledOutcome, LedgerError>, LedgerState] => {
2172
+ const parent = current.submissions.get(request.parentSubmissionId);
2173
+
2174
+ if (parent === undefined) {
2175
+ return [
2176
+ failure(
2177
+ ledgerError(
2178
+ "recordChildSettled",
2179
+ `Unknown Submission ${request.parentSubmissionId}`,
2180
+ ),
2181
+ ),
2182
+ current,
2183
+ ];
2184
+ }
2185
+ // The caller may notify after the canonical Settlement append but before ledger
2186
+ // finalization. In a single store, an exact reservation plus `terminalizing` is the
2187
+ // narrow durable prefix that makes that ordering admissible; earlier states remain
2188
+ // a caller error.
2189
+ const child = current.submissions.get(request.childSubmissionId);
2190
+
2191
+ const announced =
2192
+ child !== undefined &&
2193
+ (child.row.state === "settled" ||
2194
+ (child.row.state === "terminalizing" && child.reservation !== undefined));
2195
+
2196
+ if (!announced) {
2197
+ return [
2198
+ failure(
2199
+ ledgerError(
2200
+ "recordChildSettled",
2201
+ `Child Submission ${request.childSubmissionId} has no recorded settlement`,
2202
+ ),
2203
+ ),
2204
+ current,
2205
+ ];
2206
+ }
2207
+ if (
2208
+ parent.row.state !== "suspended" ||
2209
+ parent.suspension === undefined ||
2210
+ parent.suspension.reason._tag !== "WaitingForChild"
2211
+ ) {
2212
+ return [success("not-waiting" as const), current];
2213
+ }
2214
+ const children = parent.suspension.reason.children;
2215
+
2216
+ if (!children.some((entry) => entry.childSubmissionId === request.childSubmissionId)) {
2217
+ return [success("not-waiting" as const), current];
2218
+ }
2219
+
2220
+ // Every listed child must be either finalized or canonically announced from the
2221
+ // exact terminalizing reservation. Replays re-run this coverage check idempotently.
2222
+ const allSettled = children.every((entry) => {
2223
+ const listed = current.submissions.get(entry.childSubmissionId);
2224
+
2225
+ return (
2226
+ listed?.row.state === "settled" ||
2227
+ (listed?.row.state === "terminalizing" && listed.reservation !== undefined)
2228
+ );
2229
+ });
2230
+
2231
+ if (!allSettled) return [success("still-waiting" as const), current];
2232
+
2233
+ return [
2234
+ success("woken" as const),
2235
+ withSubmission(current, {
2236
+ ...parent,
2237
+ row: { ...parent.row, state: "input-applied" },
2238
+ suspension: undefined,
2239
+ }),
2240
+ ];
2241
+ },
2242
+ );
2243
+
2244
+ if (decision._tag === "failure") return yield* decision.error;
2245
+
2246
+ return decision.value;
2247
+ }),
2248
+ );
2249
+
2250
+ const reserveChildBudget: SubmissionLedger["Service"]["reserveChildBudget"] = Effect.fn(
2251
+ "MemorySubmissionLedger.reserveChildBudget",
2252
+ )((unvalidated) =>
2253
+ Effect.gen(function* () {
2254
+ const request = yield* validate(
2255
+ ChildBudgetReservationRequest,
2256
+ "reserveChildBudget",
2257
+ unvalidated,
2258
+ );
2259
+
2260
+ const nowMillis = yield* Clock.currentTimeMillis;
2261
+
2262
+ const decision = yield* Ref.modify(
2263
+ state,
2264
+ (
2265
+ current,
2266
+ ): readonly [
2267
+ Decision<ReservedChildBudget, ChildReservationConflict | OwnershipLost | LedgerError>,
2268
+ LedgerState,
2269
+ ] => {
2270
+ const existing = current.childReservations.get(request.reservationId);
2271
+
2272
+ if (existing !== undefined) {
2273
+ // Identical replays short-circuit before the fence, mirroring reserveSettlement:
2274
+ // a replay creates nothing, so a recovering caller resumes rather than duplicates.
2275
+ const identical =
2276
+ existing.parentSubmissionId === request.parentSubmissionId &&
2277
+ existing.parentToolCallId === request.parentToolCallId &&
2278
+ existing.allocationDigest === request.allocationDigest &&
2279
+ equivalentPersistedJson(existing.allocation, request.allocation);
2280
+
2281
+ if (!identical) {
2282
+ return [
2283
+ failure(
2284
+ ChildReservationConflict.make({
2285
+ reservationId: request.reservationId,
2286
+ status: existing.status,
2287
+ message:
2288
+ "A reservation with this identity exists with a different parent Tool Call or allocation.",
2289
+ }),
2290
+ ),
2291
+ current,
2292
+ ];
2293
+ }
2294
+
2295
+ return [
2296
+ success(
2297
+ ReservedChildBudget.make({
2298
+ reservation: toReservationSnapshot(existing),
2299
+ replayed: true,
2300
+ }),
2301
+ ),
2302
+ current,
2303
+ ];
2304
+ }
2305
+ for (const reservation of current.childReservations.values()) {
2306
+ if (
2307
+ reservation.parentSubmissionId === request.parentSubmissionId &&
2308
+ reservation.parentToolCallId === request.parentToolCallId
2309
+ ) {
2310
+ return [
2311
+ failure(
2312
+ ChildReservationConflict.make({
2313
+ reservationId: request.reservationId,
2314
+ status: reservation.status,
2315
+ message: `Parent Tool Call ${request.parentToolCallId} already owns reservation ${reservation.reservationId}.`,
2316
+ }),
2317
+ ),
2318
+ current,
2319
+ ];
2320
+ }
2321
+ }
2322
+ const parent = current.submissions.get(request.parentSubmissionId);
2323
+
2324
+ if (parent === undefined) {
2325
+ return [
2326
+ failure(
2327
+ ledgerError(
2328
+ "reserveChildBudget",
2329
+ `Unknown Submission ${request.parentSubmissionId}`,
2330
+ ),
2331
+ ),
2332
+ current,
2333
+ ];
2334
+ }
2335
+ // Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
2336
+ // parent Attempt can never create new reservation state.
2337
+ if (!ownsLane(current, parent, request.ownershipToken)) {
2338
+ return [failure(ownershipLost(current, parent)), current];
2339
+ }
2340
+
2341
+ const reservation: StoredChildReservation = {
2342
+ reservationId: request.reservationId,
2343
+ parentSubmissionId: request.parentSubmissionId,
2344
+ parentToolCallId: request.parentToolCallId,
2345
+ childSubmissionId: undefined,
2346
+ status: "reserved",
2347
+ allocation: request.allocation,
2348
+ allocationDigest: request.allocationDigest,
2349
+ accounting: undefined,
2350
+ reservedAtMillis: nowMillis,
2351
+ releaseBeganAtMillis: undefined,
2352
+ releasedAtMillis: undefined,
2353
+ };
2354
+
2355
+ return [
2356
+ success(
2357
+ ReservedChildBudget.make({
2358
+ reservation: toReservationSnapshot(reservation),
2359
+ replayed: false,
2360
+ }),
2361
+ ),
2362
+ withChildReservation(current, reservation),
2363
+ ];
2364
+ },
2365
+ );
2366
+
2367
+ if (decision._tag === "failure") return yield* decision.error;
2368
+
2369
+ return decision.value;
2370
+ }),
2371
+ );
2372
+
2373
+ const attachChildToReservation: SubmissionLedger["Service"]["attachChildToReservation"] =
2374
+ Effect.fn("MemorySubmissionLedger.attachChildToReservation")((unvalidated) =>
2375
+ Effect.gen(function* () {
2376
+ const request = yield* validate(
2377
+ AttachChildToReservationRequest,
2378
+ "attachChildToReservation",
2379
+ unvalidated,
2380
+ );
2381
+
2382
+ const decision = yield* Ref.modify(
2383
+ state,
2384
+ (
2385
+ current,
2386
+ ): readonly [
2387
+ Decision<
2388
+ ChildBudgetReservationSnapshot,
2389
+ ChildReservationConflict | OwnershipLost | LedgerError
2390
+ >,
2391
+ LedgerState,
2392
+ ] => {
2393
+ const reservation = current.childReservations.get(request.reservationId);
2394
+
2395
+ if (reservation === undefined) {
2396
+ return [
2397
+ failure(
2398
+ ledgerError(
2399
+ "attachChildToReservation",
2400
+ `Unknown child reservation ${request.reservationId}`,
2401
+ ),
2402
+ ),
2403
+ current,
2404
+ ];
2405
+ }
2406
+ if (reservation.childSubmissionId !== undefined) {
2407
+ // Idempotent replay of the recorded attachment (unfenced — it mutates nothing).
2408
+ if (reservation.childSubmissionId === request.childSubmissionId) {
2409
+ return [success(toReservationSnapshot(reservation)), current];
2410
+ }
2411
+
2412
+ return [
2413
+ failure(
2414
+ ChildReservationConflict.make({
2415
+ reservationId: request.reservationId,
2416
+ status: reservation.status,
2417
+ message: `Reservation ${request.reservationId} already records child ${reservation.childSubmissionId}.`,
2418
+ }),
2419
+ ),
2420
+ current,
2421
+ ];
2422
+ }
2423
+ const parent = current.submissions.get(reservation.parentSubmissionId);
2424
+
2425
+ if (parent === undefined) {
2426
+ return [
2427
+ failure(
2428
+ ledgerError(
2429
+ "attachChildToReservation",
2430
+ `Unknown Submission ${reservation.parentSubmissionId}`,
2431
+ ),
2432
+ ),
2433
+ current,
2434
+ ];
2435
+ }
2436
+ if (!ownsLane(current, parent, request.ownershipToken)) {
2437
+ return [failure(ownershipLost(current, parent)), current];
2438
+ }
2439
+ if (reservation.status !== "reserved") {
2440
+ return [
2441
+ failure(
2442
+ ChildReservationConflict.make({
2443
+ reservationId: request.reservationId,
2444
+ status: reservation.status,
2445
+ message: `Cannot attach a child to a ${reservation.status} reservation.`,
2446
+ }),
2447
+ ),
2448
+ current,
2449
+ ];
2450
+ }
2451
+ // Single-store latitude: the admitted child must exist here, so a dangling
2452
+ // attachment can never enter the recovery view.
2453
+ if (!current.submissions.has(request.childSubmissionId)) {
2454
+ return [
2455
+ failure(
2456
+ ledgerError(
2457
+ "attachChildToReservation",
2458
+ `Unknown child Submission ${request.childSubmissionId}`,
2459
+ ),
2460
+ ),
2461
+ current,
2462
+ ];
2463
+ }
2464
+
2465
+ const attached: StoredChildReservation = {
2466
+ ...reservation,
2467
+ childSubmissionId: request.childSubmissionId,
2468
+ };
2469
+
2470
+ return [
2471
+ success(toReservationSnapshot(attached)),
2472
+ withChildReservation(current, attached),
2473
+ ];
2474
+ },
2475
+ );
2476
+
2477
+ if (decision._tag === "failure") return yield* decision.error;
2478
+
2479
+ return decision.value;
2480
+ }),
2481
+ );
2482
+
2483
+ const beginChildBudgetRelease: SubmissionLedger["Service"]["beginChildBudgetRelease"] =
2484
+ Effect.fn("MemorySubmissionLedger.beginChildBudgetRelease")((unvalidated) =>
2485
+ Effect.gen(function* () {
2486
+ const request = yield* validate(
2487
+ BeginChildBudgetReleaseRequest,
2488
+ "beginChildBudgetRelease",
2489
+ unvalidated,
2490
+ );
2491
+
2492
+ const nowMillis = yield* Clock.currentTimeMillis;
2493
+
2494
+ const decision = yield* Ref.modify(
2495
+ state,
2496
+ (
2497
+ current,
2498
+ ): readonly [
2499
+ Decision<ChildBudgetReservationSnapshot, ChildReservationConflict | LedgerError>,
2500
+ LedgerState,
2501
+ ] => {
2502
+ const reservation = current.childReservations.get(request.reservationId);
2503
+
2504
+ if (reservation === undefined) {
2505
+ return [
2506
+ failure(
2507
+ ledgerError(
2508
+ "beginChildBudgetRelease",
2509
+ `Unknown child reservation ${request.reservationId}`,
2510
+ ),
2511
+ ),
2512
+ current,
2513
+ ];
2514
+ }
2515
+ if (reservation.status !== "reserved") {
2516
+ // The accounting decision was already frozen exactly once; an identical replay is
2517
+ // a no-op and a divergent decision conflicts (spec §12 join step 6).
2518
+ if (
2519
+ reservation.accounting !== undefined &&
2520
+ equivalentPersistedJson(reservation.accounting, request.accounting)
2521
+ ) {
2522
+ return [success(toReservationSnapshot(reservation)), current];
2523
+ }
2524
+
2525
+ return [
2526
+ failure(
2527
+ ChildReservationConflict.make({
2528
+ reservationId: request.reservationId,
2529
+ status: reservation.status,
2530
+ message:
2531
+ "A different accounting decision is already frozen for this reservation.",
2532
+ }),
2533
+ ),
2534
+ current,
2535
+ ];
2536
+ }
2537
+
2538
+ const frozen: StoredChildReservation = {
2539
+ ...reservation,
2540
+ status: "releasePending",
2541
+ accounting: request.accounting,
2542
+ releaseBeganAtMillis: nowMillis,
2543
+ };
2544
+
2545
+ return [
2546
+ success(toReservationSnapshot(frozen)),
2547
+ withChildReservation(current, frozen),
2548
+ ];
2549
+ },
2550
+ );
2551
+
2552
+ if (decision._tag === "failure") return yield* decision.error;
2553
+
2554
+ return decision.value;
2555
+ }),
2556
+ );
2557
+
2558
+ const releaseChildBudget: SubmissionLedger["Service"]["releaseChildBudget"] = Effect.fn(
2559
+ "MemorySubmissionLedger.releaseChildBudget",
2560
+ )((unvalidated) =>
2561
+ Effect.gen(function* () {
2562
+ const request = yield* validate(
2563
+ ReleaseChildBudgetRequest,
2564
+ "releaseChildBudget",
2565
+ unvalidated,
2566
+ );
2567
+
2568
+ const nowMillis = yield* Clock.currentTimeMillis;
2569
+
2570
+ const decision = yield* Ref.modify(
2571
+ state,
2572
+ (
2573
+ current,
2574
+ ): readonly [
2575
+ Decision<ChildBudgetReservationSnapshot, ChildReservationConflict | LedgerError>,
2576
+ LedgerState,
2577
+ ] => {
2578
+ const reservation = current.childReservations.get(request.reservationId);
2579
+
2580
+ if (reservation === undefined) {
2581
+ return [
2582
+ failure(
2583
+ ledgerError(
2584
+ "releaseChildBudget",
2585
+ `Unknown child reservation ${request.reservationId}`,
2586
+ ),
2587
+ ),
2588
+ current,
2589
+ ];
2590
+ }
2591
+ // Applied exactly once: replaying a released reservation returns the stored row
2592
+ // unchanged (spec §12: "never available twice").
2593
+ if (reservation.status === "released") {
2594
+ return [success(toReservationSnapshot(reservation)), current];
2595
+ }
2596
+ if (reservation.status !== "releasePending") {
2597
+ return [
2598
+ failure(
2599
+ ChildReservationConflict.make({
2600
+ reservationId: request.reservationId,
2601
+ status: reservation.status,
2602
+ message:
2603
+ "Cannot release a reservation whose accounting decision is not frozen.",
2604
+ }),
2605
+ ),
2606
+ current,
2607
+ ];
2608
+ }
2609
+
2610
+ const released: StoredChildReservation = {
2611
+ ...reservation,
2612
+ status: "released",
2613
+ releasedAtMillis: nowMillis,
2614
+ };
2615
+
2616
+ return [
2617
+ success(toReservationSnapshot(released)),
2618
+ withChildReservation(current, released),
2619
+ ];
2620
+ },
2621
+ );
2622
+
2623
+ if (decision._tag === "failure") return yield* decision.error;
2624
+
2625
+ return decision.value;
2626
+ }),
2627
+ );
2628
+
2629
+ const scanNonterminal: SubmissionLedger["Service"]["scanNonterminal"] = Stream.unwrap(
2630
+ Ref.get(state).pipe(
2631
+ Effect.map((current) => {
2632
+ const snapshots = [...current.submissions.values()]
2633
+ .filter((stored) => stored.row.state !== "settled")
2634
+ .sort((left, right) =>
2635
+ left.row.threadId < right.row.threadId
2636
+ ? -1
2637
+ : left.row.threadId > right.row.threadId
2638
+ ? 1
2639
+ : left.row.queueSequence - right.row.queueSequence,
2640
+ )
2641
+ .map(({ row }) =>
2642
+ SubmissionWorkItem.make({
2643
+ submissionId: row.submissionId,
2644
+ threadId: row.threadId,
2645
+ queueSequence: row.queueSequence,
2646
+ principal: row.principal,
2647
+ idempotencyKey: row.idempotencyKey,
2648
+ deploymentId: row.deploymentId,
2649
+ receiptId: row.receiptId,
2650
+ state: row.state,
2651
+ }),
2652
+ );
2653
+
2654
+ return Stream.fromIterable(snapshots);
2655
+ }),
2656
+ ),
2657
+ );
2658
+
2659
+ const readAbortIntent: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
2660
+ "MemorySubmissionLedger.readAbortIntent",
2661
+ )(function* (unvalidated) {
2662
+ const request = yield* validate(AbortIntentRequest, "readAbortIntent", unvalidated);
2663
+ const stored = (yield* Ref.get(state)).submissions.get(request.submissionId);
2664
+
2665
+ if (stored === undefined) {
2666
+ return yield* ledgerError("readAbortIntent", `Unknown Submission ${request.submissionId}`);
2667
+ }
2668
+
2669
+ return stored.abortIntent;
2670
+ });
2671
+
2672
+ const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
2673
+ "MemorySubmissionLedger.loadRecoverySnapshot",
2674
+ )((unvalidated) =>
2675
+ Effect.gen(function* () {
2676
+ const request = yield* validate(
2677
+ RecoverySnapshotRequest,
2678
+ "loadRecoverySnapshot",
2679
+ unvalidated,
2680
+ );
2681
+
2682
+ const current = yield* Ref.get(state);
2683
+ const stored = current.submissions.get(request.submissionId);
2684
+
2685
+ if (stored === undefined) {
2686
+ return yield* ledgerError(
2687
+ "loadRecoverySnapshot",
2688
+ `Unknown Submission ${request.submissionId}`,
2689
+ );
2690
+ }
2691
+
2692
+ const joins = [...current.submissions.values()]
2693
+ .filter((candidate) => candidate.joinedHostSubmissionId === request.submissionId)
2694
+ .sort((left, right) => left.row.queueSequence - right.row.queueSequence)
2695
+ .map((candidate) =>
2696
+ JoinSnapshot.make({
2697
+ submissionId: candidate.row.submissionId,
2698
+ state: candidate.row.state,
2699
+ hostSubmissionId: request.submissionId,
2700
+ }),
2701
+ );
2702
+
2703
+ const byToolCallId = <A extends { readonly toolCallId: ToolCallId }>(
2704
+ left: A,
2705
+ right: A,
2706
+ ): number =>
2707
+ left.toolCallId < right.toolCallId ? -1 : left.toolCallId > right.toolCallId ? 1 : 0;
2708
+
2709
+ // Parent-side subagent view: this Submission's child budget reservations in parent Tool
2710
+ // Call order, plus each attached child's current lane state (a disposable derived view;
2711
+ // the canonical records stay the recovery truth, DUR-015).
2712
+ const childReservations = [...current.childReservations.values()]
2713
+ .filter((reservation) => reservation.parentSubmissionId === request.submissionId)
2714
+ .sort((left, right) =>
2715
+ left.parentToolCallId < right.parentToolCallId
2716
+ ? -1
2717
+ : left.parentToolCallId > right.parentToolCallId
2718
+ ? 1
2719
+ : 0,
2720
+ );
2721
+
2722
+ const childAttachments: Array<ChildAttachmentSnapshot> = [];
2723
+
2724
+ for (const reservation of childReservations) {
2725
+ if (reservation.childSubmissionId === undefined) continue;
2726
+ const child = current.submissions.get(reservation.childSubmissionId);
2727
+
2728
+ if (child === undefined) continue;
2729
+ childAttachments.push(
2730
+ ChildAttachmentSnapshot.make({
2731
+ toolCallId: reservation.parentToolCallId,
2732
+ childSubmissionId: reservation.childSubmissionId,
2733
+ childState: child.row.state,
2734
+ ...(child.row.settledOutcome === undefined
2735
+ ? {}
2736
+ : { childOutcome: child.row.settledOutcome }),
2737
+ }),
2738
+ );
2739
+ }
2740
+
2741
+ return RecoverySnapshot.make({
2742
+ submission: toSnapshot(stored.row),
2743
+ joins,
2744
+ approvalDecisions: [...stored.approvalDecisions.values()].sort(byToolCallId),
2745
+ unknownResolutions: [...stored.unknownResolutions.values()]
2746
+ .map((resolution) => resolution.intent)
2747
+ .sort(byToolCallId),
2748
+ childReservations: childReservations.map(toReservationSnapshot),
2749
+ childAttachments,
2750
+ ...(stored.row.parentLinkage === undefined
2751
+ ? {}
2752
+ : { parentLinkage: stored.row.parentLinkage }),
2753
+ ...(stored.joinedHostSubmissionId === undefined
2754
+ ? {}
2755
+ : { hostSubmissionId: stored.joinedHostSubmissionId }),
2756
+ ...(stored.suspension === undefined
2757
+ ? {}
2758
+ : {
2759
+ suspension: SuspensionSnapshot.make({
2760
+ reason: stored.suspension.reason,
2761
+ suspendedAt: utc(stored.suspension.suspendedAtMillis),
2762
+ }),
2763
+ }),
2764
+ ...(stored.ownership === undefined
2765
+ ? {}
2766
+ : {
2767
+ ownership: OwnershipSnapshot.make({
2768
+ attemptId: stored.ownership.attemptId,
2769
+ ownerProducerId: stored.ownership.ownerProducerId,
2770
+ producerEpoch: stored.ownership.producerEpoch,
2771
+ leaseExpiresAt: utc(stored.ownership.leaseExpiresAtMillis),
2772
+ }),
2773
+ }),
2774
+ ...(stored.inputApplied === undefined ? {} : { inputApplied: stored.inputApplied }),
2775
+ ...(stored.reservation === undefined
2776
+ ? {}
2777
+ : {
2778
+ reservation: SettlementReservationSnapshot.make({
2779
+ settlementId: stored.reservation.settlementId,
2780
+ outcome: stored.reservation.outcome,
2781
+ record: stored.reservation.record,
2782
+ recordDigest: stored.reservation.recordDigest,
2783
+ finalized: stored.reservation.finalizedAtMillis !== undefined,
2784
+ }),
2785
+ }),
2786
+ ...(stored.abortIntent === undefined ? {} : { abortIntent: stored.abortIntent }),
2787
+ });
2788
+ }),
2789
+ );
2790
+
2791
+ return SubmissionLedger.of({
2792
+ capabilities,
2793
+ admit,
2794
+ markReady,
2795
+ lookup,
2796
+ resolveAdmission,
2797
+ claim,
2798
+ renewOwnership,
2799
+ releaseOwnership,
2800
+ markInputApplied,
2801
+ reserveSettlement,
2802
+ finalizeSettlement,
2803
+ requestAbort,
2804
+ stopWorker,
2805
+ inspectWorker,
2806
+ claimJoining,
2807
+ markJoined,
2808
+ revertJoining,
2809
+ suspend,
2810
+ recordApprovalDecision,
2811
+ markUnknown,
2812
+ recordUnknownResolution,
2813
+ recordChildSettled,
2814
+ reserveChildBudget,
2815
+ attachChildToReservation,
2816
+ beginChildBudgetRelease,
2817
+ releaseChildBudget,
2818
+ scanNonterminal,
2819
+ loadRecoverySnapshot,
2820
+ readAbortIntent,
2821
+ });
2822
+ });
2823
+
2824
+ /** Construction options for the in-memory reference SubmissionLedger. */
2825
+ export interface MemorySubmissionLedgerOptions {
2826
+ /**
2827
+ * Test-only fault seam for `resolveAdmission` (SUB-031): when the effect yields a reason,
2828
+ * the resolution answers `Indeterminate` with it instead of consulting the store — modelling
2829
+ * an authoritative child owner that is temporarily unreachable. `Option.none()` restores the
2830
+ * store-derived answer. Ledger state is never mutated by the fault.
2831
+ */
2832
+ readonly resolveAdmissionFault?: Effect.Effect<Option.Option<string>>;
2833
+ }
2834
+
2835
+ /**
2836
+ * In-memory reference SubmissionLedger Layer (durability `non-durable`). All state lives in one
2837
+ * `Ref` owned by the Layer's Scope; no daemon fibers are spawned and no wall clock is consulted.
2838
+ */
2839
+ export const memorySubmissionLedgerLayer = (
2840
+ options: MemorySubmissionLedgerOptions = {},
2841
+ ): Layer.Layer<SubmissionLedger> => Layer.effect(SubmissionLedger, makeSubmissionLedger(options));
2842
+
2843
+ export const MemorySubmissionLedgerLive: Layer.Layer<SubmissionLedger> =
2844
+ memorySubmissionLedgerLayer();