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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { canonicalJson, utf8ByteLength } from "@yielded/agent/digest";
1
2
  import {
2
3
  type ToolCallId,
3
4
  AttemptId,
@@ -10,16 +11,26 @@ import {
10
11
  import { InputMessage } from "@yielded/agent/messaging";
11
12
  import {
12
13
  PersistedJson,
14
+ MAX_RUN_EVIDENCE_RECORDS,
15
+ MAX_RUN_EVIDENCE_BYTES,
13
16
  WorkerAdmission,
14
- ProducerEpoch,
15
17
  type DefinitionDigests,
16
18
  type DeploymentId,
17
19
  type Digest,
18
- type ProducerId,
19
20
  type RecordEnvelope,
20
21
  type SettlementOutcome,
21
22
  } from "@yielded/agent/records";
23
+ import { runIdForSubmission } from "@yielded/agent/run-journal";
22
24
  import {
25
+ SettlementPublisher,
26
+ SettlementPublicationResult,
27
+ validatePublication,
28
+ validateCanonicalSettlement,
29
+ validateJoinedSettlement,
30
+ } from "@yielded/agent/settlement-publisher";
31
+ import {
32
+ FundingOwnerRequest,
33
+ FundingOwner,
23
34
  type ParentLinkage,
24
35
  AbortCommand,
25
36
  WorkerStopCommand,
@@ -36,6 +47,7 @@ import {
36
47
  ApprovalConflict,
37
48
  ApprovalDecisionCommand,
38
49
  ApprovalDecisionIntent,
50
+ approvalDecisionsCover,
39
51
  AttachChildToReservationRequest,
40
52
  BeginChildBudgetReleaseRequest,
41
53
  ChildAttachmentSnapshot,
@@ -46,7 +58,6 @@ import {
46
58
  Claim,
47
59
  ClaimJoiningRequest,
48
60
  ClaimRequest,
49
- DEFAULT_OWNERSHIP_LEASE_DURATION,
50
61
  InputAppliedMarker,
51
62
  JoinSnapshot,
52
63
  JoinedToHost,
@@ -58,24 +69,17 @@ import {
58
69
  MarkReadyRequest,
59
70
  MarkUnknownRequest,
60
71
  OwnershipLost,
61
- OwnershipRenewal,
62
- OwnershipSnapshot,
63
- OwnershipToken,
64
72
  QueueSequence,
65
73
  RecoverySnapshot,
66
74
  RecoverySnapshotRequest,
67
75
  ReleaseChildBudgetRequest,
68
- ReleaseOwnershipRequest,
69
- RenewOwnershipRequest,
70
76
  ReservedChildBudget,
71
- ReservedSettlement,
72
77
  RevertJoiningRequest,
73
78
  Settlement,
74
79
  SettlementConflict,
75
80
  SettlementFinalization,
76
- SettlementReservation,
77
- SettlementReservationSnapshot,
78
81
  settlementFailureFromRecord,
82
+ submissionSettlementRecordId,
79
83
  AbortIntent,
80
84
  AbortIntentRequest,
81
85
  SubmissionLedger,
@@ -89,6 +93,8 @@ import {
89
93
  UnknownResolutionCommand,
90
94
  UnknownResolutionConflict,
91
95
  UnknownResolutionIntent,
96
+ unknownResolutionKind,
97
+ unknownResolutionMatchesResult,
92
98
  type ChildReservationId,
93
99
  type ChildReservationStatus,
94
100
  type ChildSettledOutcome,
@@ -98,26 +104,46 @@ import {
98
104
  type SuspensionOutcome,
99
105
  type SuspensionReason,
100
106
  } from "@yielded/agent/submission-ledger";
107
+ import {
108
+ rebuildClosedSubmission,
109
+ invalidThreadArchive,
110
+ RebuiltSubmission,
111
+ ThreadImportRejected,
112
+ } from "@yielded/agent/thread-import";
113
+ import {
114
+ ThreadAdmission,
115
+ ThreadStoreError,
116
+ ThreadAbortFact,
117
+ ThreadApprovalFact,
118
+ ThreadResolutionFact,
119
+ makeTransferFactCheck,
120
+ transferPageFits,
121
+ } from "@yielded/agent/thread-store";
122
+ import { admissionWork } from "@yielded/agent/thread-work";
101
123
  import {
102
124
  Clock,
125
+ Context,
103
126
  Cause,
127
+ Crypto,
104
128
  Exit,
105
129
  Fiber,
106
130
  DateTime,
107
- Duration,
108
131
  Effect,
109
132
  Layer,
133
+ MutableRef,
110
134
  Option,
111
135
  Ref,
136
+ Result,
112
137
  Schema,
113
138
  Stream,
114
139
  } from "effect";
115
140
 
116
- const MAX_SUBMISSIONS = 65_536;
141
+ import { MemoryThreadStoreKernel } from "./internal/MemoryThreadStoreKernel.ts";
142
+ import { boundedWorkSelection } from "./internal/WorkSelection.ts";
117
143
 
118
144
  /**
119
145
  * 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
146
+ * Attempt must not erase progress markers (input-applied) that an earlier Attempt
121
147
  * already committed.
122
148
  */
123
149
  const STATE_RANK: Record<SubmissionState, number> = {
@@ -125,12 +151,10 @@ const STATE_RANK: Record<SubmissionState, number> = {
125
151
  ready: 1,
126
152
  joining: 2,
127
153
  joined: 3,
128
- running: 4,
129
154
  "input-applied": 5,
130
155
  suspended: 6,
131
156
  unknown: 7,
132
- terminalizing: 8,
133
- settled: 9,
157
+ settled: 8,
134
158
  };
135
159
 
136
160
  interface SubmissionRow {
@@ -157,20 +181,10 @@ interface SubmissionRow {
157
181
  readonly messageAdmissionJson?: string;
158
182
  }
159
183
 
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 {
184
+ interface StoredFinalization {
169
185
  readonly settlementId: SettlementId;
170
- readonly outcome: SettlementOutcome;
171
- readonly record: RecordEnvelope;
172
- readonly recordDigest: Digest;
173
- readonly finalizedAtMillis: number | undefined;
186
+ readonly finalizedAtMillis: number;
187
+ readonly recordId: RecordEnvelope["recordId"];
174
188
  }
175
189
 
176
190
  interface StoredSuspension {
@@ -189,16 +203,15 @@ interface StoredUnknownResolution {
189
203
 
190
204
  interface StoredSubmission {
191
205
  readonly row: SubmissionRow;
192
- readonly ownership: StoredOwnership | undefined;
193
206
  readonly inputApplied: InputAppliedMarker | undefined;
194
- readonly reservation: StoredReservation | undefined;
207
+ readonly finalization: StoredFinalization | undefined;
195
208
  readonly abortIntent: AbortIntent | undefined;
196
209
  /** Host linkage recorded at `claimJoining` time; cleared by `revertJoining` (DUR-016). */
197
210
  readonly joinedHostSubmissionId: SubmissionId | undefined;
198
211
  readonly suspension: StoredSuspension | undefined;
199
212
  readonly unknownMark: StoredUnknownMark | undefined;
200
213
  readonly approvalDecisions: ReadonlyMap<ToolCallId, ApprovalDecisionIntent>;
201
- readonly unknownResolutions: ReadonlyMap<ToolCallId, StoredUnknownResolution>;
214
+ readonly unknownResolutions: ReadonlyMap<string, StoredUnknownResolution>;
202
215
  }
203
216
 
204
217
  /**
@@ -214,7 +227,6 @@ const BLOCKED_HEAD_STATES: ReadonlySet<SubmissionState> = new Set([
214
227
 
215
228
  interface LaneState {
216
229
  readonly nextQueueSequence: number;
217
- readonly producerEpoch: number;
218
230
  }
219
231
 
220
232
  /** One parent-owned child budget reservation row (spec §12 steps 2 and 6). */
@@ -232,15 +244,69 @@ interface StoredChildReservation {
232
244
  readonly releasedAtMillis: number | undefined;
233
245
  }
234
246
 
247
+ interface ThreadDirectory {
248
+ readonly ordered: Array<SubmissionId>;
249
+ readonly commands: {
250
+ readonly aborts: Array<CommandLocator>;
251
+ readonly approvals: Array<CommandLocator>;
252
+ readonly resolutions: Array<CommandLocator>;
253
+ };
254
+ children: number;
255
+ workers: number;
256
+ reservations: number;
257
+ }
258
+ interface CommandLocator {
259
+ readonly key: string;
260
+ readonly submissionId: SubmissionId;
261
+ readonly toolCallId?: ToolCallId;
262
+ readonly resolutionKey?: string;
263
+ }
264
+
265
+ const commandPosition = (entries: ReadonlyArray<CommandLocator>, key: string) => {
266
+ let low = 0;
267
+ let high = entries.length;
268
+
269
+ while (low < high) {
270
+ const middle = Math.floor((low + high) / 2);
271
+
272
+ if (entries[middle].key < key) low = middle + 1;
273
+ else high = middle;
274
+ }
275
+
276
+ return low;
277
+ };
278
+
279
+ const retainCommand = (
280
+ entries: Array<CommandLocator>,
281
+ submissionId: SubmissionId,
282
+ toolCallId?: ToolCallId,
283
+ resolutionKey?: string,
284
+ ) => {
285
+ // Acceptance order is native, stable and append-only; publishing a command never shifts history.
286
+ const key = String(entries.length + 1).padStart(16, "0");
287
+
288
+ entries.push({
289
+ key,
290
+ submissionId,
291
+ ...(toolCallId === undefined ? {} : { toolCallId }),
292
+ ...(resolutionKey === undefined ? {} : { resolutionKey }),
293
+ });
294
+ };
295
+
235
296
  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"]>;
297
+ readonly byThread: Map<ThreadId, ThreadDirectory>;
298
+ readonly receipts: Map<ReceiptId, SubmissionId>;
299
+ readonly joinsByHost: Map<SubmissionId, Set<SubmissionId>>;
300
+ readonly childrenByParent: Map<SubmissionId, Set<ChildReservationId>>;
301
+ readonly submissions: Map<SubmissionId, StoredSubmission>;
302
+ readonly admissionIndex: Map<string, SubmissionId>;
303
+ readonly lanes: Map<ThreadId, LaneState>;
304
+ readonly childReservations: Map<ChildReservationId, StoredChildReservation>;
305
+ /** Bounded local ordering for fresh work identities; imported facts never seed it. */
306
+ readonly freshAdmissionSequence: number;
307
+ readonly latestByThread: Map<ThreadId, SubmissionId>;
308
+ readonly activeByThread: Map<ThreadId, Set<SubmissionId>>;
309
+ readonly stoppedWorkers: Map<ThreadId, WorkerLedgerState["terminal"]>;
244
310
  }
245
311
 
246
312
  type Decision<A, E> =
@@ -255,26 +321,31 @@ const ledgerError = (operation: string, message: string, cause?: unknown): Ledge
255
321
  ? LedgerError.make({ operation, message })
256
322
  : LedgerError.make({ operation, message, cause });
257
323
 
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
- );
324
+ const validate = <A, I>(
325
+ schema: Schema.Codec<A, I>,
326
+ operation: string,
327
+ value: unknown,
328
+ ): Effect.Effect<A, LedgerError> =>
329
+ Schema.encodeUnknownEffect(schema)(value).pipe(
330
+ Effect.flatMap(Schema.decodeUnknownEffect(schema)),
331
+ Effect.mapError((error) => ledgerError(operation, `Invalid ${operation} request`, error)),
332
+ );
269
333
 
270
334
  const decodeSubmissionId = Schema.decodeSync(SubmissionId);
271
335
  const decodeReceiptId = Schema.decodeSync(ReceiptId);
272
336
  const decodeAttemptId = Schema.decodeSync(AttemptId);
273
- const decodeOwnershipToken = Schema.decodeSync(OwnershipToken);
337
+ // Imported identities are opaque facts and never influence fresh identity sizes.
338
+ const MemoryIdentityNonce = Schema.String.check(Schema.isUUID(4));
274
339
  const decodeQueueSequence = Schema.decodeSync(QueueSequence);
275
- const decodeProducerEpoch = Schema.decodeSync(ProducerEpoch);
276
340
  const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
277
341
  const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
342
+ const admissionFitsTransfer = makeTransferFactCheck(ThreadAdmission);
343
+ const abortFitsTransfer = makeTransferFactCheck(ThreadAbortFact);
344
+ const approvalFitsTransfer = makeTransferFactCheck(ThreadApprovalFact);
345
+ const resolutionFitsTransfer = makeTransferFactCheck(ThreadResolutionFact);
346
+
347
+ const transferCapacityError = (operation: string) =>
348
+ ledgerError(operation, "Accepted fact exceeds its bounded transfer representation");
278
349
 
279
350
  const utc = (millis: number): DateTime.Utc => DateTime.toUtc(DateTime.makeUnsafe(millis));
280
351
 
@@ -337,6 +408,91 @@ const toReservationSnapshot = (row: StoredChildReservation): ChildBudgetReservat
337
408
  ...(row.releasedAtMillis === undefined ? {} : { releasedAt: utc(row.releasedAtMillis) }),
338
409
  });
339
410
 
411
+ const fromRebuilt = (rebuilt: RebuiltSubmission): StoredSubmission => {
412
+ const admission = rebuilt.admission;
413
+ const threadId = admission.threadId;
414
+ const canonicalSettlement = rebuilt.settlement?.payload;
415
+
416
+ const settlement =
417
+ canonicalSettlement?._tag === "SubmissionSettled" ? canonicalSettlement : undefined;
418
+
419
+ return {
420
+ row: {
421
+ submissionId: admission.submissionId,
422
+ threadId,
423
+ queueSequence: admission.queueSequence,
424
+ principal: admission.principal,
425
+ idempotencyKey: admission.idempotencyKey,
426
+ agentId: admission.agentId,
427
+ agentDigests: admission.agentDigests,
428
+ deploymentId: admission.deploymentId,
429
+ inputPayload: admission.inputPayload,
430
+ inputDigest: admission.inputDigest,
431
+ receiptId: admission.receiptId,
432
+ state: rebuilt.state,
433
+ settledOutcome: settlement?.outcome,
434
+ createdAtMillis: DateTime.toEpochMillis(admission.createdAt),
435
+ readyAtMillis:
436
+ rebuilt.state === "ready" ? DateTime.toEpochMillis(admission.createdAt) : undefined,
437
+ parentLinkage: admission.parentLinkage,
438
+ ...(admission.workerAdmission === undefined
439
+ ? {}
440
+ : {
441
+ workerAdmissionJson: Schema.encodeSync(Schema.fromJsonString(WorkerAdmission))(
442
+ admission.workerAdmission,
443
+ ),
444
+ }),
445
+ ...(admission.messageAdmission === undefined
446
+ ? {}
447
+ : {
448
+ messageAdmissionJson: Schema.encodeSync(Schema.fromJsonString(InputMessage))(
449
+ admission.messageAdmission,
450
+ ),
451
+ }),
452
+ ...(admission.admissionGroup === undefined
453
+ ? {}
454
+ : { admissionGroup: admission.admissionGroup }),
455
+ ...(admission.admissionFence === undefined
456
+ ? {}
457
+ : { admissionFence: admission.admissionFence }),
458
+ },
459
+ inputApplied: rebuilt.inputApplied,
460
+ finalization:
461
+ settlement === undefined ||
462
+ rebuilt.settlement === undefined ||
463
+ admission.settledAt === undefined
464
+ ? undefined
465
+ : {
466
+ settlementId: settlement.settlementId,
467
+ recordId: rebuilt.settlement.recordId,
468
+ finalizedAtMillis: DateTime.toEpochMillis(admission.settledAt),
469
+ },
470
+ abortIntent: rebuilt.abort,
471
+ joinedHostSubmissionId: rebuilt.joinedHostSubmissionId,
472
+ suspension:
473
+ rebuilt.suspension === undefined
474
+ ? undefined
475
+ : {
476
+ reason: rebuilt.suspension.reason,
477
+ suspendedAtMillis: DateTime.toEpochMillis(rebuilt.suspension.suspendedAt),
478
+ },
479
+ unknownMark:
480
+ rebuilt.unknownToolCallIds.length === 0
481
+ ? undefined
482
+ : {
483
+ reason: "Unresolved Tool execution recovered from the canonical log",
484
+ toolCallIds: rebuilt.unknownToolCallIds,
485
+ },
486
+ approvalDecisions: new Map(rebuilt.approvals.map((intent) => [intent.toolCallId, intent])),
487
+ unknownResolutions: new Map(
488
+ rebuilt.resolutions.map((intent) => [
489
+ JSON.stringify([intent.toolCallId, unknownResolutionKind(intent.resolution)]),
490
+ { intent },
491
+ ]),
492
+ ),
493
+ };
494
+ };
495
+
340
496
  /** Linkage equality: both absent, or both present naming the same parent Tool Call. */
341
497
  const sameParentLinkage = (
342
498
  left: ParentLinkage | undefined,
@@ -348,53 +504,136 @@ const sameParentLinkage = (
348
504
  left.parentSubmissionId === right.parentSubmissionId &&
349
505
  left.parentToolCallId === right.parentToolCallId;
350
506
 
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);
507
+ const ownershipLost = (stored: StoredSubmission): OwnershipLost =>
508
+ OwnershipLost.make({ submissionId: stored.row.submissionId });
369
509
 
370
510
  const withSubmission = (state: LedgerState, stored: StoredSubmission): LedgerState => {
371
- const active = new Set(state.activeByThread.get(stored.row.threadId));
511
+ const row = stored.row;
512
+ const previous = state.submissions.get(row.submissionId);
513
+ let directory = state.byThread.get(row.threadId);
514
+
515
+ if (directory === undefined) {
516
+ directory = {
517
+ ordered: [],
518
+ commands: { aborts: [], approvals: [], resolutions: [] },
519
+ children: 0,
520
+ workers: 0,
521
+ reservations: 0,
522
+ };
523
+ state.byThread.set(row.threadId, directory);
524
+ }
525
+ if (previous === undefined) {
526
+ let low = 0;
527
+ let high = directory.ordered.length;
528
+
529
+ while (low < high) {
530
+ const middle = Math.floor((low + high) / 2);
531
+ const candidate = state.submissions.get(directory.ordered[middle]);
532
+
533
+ if (candidate !== undefined && candidate.row.queueSequence < row.queueSequence)
534
+ low = middle + 1;
535
+ else high = middle;
536
+ }
537
+ directory.ordered.splice(low, 0, row.submissionId);
538
+ state.receipts.set(row.receiptId, row.submissionId);
539
+ state.admissionIndex.set(
540
+ admissionKey(row.threadId, row.principal, row.idempotencyKey),
541
+ row.submissionId,
542
+ );
543
+ const latest = state.latestByThread.get(row.threadId);
544
+
545
+ if (
546
+ latest === undefined ||
547
+ (state.submissions.get(latest)?.row.queueSequence ?? -1) < row.queueSequence
548
+ )
549
+ state.latestByThread.set(row.threadId, row.submissionId);
550
+ }
551
+ directory.children +=
552
+ Number(row.state !== "settled" && row.parentLinkage !== undefined) -
553
+ Number(
554
+ previous !== undefined &&
555
+ previous.row.state !== "settled" &&
556
+ previous.row.parentLinkage !== undefined,
557
+ );
558
+ directory.workers +=
559
+ Number(row.state !== "settled" && row.workerAdmissionJson !== undefined) -
560
+ Number(
561
+ previous !== undefined &&
562
+ previous.row.state !== "settled" &&
563
+ previous.row.workerAdmissionJson !== undefined,
564
+ );
565
+ if (stored.abortIntent !== undefined && previous?.abortIntent === undefined)
566
+ retainCommand(directory.commands.aborts, row.submissionId);
567
+ if (stored.approvalDecisions !== previous?.approvalDecisions)
568
+ for (const id of stored.approvalDecisions.keys())
569
+ if (!previous?.approvalDecisions.has(id))
570
+ retainCommand(directory.commands.approvals, row.submissionId, id);
571
+ if (stored.unknownResolutions !== previous?.unknownResolutions)
572
+ for (const [id, value] of stored.unknownResolutions)
573
+ if (!previous?.unknownResolutions.has(id))
574
+ retainCommand(
575
+ directory.commands.resolutions,
576
+ row.submissionId,
577
+ value.intent.toolCallId,
578
+ id,
579
+ );
580
+ if (
581
+ previous?.joinedHostSubmissionId !== undefined &&
582
+ previous.joinedHostSubmissionId !== stored.joinedHostSubmissionId
583
+ )
584
+ state.joinsByHost.get(previous.joinedHostSubmissionId)?.delete(row.submissionId);
585
+ if (stored.joinedHostSubmissionId !== undefined) {
586
+ let joins = state.joinsByHost.get(stored.joinedHostSubmissionId);
587
+
588
+ if (joins === undefined)
589
+ state.joinsByHost.set(stored.joinedHostSubmissionId, (joins = new Set()));
590
+ joins.add(row.submissionId);
591
+ }
592
+ let active = state.activeByThread.get(row.threadId);
372
593
 
373
- if (stored.row.state === "settled") active.delete(stored.row.submissionId);
374
- else active.add(stored.row.submissionId);
594
+ if (active === undefined) state.activeByThread.set(row.threadId, (active = new Set()));
595
+ if (row.state === "settled") active.delete(row.submissionId);
596
+ else active.add(row.submissionId);
597
+ state.submissions.set(row.submissionId, stored);
375
598
 
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
- };
599
+ return state;
381
600
  };
382
601
 
383
602
  const withChildReservation = (
384
603
  state: LedgerState,
385
604
  reservation: StoredChildReservation,
386
- ): LedgerState => ({
387
- ...state,
388
- childReservations: new Map(state.childReservations).set(reservation.reservationId, reservation),
389
- });
605
+ ): LedgerState => {
606
+ const previous = state.childReservations.get(reservation.reservationId);
607
+
608
+ state.childReservations.set(reservation.reservationId, reservation);
609
+ let children = state.childrenByParent.get(reservation.parentSubmissionId);
610
+
611
+ if (children === undefined)
612
+ state.childrenByParent.set(reservation.parentSubmissionId, (children = new Set()));
613
+ children.add(reservation.reservationId);
614
+ const parent = state.submissions.get(reservation.parentSubmissionId);
615
+
616
+ if (parent !== undefined) {
617
+ const directory = state.byThread.get(parent.row.threadId);
618
+
619
+ if (directory !== undefined) {
620
+ directory.reservations +=
621
+ Number(reservation.status !== "released") -
622
+ Number(previous !== undefined && previous.status !== "released");
623
+ }
624
+ }
625
+
626
+ return state;
627
+ };
390
628
 
391
629
  const findHead = (state: LedgerState, threadId: ThreadId): StoredSubmission | undefined => {
392
630
  let head: StoredSubmission | undefined;
393
631
 
394
- for (const stored of state.submissions.values()) {
632
+ for (const id of state.activeByThread.get(threadId) ?? []) {
633
+ const stored = state.submissions.get(id);
634
+
395
635
  if (
396
- stored.row.threadId !== threadId ||
397
- stored.row.state === "settled" ||
636
+ stored === undefined ||
398
637
  (stored.row.state === "unknown" && stored.abortIntent === undefined)
399
638
  )
400
639
  continue;
@@ -404,341 +643,411 @@ const findHead = (state: LedgerState, threadId: ThreadId): StoredSubmission | un
404
643
  return head;
405
644
  };
406
645
 
646
+ function* threadRows(state: LedgerState, threadId: ThreadId, after = -1) {
647
+ const ordered = state.byThread.get(threadId)?.ordered ?? [];
648
+ let low = 0;
649
+ let high = ordered.length;
650
+
651
+ while (low < high) {
652
+ const middle = Math.floor((low + high) / 2);
653
+
654
+ if ((state.submissions.get(ordered[middle])?.row.queueSequence ?? -1) <= after)
655
+ low = middle + 1;
656
+ else high = middle;
657
+ }
658
+ for (let index = low; index < ordered.length; index++) {
659
+ const stored = state.submissions.get(ordered[index]);
660
+
661
+ if (stored !== undefined) yield stored;
662
+ }
663
+ }
664
+
407
665
  /**
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:
666
+ * Reference in-memory SubmissionLedger. Admission, ordered selection and canonical settlement
667
+ * share the Memory kernel's mutation gate. The kernel Scope owns local Run capabilities;
668
+ * selecting work does not write execution ownership into the ledger.
414
669
  *
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.
670
+ * Joining retains the contiguous ready prefix and exact host linkage. Canonical settlement
671
+ * authorizes finalization and child wake repair, while bound sessions reject late commands.
672
+ * Child wake checks read the child rows directly, without a separate notification marker.
673
+ * Admission resolution is strongly consistent; resolveAdmissionFault injects the
674
+ * Indeterminate outcome for existing caller conformance workflows.
440
675
  */
441
676
  const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
442
677
  Effect.gen(function* () {
678
+ const journal = yield* MemoryThreadStoreKernel;
679
+ const crypto = yield* Crypto.Crypto;
680
+
681
+ const mintIdentity = (operation: string) =>
682
+ crypto.randomUUIDv4().pipe(
683
+ Effect.flatMap(Schema.decodeEffect(MemoryIdentityNonce)),
684
+ Effect.mapError((cause) => ledgerError(operation, "Cannot mint a Memory identity", cause)),
685
+ );
686
+
443
687
  const state = yield* Ref.make<LedgerState>({
688
+ byThread: new Map(),
689
+ receipts: new Map(),
690
+ joinsByHost: new Map(),
691
+ childrenByParent: new Map(),
444
692
  submissions: new Map(),
445
693
  admissionIndex: new Map(),
446
694
  lanes: new Map(),
447
695
  childReservations: new Map(),
448
- mintCounter: 0,
696
+ freshAdmissionSequence: 0,
449
697
  stoppedWorkers: new Map(),
450
698
  latestByThread: new Map(),
451
699
  activeByThread: new Map(),
452
700
  });
453
701
 
454
- const admissionFence = yield* SubmissionAdmissionFence;
455
- const leaseMillis = Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION);
702
+ yield* journal.registerWorkOwner("admissions", {
703
+ threads: (request) =>
704
+ Ref.get(state).pipe(
705
+ Effect.map((current) => {
706
+ const candidates = boundedWorkSelection<ThreadId>(
707
+ request.limit,
708
+ (id) => id,
709
+ request.afterThreadId,
710
+ );
456
711
 
457
- const capabilities = Effect.succeed(LedgerCapabilities.make({ durability: "non-durable" }));
712
+ for (const [threadId, ids] of current.activeByThread)
713
+ if (ids.size > 0) candidates.add(threadId);
714
+ const threadIds = candidates.values;
458
715
 
459
- const admit: SubmissionLedger["Service"]["admit"] = Effect.fn("MemorySubmissionLedger.admit")(
460
- (unvalidated) =>
716
+ const afterThreadId = threadIds.length === request.limit ? threadIds.at(-1) : undefined;
717
+
718
+ return {
719
+ threadIds,
720
+ ...(afterThreadId === undefined ? {} : { afterThreadId }),
721
+ };
722
+ }),
723
+ ),
724
+ page: (threadId, after, limit) =>
461
725
  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
- );
726
+ const current = yield* Ref.get(state);
472
727
 
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
- );
728
+ const candidates = boundedWorkSelection<SubmissionId>(limit + 1, (id) => id, after);
481
729
 
482
- const nowMillis = yield* Clock.currentTimeMillis;
483
- const services = yield* Effect.context<never>();
730
+ for (const id of current.activeByThread.get(threadId) ?? []) candidates.add(id);
731
+ const ids = candidates.values;
484
732
 
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
- }
733
+ const entries = [];
550
734
 
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
- }
735
+ for (const id of ids.slice(0, limit)) {
736
+ const stored = current.submissions.get(id);
564
737
 
565
- if (current.stoppedWorkers.has(request.threadId))
566
- return [
567
- failure(AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" })),
568
- current,
569
- ];
738
+ if (
739
+ stored === undefined ||
740
+ stored.row.threadId !== threadId ||
741
+ stored.row.state === "settled"
742
+ )
743
+ return yield* ThreadStoreError.make({
744
+ operation: "admission work",
745
+ message: "Active admission index is incomplete or corrupt",
746
+ });
747
+ const entry = admissionWork(stored.row);
748
+
749
+ entries.push(
750
+ stored.suspension === undefined
751
+ ? entry
752
+ : {
753
+ ...entry,
754
+ wait:
755
+ stored.suspension.reason._tag === "WaitingForChild"
756
+ ? ("child" as const)
757
+ : ("approval" as const),
758
+ },
759
+ );
760
+ }
761
+ const next = ids.length > limit ? ids[limit - 1] : undefined;
570
762
 
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
- );
763
+ return { entries, ...(next === undefined ? {} : { after: next }) };
764
+ }),
765
+ });
575
766
 
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
- );
767
+ const admissionFence = yield* SubmissionAdmissionFence;
583
768
 
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));
769
+ const capabilities = Effect.succeed(LedgerCapabilities.make({ durability: "non-durable" }));
603
770
 
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
- }
771
+ const admit: SubmissionLedger["Service"]["admit"] = Effect.fnUntraced(function* (unvalidated) {
772
+ const request = yield* validate(AdmissionRequest, "admit", unvalidated);
630
773
 
631
- const lane = current.lanes.get(request.threadId) ?? {
632
- nextQueueSequence: 1,
633
- producerEpoch: 0,
634
- };
774
+ const workerAdmissionJson =
775
+ request.workerAdmission === undefined
776
+ ? undefined
777
+ : yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
778
+ request.workerAdmission,
779
+ ).pipe(
780
+ Effect.mapError(() => ledgerError("admit", "Invalid worker admission metadata")),
781
+ );
635
782
 
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
- };
783
+ const messageAdmissionJson =
784
+ request.messageAdmission === undefined
785
+ ? undefined
786
+ : yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
787
+ request.messageAdmission,
788
+ ).pipe(
789
+ Effect.mapError(() => ledgerError("admit", "Invalid message admission metadata")),
790
+ );
664
791
 
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
- });
792
+ yield* journal
793
+ .checkThreadCapacity(request.threadId)
794
+ .pipe(Effect.mapError((cause) => ledgerError("admit", cause.message, cause)));
795
+ const nowMillis = yield* Clock.currentTimeMillis;
796
+ const retained = yield* Ref.get(state);
797
+
798
+ const identity = retained.admissionIndex.has(
799
+ admissionKey(request.threadId, request.principal, request.idempotencyKey),
800
+ )
801
+ ? Result.fail(ledgerError("admit", "Retained admission disappeared"))
802
+ : yield* Effect.result(mintIdentity("admit"));
803
+
804
+ const services = yield* Effect.context<never>();
805
+
806
+ const decision = yield* Ref.modify(
807
+ state,
808
+ (
809
+ current,
810
+ ): readonly [
811
+ (
812
+ | Decision<AdmissionResult, AdmissionConflict | AdmissionPolicyError | LedgerError>
813
+ | { readonly _tag: "cause"; readonly cause: Cause.Cause<AdmissionPolicyError> }
814
+ ),
815
+ LedgerState,
816
+ ] => {
817
+ const key = admissionKey(request.threadId, request.principal, request.idempotencyKey);
818
+ const existingId = current.admissionIndex.get(key);
677
819
 
678
- const admissionIndex = new Map(current.admissionIndex).set(key, row.submissionId);
820
+ if (existingId !== undefined) {
821
+ const existing = current.submissions.get(existingId);
679
822
 
680
- const lanes = new Map(current.lanes).set(request.threadId, {
681
- nextQueueSequence: lane.nextQueueSequence + 1,
682
- producerEpoch: lane.producerEpoch,
683
- });
823
+ if (existing === undefined) {
824
+ return [
825
+ failure(ledgerError("admit", "Admission index references a missing Submission")),
826
+ current,
827
+ ];
828
+ }
829
+ // A replay must repeat the exact canonical input AND the exact parent linkage
830
+ // (or its absence): linkage is immutable lineage (spec §12 step 5, SUB-016).
831
+ if (
832
+ existing.row.inputDigest !== request.inputDigest ||
833
+ !sameParentLinkage(existing.row.parentLinkage, request.parentLinkage) ||
834
+ existing.row.admissionGroup !== request.admissionGroup ||
835
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission))(
836
+ existing.row.workerAdmissionJson === undefined
837
+ ? undefined
838
+ : Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
839
+ existing.row.workerAdmissionJson,
840
+ ),
841
+ request.workerAdmission,
842
+ ) ||
843
+ !Schema.toEquivalence(Schema.optional(InputMessage))(
844
+ existing.row.messageAdmissionJson === undefined
845
+ ? undefined
846
+ : Schema.decodeSync(Schema.fromJsonString(InputMessage))(
847
+ existing.row.messageAdmissionJson,
848
+ ),
849
+ request.messageAdmission,
850
+ ) ||
851
+ !Schema.toEquivalence(Schema.optional(Schema.Json))(
852
+ existing.row.admissionFence,
853
+ request.admissionFence,
854
+ )
855
+ ) {
856
+ return [
857
+ failure(
858
+ AdmissionConflict.make({
859
+ threadId: request.threadId,
860
+ principal: request.principal,
861
+ idempotencyKey: request.idempotencyKey,
862
+ existingInputDigest: existing.row.inputDigest,
863
+ attemptedInputDigest: request.inputDigest,
864
+ }),
865
+ ),
866
+ current,
867
+ ];
868
+ }
869
+
870
+ return [
871
+ success(
872
+ AdmissionResult.make({
873
+ submissionId: existing.row.submissionId,
874
+ receiptId: existing.row.receiptId,
875
+ queueSequence: existing.row.queueSequence,
876
+ state: existing.row.state,
877
+ replayed: true,
878
+ }),
879
+ ),
880
+ current,
881
+ ];
882
+ }
883
+
884
+ if (current.stoppedWorkers.has(request.threadId))
885
+ return [
886
+ failure(AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" })),
887
+ current,
888
+ ];
684
889
 
890
+ // A Thread's first admission fixes its worker origin before canonical materialization.
891
+ const first = threadRows(current, request.threadId).next().value;
892
+
893
+ if (first !== undefined) {
894
+ const previous =
895
+ first.row.workerAdmissionJson === undefined
896
+ ? undefined
897
+ : Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
898
+ first.row.workerAdmissionJson,
899
+ );
900
+
901
+ if (
902
+ !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
903
+ previous?.origin,
904
+ request.workerAdmission?.origin,
905
+ )
906
+ )
685
907
  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,
