@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.
- package/dist/MemoryMessageDeliveryStore.d.mts +7 -3
- package/dist/MemoryMessageDeliveryStore.mjs +427 -64
- package/dist/MemoryMessageDeliveryStore.mjs.map +1 -1
- package/dist/MemoryScheduleStore.mjs +8 -8
- package/dist/MemoryScheduleStore.mjs.map +1 -1
- package/dist/MemorySemanticIndex.mjs +7 -9
- package/dist/MemorySemanticIndex.mjs.map +1 -1
- package/dist/MemorySubmissionLedger.d.mts +7 -4
- package/dist/MemorySubmissionLedger.mjs +761 -377
- package/dist/MemorySubmissionLedger.mjs.map +1 -1
- package/dist/MemorySubscriptionStore.mjs +23 -23
- package/dist/MemorySubscriptionStore.mjs.map +1 -1
- package/dist/MemoryThreadStore.d.mts +8 -4
- package/dist/MemoryThreadStore.mjs +1325 -277
- package/dist/MemoryThreadStore.mjs.map +1 -1
- package/dist/MemoryThreadStoreKernel-DYNDjlhO.d.mts +85 -0
- package/dist/WorkSelection-CmqTuws3.mjs +31 -0
- package/dist/WorkSelection-CmqTuws3.mjs.map +1 -0
- package/package.json +1 -1
- package/src/MemoryMessageDeliveryStore.ts +766 -230
- package/src/MemoryScheduleStore.ts +190 -200
- package/src/MemorySemanticIndex.ts +94 -95
- package/src/MemorySubmissionLedger.ts +1955 -1245
- package/src/MemorySubscriptionStore.ts +1122 -1117
- package/src/MemoryThreadStore.ts +2609 -628
- package/src/internal/MemoryThreadStoreKernel.ts +156 -0
- package/src/internal/WorkSelection.ts +29 -0
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
171
|
-
readonly
|
|
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
|
|
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<
|
|
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
|
|
237
|
-
readonly
|
|
238
|
-
readonly
|
|
239
|
-
readonly
|
|
240
|
-
readonly
|
|
241
|
-
readonly
|
|
242
|
-
readonly
|
|
243
|
-
readonly
|
|
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 =
|
|
259
|
-
<A, I
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
264
|
-
Schema.
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
|
352
|
-
|
|
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
|
|
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 (
|
|
374
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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
|
|
632
|
+
for (const id of state.activeByThread.get(threadId) ?? []) {
|
|
633
|
+
const stored = state.submissions.get(id);
|
|
634
|
+
|
|
395
635
|
if (
|
|
396
|
-
stored
|
|
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.
|
|
409
|
-
*
|
|
410
|
-
*
|
|
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
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
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
|
-
|
|
696
|
+
freshAdmissionSequence: 0,
|
|
449
697
|
stoppedWorkers: new Map(),
|
|
450
698
|
latestByThread: new Map(),
|
|
451
699
|
activeByThread: new Map(),
|
|
452
700
|
});
|
|
453
701
|
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
712
|
+
for (const [threadId, ids] of current.activeByThread)
|
|
713
|
+
if (ids.size > 0) candidates.add(threadId);
|
|
714
|
+
const threadIds = candidates.values;
|
|
458
715
|
|
|
459
|
-
|
|
460
|
-
|
|
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
|
|
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
|
|
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
|
|
483
|
-
const
|
|
730
|
+
for (const id of current.activeByThread.get(threadId) ?? []) candidates.add(id);
|
|
731
|
+
const ids = candidates.values;
|
|
484
732
|
|
|
485
|
-
const
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
);
|
|
763
|
+
return { entries, ...(next === undefined ? {} : { after: next }) };
|
|
764
|
+
}),
|
|
765
|
+
});
|
|
575
766
|
|
|
576
|
-
|
|
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
|
-
|
|
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
|
-
|
|
605
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
820
|
+
if (existingId !== undefined) {
|
|
821
|
+
const existing = current.submissions.get(existingId);
|
|
679
822
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
718
|
-
if (
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
Effect.uninterruptible,
|
|
736
|
-
);
|
|
1043
|
+
return yield* Effect.failCause(decision.cause);
|
|
1044
|
+
}
|
|
737
1045
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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.
|
|
773
|
-
|
|
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.
|
|
794
|
-
|
|
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
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
844
|
-
|
|
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
|
-
|
|
847
|
-
|
|
1141
|
+
if (request.handoff !== undefined) {
|
|
1142
|
+
const handoff = request.handoff;
|
|
848
1143
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
|
|
880
|
-
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
-
|
|
901
|
-
head.row.state === "ready" ? { ...head.row, state: "running" } : head.row;
|
|
1192
|
+
if (decision._tag === "failure") return yield* decision.error;
|
|
902
1193
|
|
|
903
|
-
|
|
1194
|
+
return decision.value;
|
|
1195
|
+
}),
|
|
1196
|
+
);
|
|
904
1197
|
|
|
905
|
-
|
|
906
|
-
|
|
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 (!
|
|
1040
|
-
return [failure(ownershipLost(
|
|
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
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
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
|
-
|
|
1100
|
-
|
|
1101
|
-
failure(
|
|
1102
|
-
ledgerError("reserveSettlement", `Unknown Submission ${request.submissionId}`),
|
|
1103
|
-
),
|
|
1104
|
-
current,
|
|
1105
|
-
];
|
|
1106
|
-
}
|
|
1268
|
+
return { record, settlement };
|
|
1269
|
+
});
|
|
1107
1270
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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:
|
|
1408
|
+
existingOutcome: settlement.outcome,
|
|
1254
1409
|
}),
|
|
1255
1410
|
),
|
|
1256
1411
|
current,
|
|
1257
1412
|
];
|
|
1258
1413
|
}
|
|
1259
|
-
if (
|
|
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:
|
|
1435
|
+
settlementId: settlement.settlementId,
|
|
1265
1436
|
receiptId: stored.row.receiptId,
|
|
1266
|
-
outcome:
|
|
1437
|
+
outcome: settlement.outcome,
|
|
1267
1438
|
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
1268
|
-
settledAt: utc(
|
|
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),
|
|
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 =
|
|
1475
|
+
const submissions = current.submissions;
|
|
1300
1476
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1477
|
+
if (!transferPageFits(stored.row.threadId))
|
|
1478
|
+
return [failure(transferCapacityError("finalizeSettlement")), current];
|
|
1303
1479
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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:
|
|
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:
|
|
1334
|
-
|
|
1335
|
-
|
|
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:
|
|
1532
|
+
settlementId: settlement.settlementId,
|
|
1343
1533
|
receiptId: stored.row.receiptId,
|
|
1344
|
-
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.
|
|
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.
|
|
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
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1426
|
-
|
|
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.
|
|
1517
|
-
|
|
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 (!
|
|
1554
|
-
return [failure(ownershipLost(
|
|
1770
|
+
if (!journal.owner.hasOwner(host.row.threadId, host.row.submissionId)) {
|
|
1771
|
+
return [failure(ownershipLost(host)), current];
|
|
1555
1772
|
}
|
|
1556
1773
|
|
|
1557
|
-
const later =
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
1612
|
-
|
|
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 (!
|
|
1655
|
-
return [failure(ownershipLost(
|
|
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.
|
|
1708
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
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
|
-
//
|
|
1790
|
-
|
|
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:
|
|
2049
|
+
existingOutcome: canonical.settlement.outcome,
|
|
1797
2050
|
}),
|
|
1798
2051
|
),
|
|
1799
2052
|
current,
|
|
1800
2053
|
];
|
|
1801
2054
|
}
|
|
1802
|
-
if (!
|
|
1803
|
-
return [failure(ownershipLost(
|
|
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
|
-
?
|
|
1812
|
-
|
|
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"] =
|
|
1842
|
-
|
|
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
|
-
//
|
|
1931
|
-
//
|
|
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
|
-
|
|
1938
|
-
|
|
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.
|
|
1960
|
-
|
|
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
|
-
//
|
|
2000
|
-
|
|
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:
|
|
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.
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
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
|
|
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
|
-
|
|
2050
|
-
|
|
2051
|
-
(
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
2395
|
+
}
|
|
2396
|
+
const key = JSON.stringify([command.toolCallId, kind]);
|
|
2397
|
+
const existing = stored.unknownResolutions.get(key);
|
|
2097
2398
|
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
!equivalentUnknownResolution(existing.intent.resolution, command.resolution)
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
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
|
-
|
|
2125
|
-
|
|
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
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
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
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2156
|
-
}),
|
|
2157
|
-
);
|
|
2462
|
+
if (decision._tag === "failure") return yield* decision.error;
|
|
2158
2463
|
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
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
|
-
//
|
|
2186
|
-
|
|
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
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
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.
|
|
2251
|
-
|
|
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,
|
|
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
|
|
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 (!
|
|
2338
|
-
return [failure(ownershipLost(
|
|
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.
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
);
|
|
2685
|
+
Effect.fnUntraced(function* (unvalidated) {
|
|
2686
|
+
const request = yield* validate(
|
|
2687
|
+
AttachChildToReservationRequest,
|
|
2688
|
+
"attachChildToReservation",
|
|
2689
|
+
unvalidated,
|
|
2690
|
+
);
|
|
2381
2691
|
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
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
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
}),
|
|
2705
|
+
if (reservation === undefined) {
|
|
2706
|
+
return [
|
|
2707
|
+
failure(
|
|
2708
|
+
ledgerError(
|
|
2709
|
+
"attachChildToReservation",
|
|
2710
|
+
`Unknown child reservation ${request.reservationId}`,
|
|
2419
2711
|
),
|
|
2420
|
-
|
|
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
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
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
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
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
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2770
|
+
),
|
|
2771
|
+
current,
|
|
2772
|
+
];
|
|
2773
|
+
}
|
|
2464
2774
|
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2775
|
+
const attached: StoredChildReservation = {
|
|
2776
|
+
...reservation,
|
|
2777
|
+
childSubmissionId: request.childSubmissionId,
|
|
2778
|
+
};
|
|
2469
2779
|
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2780
|
+
return [
|
|
2781
|
+
success(toReservationSnapshot(attached)),
|
|
2782
|
+
withChildReservation(current, attached),
|
|
2783
|
+
];
|
|
2784
|
+
},
|
|
2785
|
+
);
|
|
2476
2786
|
|
|
2477
|
-
|
|
2787
|
+
if (decision._tag === "failure") return yield* decision.error;
|
|
2478
2788
|
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
);
|
|
2789
|
+
return decision.value;
|
|
2790
|
+
});
|
|
2482
2791
|
|
|
2483
2792
|
const beginChildBudgetRelease: SubmissionLedger["Service"]["beginChildBudgetRelease"] =
|
|
2484
|
-
Effect.
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
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
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2547
|
-
|
|
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
|
-
|
|
2846
|
+
const frozen: StoredChildReservation = {
|
|
2847
|
+
...reservation,
|
|
2848
|
+
status: "releasePending",
|
|
2849
|
+
accounting: request.accounting,
|
|
2850
|
+
releaseBeganAtMillis: nowMillis,
|
|
2851
|
+
};
|
|
2553
2852
|
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2853
|
+
return [success(toReservationSnapshot(frozen)), withChildReservation(current, frozen)];
|
|
2854
|
+
},
|
|
2855
|
+
);
|
|
2557
2856
|
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
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.
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
)
|
|
2641
|
-
|
|
2642
|
-
|
|
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
|
-
|
|
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.
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
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
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2969
|
+
if (stored === undefined) {
|
|
2970
|
+
return yield* ledgerError(
|
|
2971
|
+
"readAbortIntent",
|
|
2972
|
+
`Unknown Submission ${request.submissionId}`,
|
|
2973
|
+
);
|
|
2974
|
+
}
|
|
2668
2975
|
|
|
2669
|
-
|
|
2670
|
-
|
|
2976
|
+
return stored.abortIntent;
|
|
2977
|
+
},
|
|
2978
|
+
);
|
|
2671
2979
|
|
|
2672
|
-
const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] =
|
|
2673
|
-
|
|
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 =
|
|
2693
|
-
.
|
|
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 =
|
|
2713
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
3081
|
+
const ledger = SubmissionLedger.of({
|
|
2792
3082
|
capabilities,
|
|
2793
|
-
|
|
2794
|
-
|
|
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
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
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
|
-
|
|
2817
|
-
|
|
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
|
|
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<
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
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();
|