908
+ failure(
909
+ AdmissionPolicyError.make({
910
+ reason: "refused",
911
+ code: "worker-origin-conflict",
693
912
  }),
694
913
  ),
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
- },
914
+ current,
713
915
  ];
714
- },
715
- );
916
+ }
917
+ // A memory policy and the ledger mutation share one synchronous critical section.
918
+ // An asynchronous policy cannot fence this Ref and therefore fails closed.
919
+ const checked = Effect.runSyncExitWith(services)(admissionFence.check(request));
920
+
921
+ if (Exit.isFailure(checked)) {
922
+ return [{ _tag: "cause", cause: checked.cause }, current];
923
+ }
924
+ if (
925
+ request.admissionGroup !== undefined &&
926
+ Array.from(current.activeByThread.get(request.threadId) ?? []).some(
927
+ (id) => current.submissions.get(id)?.row.admissionGroup === request.admissionGroup,
928
+ )
929
+ )
930
+ return [
931
+ failure(AdmissionPolicyError.make({ reason: "occupied", code: "admission-group" })),
932
+ current,
933
+ ];
716
934
 
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);
935
+ // A concurrent matching acceptance still wins over a failed fresh allocation.
936
+ if (Result.isFailure(identity)) return [failure(identity.failure), current];
722
937
 
723
- return yield* AdmissionPolicyError.make({
724
- reason: "unavailable",
725
- code: "synchronous-memory-policy-required",
726
- });
727
- }
728
- }
938
+ const lane = current.lanes.get(request.threadId) ?? {
939
+ nextQueueSequence: 1,
940
+ };
941
+
942
+ const freshAdmissionSequence = current.freshAdmissionSequence + 1;
943
+
944
+ if (!Number.isSafeInteger(freshAdmissionSequence))
945
+ return [
946
+ failure(ledgerError("admit", "Fresh admission sequence is exhausted")),
947
+ current,
948
+ ];
949
+
950
+ // Native work scans compare Submission identities lexically.
951
+ const orderedIdentity = `${String(freshAdmissionSequence).padStart(16, "0")}-${identity.success}`;
952
+ const submissionId = decodeSubmissionId(`submission-memory-${orderedIdentity}`);
953
+ const receiptId = decodeReceiptId(`receipt-memory-${orderedIdentity}`);
954
+
955
+ if (current.submissions.has(submissionId) || current.receipts.has(receiptId))
956
+ return [
957
+ failure(ledgerError("admit", "Fresh admission identity already exists")),
958
+ current,
959
+ ];
960
+
961
+ const row: SubmissionRow = {
962
+ submissionId,
963
+ threadId: request.threadId,
964
+ queueSequence: decodeQueueSequence(lane.nextQueueSequence),
965
+ principal: request.principal,
966
+ idempotencyKey: request.idempotencyKey,
967
+ agentId: request.agentId,
968
+ agentDigests: request.agentDigests,
969
+ deploymentId: request.deploymentId,
970
+ inputPayload: request.inputPayload,
971
+ inputDigest: request.inputDigest,
972
+ receiptId,
973
+ state: "admitted",
974
+ settledOutcome: undefined,
975
+ createdAtMillis: nowMillis,
976
+ readyAtMillis: undefined,
977
+ parentLinkage: request.parentLinkage,
978
+ ...(workerAdmissionJson === undefined ? {} : { workerAdmissionJson }),
979
+ ...(messageAdmissionJson === undefined ? {} : { messageAdmissionJson }),
980
+ ...(request.admissionGroup === undefined
981
+ ? {}
982
+ : { admissionGroup: request.admissionGroup }),
983
+ ...(request.admissionFence === undefined
984
+ ? {}
985
+ : { admissionFence: request.admissionFence }),
986
+ };
987
+
988
+ const stored: StoredSubmission = {
989
+ row,
990
+ inputApplied: undefined,
991
+ finalization: undefined,
992
+ abortIntent: undefined,
993
+ joinedHostSubmissionId: undefined,
994
+ suspension: undefined,
995
+ unknownMark: undefined,
996
+ approvalDecisions: new Map<ToolCallId, ApprovalDecisionIntent>(),
997
+ unknownResolutions: new Map<string, StoredUnknownResolution>(),
998
+ };
999
+
1000
+ if (!admissionFitsTransfer(request.threadId, toAdmission(row)))
1001
+ return [failure(transferCapacityError("admit")), current];
1002
+
1003
+ journal.initializeWork(request.threadId);
1004
+ const admissionIndex = current.admissionIndex;
1005
+
1006
+ const lanes = current.lanes.set(request.threadId, {
1007
+ nextQueueSequence: lane.nextQueueSequence + 1,
1008
+ });
1009
+
1010
+ return [
1011
+ success(
1012
+ AdmissionResult.make({
1013
+ submissionId: row.submissionId,
1014
+ receiptId: row.receiptId,
1015
+ queueSequence: row.queueSequence,
1016
+ state: row.state,
1017
+ replayed: false,
1018
+ }),
1019
+ ),
1020
+ {
1021
+ ...withSubmission(current, stored),
1022
+ admissionIndex,
1023
+ lanes,
1024
+ freshAdmissionSequence,
1025
+ },
1026
+ ];
1027
+ },
1028
+ );
729
1029
 
730
- return yield* Effect.failCause(decision.cause);
1030
+ if (decision._tag === "failure") return yield* decision.error;
1031
+ if (decision._tag === "cause") {
1032
+ for (const reason of decision.cause.reasons) {
1033
+ if (Cause.isDieReason(reason) && Cause.isAsyncFiberError(reason.defect)) {
1034
+ yield* Fiber.interrupt(reason.defect.fiber);
1035
+
1036
+ return yield* AdmissionPolicyError.make({
1037
+ reason: "unavailable",
1038
+ code: "synchronous-memory-policy-required",
1039
+ });
731
1040
  }
1041
+ }
732
1042
 
733
- return decision.value;
734
- }),
735
- Effect.uninterruptible,
736
- );
1043
+ return yield* Effect.failCause(decision.cause);
1044
+ }
737
1045
 
738
- const markReady: SubmissionLedger["Service"]["markReady"] = Effect.fn(
739
- "MemorySubmissionLedger.markReady",
740
- )((unvalidated) =>
741
- Effect.gen(function* () {
1046
+ return decision.value;
1047
+ }, Effect.uninterruptible);
1048
+
1049
+ const markReady: SubmissionLedger["Service"]["markReady"] = Effect.fnUntraced(
1050
+ function* (unvalidated) {
742
1051
  const request = yield* validate(MarkReadyRequest, "markReady", unvalidated);
743
1052
  const nowMillis = yield* Clock.currentTimeMillis;
744
1053
 
@@ -766,13 +1075,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
766
1075
  );
767
1076
 
768
1077
  if (decision._tag === "failure") return yield* decision.error;
769
- }),
1078
+ },
770
1079
  );
771
1080
 
772
- const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn(
773
- "MemorySubmissionLedger.lookup",
774
- )((unvalidated) =>
775
- Effect.gen(function* () {
1081
+ const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fnUntraced(
1082
+ function* (unvalidated) {
776
1083
  const request = yield* validate(SubmissionLookup, "lookup", unvalidated);
777
1084
  const current = yield* Ref.get(state);
778
1085
 
@@ -787,13 +1094,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
787
1094
  submissionId === undefined ? undefined : current.submissions.get(submissionId);
788
1095
 
789
1096
  return stored === undefined ? Option.none() : Option.some(toSnapshot(stored.row));
790
- }),
1097
+ },
791
1098
  );
792
1099
 
793
- const resolveAdmission: SubmissionLedger["Service"]["resolveAdmission"] = Effect.fn(
794
- "MemorySubmissionLedger.resolveAdmission",
795
- )((unvalidated) =>
796
- Effect.gen(function* () {
1100
+ const resolveAdmission: SubmissionLedger["Service"]["resolveAdmission"] = Effect.fnUntraced(
1101
+ function* (unvalidated) {
797
1102
  const request = yield* validate(SubmissionLookupByKey, "resolveAdmission", unvalidated);
798
1103
 
799
1104
  // Test-only fault seam: lets suites exercise the Indeterminate classification that a
@@ -817,210 +1122,81 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
817
1122
  return stored === undefined
818
1123
  ? AdmissionNotAdmitted.make()
819
1124
  : AdmissionAdmitted.make({ submission: toSnapshot(stored.row) });
820
- }),
1125
+ },
821
1126
  );
822
1127
 
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);
1128
+ const selectHead: SubmissionLedger["Service"]["selectHead"] = Effect.fn(
1129
+ "MemorySubmissionLedger.selectHead",
1130
+ )((unvalidated) =>
1131
+ Effect.gen(function* () {
1132
+ const request = yield* validate(ClaimRequest, "claim", unvalidated);
1133
+ const identity = yield* mintIdentity("claim");
842
1134
 
843
- if (request.handoff !== undefined) {
844
- const handoff = request.handoff;
1135
+ const decision = yield* Ref.modify(
1136
+ state,
1137
+ (current): readonly [Decision<Option.Option<Claim>, LedgerError>, LedgerState] => {
1138
+ if (journal.owner.hasOwner(request.threadId)) return [success(Option.none()), current];
1139
+ let head = findHead(current, request.threadId);
845
1140
 
846
- if (current.lanes.get(request.threadId)?.producerEpoch !== handoff.producerEpoch)
847
- return [success(Option.none()), current];
1141
+ if (request.handoff !== undefined) {
1142
+ const handoff = request.handoff;
848
1143
 
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);
1144
+ const candidates = Array.from(
1145
+ current.activeByThread.get(request.threadId) ?? [],
1146
+ (id) => current.submissions.get(id),
1147
+ )
1148
+ .filter(
1149
+ (entry): entry is StoredSubmission =>
1150
+ entry !== undefined &&
1151
+ !(entry.row.state === "unknown" && entry.abortIntent === undefined),
1152
+ )
1153
+ .sort((a, b) => a.row.queueSequence - b.row.queueSequence);
857
1154
 
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];
1155
+ for (const candidate of candidates) {
1156
+ if (candidate.row.submissionId === handoff.submissionId) {
1157
+ head = candidate;
1158
+ break;
869
1159
  }
870
1160
  if (
871
- head?.row.submissionId !== handoff.submissionId ||
872
- (head.row.state !== "ready" &&
873
- head.row.state !== "running" &&
874
- head.row.state !== "input-applied")
1161
+ !handoff.deferredSubmissionIds.includes(candidate.row.submissionId) ||
1162
+ candidate.row.state !== "input-applied" ||
1163
+ candidate.abortIntent !== undefined
875
1164
  )
876
1165
  return [success(Option.none()), current];
877
1166
  }
1167
+ if (
1168
+ head?.row.submissionId !== handoff.submissionId ||
1169
+ (head.row.state !== "ready" && head.row.state !== "input-applied")
1170
+ )
1171
+ return [success(Option.none()), current];
1172
+ }
878
1173
 
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);
1174
+ if (head === undefined) return [success(Option.none()), current];
1175
+ if (BLOCKED_HEAD_STATES.has(head.row.state)) return [success(Option.none()), current];
882
1176
 
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
- };
1177
+ return [
1178
+ success(
1179
+ Option.some(
1180
+ Claim.make({
1181
+ submissionId: head.row.submissionId,
1182
+ attemptId: decodeAttemptId(`attempt-memory-${identity}`),
1183
+ inputPayload: head.row.inputPayload,
1184
+ }),
1185
+ ),
1186
+ ),
1187
+ current,
1188
+ ];
1189
+ },
1190
+ );
899
1191
 
900
- const row: SubmissionRow =
901
- head.row.state === "ready" ? { ...head.row, state: "running" } : head.row;
1192
+ if (decision._tag === "failure") return yield* decision.error;
902
1193
 
903
- const next = withSubmission(current, { ...head, row, ownership });
1194
+ return decision.value;
1195
+ }),
1196
+ );
904
1197
 
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* () {
1198
+ const markInputApplied: SubmissionLedger["Service"]["markInputApplied"] = Effect.fnUntraced(
1199
+ function* (unvalidated) {
1024
1200
  const request = yield* validate(MarkInputAppliedRequest, "markInputApplied", unvalidated);
1025
1201
 
1026
1202
  const decision = yield* Ref.modify(
@@ -1036,8 +1212,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1036
1212
  current,
1037
1213
  ];
1038
1214
  }
1039
- if (!ownsLane(current, stored, request.ownershipToken)) {
1040
- return [failure(ownershipLost(current, stored)), current];
1215
+ if (!journal.owner.hasOwner(stored.row.threadId, stored.row.submissionId)) {
1216
+ return [failure(ownershipLost(stored)), current];
1041
1217
  }
1042
1218
 
1043
1219
  if (stored.inputApplied !== undefined) {
@@ -1077,131 +1253,134 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1077
1253
  );
1078
1254
 
1079
1255
  if (decision._tag === "failure") return yield* decision.error;
1080
- }),
1256
+ },
1081
1257
  );
1082
1258
 
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);
1259
+ const canonicalSettlement = Effect.fnUntraced(function* (submission: SubmissionRow) {
1260
+ const record = yield* journal.record(
1261
+ submission.threadId,
1262
+ submissionSettlementRecordId(submission.submissionId),
1263
+ );
1088
1264
 
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);
1265
+ if (record === undefined) return undefined;
1266
+ const settlement = yield* validateCanonicalSettlement(record, submission);
1098
1267
 
1099
- if (stored === undefined) {
1100
- return [
1101
- failure(
1102
- ledgerError("reserveSettlement", `Unknown Submission ${request.submissionId}`),
1103
- ),
1104
- current,
1105
- ];
1106
- }
1268
+ return { record, settlement };
1269
+ });
1107
1270
 
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;
1271
+ const publish: SettlementPublisher["Service"]["publish"] = Effect.fn(
1272
+ "MemorySettlementPublisher.publish",
1273
+ )(function* (input) {
1274
+ const { request, record, settlement } = yield* validatePublication(input);
1275
+ const prepared = yield* journal.prepareAppend(request.append);
1276
+
1277
+ return yield* journal.owner.withProgress(
1278
+ request.append.threadId,
1279
+ journal.withMutation(
1280
+ Effect.gen(function* () {
1281
+ const current = yield* Ref.get(state);
1282
+ const stored = current.submissions.get(request.submissionId);
1113
1283
 
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");
1284
+ if (stored === undefined)
1285
+ return yield* ledgerError("publish settlement", "Unknown Submission");
1286
+ if (stored.row.threadId !== prepared.request.threadId)
1287
+ return yield* ledgerError("publish settlement", "Publication targets another Thread");
1288
+ yield* validateCanonicalSettlement(record, stored.row);
1289
+ const existing = yield* canonicalSettlement(stored.row);
1290
+ const tail = yield* journal.tail(stored.row.threadId);
1291
+
1292
+ // Replay still requires authority: finalization releases Owned tokens, while
1293
+ // retained host linkage or abort intent can authorize tokenless settled replay.
1294
+ switch (request.authority._tag) {
1295
+ case "Owned":
1296
+ if (!journal.owner.hasOwner(stored.row.threadId, stored.row.submissionId))
1297
+ return yield* ownershipLost(stored);
1298
+ yield* validateCanonicalSettlement(record, {
1299
+ submissionId: request.submissionId,
1300
+ receiptId: stored.row.receiptId,
1301
+ ...(settlement.outcome === "aborted" && settlement.runId === undefined
1302
+ ? {}
1303
+ : { runId: runIdForSubmission(request.submissionId) }),
1304
+ });
1305
+ break;
1306
+ case "Joined": {
1307
+ if (
1308
+ (stored.row.state !== "joined" &&
1309
+ !(stored.row.state === "settled" && existing !== undefined)) ||
1310
+ stored.joinedHostSubmissionId !== request.authority.hostSubmissionId
1311
+ )
1312
+ return yield* ledgerError(
1313
+ "publish settlement",
1314
+ "Submission is not joined to this host",
1315
+ );
1316
+ const host = current.submissions.get(request.authority.hostSubmissionId);
1123
1317
 
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;
1318
+ const canonicalHost =
1319
+ host === undefined || host.row.threadId !== stored.row.threadId
1320
+ ? undefined
1321
+ : yield* canonicalSettlement(host.row);
1132
1322
 
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
- ];
1323
+ if (canonicalHost === undefined)
1324
+ return yield* ledgerError(
1325
+ "publish settlement",
1326
+ "Joined host has no canonical settlement",
1327
+ );
1328
+ yield* validateJoinedSettlement(settlement, canonicalHost.settlement);
1329
+ break;
1148
1330
  }
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({
1331
+ case "QueuedAbort":
1332
+ if (
1333
+ (stored.row.state !== "ready" &&
1334
+ !(stored.row.state === "settled" && existing !== undefined)) ||
1335
+ stored.abortIntent === undefined ||
1336
+ journal.owner.hasOwner(stored.row.threadId, stored.row.submissionId) ||
1337
+ settlement.outcome !== "aborted"
1338
+ )
1339
+ return yield* ledgerError(
1340
+ "publish settlement",
1341
+ "Submission is not an unclaimed queued abort",
1342
+ );
1343
+ yield* validateCanonicalSettlement(record, {
1181
1344
  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;
1345
+ receiptId: stored.row.receiptId,
1346
+ runId: undefined,
1347
+ });
1348
+ break;
1349
+ }
1350
+ if (existing !== undefined)
1351
+ return SettlementPublicationResult.make({
1352
+ record: existing.record,
1353
+ tailSequence: tail.tailSequence,
1354
+ replayed: true,
1355
+ });
1356
+ if (stored.row.state === "settled")
1357
+ return yield* ledgerError(
1358
+ "publish settlement",
1359
+ "Finalized Submission has no canonical settlement",
1360
+ );
1361
+ const appended = yield* journal.appendPrepared(prepared);
1195
1362
 
1196
- return decision.value;
1197
- }),
1198
- );
1363
+ return SettlementPublicationResult.make({
1364
+ record,
1365
+ tailSequence: appended.lastSequence,
1366
+ replayed: false,
1367
+ });
1368
+ }),
1369
+ ),
1370
+ );
1371
+ });
1199
1372
 
1200
1373
  const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
1201
1374
  "MemorySubmissionLedger.finalizeSettlement",
1202
1375
  )((unvalidated) =>
1203
1376
  Effect.gen(function* () {
1204
1377
  const request = yield* validate(SettlementFinalization, "finalizeSettlement", unvalidated);
1378
+ const before = (yield* Ref.get(state)).submissions.get(request.submissionId);
1379
+ const canonical = before === undefined ? undefined : yield* canonicalSettlement(before.row);
1380
+
1381
+ if (canonical === undefined)
1382
+ return yield* ledgerError("finalizeSettlement", "Submission has no canonical settlement");
1383
+ const { record, settlement } = canonical;
1205
1384
  const nowMillis = yield* Clock.currentTimeMillis;
1206
1385
 
1207
1386
  const decision = yield* Ref.modify(
@@ -1219,63 +1398,60 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1219
1398
  current,
1220
1399
  ];
1221
1400
  }
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);
1401
+ const settlementFailure = settlementFailureFromRecord(record);
1236
1402
 
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) {
1403
+ if (settlement.settlementId !== request.settlementId) {
1249
1404
  return [
1250
1405
  failure(
1251
1406
  SettlementConflict.make({
1252
1407
  submissionId: request.submissionId,
1253
- existingOutcome: reservation.outcome,
1408
+ existingOutcome: settlement.outcome,
1254
1409
  }),
1255
1410
  ),
1256
1411
  current,
1257
1412
  ];
1258
1413
  }
1259
- if (reservation.finalizedAtMillis !== undefined) {
1414
+ if (stored.finalization !== undefined) {
1415
+ if (
1416
+ stored.row.state !== "settled" ||
1417
+ stored.row.settledOutcome !== settlement.outcome ||
1418
+ stored.finalization.settlementId !== request.settlementId ||
1419
+ stored.finalization.recordId !== record.recordId
1420
+ )
1421
+ return [
1422
+ failure(
1423
+ ledgerError(
1424
+ "finalizeSettlement",
1425
+ "Finalized identity disagrees with canonical settlement",
1426
+ ),
1427
+ ),
1428
+ current,
1429
+ ];
1430
+
1260
1431
  return [
1261
1432
  success(
1262
1433
  Settlement.make({
1263
1434
  submissionId: stored.row.submissionId,
1264
- settlementId: reservation.settlementId,
1435
+ settlementId: settlement.settlementId,
1265
1436
  receiptId: stored.row.receiptId,
1266
- outcome: reservation.outcome,
1437
+ outcome: settlement.outcome,
1267
1438
  ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1268
- settledAt: utc(reservation.finalizedAtMillis),
1439
+ settledAt: utc(stored.finalization.finalizedAtMillis),
1269
1440
  }),
1270
1441
  ),
1271
1442
  current,
1272
1443
  ];
1273
1444
  }
1445
+ if (stored.row.state === "settled")
1446
+ return [
1447
+ failure(ledgerError("finalizeSettlement", "Finalized timestamp is missing")),
1448
+ current,
1449
+ ];
1274
1450
 
1275
1451
  const terminal =
1276
1452
  stored.row.workerAdmissionJson === undefined
1277
1453
  ? undefined
1278
- : workerTerminalFromRecord(toSnapshot(stored.row), reservation.record);
1454
+ : workerTerminalFromRecord(toSnapshot(stored.row), record);
1279
1455
 
1280
1456
  const latestId = current.latestByThread.get(stored.row.threadId);
1281
1457
  const latest = latestId === undefined ? undefined : current.submissions.get(latestId);
@@ -1296,52 +1472,66 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1296
1472
  !pendingNewer &&
1297
1473
  !current.stoppedWorkers.has(stored.row.threadId)
1298
1474
  ) {
1299
- const submissions = new Map(current.submissions);
1475
+ const submissions = current.submissions;
1300
1476
 
1301
- for (const id of current.activeByThread.get(stored.row.threadId) ?? []) {
1302
- const other = submissions.get(id);
1477
+ if (!transferPageFits(stored.row.threadId))
1478
+ return [failure(transferCapacityError("finalizeSettlement")), current];
1303
1479
 
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({
1480
+ // Validate every fresh fact before mutating, without retaining a second active set.
1481
+ for (const apply of [false, true]) {
1482
+ for (const id of current.activeByThread.get(stored.row.threadId) ?? []) {
1483
+ const other = submissions.get(id);
1484
+
1485
+ if (other === undefined) continue;
1486
+ if (
1487
+ id !== stored.row.submissionId &&
1488
+ !(
1489
+ other.joinedHostSubmissionId === stored.row.submissionId &&
1490
+ other.inputApplied !== undefined
1491
+ ) &&
1492
+ other.abortIntent === undefined
1493
+ ) {
1494
+ const abortIntent = AbortIntent.make({
1316
1495
  submissionId: id,
1317
1496
  author: stored.row.principal,
1318
1497
  reason: `Worker assignment ${terminal}`,
1319
1498
  requestedAt: utc(nowMillis),
1320
- }),
1321
- });
1499
+ });
1500
+
1501
+ if (!apply && !abortFitsTransfer(other.row.threadId, abortIntent))
1502
+ return [failure(transferCapacityError("finalizeSettlement")), current];
1503
+ if (apply)
1504
+ withSubmission(current, {
1505
+ ...other,
1506
+ abortIntent,
1507
+ });
1508
+ }
1322
1509
  }
1323
1510
  }
1324
1511
  sealed = {
1325
1512
  ...current,
1326
1513
  submissions,
1327
- stoppedWorkers: new Map(current.stoppedWorkers).set(stored.row.threadId, terminal),
1514
+ stoppedWorkers: current.stoppedWorkers.set(stored.row.threadId, terminal),
1328
1515
  };
1329
1516
  }
1330
1517
 
1331
1518
  const next = withSubmission(sealed, {
1332
1519
  ...stored,
1333
- row: { ...stored.row, state: "settled", settledOutcome: reservation.outcome },
1334
- ownership: undefined,
1335
- reservation: { ...reservation, finalizedAtMillis: nowMillis },
1520
+ row: { ...stored.row, state: "settled", settledOutcome: settlement.outcome },
1521
+ finalization: {
1522
+ settlementId: settlement.settlementId,
1523
+ recordId: record.recordId,
1524
+ finalizedAtMillis: nowMillis,
1525
+ },
1336
1526
  });
1337
1527
 
1338
1528
  return [
1339
1529
  success(
1340
1530
  Settlement.make({
1341
1531
  submissionId: stored.row.submissionId,
1342
- settlementId: reservation.settlementId,
1532
+ settlementId: settlement.settlementId,
1343
1533
  receiptId: stored.row.receiptId,
1344
- outcome: reservation.outcome,
1534
+ outcome: settlement.outcome,
1345
1535
  ...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
1346
1536
  settledAt: utc(nowMillis),
1347
1537
  }),
@@ -1353,13 +1543,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1353
1543
 
1354
1544
  if (decision._tag === "failure") return yield* decision.error;
1355
1545
 
1546
+ if (before !== undefined)
1547
+ journal.owner.closeSubmission(before.row.threadId, request.submissionId);
1548
+
1356
1549
  return decision.value;
1357
1550
  }),
1358
1551
  );
1359
1552
 
1360
- const inspectWorker = Effect.fn("MemorySubmissionLedger.inspectWorker")(function* (
1361
- threadId: ThreadId,
1362
- ) {
1553
+ const inspectWorker = Effect.fnUntraced(function* (threadId: ThreadId) {
1363
1554
  const current = yield* Ref.get(state);
1364
1555
  const latestId = current.latestByThread.get(threadId);
1365
1556
  const latest = latestId === undefined ? undefined : current.submissions.get(latestId);
@@ -1382,50 +1573,62 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1382
1573
  });
1383
1574
  });
1384
1575
 
1385
- const stopWorker = Effect.fn("MemorySubmissionLedger.stopWorker")(function* (
1386
- unvalidated: WorkerStopCommand,
1387
- ) {
1576
+ const stopWorker = Effect.fnUntraced(function* (unvalidated: WorkerStopCommand) {
1388
1577
  const request = yield* validate(WorkerStopCommand, "stopWorker", unvalidated);
1389
1578
  const now = yield* Clock.currentTimeMillis;
1390
1579
 
1391
- return yield* Ref.modify(state, (current) => {
1392
- const submissions = new Map(current.submissions);
1393
- let owned = 0;
1580
+ const decision = yield* Ref.modify(
1581
+ state,
1582
+ (current): readonly [Decision<number, LedgerError>, LedgerState] => {
1583
+ const submissions = current.submissions;
1584
+ let owned = 0;
1585
+
1586
+ if (!current.stoppedWorkers.has(request.threadId) && !transferPageFits(request.threadId))
1587
+ return [failure(transferCapacityError("stopWorker")), current];
1588
+
1589
+ // Both passes run in this Ref.modify; no mutation precedes complete validation.
1590
+ for (const apply of [false, true]) {
1591
+ for (const id of current.activeByThread.get(request.threadId) ?? []) {
1592
+ const stored = submissions.get(id);
1593
+
1594
+ if (stored === undefined) continue;
1595
+ if (!apply && journal.owner.hasOwner(stored.row.threadId, stored.row.submissionId))
1596
+ owned++;
1597
+ if (stored.abortIntent === undefined) {
1598
+ const abortIntent = AbortIntent.make({
1599
+ submissionId: id,
1600
+ author: request.author,
1601
+ reason: "Worker owner stopped the worker",
1602
+ requestedAt: utc(now),
1603
+ });
1604
+
1605
+ if (!apply && !abortFitsTransfer(stored.row.threadId, abortIntent))
1606
+ return [failure(transferCapacityError("stopWorker")), current];
1607
+ if (apply) withSubmission(current, { ...stored, abortIntent });
1608
+ }
1609
+ }
1610
+ }
1394
1611
 
1395
- for (const id of current.activeByThread.get(request.threadId) ?? []) {
1396
- const stored = submissions.get(id);
1612
+ return [
1613
+ success(owned),
1614
+ {
1615
+ ...current,
1616
+ submissions,
1617
+ stoppedWorkers: current.stoppedWorkers.has(request.threadId)
1618
+ ? current.stoppedWorkers
1619
+ : current.stoppedWorkers.set(request.threadId, undefined),
1620
+ },
1621
+ ] as const;
1622
+ },
1623
+ );
1397
1624
 
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
- }
1625
+ if (decision._tag === "failure") return yield* decision.error;
1411
1626
 
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
- });
1627
+ return decision.value;
1423
1628
  });
1424
1629
 
1425
- const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fn(
1426
- "MemorySubmissionLedger.requestAbort",
1427
- )((unvalidated) =>
1428
- Effect.gen(function* () {
1630
+ const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fnUntraced(
1631
+ function* (unvalidated) {
1429
1632
  const request = yield* validate(AbortCommand, "requestAbort", unvalidated);
1430
1633
  const nowMillis = yield* Clock.currentTimeMillis;
1431
1634
 
@@ -1503,6 +1706,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1503
1706
  requestedAt: utc(nowMillis),
1504
1707
  });
1505
1708
 
1709
+ if (!abortFitsTransfer(stored.row.threadId, intent))
1710
+ return [failure(transferCapacityError("requestAbort")), current];
1711
+
1506
1712
  return [success(intent), withSubmission(current, { ...stored, abortIntent: intent })];
1507
1713
  },
1508
1714
  );
@@ -1510,14 +1716,25 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1510
1716
  if (decision._tag === "failure") return yield* decision.error;
1511
1717
 
1512
1718
  return decision.value;
1513
- }),
1719
+ },
1514
1720
  );
1515
1721
 
1516
- const claimJoining: SubmissionLedger["Service"]["claimJoining"] = Effect.fn(
1517
- "MemorySubmissionLedger.claimJoining",
1518
- )((unvalidated) =>
1519
- Effect.gen(function* () {
1722
+ const claimJoining: SubmissionLedger["Service"]["claimJoining"] = Effect.fnUntraced(
1723
+ function* (unvalidated) {
1520
1724
  const request = yield* validate(ClaimJoiningRequest, "claimJoining", unvalidated);
1725
+ const published = new Map<SubmissionId, Settlement["outcome"]>();
1726
+
1727
+ const before = yield* Ref.get(state);
1728
+
1729
+ for (const id of before.activeByThread.get(request.threadId) ?? []) {
1730
+ const stored = before.submissions.get(id);
1731
+
1732
+ if (stored === undefined || stored.row.state !== "ready") continue;
1733
+ const canonical = yield* canonicalSettlement(stored.row);
1734
+
1735
+ if (canonical !== undefined)
1736
+ published.set(stored.row.submissionId, canonical.settlement.outcome);
1737
+ }
1521
1738
 
1522
1739
  const decision = yield* Ref.modify(
1523
1740
  state,
@@ -1550,20 +1767,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1550
1767
  current,
1551
1768
  ];
1552
1769
  }
1553
- if (!ownsLane(current, host, request.ownershipToken)) {
1554
- return [failure(ownershipLost(current, host)), current];
1770
+ if (!journal.owner.hasOwner(host.row.threadId, host.row.submissionId)) {
1771
+ return [failure(ownershipLost(host)), current];
1555
1772
  }
1556
1773
 
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);
1774
+ const later = threadRows(current, request.threadId, host.row.queueSequence);
1564
1775
 
1565
1776
  const claims: Array<JoiningClaim> = [];
1566
- const submissions = new Map(current.submissions);
1777
+ const submissions = current.submissions;
1567
1778
 
1568
1779
  for (const stored of later) {
1569
1780
  if (claims.length >= request.maxCount) break;
@@ -1584,7 +1795,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1584
1795
  // Any other non-ready row — an admitted-not-ready gap in particular — breaks the
1585
1796
  // contiguous ready prefix (plan §2.5); later ready work stays queued (DUR-004).
1586
1797
  if (stored.row.state !== "ready") break;
1587
- submissions.set(stored.row.submissionId, {
1798
+ const terminal = published.get(stored.row.submissionId);
1799
+
1800
+ if (terminal === "aborted") continue;
1801
+ if (terminal !== undefined) break;
1802
+ withSubmission(current, {
1588
1803
  ...stored,
1589
1804
  row: { ...stored.row, state: "joining" },
1590
1805
  joinedHostSubmissionId: request.hostSubmissionId,
@@ -1605,13 +1820,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1605
1820
  if (decision._tag === "failure") return yield* decision.error;
1606
1821
 
1607
1822
  return decision.value;
1608
- }),
1823
+ },
1609
1824
  );
1610
1825
 
1611
- const markJoined: SubmissionLedger["Service"]["markJoined"] = Effect.fn(
1612
- "MemorySubmissionLedger.markJoined",
1613
- )((unvalidated) =>
1614
- Effect.gen(function* () {
1826
+ const markJoined: SubmissionLedger["Service"]["markJoined"] = Effect.fnUntraced(
1827
+ function* (unvalidated) {
1615
1828
  const request = yield* validate(MarkJoinedRequest, "markJoined", unvalidated);
1616
1829
 
1617
1830
  const decision = yield* Ref.modify(
@@ -1651,8 +1864,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1651
1864
  }
1652
1865
  // The lane is host-owned: the presented token must own the HOST's ownership period,
1653
1866
  // 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];
1867
+ if (!journal.owner.hasOwner(host.row.threadId, host.row.submissionId)) {
1868
+ return [failure(ownershipLost(host)), current];
1656
1869
  }
1657
1870
  if (stored.inputApplied !== undefined) {
1658
1871
  if (
@@ -1701,13 +1914,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1701
1914
  );
1702
1915
 
1703
1916
  if (decision._tag === "failure") return yield* decision.error;
1704
- }),
1917
+ },
1705
1918
  );
1706
1919
 
1707
- const revertJoining: SubmissionLedger["Service"]["revertJoining"] = Effect.fn(
1708
- "MemorySubmissionLedger.revertJoining",
1709
- )((unvalidated) =>
1710
- Effect.gen(function* () {
1920
+ const revertJoining: SubmissionLedger["Service"]["revertJoining"] = Effect.fnUntraced(
1921
+ function* (unvalidated) {
1711
1922
  const request = yield* validate(RevertJoiningRequest, "revertJoining", unvalidated);
1712
1923
 
1713
1924
  const decision = yield* Ref.modify(
@@ -1725,28 +1936,71 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1725
1936
  // already-joined (or already-reverted) Submission is a no-op (DUR-016).
1726
1937
  if (stored.row.state !== "joining") return [success(undefined), current];
1727
1938
 
1728
- return [
1729
- success(undefined),
1730
- withSubmission(current, {
1731
- ...stored,
1732
- row: { ...stored.row, state: "ready" },
1733
- joinedHostSubmissionId: undefined,
1734
- }),
1735
- ];
1736
- },
1737
- );
1939
+ const guard = request.guard;
1738
1940
 
1739
- if (decision._tag === "failure") return yield* decision.error;
1740
- }),
1741
- );
1941
+ if (guard !== undefined) {
1942
+ if (stored.joinedHostSubmissionId !== guard.hostSubmissionId)
1943
+ return [success(undefined), current];
1944
+ const host = current.submissions.get(guard.hostSubmissionId);
1742
1945
 
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);
1946
+ if (host === undefined || host.row.threadId !== stored.row.threadId) {
1947
+ return [
1948
+ failure(
1949
+ ledgerError(
1950
+ "revertJoining",
1951
+ `Host Submission ${guard.hostSubmissionId} is missing or belongs to another Thread`,
1952
+ ),
1953
+ ),
1954
+ current,
1955
+ ];
1956
+ }
1957
+ if (
1958
+ host.row.state !== "settled" &&
1959
+ !journal.owner.hasOwner(host.row.threadId, host.row.submissionId)
1960
+ )
1961
+ return [
1962
+ failure(
1963
+ ledgerError("revertJoining", "The linked host has no active local session"),
1964
+ ),
1965
+ current,
1966
+ ];
1967
+ }
1968
+
1969
+ return [
1970
+ success(undefined),
1971
+ withSubmission(current, {
1972
+ ...stored,
1973
+ row: { ...stored.row, state: "ready" },
1974
+ joinedHostSubmissionId: undefined,
1975
+ }),
1976
+ ];
1977
+ },
1978
+ );
1979
+
1980
+ if (decision._tag === "failure") return yield* decision.error;
1981
+ },
1982
+ );
1983
+
1984
+ const suspend: SubmissionLedger["Service"]["suspend"] = Effect.fnUntraced(
1985
+ function* (unvalidated) {
1986
+ const request = yield* validate(SuspendRequest, "suspend", unvalidated);
1748
1987
  const nowMillis = yield* Clock.currentTimeMillis;
1749
1988
 
1989
+ const target = (yield* Ref.get(state)).submissions.get(request.submissionId);
1990
+ const canonical = target === undefined ? undefined : yield* canonicalSettlement(target.row);
1991
+ const announcedChildren = new Set<SubmissionId>();
1992
+
1993
+ if (request.reason._tag === "WaitingForChild") {
1994
+ const current = yield* Ref.get(state);
1995
+
1996
+ for (const { childSubmissionId } of request.reason.children) {
1997
+ const child = current.submissions.get(childSubmissionId);
1998
+
1999
+ if (child !== undefined && (yield* canonicalSettlement(child.row)) !== undefined)
2000
+ announcedChildren.add(childSubmissionId);
2001
+ }
2002
+ }
2003
+
1750
2004
  const decision = yield* Ref.modify(
1751
2005
  state,
1752
2006
  (
@@ -1786,21 +2040,20 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1786
2040
  current,
1787
2041
  ];
1788
2042
  }
1789
- // An exact terminal outcome is already reserved (DUR-011); suspension would
1790
- // contradict it, so the reservation wins.
1791
- if (stored.reservation !== undefined) {
2043
+ // Canonical publication wins over a late parking transition.
2044
+ if (canonical !== undefined) {
1792
2045
  return [
1793
2046
  failure(
1794
2047
  SettlementConflict.make({
1795
2048
  submissionId: request.submissionId,
1796
- existingOutcome: stored.reservation.outcome,
2049
+ existingOutcome: canonical.settlement.outcome,
1797
2050
  }),
1798
2051
  ),
1799
2052
  current,
1800
2053
  ];
1801
2054
  }
1802
- if (!ownsLane(current, stored, request.ownershipToken)) {
1803
- return [failure(ownershipLost(current, stored)), current];
2055
+ if (!journal.owner.hasOwner(stored.row.threadId, stored.row.submissionId)) {
2056
+ return [failure(ownershipLost(stored)), current];
1804
2057
  }
1805
2058
 
1806
2059
  // A covering event that raced ahead of the suspend transaction (an approval decision,
@@ -1808,12 +2061,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1808
2061
  // resumes the caller immediately WITHOUT releasing the lane (plan §2.6, spec §12).
1809
2062
  const alreadyCovered =
1810
2063
  request.reason._tag === "ApprovalPending"
1811
- ? request.reason.toolCallIds.every((toolCallId) =>
1812
- stored.approvalDecisions.has(toolCallId),
2064
+ ? approvalDecisionsCover(
2065
+ request.reason.toolCallIds,
2066
+ (toolCallId) => stored.approvalDecisions.get(toolCallId)?.decision,
1813
2067
  )
1814
- : request.reason.children.every(
1815
- (child) =>
1816
- current.submissions.get(child.childSubmissionId)?.row.state === "settled",
2068
+ : request.reason.children.every((child) =>
2069
+ announcedChildren.has(child.childSubmissionId),
1817
2070
  );
1818
2071
 
1819
2072
  if (alreadyCovered) {
@@ -1825,7 +2078,6 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1825
2078
  withSubmission(current, {
1826
2079
  ...stored,
1827
2080
  row: { ...stored.row, state: "suspended" },
1828
- ownership: undefined,
1829
2081
  suspension: { reason: request.reason, suspendedAtMillis: nowMillis },
1830
2082
  }),
1831
2083
  ];
@@ -1834,14 +2086,15 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1834
2086
 
1835
2087
  if (decision._tag === "failure") return yield* decision.error;
1836
2088
 
2089
+ if (decision.value === "suspended" && target !== undefined)
2090
+ journal.owner.closeSubmission(target.row.threadId, request.submissionId);
2091
+
1837
2092
  return decision.value;
1838
- }),
2093
+ },
1839
2094
  );
1840
2095
 
1841
- const recordApprovalDecision: SubmissionLedger["Service"]["recordApprovalDecision"] = Effect.fn(
1842
- "MemorySubmissionLedger.recordApprovalDecision",
1843
- )((unvalidated) =>
1844
- Effect.gen(function* () {
2096
+ const recordApprovalDecision: SubmissionLedger["Service"]["recordApprovalDecision"] =
2097
+ Effect.fnUntraced(function* (unvalidated) {
1845
2098
  const command = yield* validate(
1846
2099
  ApprovalDecisionCommand,
1847
2100
  "recordApprovalDecision",
@@ -1922,20 +2175,24 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1922
2175
  decidedAt: utc(nowMillis),
1923
2176
  });
1924
2177
 
2178
+ if (!approvalFitsTransfer(stored.row.threadId, intent))
2179
+ return [failure(transferCapacityError("recordApprovalDecision")), current];
2180
+
1925
2181
  const approvalDecisions = new Map(stored.approvalDecisions).set(
1926
2182
  command.toolCallId,
1927
2183
  intent,
1928
2184
  );
1929
2185
 
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
2186
+ // A denial or decisions for every pending call wake an ApprovalPending lane:
2187
+ // suspended → input-applied. A WaitingForChild suspension wakes
1932
2188
  // only through recordChildSettled.
1933
2189
  const wakes =
1934
2190
  stored.row.state === "suspended" &&
1935
2191
  stored.suspension !== undefined &&
1936
2192
  stored.suspension.reason._tag === "ApprovalPending" &&
1937
- stored.suspension.reason.toolCallIds.every((toolCallId) =>
1938
- approvalDecisions.has(toolCallId),
2193
+ approvalDecisionsCover(
2194
+ stored.suspension.reason.toolCallIds,
2195
+ (toolCallId) => approvalDecisions.get(toolCallId)?.decision,
1939
2196
  );
1940
2197
 
1941
2198
  return [
@@ -1953,15 +2210,15 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1953
2210
  if (decision._tag === "failure") return yield* decision.error;
1954
2211
 
1955
2212
  return decision.value;
1956
- }),
1957
- );
2213
+ });
1958
2214
 
1959
- const markUnknown: SubmissionLedger["Service"]["markUnknown"] = Effect.fn(
1960
- "MemorySubmissionLedger.markUnknown",
1961
- )((unvalidated) =>
1962
- Effect.gen(function* () {
2215
+ const markUnknown: SubmissionLedger["Service"]["markUnknown"] = Effect.fnUntraced(
2216
+ function* (unvalidated) {
1963
2217
  const request = yield* validate(MarkUnknownRequest, "markUnknown", unvalidated);
1964
2218
 
2219
+ const target = (yield* Ref.get(state)).submissions.get(request.submissionId);
2220
+ const canonical = target === undefined ? undefined : yield* canonicalSettlement(target.row);
2221
+
1965
2222
  const decision = yield* Ref.modify(
1966
2223
  state,
1967
2224
  (current): readonly [Decision<void, SettlementConflict | LedgerError>, LedgerState] => {
@@ -1996,14 +2253,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
1996
2253
  current,
1997
2254
  ];
1998
2255
  }
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) {
2256
+ // Canonical publication wins over a late Unknown marking.
2257
+ if (canonical !== undefined) {
2002
2258
  return [
2003
2259
  failure(
2004
2260
  SettlementConflict.make({
2005
2261
  submissionId: request.submissionId,
2006
- existingOutcome: stored.reservation.outcome,
2262
+ existingOutcome: canonical.settlement.outcome,
2007
2263
  }),
2008
2264
  ),
2009
2265
  current,
@@ -2032,57 +2288,101 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2032
2288
  );
2033
2289
 
2034
2290
  if (decision._tag === "failure") return yield* decision.error;
2035
- }),
2291
+ },
2036
2292
  );
2037
2293
 
2038
2294
  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,
2295
+ Effect.fnUntraced(function* (unvalidated) {
2296
+ const command = yield* validate(
2297
+ UnknownResolutionCommand,
2298
+ "recordUnknownResolution",
2299
+ unvalidated,
2300
+ );
2301
+
2302
+ const nowMillis = yield* Clock.currentTimeMillis;
2303
+
2304
+ const before = (yield* Ref.get(state)).submissions.get(command.submissionId);
2305
+ const kind = unknownResolutionKind(command.resolution);
2306
+
2307
+ if (
2308
+ before !== undefined &&
2309
+ kind === "factual" &&
2310
+ !before.unknownResolutions.has(JSON.stringify([command.toolCallId, kind]))
2311
+ ) {
2312
+ const runId = runIdForSubmission(before.row.submissionId);
2313
+
2314
+ const outcomes = yield* journal.toolCallResults(
2315
+ before.row.threadId,
2316
+ runId,
2317
+ command.toolCallId,
2045
2318
  );
2046
2319
 
2047
- const nowMillis = yield* Clock.currentTimeMillis;
2320
+ const result = outcomes[0]?.payload;
2321
+
2322
+ if (
2323
+ outcomes.length > 1 ||
2324
+ (result !== undefined &&
2325
+ (result._tag !== "ToolCallSettled" ||
2326
+ result.runId !== runId ||
2327
+ result.toolCallId !== command.toolCallId))
2328
+ )
2329
+ return yield* ledgerError(
2330
+ "recordUnknownResolution",
2331
+ "Canonical Tool result identity is inconsistent",
2332
+ );
2048
2333
 
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) {
2334
+ if (
2335
+ result?._tag === "ToolCallSettled" &&
2336
+ !unknownResolutionMatchesResult(command.resolution, result)
2337
+ )
2338
+ return yield* UnknownResolutionConflict.make({
2339
+ submissionId: command.submissionId,
2340
+ toolCallId: command.toolCallId,
2341
+ });
2342
+ }
2343
+
2344
+ const decision = yield* Ref.modify(
2345
+ state,
2346
+ (
2347
+ current,
2348
+ ): readonly [
2349
+ Decision<
2350
+ UnknownResolutionIntent,
2351
+ UnknownResolutionConflict | SettlementConflict | LedgerError
2352
+ >,
2353
+ LedgerState,
2354
+ ] => {
2355
+ const stored = current.submissions.get(command.submissionId);
2356
+
2357
+ if (stored === undefined) {
2358
+ return [
2359
+ failure(
2360
+ ledgerError(
2361
+ "recordUnknownResolution",
2362
+ `Unknown Submission ${command.submissionId}`,
2363
+ ),
2364
+ ),
2365
+ current,
2366
+ ];
2367
+ }
2368
+ if (stored.row.state === "settled") {
2369
+ if (stored.row.settledOutcome === undefined) {
2063
2370
  return [
2064
2371
  failure(
2065
2372
  ledgerError(
2066
2373
  "recordUnknownResolution",
2067
- `Unknown Submission ${command.submissionId}`,
2374
+ `Settled Submission ${command.submissionId} is missing its outcome`,
2068
2375
  ),
2069
2376
  ),
2070
2377
  current,
2071
2378
  ];
2072
2379
  }
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
2380
 
2381
+ // Factual closure may outlive settlement; it never restores execution authority.
2382
+ if (
2383
+ command.resolution._tag !== "CompletedWithResult" &&
2384
+ command.resolution._tag !== "NeverHappened"
2385
+ )
2086
2386
  return [
2087
2387
  failure(
2088
2388
  SettlementConflict.make({
@@ -2092,80 +2392,105 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2092
2392
  ),
2093
2393
  current,
2094
2394
  ];
2095
- }
2096
- const existing = stored.unknownResolutions.get(command.toolCallId);
2395
+ }
2396
+ const key = JSON.stringify([command.toolCallId, kind]);
2397
+ const existing = stored.unknownResolutions.get(key);
2097
2398
 
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
- }
2399
+ if (
2400
+ (existing !== undefined &&
2401
+ !equivalentUnknownResolution(existing.intent.resolution, command.resolution)) ||
2402
+ (existing === undefined &&
2403
+ kind === "execution" &&
2404
+ stored.unknownResolutions.has(JSON.stringify([command.toolCallId, "factual"])))
2405
+ ) {
2406
+ return [
2407
+ failure(
2408
+ UnknownResolutionConflict.make({
2409
+ submissionId: command.submissionId,
2410
+ toolCallId: command.toolCallId,
2411
+ }),
2412
+ ),
2413
+ current,
2414
+ ];
2415
+ }
2112
2416
 
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
- });
2417
+ const intent =
2418
+ existing?.intent ??
2419
+ UnknownResolutionIntent.make({
2420
+ submissionId: command.submissionId,
2421
+ toolCallId: command.toolCallId,
2422
+ author: command.author,
2423
+ reason: command.reason,
2424
+ resolution: command.resolution,
2425
+ resolvedAt: utc(nowMillis),
2426
+ });
2123
2427
 
2124
- const unknownResolutions =
2125
- existing !== undefined
2126
- ? stored.unknownResolutions
2127
- : new Map(stored.unknownResolutions).set(command.toolCallId, {
2128
- intent,
2129
- });
2428
+ if (existing === undefined && !resolutionFitsTransfer(stored.row.threadId, intent))
2429
+ return [failure(transferCapacityError("recordUnknownResolution")), current];
2130
2430
 
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
- );
2431
+ const unknownResolutions =
2432
+ existing !== undefined
2433
+ ? stored.unknownResolutions
2434
+ : new Map(stored.unknownResolutions).set(key, {
2435
+ intent,
2436
+ });
2140
2437
 
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
- );
2438
+ // The lane reopens only when EVERY marked open call has a durable resolution
2439
+ // intent: unknown → input-applied (DUR-017). Replays re-run the coverage check so
2440
+ // a recovering caller can wake the lane idempotently.
2441
+ const wakes =
2442
+ stored.row.state === "unknown" &&
2443
+ stored.unknownMark !== undefined &&
2444
+ stored.unknownMark.toolCallIds.every(
2445
+ (toolCallId) =>
2446
+ unknownResolutions.has(JSON.stringify([toolCallId, "execution"])) ||
2447
+ unknownResolutions.has(JSON.stringify([toolCallId, "factual"])),
2448
+ );
2152
2449
 
2153
- if (decision._tag === "failure") return yield* decision.error;
2450
+ return [
2451
+ success(intent),
2452
+ withSubmission(current, {
2453
+ ...stored,
2454
+ row: wakes ? { ...stored.row, state: "input-applied" } : stored.row,
2455
+ unknownMark: wakes ? undefined : stored.unknownMark,
2456
+ unknownResolutions,
2457
+ }),
2458
+ ];
2459
+ },
2460
+ );
2154
2461
 
2155
- return decision.value;
2156
- }),
2157
- );
2462
+ if (decision._tag === "failure") return yield* decision.error;
2158
2463
 
2159
- const recordChildSettled: SubmissionLedger["Service"]["recordChildSettled"] = Effect.fn(
2160
- "MemorySubmissionLedger.recordChildSettled",
2161
- )((unvalidated) =>
2162
- Effect.gen(function* () {
2464
+ return decision.value;
2465
+ });
2466
+
2467
+ const recordChildSettled: SubmissionLedger["Service"]["recordChildSettled"] = Effect.fnUntraced(
2468
+ function* (unvalidated) {
2163
2469
  const request = yield* validate(
2164
2470
  ChildSettledNotification,
2165
2471
  "recordChildSettled",
2166
2472
  unvalidated,
2167
2473
  );
2168
2474
 
2475
+ const before = yield* Ref.get(state);
2476
+ const waiting = before.submissions.get(request.parentSubmissionId)?.suspension?.reason;
2477
+
2478
+ const ids = new Set([
2479
+ request.childSubmissionId,
2480
+ ...(waiting?._tag === "WaitingForChild"
2481
+ ? waiting.children.map((child) => child.childSubmissionId)
2482
+ : []),
2483
+ ]);
2484
+
2485
+ const announcedChildren = new Set<SubmissionId>();
2486
+
2487
+ for (const id of ids) {
2488
+ const child = before.submissions.get(id);
2489
+
2490
+ if (child !== undefined && (yield* canonicalSettlement(child.row)) !== undefined)
2491
+ announcedChildren.add(id);
2492
+ }
2493
+
2169
2494
  const decision = yield* Ref.modify(
2170
2495
  state,
2171
2496
  (current): readonly [Decision<ChildSettledOutcome, LedgerError>, LedgerState] => {
@@ -2182,16 +2507,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2182
2507
  current,
2183
2508
  ];
2184
2509
  }
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));
2510
+ // Canonical publication is visible before child ledger finalization.
2511
+ const announced = announcedChildren.has(request.childSubmissionId);
2195
2512
 
2196
2513
  if (!announced) {
2197
2514
  return [
@@ -2217,16 +2534,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2217
2534
  return [success("not-waiting" as const), current];
2218
2535
  }
2219
2536
 
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
- });
2537
+ const allSettled = children.every((entry) =>
2538
+ announcedChildren.has(entry.childSubmissionId),
2539
+ );
2230
2540
 
2231
2541
  if (!allSettled) return [success("still-waiting" as const), current];
2232
2542
 
@@ -2244,13 +2554,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2244
2554
  if (decision._tag === "failure") return yield* decision.error;
2245
2555
 
2246
2556
  return decision.value;
2247
- }),
2557
+ },
2248
2558
  );
2249
2559
 
2250
- const reserveChildBudget: SubmissionLedger["Service"]["reserveChildBudget"] = Effect.fn(
2251
- "MemorySubmissionLedger.reserveChildBudget",
2252
- )((unvalidated) =>
2253
- Effect.gen(function* () {
2560
+ const reserveChildBudget: SubmissionLedger["Service"]["reserveChildBudget"] = Effect.fnUntraced(
2561
+ function* (unvalidated) {
2254
2562
  const request = yield* validate(
2255
2563
  ChildBudgetReservationRequest,
2256
2564
  "reserveChildBudget",
@@ -2270,7 +2578,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2270
2578
  const existing = current.childReservations.get(request.reservationId);
2271
2579
 
2272
2580
  if (existing !== undefined) {
2273
- // Identical replays short-circuit before the fence, mirroring reserveSettlement:
2581
+ // Identical replays short-circuit before the fence, retaining the first committed allocation:
2274
2582
  // a replay creates nothing, so a recovering caller resumes rather than duplicates.
2275
2583
  const identical =
2276
2584
  existing.parentSubmissionId === request.parentSubmissionId &&
@@ -2302,7 +2610,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2302
2610
  current,
2303
2611
  ];
2304
2612
  }
2305
- for (const reservation of current.childReservations.values()) {
2613
+ for (const id of current.childrenByParent.get(request.parentSubmissionId) ?? []) {
2614
+ const reservation = current.childReservations.get(id);
2615
+
2616
+ if (reservation === undefined) continue;
2306
2617
  if (
2307
2618
  reservation.parentSubmissionId === request.parentSubmissionId &&
2308
2619
  reservation.parentToolCallId === request.parentToolCallId
@@ -2334,8 +2645,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2334
2645
  }
2335
2646
  // Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
2336
2647
  // parent Attempt can never create new reservation state.
2337
- if (!ownsLane(current, parent, request.ownershipToken)) {
2338
- return [failure(ownershipLost(current, parent)), current];
2648
+ if (!journal.owner.hasOwner(parent.row.threadId, parent.row.submissionId)) {
2649
+ return [failure(ownershipLost(parent)), current];
2339
2650
  }
2340
2651
 
2341
2652
  const reservation: StoredChildReservation = {
@@ -2367,198 +2678,189 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2367
2678
  if (decision._tag === "failure") return yield* decision.error;
2368
2679
 
2369
2680
  return decision.value;
2370
- }),
2681
+ },
2371
2682
  );
2372
2683
 
2373
2684
  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
- );
2685
+ Effect.fnUntraced(function* (unvalidated) {
2686
+ const request = yield* validate(
2687
+ AttachChildToReservationRequest,
2688
+ "attachChildToReservation",
2689
+ unvalidated,
2690
+ );
2381
2691
 
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
- }
2692
+ const decision = yield* Ref.modify(
2693
+ state,
2694
+ (
2695
+ current,
2696
+ ): readonly [
2697
+ Decision<
2698
+ ChildBudgetReservationSnapshot,
2699
+ ChildReservationConflict | OwnershipLost | LedgerError
2700
+ >,
2701
+ LedgerState,
2702
+ ] => {
2703
+ const reservation = current.childReservations.get(request.reservationId);
2411
2704
 
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
- }),
2705
+ if (reservation === undefined) {
2706
+ return [
2707
+ failure(
2708
+ ledgerError(
2709
+ "attachChildToReservation",
2710
+ `Unknown child reservation ${request.reservationId}`,
2419
2711
  ),
2420
- current,
2421
- ];
2712
+ ),
2713
+ current,
2714
+ ];
2715
+ }
2716
+ if (reservation.childSubmissionId !== undefined) {
2717
+ // Idempotent replay of the recorded attachment (unfenced — it mutates nothing).
2718
+ if (reservation.childSubmissionId === request.childSubmissionId) {
2719
+ return [success(toReservationSnapshot(reservation)), current];
2422
2720
  }
2423
- const parent = current.submissions.get(reservation.parentSubmissionId);
2424
2721
 
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
- }),
2722
+ return [
2723
+ failure(
2724
+ ChildReservationConflict.make({
2725
+ reservationId: request.reservationId,
2726
+ status: reservation.status,
2727
+ message: `Reservation ${request.reservationId} already records child ${reservation.childSubmissionId}.`,
2728
+ }),
2729
+ ),
2730
+ current,
2731
+ ];
2732
+ }
2733
+ const parent = current.submissions.get(reservation.parentSubmissionId);
2734
+
2735
+ if (parent === undefined) {
2736
+ return [
2737
+ failure(
2738
+ ledgerError(
2739
+ "attachChildToReservation",
2740
+ `Unknown Submission ${reservation.parentSubmissionId}`,
2447
2741
  ),
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
- ),
2742
+ ),
2743
+ current,
2744
+ ];
2745
+ }
2746
+ if (!journal.owner.hasOwner(parent.row.threadId, parent.row.submissionId)) {
2747
+ return [failure(ownershipLost(parent)), current];
2748
+ }
2749
+ if (reservation.status !== "reserved") {
2750
+ return [
2751
+ failure(
2752
+ ChildReservationConflict.make({
2753
+ reservationId: request.reservationId,
2754
+ status: reservation.status,
2755
+ message: `Cannot attach a child to a ${reservation.status} reservation.`,
2756
+ }),
2757
+ ),
2758
+ current,
2759
+ ];
2760
+ }
2761
+ // Single-store latitude: the admitted child must exist here, so a dangling
2762
+ // attachment can never enter the recovery view.
2763
+ if (!current.submissions.has(request.childSubmissionId)) {
2764
+ return [
2765
+ failure(
2766
+ ledgerError(
2767
+ "attachChildToReservation",
2768
+ `Unknown child Submission ${request.childSubmissionId}`,
2460
2769
  ),
2461
- current,
2462
- ];
2463
- }
2770
+ ),
2771
+ current,
2772
+ ];
2773
+ }
2464
2774
 
2465
- const attached: StoredChildReservation = {
2466
- ...reservation,
2467
- childSubmissionId: request.childSubmissionId,
2468
- };
2775
+ const attached: StoredChildReservation = {
2776
+ ...reservation,
2777
+ childSubmissionId: request.childSubmissionId,
2778
+ };
2469
2779
 
2470
- return [
2471
- success(toReservationSnapshot(attached)),
2472
- withChildReservation(current, attached),
2473
- ];
2474
- },
2475
- );
2780
+ return [
2781
+ success(toReservationSnapshot(attached)),
2782
+ withChildReservation(current, attached),
2783
+ ];
2784
+ },
2785
+ );
2476
2786
 
2477
- if (decision._tag === "failure") return yield* decision.error;
2787
+ if (decision._tag === "failure") return yield* decision.error;
2478
2788
 
2479
- return decision.value;
2480
- }),
2481
- );
2789
+ return decision.value;
2790
+ });
2482
2791
 
2483
2792
  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;
2793
+ Effect.fnUntraced(function* (unvalidated) {
2794
+ const request = yield* validate(
2795
+ BeginChildBudgetReleaseRequest,
2796
+ "beginChildBudgetRelease",
2797
+ unvalidated,
2798
+ );
2493
2799
 
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);
2800
+ const nowMillis = yield* Clock.currentTimeMillis;
2503
2801
 
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
- }
2802
+ const decision = yield* Ref.modify(
2803
+ state,
2804
+ (
2805
+ current,
2806
+ ): readonly [
2807
+ Decision<ChildBudgetReservationSnapshot, ChildReservationConflict | LedgerError>,
2808
+ LedgerState,
2809
+ ] => {
2810
+ const reservation = current.childReservations.get(request.reservationId);
2524
2811
 
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
- }),
2812
+ if (reservation === undefined) {
2813
+ return [
2814
+ failure(
2815
+ ledgerError(
2816
+ "beginChildBudgetRelease",
2817
+ `Unknown child reservation ${request.reservationId}`,
2533
2818
  ),
2534
- current,
2535
- ];
2819
+ ),
2820
+ current,
2821
+ ];
2822
+ }
2823
+ if (reservation.status !== "reserved") {
2824
+ // The accounting decision was already frozen exactly once; an identical replay is
2825
+ // a no-op and a divergent decision conflicts (spec §12 join step 6).
2826
+ if (
2827
+ reservation.accounting !== undefined &&
2828
+ equivalentPersistedJson(reservation.accounting, request.accounting)
2829
+ ) {
2830
+ return [success(toReservationSnapshot(reservation)), current];
2536
2831
  }
2537
2832
 
2538
- const frozen: StoredChildReservation = {
2539
- ...reservation,
2540
- status: "releasePending",
2541
- accounting: request.accounting,
2542
- releaseBeganAtMillis: nowMillis,
2543
- };
2544
-
2545
2833
  return [
2546
- success(toReservationSnapshot(frozen)),
2547
- withChildReservation(current, frozen),
2834
+ failure(
2835
+ ChildReservationConflict.make({
2836
+ reservationId: request.reservationId,
2837
+ status: reservation.status,
2838
+ message:
2839
+ "A different accounting decision is already frozen for this reservation.",
2840
+ }),
2841
+ ),
2842
+ current,
2548
2843
  ];
2549
- },
2550
- );
2844
+ }
2551
2845
 
2552
- if (decision._tag === "failure") return yield* decision.error;
2846
+ const frozen: StoredChildReservation = {
2847
+ ...reservation,
2848
+ status: "releasePending",
2849
+ accounting: request.accounting,
2850
+ releaseBeganAtMillis: nowMillis,
2851
+ };
2553
2852
 
2554
- return decision.value;
2555
- }),
2556
- );
2853
+ return [success(toReservationSnapshot(frozen)), withChildReservation(current, frozen)];
2854
+ },
2855
+ );
2557
2856
 
2558
- const releaseChildBudget: SubmissionLedger["Service"]["releaseChildBudget"] = Effect.fn(
2559
- "MemorySubmissionLedger.releaseChildBudget",
2560
- )((unvalidated) =>
2561
- Effect.gen(function* () {
2857
+ if (decision._tag === "failure") return yield* decision.error;
2858
+
2859
+ return decision.value;
2860
+ });
2861
+
2862
+ const releaseChildBudget: SubmissionLedger["Service"]["releaseChildBudget"] = Effect.fnUntraced(
2863
+ function* (unvalidated) {
2562
2864
  const request = yield* validate(
2563
2865
  ReleaseChildBudgetRequest,
2564
2866
  "releaseChildBudget",
@@ -2623,56 +2925,60 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2623
2925
  if (decision._tag === "failure") return yield* decision.error;
2624
2926
 
2625
2927
  return decision.value;
2626
- }),
2928
+ },
2627
2929
  );
2628
2930
 
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
- );
2931
+ const scanNonterminal: SubmissionLedger["Service"]["scanNonterminal"] = Stream.paginate<
2932
+ string | undefined,
2933
+ SubmissionWorkItem,
2934
+ never
2935
+ >(undefined, (after) =>
2936
+ Effect.sync(() => {
2937
+ const current = MutableRef.get(state.ref);
2938
+
2939
+ const candidates = boundedWorkSelection<StoredSubmission>(
2940
+ 8,
2941
+ (stored) =>
2942
+ `${stored.row.threadId}\u0000${String(stored.row.queueSequence).padStart(16, "0")}`,
2943
+ after,
2944
+ );
2653
2945
 
2654
- return Stream.fromIterable(snapshots);
2655
- }),
2656
- ),
2946
+ for (const ids of current.activeByThread.values())
2947
+ for (const id of ids) {
2948
+ const stored = current.submissions.get(id);
2949
+
2950
+ if (stored !== undefined) candidates.add(stored);
2951
+ }
2952
+ const items = candidates.values.map(({ row }) => SubmissionWorkItem.make(row));
2953
+ const last = candidates.values.at(-1)?.row;
2954
+
2955
+ return [
2956
+ items,
2957
+ last === undefined
2958
+ ? Option.none<string | undefined>()
2959
+ : Option.some(`${last.threadId}\u0000${String(last.queueSequence).padStart(16, "0")}`),
2960
+ ] as const;
2961
+ }),
2657
2962
  );
2658
2963
 
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);
2964
+ const readAbortIntent: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fnUntraced(
2965
+ function* (unvalidated) {
2966
+ const request = yield* validate(AbortIntentRequest, "readAbortIntent", unvalidated);
2967
+ const stored = (yield* Ref.get(state)).submissions.get(request.submissionId);
2664
2968
 
2665
- if (stored === undefined) {
2666
- return yield* ledgerError("readAbortIntent", `Unknown Submission ${request.submissionId}`);
2667
- }
2969
+ if (stored === undefined) {
2970
+ return yield* ledgerError(
2971
+ "readAbortIntent",
2972
+ `Unknown Submission ${request.submissionId}`,
2973
+ );
2974
+ }
2668
2975
 
2669
- return stored.abortIntent;
2670
- });
2976
+ return stored.abortIntent;
2977
+ },
2978
+ );
2671
2979
 
2672
- const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
2673
- "MemorySubmissionLedger.loadRecoverySnapshot",
2674
- )((unvalidated) =>
2675
- Effect.gen(function* () {
2980
+ const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] =
2981
+ Effect.fnUntraced(function* (unvalidated) {
2676
2982
  const request = yield* validate(
2677
2983
  RecoverySnapshotRequest,
2678
2984
  "loadRecoverySnapshot",
@@ -2689,8 +2995,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2689
2995
  );
2690
2996
  }
2691
2997
 
2692
- const joins = [...current.submissions.values()]
2693
- .filter((candidate) => candidate.joinedHostSubmissionId === request.submissionId)
2998
+ const joins = Array.from(current.joinsByHost.get(request.submissionId) ?? [], (id) =>
2999
+ current.submissions.get(id),
3000
+ )
3001
+ .filter((candidate): candidate is StoredSubmission => candidate !== undefined)
2694
3002
  .sort((left, right) => left.row.queueSequence - right.row.queueSequence)
2695
3003
  .map((candidate) =>
2696
3004
  JoinSnapshot.make({
@@ -2709,8 +3017,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2709
3017
  // Parent-side subagent view: this Submission's child budget reservations in parent Tool
2710
3018
  // Call order, plus each attached child's current lane state (a disposable derived view;
2711
3019
  // the canonical records stay the recovery truth, DUR-015).
2712
- const childReservations = [...current.childReservations.values()]
2713
- .filter((reservation) => reservation.parentSubmissionId === request.submissionId)
3020
+ const childReservations = Array.from(
3021
+ current.childrenByParent.get(request.submissionId) ?? [],
3022
+ (id) => current.childReservations.get(id),
3023
+ )
3024
+ .filter((reservation): reservation is StoredChildReservation => reservation !== undefined)
2714
3025
  .sort((left, right) =>
2715
3026
  left.parentToolCallId < right.parentToolCallId
2716
3027
  ? -1
@@ -2761,64 +3072,455 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
2761
3072
  suspendedAt: utc(stored.suspension.suspendedAtMillis),
2762
3073
  }),
2763
3074
  }),
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
- }),
3075
+ hasOwner: journal.owner.hasOwner(stored.row.threadId, stored.row.submissionId),
2774
3076
  ...(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
3077
  ...(stored.abortIntent === undefined ? {} : { abortIntent: stored.abortIntent }),
2787
3078
  });
2788
- }),
2789
- );
3079
+ });
2790
3080
 
2791
- return SubmissionLedger.of({
3081
+ const ledger = SubmissionLedger.of({
2792
3082
  capabilities,
2793
- admit,
2794
- markReady,
3083
+ resolveFundingOwner: Effect.fnUntraced(function* (unvalidated) {
3084
+ const request = yield* validate(FundingOwnerRequest, "funding owner", unvalidated);
3085
+ const current = MutableRef.get(state.ref);
3086
+ const selected = current.submissions.get(request.submissionId);
3087
+
3088
+ if (selected === undefined || selected.row.threadId !== request.threadId)
3089
+ return yield* ledgerError(
3090
+ "funding owner",
3091
+ "Selected admission belongs to another Thread or is missing",
3092
+ );
3093
+ if (
3094
+ selected.row.state === "joining" ||
3095
+ (selected.joinedHostSubmissionId !== undefined && selected.inputApplied === undefined)
3096
+ )
3097
+ return yield* ledgerError(
3098
+ "funding owner",
3099
+ "Joining funding owner is not yet frozen by exact applied input evidence",
3100
+ );
3101
+
3102
+ const owner =
3103
+ selected.joinedHostSubmissionId === undefined
3104
+ ? selected
3105
+ : current.submissions.get(selected.joinedHostSubmissionId);
3106
+
3107
+ if (
3108
+ owner === undefined ||
3109
+ owner.row.threadId !== request.threadId ||
3110
+ (owner !== selected && owner.row.queueSequence >= selected.row.queueSequence)
3111
+ )
3112
+ return yield* ledgerError(
3113
+ "funding owner",
3114
+ "Exact joined funding owner is missing or inconsistent",
3115
+ );
3116
+
3117
+ return FundingOwner.make({
3118
+ selected: toSnapshot(selected.row),
3119
+ owner: toSnapshot(owner.row),
3120
+ });
3121
+ }),
3122
+ admit: (request) => journal.withMutation(admit(request)),
3123
+ markReady: (request) => journal.withMutation(markReady(request)),
2795
3124
  lookup,
2796
3125
  resolveAdmission,
2797
- claim,
2798
- renewOwnership,
2799
- releaseOwnership,
2800
- markInputApplied,
2801
- reserveSettlement,
2802
- finalizeSettlement,
2803
- requestAbort,
2804
- stopWorker,
3126
+ selectHead: (request) => journal.withMutation(selectHead(request)),
3127
+ markInputApplied: (request) => journal.withMutation(markInputApplied(request)),
3128
+ finalizeSettlement: (request) => journal.withMutation(finalizeSettlement(request)),
3129
+ requestAbort: (request) => journal.withMutation(requestAbort(request)),
3130
+ stopWorker: (request) => journal.withMutation(stopWorker(request)),
2805
3131
  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,
3132
+ claimJoining: (request) => journal.withMutation(claimJoining(request)),
3133
+ markJoined: (request) => journal.withMutation(markJoined(request)),
3134
+ revertJoining: (request) => journal.withMutation(revertJoining(request)),
3135
+ suspend: (request) => journal.withMutation(suspend(request)),
3136
+ recordApprovalDecision: (request) => journal.withMutation(recordApprovalDecision(request)),
3137
+ markUnknown: (request) => journal.withMutation(markUnknown(request)),
3138
+ recordUnknownResolution: (request) => journal.withMutation(recordUnknownResolution(request)),
3139
+ recordChildSettled: (request) => journal.withMutation(recordChildSettled(request)),
3140
+ reserveChildBudget: (request) => journal.withMutation(reserveChildBudget(request)),
3141
+ attachChildToReservation: (request) =>
3142
+ journal.withMutation(attachChildToReservation(request)),
3143
+ beginChildBudgetRelease: (request) => journal.withMutation(beginChildBudgetRelease(request)),
3144
+ releaseChildBudget: (request) => journal.withMutation(releaseChildBudget(request)),
2818
3145
  scanNonterminal,
2819
3146
  loadRecoverySnapshot,
2820
3147
  readAbortIntent,
2821
3148
  });
3149
+
3150
+ const toAdmission = (row: SubmissionRow, settledAtMillis?: number) =>
3151
+ ThreadAdmission.make({
3152
+ ...toSnapshot(row),
3153
+ ...(settledAtMillis === undefined ? {} : { settledAt: utc(settledAtMillis) }),
3154
+ });
3155
+
3156
+ const cleanCommand = <A extends { readonly canonicalRecordId?: RecordEnvelope["recordId"] }>(
3157
+ value: A,
3158
+ ) => {
3159
+ const { canonicalRecordId: _, ...fact } = value;
3160
+
3161
+ return fact;
3162
+ };
3163
+
3164
+ const commands = (stored: StoredSubmission | undefined) => {
3165
+ return {
3166
+ aborts: stored?.abortIntent === undefined ? [] : [cleanCommand(stored.abortIntent)],
3167
+ approvals: Array.from(stored?.approvalDecisions.values() ?? [], cleanCommand),
3168
+ resolutions: Array.from(stored?.unknownResolutions.values() ?? [], ({ intent }) =>
3169
+ cleanCommand(intent),
3170
+ ),
3171
+ };
3172
+ };
3173
+
3174
+ const readCommands = Effect.fnUntraced(function* (stored: StoredSubmission | undefined) {
3175
+ if (
3176
+ (stored?.approvalDecisions.size ?? 0) > MAX_RUN_EVIDENCE_RECORDS ||
3177
+ (stored?.unknownResolutions.size ?? 0) > MAX_RUN_EVIDENCE_RECORDS
3178
+ )
3179
+ return yield* ThreadStoreError.make({
3180
+ operation: "read commands",
3181
+ message: "Accepted commands exceed Run record bounds",
3182
+ });
3183
+ let bytes = 0;
3184
+
3185
+ const add = <A, I>(schema: Schema.Codec<A, I>, value: A) =>
3186
+ Schema.encodeEffect(Schema.toCodecJson(schema))(value).pipe(
3187
+ Effect.mapError(() =>
3188
+ ThreadStoreError.make({
3189
+ operation: "read commands",
3190
+ message: "Invalid accepted command wire",
3191
+ }),
3192
+ ),
3193
+ Effect.flatMap((wire) => {
3194
+ bytes += utf8ByteLength(canonicalJson(wire));
3195
+
3196
+ return bytes > MAX_RUN_EVIDENCE_BYTES
3197
+ ? Effect.fail(
3198
+ ThreadStoreError.make({
3199
+ operation: "read commands",
3200
+ message: "Accepted commands exceed Run byte bounds",
3201
+ }),
3202
+ )
3203
+ : Effect.void;
3204
+ }),
3205
+ );
3206
+
3207
+ if (stored?.abortIntent !== undefined) yield* add(AbortIntent, stored.abortIntent);
3208
+ for (const value of stored?.approvalDecisions.values() ?? [])
3209
+ yield* add(ApprovalDecisionIntent, value);
3210
+ for (const { intent } of stored?.unknownResolutions.values() ?? [])
3211
+ yield* add(UnknownResolutionIntent, intent);
3212
+
3213
+ return commands(stored);
3214
+ });
3215
+
3216
+ const lookupAdmission = (threadId: ThreadId, submissionId: SubmissionId) =>
3217
+ Effect.sync(() => {
3218
+ const stored = MutableRef.get(state.ref).submissions.get(submissionId);
3219
+
3220
+ return stored?.row.threadId === threadId
3221
+ ? toAdmission(stored.row, stored.finalization?.finalizedAtMillis)
3222
+ : undefined;
3223
+ });
3224
+
3225
+ yield* journal.registerLedgerTransfer({
3226
+ admission: lookupAdmission,
3227
+ commands: (id) => readCommands(MutableRef.get(state.ref).submissions.get(id)),
3228
+ snapshot: (threadId) =>
3229
+ Effect.sync(() => {
3230
+ const current = MutableRef.get(state.ref);
3231
+ const directory = current.byThread.get(threadId);
3232
+
3233
+ return {
3234
+ idle:
3235
+ (current.activeByThread.get(threadId)?.size ?? 0) === 0 &&
3236
+ (directory?.reservations ?? 0) === 0,
3237
+ admissions: directory?.ordered.length ?? 0,
3238
+ aborts: directory?.commands.aborts.length ?? 0,
3239
+ approvals: directory?.commands.approvals.length ?? 0,
3240
+ resolutions: directory?.commands.resolutions.length ?? 0,
3241
+ ...(current.stoppedWorkers.has(threadId)
3242
+ ? {
3243
+ workerSeal:
3244
+ current.stoppedWorkers.get(threadId) === undefined
3245
+ ? {}
3246
+ : { terminal: current.stoppedWorkers.get(threadId) },
3247
+ }
3248
+ : {}),
3249
+ };
3250
+ }),
3251
+ facts: (threadId, section, after) =>
3252
+ Effect.sync(() => {
3253
+ const current = MutableRef.get(state.ref);
3254
+
3255
+ const facts = {
3256
+ admissions: new Array<ThreadAdmission>(),
3257
+ commands: commands(undefined),
3258
+ deliveries: [],
3259
+ };
3260
+
3261
+ let next = after ?? "";
3262
+
3263
+ if (section === "admissions") {
3264
+ const stored = threadRows(
3265
+ current,
3266
+ threadId,
3267
+ after === undefined ? -1 : Number(after),
3268
+ ).next().value;
3269
+
3270
+ if (stored !== undefined) {
3271
+ facts.admissions.push(
3272
+ toAdmission(stored.row, stored.finalization?.finalizedAtMillis),
3273
+ );
3274
+ next = String(stored.row.queueSequence);
3275
+ }
3276
+ } else {
3277
+ const entries = current.byThread.get(threadId)?.commands[section] ?? [];
3278
+ const at = after === undefined ? 0 : commandPosition(entries, after);
3279
+ const choice = entries[at + Number(after !== undefined && entries[at]?.key === after)];
3280
+
3281
+ if (choice !== undefined) {
3282
+ next = choice.key;
3283
+ const stored = current.submissions.get(choice.submissionId);
3284
+
3285
+ if (section === "aborts" && stored?.abortIntent !== undefined)
3286
+ facts.commands.aborts.push(cleanCommand(stored.abortIntent));
3287
+ else if (section === "approvals" && choice.toolCallId !== undefined) {
3288
+ const value = stored?.approvalDecisions.get(choice.toolCallId);
3289
+
3290
+ if (value !== undefined) facts.commands.approvals.push(cleanCommand(value));
3291
+ } else if (section === "resolutions" && choice.resolutionKey !== undefined) {
3292
+ const value = stored?.unknownResolutions.get(choice.resolutionKey);
3293
+
3294
+ if (value !== undefined)
3295
+ facts.commands.resolutions.push(cleanCommand(value.intent));
3296
+ }
3297
+ }
3298
+ }
3299
+
3300
+ return { facts, after: next };
3301
+ }),
3302
+ submissions: (threadId) =>
3303
+ Stream.paginate(-1, (after) =>
3304
+ Effect.sync(() => {
3305
+ const stored = threadRows(MutableRef.get(state.ref), threadId, after).next().value;
3306
+
3307
+ return [
3308
+ stored === undefined ? [] : [toSnapshot(stored.row)],
3309
+ stored === undefined ? Option.none<number>() : Option.some(stored.row.queueSequence),
3310
+ ] as const;
3311
+ }),
3312
+ ),
3313
+ startImport: Effect.fnUntraced(function* (threadId) {
3314
+ const current = yield* Ref.get(state);
3315
+
3316
+ if (
3317
+ current.lanes.has(threadId) ||
3318
+ current.byThread.has(threadId) ||
3319
+ current.stoppedWorkers.has(threadId)
3320
+ )
3321
+ return yield* ThreadImportRejected.make({
3322
+ threadId,
3323
+ reason: "target-not-empty",
3324
+ message: "Destination ledger contains Thread data",
3325
+ });
3326
+
3327
+ const staged: LedgerState = {
3328
+ submissions: new Map(),
3329
+ admissionIndex: new Map(),
3330
+ lanes: new Map(),
3331
+ childReservations: new Map(),
3332
+ freshAdmissionSequence: current.freshAdmissionSequence,
3333
+ stoppedWorkers: new Map(),
3334
+ latestByThread: new Map(),
3335
+ activeByThread: new Map(),
3336
+ byThread: new Map(),
3337
+ receipts: new Map(),
3338
+ joinsByHost: new Map(),
3339
+ childrenByParent: new Map(),
3340
+ };
3341
+
3342
+ const commandBytes = new Map<SubmissionId, number>();
3343
+ const settledTimes = new Map<SubmissionId, number>();
3344
+
3345
+ const retainCommandBytes = Effect.fnUntraced(function* <A, I>(
3346
+ schema: Schema.Codec<A, I>,
3347
+ value: A,
3348
+ id: SubmissionId,
3349
+ ) {
3350
+ const wire = yield* Schema.encodeEffect(Schema.toCodecJson(schema))(value).pipe(
3351
+ Effect.mapError(() => invalidThreadArchive("Invalid accepted command wire", threadId)),
3352
+ );
3353
+
3354
+ const bytes = (commandBytes.get(id) ?? 0) + utf8ByteLength(canonicalJson(wire));
3355
+
3356
+ if (bytes > MAX_RUN_EVIDENCE_BYTES)
3357
+ return yield* invalidThreadArchive(
3358
+ "Accepted commands exceed Run byte bounds",
3359
+ threadId,
3360
+ );
3361
+ commandBytes.set(id, bytes);
3362
+ });
3363
+
3364
+ const reader = {
3365
+ admission: (id: SubmissionId) =>
3366
+ Effect.sync(() => {
3367
+ const stored = staged.submissions.get(id);
3368
+
3369
+ return stored === undefined
3370
+ ? undefined
3371
+ : toAdmission(stored.row, settledTimes.get(id));
3372
+ }),
3373
+ runOwner: (runId: import("@yielded/agent/identifiers").RunId) =>
3374
+ Effect.sync(() => {
3375
+ if (!runId.startsWith("run:")) return undefined;
3376
+ const id = Schema.decodeSync(SubmissionId)(runId.slice(4));
3377
+ const stored = staged.submissions.get(id);
3378
+
3379
+ return stored === undefined
3380
+ ? undefined
3381
+ : toAdmission(stored.row, settledTimes.get(id));
3382
+ }),
3383
+ commands: (id: SubmissionId) => readCommands(staged.submissions.get(id)),
3384
+ };
3385
+
3386
+ return {
3387
+ reader,
3388
+ stage: Effect.fnUntraced(function* (page) {
3389
+ for (const a of page.archive.admissions) {
3390
+ const key = admissionKey(threadId, a.principal, a.idempotencyKey);
3391
+
3392
+ if (
3393
+ current.submissions.has(a.submissionId) ||
3394
+ staged.submissions.has(a.submissionId) ||
3395
+ current.receipts.has(a.receiptId) ||
3396
+ staged.receipts.has(a.receiptId) ||
3397
+ current.admissionIndex.has(key) ||
3398
+ staged.admissionIndex.has(key)
3399
+ )
3400
+ return yield* ThreadImportRejected.make({
3401
+ threadId,
3402
+ reason: "destination-conflict",
3403
+ message: "Admission, receipt, or immutable admission key already exists",
3404
+ });
3405
+ const previous = threadRows(staged, threadId, a.queueSequence - 1).next().value;
3406
+
3407
+ if (previous?.row.queueSequence === a.queueSequence)
3408
+ return yield* invalidThreadArchive("Duplicate admission queue position", threadId);
3409
+ if (a.settledAt !== undefined)
3410
+ settledTimes.set(a.submissionId, DateTime.toEpochMillis(a.settledAt));
3411
+ withSubmission(
3412
+ staged,
3413
+ fromRebuilt(
3414
+ RebuiltSubmission.make({
3415
+ admission: a,
3416
+ state: "admitted",
3417
+ unknownToolCallIds: [],
3418
+ approvals: [],
3419
+ resolutions: [],
3420
+ }),
3421
+ ),
3422
+ );
3423
+ }
3424
+ for (const fact of page.archive.commands.aborts) {
3425
+ const stored = staged.submissions.get(fact.submissionId);
3426
+
3427
+ if (stored === undefined || stored.abortIntent !== undefined)
3428
+ return yield* invalidThreadArchive(
3429
+ "Missing admission or duplicate abort",
3430
+ threadId,
3431
+ );
3432
+ const intent = AbortIntent.make(fact);
3433
+
3434
+ yield* retainCommandBytes(AbortIntent, intent, fact.submissionId);
3435
+ withSubmission(staged, { ...stored, abortIntent: intent });
3436
+ }
3437
+ for (const fact of page.archive.commands.approvals) {
3438
+ const stored = staged.submissions.get(fact.submissionId);
3439
+
3440
+ if (stored === undefined || stored.approvalDecisions.has(fact.toolCallId))
3441
+ return yield* invalidThreadArchive(
3442
+ "Missing admission or duplicate approval",
3443
+ threadId,
3444
+ );
3445
+ if (stored.approvalDecisions.size >= MAX_RUN_EVIDENCE_RECORDS)
3446
+ return yield* invalidThreadArchive(
3447
+ "Accepted approvals exceed Run record bounds",
3448
+ threadId,
3449
+ );
3450
+ const intent = ApprovalDecisionIntent.make(fact);
3451
+
3452
+ yield* retainCommandBytes(ApprovalDecisionIntent, intent, fact.submissionId);
3453
+
3454
+ const approvals = new Map(stored.approvalDecisions).set(fact.toolCallId, intent);
3455
+
3456
+ withSubmission(staged, { ...stored, approvalDecisions: approvals });
3457
+ }
3458
+ for (const fact of page.archive.commands.resolutions) {
3459
+ const stored = staged.submissions.get(fact.submissionId);
3460
+ const key = JSON.stringify([fact.toolCallId, unknownResolutionKind(fact.resolution)]);
3461
+
3462
+ if (stored === undefined || stored.unknownResolutions.has(key))
3463
+ return yield* invalidThreadArchive(
3464
+ "Missing admission or duplicate resolution",
3465
+ threadId,
3466
+ );
3467
+ if (stored.unknownResolutions.size >= MAX_RUN_EVIDENCE_RECORDS)
3468
+ return yield* invalidThreadArchive(
3469
+ "Accepted resolutions exceed Run record bounds",
3470
+ threadId,
3471
+ );
3472
+ const intent = UnknownResolutionIntent.make(fact);
3473
+
3474
+ yield* retainCommandBytes(UnknownResolutionIntent, intent, fact.submissionId);
3475
+
3476
+ const resolutions = new Map(stored.unknownResolutions).set(key, { intent });
3477
+
3478
+ withSubmission(staged, { ...stored, unknownResolutions: resolutions });
3479
+ }
3480
+ }),
3481
+ prepareCommit: Effect.fnUntraced(function* (workerSeal) {
3482
+ let lastQueue = 0;
3483
+ let terminalEvidence = workerSeal?.terminal === undefined;
3484
+
3485
+ for (const stored of threadRows(staged, threadId)) {
3486
+ const a = toAdmission(stored.row, settledTimes.get(stored.row.submissionId));
3487
+ const rebuilt = yield* rebuildClosedSubmission(a);
3488
+
3489
+ if (
3490
+ rebuilt.settlement !== undefined &&
3491
+ workerTerminalFromRecord(toSnapshot(stored.row), rebuilt.settlement) ===
3492
+ workerSeal?.terminal
3493
+ )
3494
+ terminalEvidence = true;
3495
+ withSubmission(staged, fromRebuilt(rebuilt));
3496
+ lastQueue = Math.max(lastQueue, a.queueSequence);
3497
+ }
3498
+ if (!terminalEvidence)
3499
+ return yield* invalidThreadArchive(
3500
+ "Worker seal terminal has no exact completed input evidence",
3501
+ threadId,
3502
+ );
3503
+ if (!Number.isSafeInteger(lastQueue + 1))
3504
+ return yield* ThreadImportRejected.make({
3505
+ threadId,
3506
+ reason: "unsupported-capacity",
3507
+ message: "Admission queue sequence is exhausted",
3508
+ });
3509
+
3510
+ return () => {
3511
+ for (const stored of staged.submissions.values()) withSubmission(current, stored);
3512
+ current.lanes.set(threadId, { nextQueueSequence: lastQueue + 1 });
3513
+ if (workerSeal !== undefined)
3514
+ current.stoppedWorkers.set(threadId, workerSeal.terminal);
3515
+ };
3516
+ }),
3517
+ };
3518
+ }),
3519
+ });
3520
+
3521
+ return Context.make(SubmissionLedger, ledger).pipe(
3522
+ Context.add(SettlementPublisher, { publish }),
3523
+ );
2822
3524
  });
2823
3525
 
2824
3526
  /** Construction options for the in-memory reference SubmissionLedger. */
@@ -2834,11 +3536,19 @@ export interface MemorySubmissionLedgerOptions {
2834
3536
 
2835
3537
  /**
2836
3538
  * 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.
3539
+ * `Ref` owned by the Layer's Scope. Fresh identities use the supplied `Crypto` service;
3540
+ * imported identities remain unchanged. No daemon fibers are spawned and no wall clock is consulted.
2838
3541
  */
2839
3542
  export const memorySubmissionLedgerLayer = (
2840
3543
  options: MemorySubmissionLedgerOptions = {},
2841
- ): Layer.Layer<SubmissionLedger> => Layer.effect(SubmissionLedger, makeSubmissionLedger(options));
2842
-
2843
- export const MemorySubmissionLedgerLive: Layer.Layer<SubmissionLedger> =
2844
- memorySubmissionLedgerLayer();
3544
+ ): Layer.Layer<
3545
+ SubmissionLedger | SettlementPublisher,
3546
+ never,
3547
+ MemoryThreadStoreKernel | Crypto.Crypto
3548
+ > => Layer.effectContext(makeSubmissionLedger(options));
3549
+
3550
+ export const MemorySubmissionLedgerLive: Layer.Layer<
3551
+ SubmissionLedger | SettlementPublisher,
3552
+ never,
3553
+ MemoryThreadStoreKernel | Crypto.Crypto
3554
+ > = memorySubmissionLedgerLayer();