@effect-agent/storage-memory 0.1.0-beta.12 → 0.1.0-beta.121
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 +15 -0
- package/dist/MemoryMessageDeliveryStore.mjs +190 -0
- package/dist/MemoryMessageDeliveryStore.mjs.map +1 -0
- package/dist/MemoryScheduleStore.d.mts +10 -0
- package/dist/MemoryScheduleStore.mjs +169 -0
- package/dist/MemoryScheduleStore.mjs.map +1 -0
- package/dist/MemorySemanticIndex.d.mts +21 -0
- package/dist/MemorySemanticIndex.mjs +222 -0
- package/dist/MemorySemanticIndex.mjs.map +1 -0
- package/dist/MemorySubmissionLedger.d.mts +24 -0
- package/dist/MemorySubmissionLedger.mjs +1154 -0
- package/dist/MemorySubmissionLedger.mjs.map +1 -0
- package/dist/MemorySubscriptionStore.d.mts +9 -0
- package/dist/MemorySubscriptionStore.mjs +849 -0
- package/dist/MemorySubscriptionStore.mjs.map +1 -0
- package/dist/MemoryThreadStore.d.mts +17 -0
- package/dist/MemoryThreadStore.mjs +461 -0
- package/dist/MemoryThreadStore.mjs.map +1 -0
- package/dist/index.d.mts +7 -30
- package/dist/index.mjs +7 -1298
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -45
- package/src/MemoryMessageDeliveryStore.ts +373 -0
- package/src/MemoryScheduleStore.ts +328 -0
- package/src/MemorySemanticIndex.ts +357 -0
- package/src/{memory-ledger.ts → MemorySubmissionLedger.ts} +695 -114
- package/src/MemorySubscriptionStore.ts +1549 -0
- package/src/MemoryThreadStore.ts +926 -0
- package/src/index.ts +6 -2
- package/dist/index.mjs.map +0 -1
- package/dist/testing.d.mts +0 -2
- package/dist/testing.mjs +0 -2
- package/src/memory-storage.ts +0 -614
- package/src/testing.ts +0 -10
|
@@ -1,19 +1,51 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Clock,
|
|
3
|
+
Cause,
|
|
4
|
+
Exit,
|
|
5
|
+
Fiber,
|
|
6
|
+
DateTime,
|
|
7
|
+
Duration,
|
|
8
|
+
Effect,
|
|
9
|
+
Layer,
|
|
10
|
+
Option,
|
|
11
|
+
Ref,
|
|
12
|
+
Schema,
|
|
13
|
+
Stream,
|
|
14
|
+
} from "effect";
|
|
15
|
+
import {
|
|
16
|
+
type ToolCallId,
|
|
2
17
|
AttemptId,
|
|
3
18
|
ReceiptId,
|
|
4
19
|
SubmissionId,
|
|
5
|
-
ToolCallId,
|
|
6
20
|
type AgentId,
|
|
7
|
-
type
|
|
21
|
+
type ThreadId,
|
|
8
22
|
type SettlementId,
|
|
9
|
-
} from "
|
|
23
|
+
} from "effect-agent/identifiers";
|
|
24
|
+
import { InputMessage } from "effect-agent/messaging";
|
|
10
25
|
import {
|
|
26
|
+
PersistedJson,
|
|
27
|
+
WorkerAdmission,
|
|
28
|
+
ProducerEpoch,
|
|
29
|
+
type DefinitionDigests,
|
|
30
|
+
type DeploymentId,
|
|
31
|
+
type Digest,
|
|
32
|
+
type ProducerId,
|
|
33
|
+
type RecordEnvelope,
|
|
34
|
+
type SettlementOutcome,
|
|
35
|
+
} from "effect-agent/records";
|
|
36
|
+
import {
|
|
37
|
+
type ParentLinkage,
|
|
11
38
|
AbortCommand,
|
|
39
|
+
WorkerStopCommand,
|
|
40
|
+
WorkerLedgerState,
|
|
41
|
+
workerTerminalFromRecord,
|
|
12
42
|
AdmissionAdmitted,
|
|
13
43
|
AdmissionConflict,
|
|
44
|
+
AdmissionPolicyError,
|
|
14
45
|
AdmissionIndeterminate,
|
|
15
46
|
AdmissionNotAdmitted,
|
|
16
47
|
AdmissionRequest,
|
|
48
|
+
SubmissionAdmissionFence,
|
|
17
49
|
AdmissionResult,
|
|
18
50
|
ApprovalConflict,
|
|
19
51
|
ApprovalDecisionCommand,
|
|
@@ -43,8 +75,6 @@ import {
|
|
|
43
75
|
OwnershipRenewal,
|
|
44
76
|
OwnershipSnapshot,
|
|
45
77
|
OwnershipToken,
|
|
46
|
-
ParentLinkage,
|
|
47
|
-
ProducerEpoch,
|
|
48
78
|
QueueSequence,
|
|
49
79
|
RecoverySnapshot,
|
|
50
80
|
RecoverySnapshotRequest,
|
|
@@ -59,11 +89,14 @@ import {
|
|
|
59
89
|
SettlementFinalization,
|
|
60
90
|
SettlementReservation,
|
|
61
91
|
SettlementReservationSnapshot,
|
|
92
|
+
settlementFailureFromRecord,
|
|
62
93
|
AbortIntent,
|
|
94
|
+
AbortIntentRequest,
|
|
63
95
|
SubmissionLedger,
|
|
64
96
|
SubmissionLookup,
|
|
65
97
|
SubmissionLookupByKey,
|
|
66
98
|
SubmissionSnapshot,
|
|
99
|
+
SubmissionWorkItem,
|
|
67
100
|
SuspendRequest,
|
|
68
101
|
SuspensionSnapshot,
|
|
69
102
|
UnknownResolution,
|
|
@@ -73,20 +106,12 @@ import {
|
|
|
73
106
|
type ChildReservationId,
|
|
74
107
|
type ChildReservationStatus,
|
|
75
108
|
type ChildSettledOutcome,
|
|
76
|
-
type DefinitionDigests,
|
|
77
|
-
type DeploymentId,
|
|
78
|
-
type Digest,
|
|
79
109
|
type IdempotencyKey,
|
|
80
|
-
type PersistedJson,
|
|
81
110
|
type Principal,
|
|
82
|
-
type ProducerId,
|
|
83
|
-
type RecordEnvelope,
|
|
84
|
-
type SettlementOutcome,
|
|
85
111
|
type SubmissionState,
|
|
86
112
|
type SuspensionOutcome,
|
|
87
113
|
type SuspensionReason,
|
|
88
|
-
} from "
|
|
89
|
-
import { Clock, DateTime, Duration, Effect, Layer, Option, Ref, Schema, Stream } from "effect";
|
|
114
|
+
} from "effect-agent/submission-ledger";
|
|
90
115
|
|
|
91
116
|
const MAX_SUBMISSIONS = 65_536;
|
|
92
117
|
|
|
@@ -110,7 +135,7 @@ const STATE_RANK: Record<SubmissionState, number> = {
|
|
|
110
135
|
|
|
111
136
|
interface SubmissionRow {
|
|
112
137
|
readonly submissionId: SubmissionId;
|
|
113
|
-
readonly
|
|
138
|
+
readonly threadId: ThreadId;
|
|
114
139
|
readonly queueSequence: QueueSequence;
|
|
115
140
|
readonly principal: Principal;
|
|
116
141
|
readonly idempotencyKey: IdempotencyKey;
|
|
@@ -126,6 +151,10 @@ interface SubmissionRow {
|
|
|
126
151
|
readonly readyAtMillis: number | undefined;
|
|
127
152
|
/** Immutable child-side lineage recorded at admission (spec §12 step 5). */
|
|
128
153
|
readonly parentLinkage: ParentLinkage | undefined;
|
|
154
|
+
readonly admissionGroup?: string;
|
|
155
|
+
readonly admissionFence?: AdmissionRequest["admissionFence"];
|
|
156
|
+
readonly workerAdmissionJson?: string;
|
|
157
|
+
readonly messageAdmissionJson?: string;
|
|
129
158
|
}
|
|
130
159
|
|
|
131
160
|
interface StoredOwnership {
|
|
@@ -156,8 +185,6 @@ interface StoredUnknownMark {
|
|
|
156
185
|
|
|
157
186
|
interface StoredUnknownResolution {
|
|
158
187
|
readonly intent: UnknownResolutionIntent;
|
|
159
|
-
/** Canonical JSON of the Schema-encoded resolution, for divergent re-resolution detection. */
|
|
160
|
-
readonly resolutionJson: string;
|
|
161
188
|
}
|
|
162
189
|
|
|
163
190
|
interface StoredSubmission {
|
|
@@ -176,13 +203,13 @@ interface StoredSubmission {
|
|
|
176
203
|
|
|
177
204
|
/**
|
|
178
205
|
* States in which `claim` never grants the head: the lane is host-owned (`joining`/`joined`)
|
|
179
|
-
* or durably
|
|
206
|
+
* or durably suspended rather than worker-claimable. Unknown heads are checked against abort
|
|
207
|
+
* intent separately: abort authorizes cleanup and settlement, never ordinary Tool replay.
|
|
180
208
|
*/
|
|
181
209
|
const BLOCKED_HEAD_STATES: ReadonlySet<SubmissionState> = new Set([
|
|
182
210
|
"joining",
|
|
183
211
|
"joined",
|
|
184
212
|
"suspended",
|
|
185
|
-
"unknown",
|
|
186
213
|
]);
|
|
187
214
|
|
|
188
215
|
interface LaneState {
|
|
@@ -198,12 +225,8 @@ interface StoredChildReservation {
|
|
|
198
225
|
readonly childSubmissionId: SubmissionId | undefined;
|
|
199
226
|
readonly status: ChildReservationStatus;
|
|
200
227
|
readonly allocation: PersistedJson;
|
|
201
|
-
/** Canonical JSON of the allocation, for divergent-replay detection. */
|
|
202
|
-
readonly allocationJson: string;
|
|
203
228
|
readonly allocationDigest: Digest;
|
|
204
229
|
readonly accounting: PersistedJson | undefined;
|
|
205
|
-
/** Canonical JSON of the frozen accounting decision, for divergent-freeze detection. */
|
|
206
|
-
readonly accountingJson: string | undefined;
|
|
207
230
|
readonly reservedAtMillis: number;
|
|
208
231
|
readonly releaseBeganAtMillis: number | undefined;
|
|
209
232
|
readonly releasedAtMillis: number | undefined;
|
|
@@ -212,9 +235,12 @@ interface StoredChildReservation {
|
|
|
212
235
|
interface LedgerState {
|
|
213
236
|
readonly submissions: ReadonlyMap<SubmissionId, StoredSubmission>;
|
|
214
237
|
readonly admissionIndex: ReadonlyMap<string, SubmissionId>;
|
|
215
|
-
readonly lanes: ReadonlyMap<
|
|
238
|
+
readonly lanes: ReadonlyMap<ThreadId, LaneState>;
|
|
216
239
|
readonly childReservations: ReadonlyMap<ChildReservationId, StoredChildReservation>;
|
|
217
240
|
readonly mintCounter: number;
|
|
241
|
+
readonly latestByThread: ReadonlyMap<ThreadId, SubmissionId>;
|
|
242
|
+
readonly activeByThread: ReadonlyMap<ThreadId, ReadonlySet<SubmissionId>>;
|
|
243
|
+
readonly stoppedWorkers: ReadonlyMap<ThreadId, WorkerLedgerState["terminal"]>;
|
|
218
244
|
}
|
|
219
245
|
|
|
220
246
|
type Decision<A, E> =
|
|
@@ -247,19 +273,21 @@ const decodeAttemptId = Schema.decodeSync(AttemptId);
|
|
|
247
273
|
const decodeOwnershipToken = Schema.decodeSync(OwnershipToken);
|
|
248
274
|
const decodeQueueSequence = Schema.decodeSync(QueueSequence);
|
|
249
275
|
const decodeProducerEpoch = Schema.decodeSync(ProducerEpoch);
|
|
276
|
+
const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
|
|
277
|
+
const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
|
|
250
278
|
|
|
251
279
|
const utc = (millis: number): DateTime.Utc => DateTime.toUtc(DateTime.makeUnsafe(millis));
|
|
252
280
|
|
|
253
281
|
const admissionKey = (
|
|
254
|
-
|
|
282
|
+
threadId: ThreadId,
|
|
255
283
|
principal: Principal,
|
|
256
284
|
idempotencyKey: IdempotencyKey,
|
|
257
|
-
): string =>
|
|
285
|
+
): string => JSON.stringify([threadId, principal, idempotencyKey]);
|
|
258
286
|
|
|
259
287
|
const toSnapshot = (row: SubmissionRow): SubmissionSnapshot =>
|
|
260
288
|
SubmissionSnapshot.make({
|
|
261
289
|
submissionId: row.submissionId,
|
|
262
|
-
|
|
290
|
+
threadId: row.threadId,
|
|
263
291
|
queueSequence: row.queueSequence,
|
|
264
292
|
principal: row.principal,
|
|
265
293
|
idempotencyKey: row.idempotencyKey,
|
|
@@ -271,6 +299,22 @@ const toSnapshot = (row: SubmissionRow): SubmissionSnapshot =>
|
|
|
271
299
|
receiptId: row.receiptId,
|
|
272
300
|
state: row.state,
|
|
273
301
|
createdAt: utc(row.createdAtMillis),
|
|
302
|
+
...(row.admissionGroup === undefined ? {} : { admissionGroup: row.admissionGroup }),
|
|
303
|
+
...(row.admissionFence === undefined ? {} : { admissionFence: row.admissionFence }),
|
|
304
|
+
...(row.workerAdmissionJson === undefined
|
|
305
|
+
? {}
|
|
306
|
+
: {
|
|
307
|
+
workerAdmission: Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
|
|
308
|
+
row.workerAdmissionJson,
|
|
309
|
+
),
|
|
310
|
+
}),
|
|
311
|
+
...(row.messageAdmissionJson === undefined
|
|
312
|
+
? {}
|
|
313
|
+
: {
|
|
314
|
+
messageAdmission: Schema.decodeSync(Schema.fromJsonString(InputMessage))(
|
|
315
|
+
row.messageAdmissionJson,
|
|
316
|
+
),
|
|
317
|
+
}),
|
|
274
318
|
...(row.settledOutcome === undefined ? {} : { settledOutcome: row.settledOutcome }),
|
|
275
319
|
...(row.readyAtMillis === undefined ? {} : { readyAt: utc(row.readyAtMillis) }),
|
|
276
320
|
...(row.parentLinkage === undefined ? {} : { parentLinkage: row.parentLinkage }),
|
|
@@ -304,23 +348,37 @@ const sameParentLinkage = (
|
|
|
304
348
|
left.parentSubmissionId === right.parentSubmissionId &&
|
|
305
349
|
left.parentToolCallId === right.parentToolCallId;
|
|
306
350
|
|
|
307
|
-
const laneEpoch = (state: LedgerState,
|
|
308
|
-
state.lanes.get(
|
|
351
|
+
const laneEpoch = (state: LedgerState, threadId: ThreadId): number =>
|
|
352
|
+
state.lanes.get(threadId)?.producerEpoch ?? 0;
|
|
309
353
|
|
|
310
354
|
const ownershipLost = (state: LedgerState, stored: StoredSubmission): OwnershipLost =>
|
|
311
355
|
OwnershipLost.make({
|
|
312
356
|
submissionId: stored.row.submissionId,
|
|
313
|
-
actualEpoch: decodeProducerEpoch(laneEpoch(state, stored.row.
|
|
357
|
+
actualEpoch: decodeProducerEpoch(laneEpoch(state, stored.row.threadId)),
|
|
314
358
|
});
|
|
315
359
|
|
|
316
|
-
/**
|
|
317
|
-
const ownsLane = (
|
|
318
|
-
|
|
360
|
+
/** A retained row token cannot outlive a newer owner of the same Thread. */
|
|
361
|
+
const ownsLane = (
|
|
362
|
+
state: LedgerState,
|
|
363
|
+
stored: StoredSubmission,
|
|
364
|
+
ownershipToken: OwnershipToken,
|
|
365
|
+
): boolean =>
|
|
366
|
+
stored.ownership !== undefined &&
|
|
367
|
+
stored.ownership.ownershipToken === ownershipToken &&
|
|
368
|
+
stored.ownership.producerEpoch === laneEpoch(state, stored.row.threadId);
|
|
369
|
+
|
|
370
|
+
const withSubmission = (state: LedgerState, stored: StoredSubmission): LedgerState => {
|
|
371
|
+
const active = new Set(state.activeByThread.get(stored.row.threadId));
|
|
319
372
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
373
|
+
if (stored.row.state === "settled") active.delete(stored.row.submissionId);
|
|
374
|
+
else active.add(stored.row.submissionId);
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
...state,
|
|
378
|
+
submissions: new Map(state.submissions).set(stored.row.submissionId, stored),
|
|
379
|
+
activeByThread: new Map(state.activeByThread).set(stored.row.threadId, active),
|
|
380
|
+
};
|
|
381
|
+
};
|
|
324
382
|
|
|
325
383
|
const withChildReservation = (
|
|
326
384
|
state: LedgerState,
|
|
@@ -330,21 +388,25 @@ const withChildReservation = (
|
|
|
330
388
|
childReservations: new Map(state.childReservations).set(reservation.reservationId, reservation),
|
|
331
389
|
});
|
|
332
390
|
|
|
333
|
-
const findHead = (
|
|
334
|
-
state: LedgerState,
|
|
335
|
-
conversationId: ConversationId,
|
|
336
|
-
): StoredSubmission | undefined => {
|
|
391
|
+
const findHead = (state: LedgerState, threadId: ThreadId): StoredSubmission | undefined => {
|
|
337
392
|
let head: StoredSubmission | undefined;
|
|
393
|
+
|
|
338
394
|
for (const stored of state.submissions.values()) {
|
|
339
|
-
if (
|
|
395
|
+
if (
|
|
396
|
+
stored.row.threadId !== threadId ||
|
|
397
|
+
stored.row.state === "settled" ||
|
|
398
|
+
(stored.row.state === "unknown" && stored.abortIntent === undefined)
|
|
399
|
+
)
|
|
400
|
+
continue;
|
|
340
401
|
if (head === undefined || stored.row.queueSequence < head.row.queueSequence) head = stored;
|
|
341
402
|
}
|
|
403
|
+
|
|
342
404
|
return head;
|
|
343
405
|
};
|
|
344
406
|
|
|
345
407
|
/**
|
|
346
408
|
* Reference in-memory SubmissionLedger. It implements the full port contract — atomic idempotent
|
|
347
|
-
* admission, FIFO
|
|
409
|
+
* admission, eligible FIFO claims, producer-epoch fencing, Clock-driven ownership leases, idempotent
|
|
348
410
|
* settlement reservation/finalization, and durable abort intent — with every transition applied
|
|
349
411
|
* as one atomic `Ref.modify`, but its state does not survive the process (`non-durable`).
|
|
350
412
|
*
|
|
@@ -354,8 +416,8 @@ const findHead = (
|
|
|
354
416
|
* deterministically; no wall clock is consulted.
|
|
355
417
|
* - The ownership lease is pinned to `DEFAULT_OWNERSHIP_LEASE_DURATION` (D5); durable adapters
|
|
356
418
|
* own the configuration seam.
|
|
357
|
-
* -
|
|
358
|
-
*
|
|
419
|
+
* - Any live lease in the Thread blocks every new claim, including the same `producerId`.
|
|
420
|
+
* Unresolved unknown work is skipped only after ownership is released or expires.
|
|
359
421
|
* - Claiming advances `ready` to `running` and otherwise preserves the recorded state, so
|
|
360
422
|
* progress markers from an earlier Attempt survive a reclaim.
|
|
361
423
|
* - `renewOwnership` keeps the token stable (the port allows rotation); a replayed admission
|
|
@@ -384,7 +446,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
384
446
|
lanes: new Map(),
|
|
385
447
|
childReservations: new Map(),
|
|
386
448
|
mintCounter: 0,
|
|
449
|
+
stoppedWorkers: new Map(),
|
|
450
|
+
latestByThread: new Map(),
|
|
451
|
+
activeByThread: new Map(),
|
|
387
452
|
});
|
|
453
|
+
|
|
454
|
+
const admissionFence = yield* SubmissionAdmissionFence;
|
|
388
455
|
const leaseMillis = Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION);
|
|
389
456
|
|
|
390
457
|
const capabilities = Effect.succeed(LedgerCapabilities.make({ durability: "non-durable" }));
|
|
@@ -393,23 +460,45 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
393
460
|
(unvalidated) =>
|
|
394
461
|
Effect.gen(function* () {
|
|
395
462
|
const request = yield* validate(AdmissionRequest, "admit", unvalidated);
|
|
463
|
+
|
|
464
|
+
const workerAdmissionJson =
|
|
465
|
+
request.workerAdmission === undefined
|
|
466
|
+
? undefined
|
|
467
|
+
: yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
468
|
+
request.workerAdmission,
|
|
469
|
+
).pipe(
|
|
470
|
+
Effect.mapError(() => ledgerError("admit", "Invalid worker admission metadata")),
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const messageAdmissionJson =
|
|
474
|
+
request.messageAdmission === undefined
|
|
475
|
+
? undefined
|
|
476
|
+
: yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
|
|
477
|
+
request.messageAdmission,
|
|
478
|
+
).pipe(
|
|
479
|
+
Effect.mapError(() => ledgerError("admit", "Invalid message admission metadata")),
|
|
480
|
+
);
|
|
481
|
+
|
|
396
482
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
483
|
+
const services = yield* Effect.context<never>();
|
|
484
|
+
|
|
397
485
|
const decision = yield* Ref.modify(
|
|
398
486
|
state,
|
|
399
487
|
(
|
|
400
488
|
current,
|
|
401
489
|
): readonly [
|
|
402
|
-
|
|
490
|
+
(
|
|
491
|
+
| Decision<AdmissionResult, AdmissionConflict | AdmissionPolicyError | LedgerError>
|
|
492
|
+
| { readonly _tag: "cause"; readonly cause: Cause.Cause<AdmissionPolicyError> }
|
|
493
|
+
),
|
|
403
494
|
LedgerState,
|
|
404
495
|
] => {
|
|
405
|
-
const key = admissionKey(
|
|
406
|
-
request.conversationId,
|
|
407
|
-
request.principal,
|
|
408
|
-
request.idempotencyKey,
|
|
409
|
-
);
|
|
496
|
+
const key = admissionKey(request.threadId, request.principal, request.idempotencyKey);
|
|
410
497
|
const existingId = current.admissionIndex.get(key);
|
|
498
|
+
|
|
411
499
|
if (existingId !== undefined) {
|
|
412
500
|
const existing = current.submissions.get(existingId);
|
|
501
|
+
|
|
413
502
|
if (existing === undefined) {
|
|
414
503
|
return [
|
|
415
504
|
failure(
|
|
@@ -422,12 +511,33 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
422
511
|
// (or its absence): linkage is immutable lineage (spec §12 step 5, SUB-016).
|
|
423
512
|
if (
|
|
424
513
|
existing.row.inputDigest !== request.inputDigest ||
|
|
425
|
-
!sameParentLinkage(existing.row.parentLinkage, request.parentLinkage)
|
|
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
|
+
)
|
|
426
536
|
) {
|
|
427
537
|
return [
|
|
428
538
|
failure(
|
|
429
539
|
AdmissionConflict.make({
|
|
430
|
-
|
|
540
|
+
threadId: request.threadId,
|
|
431
541
|
principal: request.principal,
|
|
432
542
|
idempotencyKey: request.idempotencyKey,
|
|
433
543
|
existingInputDigest: existing.row.inputDigest,
|
|
@@ -437,6 +547,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
437
547
|
current,
|
|
438
548
|
];
|
|
439
549
|
}
|
|
550
|
+
|
|
440
551
|
return [
|
|
441
552
|
success(
|
|
442
553
|
AdmissionResult.make({
|
|
@@ -450,6 +561,64 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
450
561
|
current,
|
|
451
562
|
];
|
|
452
563
|
}
|
|
564
|
+
|
|
565
|
+
if (current.stoppedWorkers.has(request.threadId))
|
|
566
|
+
return [
|
|
567
|
+
failure(AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" })),
|
|
568
|
+
current,
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
// A Thread's first admission fixes its worker origin before canonical materialization.
|
|
572
|
+
const first = [...current.submissions.values()].find(
|
|
573
|
+
({ row }) => row.threadId === request.threadId,
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
if (first !== undefined) {
|
|
577
|
+
const previous =
|
|
578
|
+
first.row.workerAdmissionJson === undefined
|
|
579
|
+
? undefined
|
|
580
|
+
: Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
|
|
581
|
+
first.row.workerAdmissionJson,
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
if (
|
|
585
|
+
!Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
|
|
586
|
+
previous?.origin,
|
|
587
|
+
request.workerAdmission?.origin,
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
return [
|
|
591
|
+
failure(
|
|
592
|
+
AdmissionPolicyError.make({
|
|
593
|
+
reason: "refused",
|
|
594
|
+
code: "worker-origin-conflict",
|
|
595
|
+
}),
|
|
596
|
+
),
|
|
597
|
+
current,
|
|
598
|
+
];
|
|
599
|
+
}
|
|
600
|
+
// A memory policy and the ledger mutation share one synchronous critical section.
|
|
601
|
+
// An asynchronous policy cannot fence this Ref and therefore fails closed.
|
|
602
|
+
const checked = Effect.runSyncExitWith(services)(admissionFence.check(request));
|
|
603
|
+
|
|
604
|
+
if (Exit.isFailure(checked)) {
|
|
605
|
+
return [{ _tag: "cause", cause: checked.cause }, current];
|
|
606
|
+
}
|
|
607
|
+
if (
|
|
608
|
+
request.admissionGroup !== undefined &&
|
|
609
|
+
[...current.submissions.values()].some(
|
|
610
|
+
({ row }) =>
|
|
611
|
+
row.threadId === request.threadId &&
|
|
612
|
+
row.admissionGroup === request.admissionGroup &&
|
|
613
|
+
row.state !== "settled",
|
|
614
|
+
)
|
|
615
|
+
)
|
|
616
|
+
return [
|
|
617
|
+
failure(
|
|
618
|
+
AdmissionPolicyError.make({ reason: "occupied", code: "admission-group" }),
|
|
619
|
+
),
|
|
620
|
+
current,
|
|
621
|
+
];
|
|
453
622
|
if (current.submissions.size >= MAX_SUBMISSIONS) {
|
|
454
623
|
return [
|
|
455
624
|
failure(
|
|
@@ -458,14 +627,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
458
627
|
current,
|
|
459
628
|
];
|
|
460
629
|
}
|
|
461
|
-
|
|
630
|
+
|
|
631
|
+
const lane = current.lanes.get(request.threadId) ?? {
|
|
462
632
|
nextQueueSequence: 1,
|
|
463
633
|
producerEpoch: 0,
|
|
464
634
|
};
|
|
635
|
+
|
|
465
636
|
const mintCounter = current.mintCounter + 1;
|
|
637
|
+
|
|
466
638
|
const row: SubmissionRow = {
|
|
467
639
|
submissionId: decodeSubmissionId(`submission-memory-${mintCounter}`),
|
|
468
|
-
|
|
640
|
+
threadId: request.threadId,
|
|
469
641
|
queueSequence: decodeQueueSequence(lane.nextQueueSequence),
|
|
470
642
|
principal: request.principal,
|
|
471
643
|
idempotencyKey: request.idempotencyKey,
|
|
@@ -480,7 +652,16 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
480
652
|
createdAtMillis: nowMillis,
|
|
481
653
|
readyAtMillis: undefined,
|
|
482
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 }),
|
|
483
663
|
};
|
|
664
|
+
|
|
484
665
|
const submissions = new Map(current.submissions).set(row.submissionId, {
|
|
485
666
|
row,
|
|
486
667
|
ownership: undefined,
|
|
@@ -493,11 +674,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
493
674
|
approvalDecisions: new Map<ToolCallId, ApprovalDecisionIntent>(),
|
|
494
675
|
unknownResolutions: new Map<ToolCallId, StoredUnknownResolution>(),
|
|
495
676
|
});
|
|
677
|
+
|
|
496
678
|
const admissionIndex = new Map(current.admissionIndex).set(key, row.submissionId);
|
|
497
|
-
|
|
679
|
+
|
|
680
|
+
const lanes = new Map(current.lanes).set(request.threadId, {
|
|
498
681
|
nextQueueSequence: lane.nextQueueSequence + 1,
|
|
499
682
|
producerEpoch: lane.producerEpoch,
|
|
500
683
|
});
|
|
684
|
+
|
|
501
685
|
return [
|
|
502
686
|
success(
|
|
503
687
|
AdmissionResult.make({
|
|
@@ -508,13 +692,47 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
508
692
|
replayed: false,
|
|
509
693
|
}),
|
|
510
694
|
),
|
|
511
|
-
{
|
|
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
|
+
},
|
|
512
713
|
];
|
|
513
714
|
},
|
|
514
715
|
);
|
|
716
|
+
|
|
515
717
|
if (decision._tag === "failure") return yield* decision.error;
|
|
718
|
+
if (decision._tag === "cause") {
|
|
719
|
+
for (const reason of decision.cause.reasons) {
|
|
720
|
+
if (Cause.isDieReason(reason) && Cause.isAsyncFiberError(reason.defect)) {
|
|
721
|
+
yield* Fiber.interrupt(reason.defect.fiber);
|
|
722
|
+
|
|
723
|
+
return yield* AdmissionPolicyError.make({
|
|
724
|
+
reason: "unavailable",
|
|
725
|
+
code: "synchronous-memory-policy-required",
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return yield* Effect.failCause(decision.cause);
|
|
731
|
+
}
|
|
732
|
+
|
|
516
733
|
return decision.value;
|
|
517
734
|
}),
|
|
735
|
+
Effect.uninterruptible,
|
|
518
736
|
);
|
|
519
737
|
|
|
520
738
|
const markReady: SubmissionLedger["Service"]["markReady"] = Effect.fn(
|
|
@@ -523,10 +741,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
523
741
|
Effect.gen(function* () {
|
|
524
742
|
const request = yield* validate(MarkReadyRequest, "markReady", unvalidated);
|
|
525
743
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
744
|
+
|
|
526
745
|
const decision = yield* Ref.modify(
|
|
527
746
|
state,
|
|
528
747
|
(current): readonly [Decision<void, LedgerError>, LedgerState] => {
|
|
529
748
|
const stored = current.submissions.get(request.submissionId);
|
|
749
|
+
|
|
530
750
|
if (stored === undefined) {
|
|
531
751
|
return [
|
|
532
752
|
failure(ledgerError("markReady", `Unknown Submission ${request.submissionId}`)),
|
|
@@ -534,6 +754,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
534
754
|
];
|
|
535
755
|
}
|
|
536
756
|
if (stored.row.state !== "admitted") return [success(undefined), current];
|
|
757
|
+
|
|
537
758
|
return [
|
|
538
759
|
success(undefined),
|
|
539
760
|
withSubmission(current, {
|
|
@@ -543,6 +764,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
543
764
|
];
|
|
544
765
|
},
|
|
545
766
|
);
|
|
767
|
+
|
|
546
768
|
if (decision._tag === "failure") return yield* decision.error;
|
|
547
769
|
}),
|
|
548
770
|
);
|
|
@@ -553,14 +775,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
553
775
|
Effect.gen(function* () {
|
|
554
776
|
const request = yield* validate(SubmissionLookup, "lookup", unvalidated);
|
|
555
777
|
const current = yield* Ref.get(state);
|
|
778
|
+
|
|
556
779
|
const submissionId =
|
|
557
780
|
request._tag === "SubmissionLookupById"
|
|
558
781
|
? request.submissionId
|
|
559
782
|
: current.admissionIndex.get(
|
|
560
|
-
admissionKey(request.
|
|
783
|
+
admissionKey(request.threadId, request.principal, request.idempotencyKey),
|
|
561
784
|
);
|
|
785
|
+
|
|
562
786
|
const stored =
|
|
563
787
|
submissionId === undefined ? undefined : current.submissions.get(submissionId);
|
|
788
|
+
|
|
564
789
|
return stored === undefined ? Option.none() : Option.some(toSnapshot(stored.row));
|
|
565
790
|
}),
|
|
566
791
|
);
|
|
@@ -570,20 +795,25 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
570
795
|
)((unvalidated) =>
|
|
571
796
|
Effect.gen(function* () {
|
|
572
797
|
const request = yield* validate(SubmissionLookupByKey, "resolveAdmission", unvalidated);
|
|
798
|
+
|
|
573
799
|
// Test-only fault seam: lets suites exercise the Indeterminate classification that a
|
|
574
800
|
// single strongly consistent store never produces on its own (SUB-031, P6 honesty).
|
|
575
801
|
if (options.resolveAdmissionFault !== undefined) {
|
|
576
802
|
const fault = yield* options.resolveAdmissionFault;
|
|
803
|
+
|
|
577
804
|
if (Option.isSome(fault)) {
|
|
578
805
|
return AdmissionIndeterminate.make({ reason: fault.value });
|
|
579
806
|
}
|
|
580
807
|
}
|
|
581
808
|
const current = yield* Ref.get(state);
|
|
809
|
+
|
|
582
810
|
const submissionId = current.admissionIndex.get(
|
|
583
|
-
admissionKey(request.
|
|
811
|
+
admissionKey(request.threadId, request.principal, request.idempotencyKey),
|
|
584
812
|
);
|
|
813
|
+
|
|
585
814
|
const stored =
|
|
586
815
|
submissionId === undefined ? undefined : current.submissions.get(submissionId);
|
|
816
|
+
|
|
587
817
|
return stored === undefined
|
|
588
818
|
? AdmissionNotAdmitted.make()
|
|
589
819
|
: AdmissionAdmitted.make({ submission: toSnapshot(stored.row) });
|
|
@@ -595,30 +825,34 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
595
825
|
Effect.gen(function* () {
|
|
596
826
|
const request = yield* validate(ClaimRequest, "claim", unvalidated);
|
|
597
827
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
828
|
+
|
|
598
829
|
const decision = yield* Ref.modify(
|
|
599
830
|
state,
|
|
600
831
|
(current): readonly [Decision<Option.Option<Claim>, LedgerError>, LedgerState] => {
|
|
601
|
-
const
|
|
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
|
+
const head = findHead(current, request.threadId);
|
|
842
|
+
|
|
602
843
|
if (head === undefined) return [success(Option.none()), current];
|
|
603
|
-
// A joining/joined head is host-owned and a suspended/unknown head is durably
|
|
604
|
-
// blocked; the lane produces no claim and later ready work is never skipped.
|
|
605
844
|
if (BLOCKED_HEAD_STATES.has(head.row.state)) return [success(Option.none()), current];
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
head.ownership.leaseExpiresAtMillis > nowMillis &&
|
|
609
|
-
head.ownership.ownerProducerId !== request.producerId
|
|
610
|
-
) {
|
|
611
|
-
return [success(Option.none()), current];
|
|
612
|
-
}
|
|
613
|
-
const lane = current.lanes.get(request.conversationId);
|
|
845
|
+
const lane = current.lanes.get(request.threadId);
|
|
846
|
+
|
|
614
847
|
if (lane === undefined) {
|
|
615
848
|
return [
|
|
616
|
-
failure(ledgerError("claim", "Claimable head without a
|
|
849
|
+
failure(ledgerError("claim", "Claimable head without a Thread lane")),
|
|
617
850
|
current,
|
|
618
851
|
];
|
|
619
852
|
}
|
|
620
853
|
const producerEpoch = decodeProducerEpoch(lane.producerEpoch + 1);
|
|
621
854
|
const mintCounter = current.mintCounter + 1;
|
|
855
|
+
|
|
622
856
|
const ownership: StoredOwnership = {
|
|
623
857
|
attemptId: decodeAttemptId(`attempt-memory-${mintCounter}`),
|
|
624
858
|
ownershipToken: decodeOwnershipToken(`ownership-memory-${mintCounter}`),
|
|
@@ -626,13 +860,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
626
860
|
ownerProducerId: request.producerId,
|
|
627
861
|
leaseExpiresAtMillis: nowMillis + leaseMillis,
|
|
628
862
|
};
|
|
863
|
+
|
|
629
864
|
const row: SubmissionRow =
|
|
630
865
|
head.row.state === "ready" ? { ...head.row, state: "running" } : head.row;
|
|
866
|
+
|
|
631
867
|
const next = withSubmission(current, { ...head, row, ownership });
|
|
632
|
-
|
|
868
|
+
|
|
869
|
+
const lanes = new Map(next.lanes).set(request.threadId, {
|
|
633
870
|
nextQueueSequence: lane.nextQueueSequence,
|
|
634
871
|
producerEpoch: lane.producerEpoch + 1,
|
|
635
872
|
});
|
|
873
|
+
|
|
636
874
|
return [
|
|
637
875
|
success(
|
|
638
876
|
Option.some(
|
|
@@ -650,7 +888,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
650
888
|
];
|
|
651
889
|
},
|
|
652
890
|
);
|
|
891
|
+
|
|
653
892
|
if (decision._tag === "failure") return yield* decision.error;
|
|
893
|
+
|
|
654
894
|
return decision.value;
|
|
655
895
|
}),
|
|
656
896
|
);
|
|
@@ -661,12 +901,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
661
901
|
Effect.gen(function* () {
|
|
662
902
|
const request = yield* validate(RenewOwnershipRequest, "renewOwnership", unvalidated);
|
|
663
903
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
904
|
+
|
|
664
905
|
const decision = yield* Ref.modify(
|
|
665
906
|
state,
|
|
666
907
|
(
|
|
667
908
|
current,
|
|
668
909
|
): readonly [Decision<OwnershipRenewal, OwnershipLost | LedgerError>, LedgerState] => {
|
|
669
910
|
const stored = current.submissions.get(request.submissionId);
|
|
911
|
+
|
|
670
912
|
if (stored === undefined) {
|
|
671
913
|
return [
|
|
672
914
|
failure(
|
|
@@ -675,13 +917,18 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
675
917
|
current,
|
|
676
918
|
];
|
|
677
919
|
}
|
|
678
|
-
if (
|
|
920
|
+
if (
|
|
921
|
+
stored.ownership === undefined ||
|
|
922
|
+
!ownsLane(current, stored, request.ownershipToken)
|
|
923
|
+
) {
|
|
679
924
|
return [failure(ownershipLost(current, stored)), current];
|
|
680
925
|
}
|
|
926
|
+
|
|
681
927
|
const ownership: StoredOwnership = {
|
|
682
928
|
...stored.ownership,
|
|
683
929
|
leaseExpiresAtMillis: nowMillis + leaseMillis,
|
|
684
930
|
};
|
|
931
|
+
|
|
685
932
|
return [
|
|
686
933
|
success(
|
|
687
934
|
OwnershipRenewal.make({
|
|
@@ -693,7 +940,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
693
940
|
];
|
|
694
941
|
},
|
|
695
942
|
);
|
|
943
|
+
|
|
696
944
|
if (decision._tag === "failure") return yield* decision.error;
|
|
945
|
+
|
|
697
946
|
return decision.value;
|
|
698
947
|
}),
|
|
699
948
|
);
|
|
@@ -703,10 +952,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
703
952
|
)((unvalidated) =>
|
|
704
953
|
Effect.gen(function* () {
|
|
705
954
|
const request = yield* validate(ReleaseOwnershipRequest, "releaseOwnership", unvalidated);
|
|
955
|
+
|
|
706
956
|
const decision = yield* Ref.modify(
|
|
707
957
|
state,
|
|
708
958
|
(current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
|
|
709
959
|
const stored = current.submissions.get(request.submissionId);
|
|
960
|
+
|
|
710
961
|
if (stored === undefined) {
|
|
711
962
|
return [
|
|
712
963
|
failure(
|
|
@@ -715,15 +966,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
715
966
|
current,
|
|
716
967
|
];
|
|
717
968
|
}
|
|
718
|
-
if (!ownsLane(stored, request.ownershipToken)) {
|
|
969
|
+
if (!ownsLane(current, stored, request.ownershipToken)) {
|
|
719
970
|
return [failure(ownershipLost(current, stored)), current];
|
|
720
971
|
}
|
|
972
|
+
|
|
721
973
|
return [
|
|
722
974
|
success(undefined),
|
|
723
975
|
withSubmission(current, { ...stored, ownership: undefined }),
|
|
724
976
|
];
|
|
725
977
|
},
|
|
726
978
|
);
|
|
979
|
+
|
|
727
980
|
if (decision._tag === "failure") return yield* decision.error;
|
|
728
981
|
}),
|
|
729
982
|
);
|
|
@@ -733,10 +986,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
733
986
|
)((unvalidated) =>
|
|
734
987
|
Effect.gen(function* () {
|
|
735
988
|
const request = yield* validate(MarkInputAppliedRequest, "markInputApplied", unvalidated);
|
|
989
|
+
|
|
736
990
|
const decision = yield* Ref.modify(
|
|
737
991
|
state,
|
|
738
992
|
(current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
|
|
739
993
|
const stored = current.submissions.get(request.submissionId);
|
|
994
|
+
|
|
740
995
|
if (stored === undefined) {
|
|
741
996
|
return [
|
|
742
997
|
failure(
|
|
@@ -745,23 +1000,46 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
745
1000
|
current,
|
|
746
1001
|
];
|
|
747
1002
|
}
|
|
748
|
-
if (!ownsLane(stored, request.ownershipToken)) {
|
|
1003
|
+
if (!ownsLane(current, stored, request.ownershipToken)) {
|
|
749
1004
|
return [failure(ownershipLost(current, stored)), current];
|
|
750
1005
|
}
|
|
1006
|
+
|
|
1007
|
+
if (stored.inputApplied !== undefined) {
|
|
1008
|
+
if (
|
|
1009
|
+
stored.inputApplied.recordId === request.recordId &&
|
|
1010
|
+
stored.inputApplied.sequence === request.sequence
|
|
1011
|
+
) {
|
|
1012
|
+
return [success(undefined), current];
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return [
|
|
1016
|
+
failure(
|
|
1017
|
+
ledgerError(
|
|
1018
|
+
"markInputApplied",
|
|
1019
|
+
`A different canonical input marker is already recorded for Submission ${request.submissionId}`,
|
|
1020
|
+
),
|
|
1021
|
+
),
|
|
1022
|
+
current,
|
|
1023
|
+
];
|
|
1024
|
+
}
|
|
1025
|
+
|
|
751
1026
|
const marker = InputAppliedMarker.make({
|
|
752
1027
|
recordId: request.recordId,
|
|
753
1028
|
sequence: request.sequence,
|
|
754
1029
|
});
|
|
1030
|
+
|
|
755
1031
|
const row: SubmissionRow =
|
|
756
1032
|
STATE_RANK[stored.row.state] < STATE_RANK["input-applied"]
|
|
757
1033
|
? { ...stored.row, state: "input-applied" }
|
|
758
1034
|
: stored.row;
|
|
1035
|
+
|
|
759
1036
|
return [
|
|
760
1037
|
success(undefined),
|
|
761
1038
|
withSubmission(current, { ...stored, row, inputApplied: marker }),
|
|
762
1039
|
];
|
|
763
1040
|
},
|
|
764
1041
|
);
|
|
1042
|
+
|
|
765
1043
|
if (decision._tag === "failure") return yield* decision.error;
|
|
766
1044
|
}),
|
|
767
1045
|
);
|
|
@@ -771,6 +1049,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
771
1049
|
)((unvalidated) =>
|
|
772
1050
|
Effect.gen(function* () {
|
|
773
1051
|
const request = yield* validate(SettlementReservation, "reserveSettlement", unvalidated);
|
|
1052
|
+
|
|
774
1053
|
const decision = yield* Ref.modify(
|
|
775
1054
|
state,
|
|
776
1055
|
(
|
|
@@ -780,6 +1059,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
780
1059
|
LedgerState,
|
|
781
1060
|
] => {
|
|
782
1061
|
const stored = current.submissions.get(request.submissionId);
|
|
1062
|
+
|
|
783
1063
|
if (stored === undefined) {
|
|
784
1064
|
return [
|
|
785
1065
|
failure(
|
|
@@ -788,11 +1068,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
788
1068
|
current,
|
|
789
1069
|
];
|
|
790
1070
|
}
|
|
1071
|
+
|
|
791
1072
|
// A `joined` Submission settles WITH its host (plan §2.5) and its lane is never
|
|
792
1073
|
// worker-claimable, so no ownership token can exist for it: the recorded host
|
|
793
1074
|
// linkage authorizes the reservation and the presented token is not consulted.
|
|
794
1075
|
const joinedSettlement =
|
|
795
1076
|
stored.row.state === "joined" && stored.joinedHostSubmissionId !== undefined;
|
|
1077
|
+
|
|
796
1078
|
// P7 §7(c): an aborted, never-claimed, still-queued Submission likewise has no
|
|
797
1079
|
// live ownership to fence against — its durable abort intent authorizes exactly
|
|
798
1080
|
// its ABORTED settlement (`terminalizing` is the same pass's crash replay). Every
|
|
@@ -802,14 +1084,16 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
802
1084
|
stored.abortIntent !== undefined &&
|
|
803
1085
|
stored.ownership === undefined &&
|
|
804
1086
|
(stored.row.state === "ready" || stored.row.state === "terminalizing");
|
|
1087
|
+
|
|
805
1088
|
if (
|
|
806
1089
|
!joinedSettlement &&
|
|
807
1090
|
!queuedAbortSettlement &&
|
|
808
|
-
!ownsLane(stored, request.ownershipToken)
|
|
1091
|
+
!ownsLane(current, stored, request.ownershipToken)
|
|
809
1092
|
) {
|
|
810
1093
|
return [failure(ownershipLost(current, stored)), current];
|
|
811
1094
|
}
|
|
812
1095
|
const existing = stored.reservation;
|
|
1096
|
+
|
|
813
1097
|
if (existing !== undefined) {
|
|
814
1098
|
if (
|
|
815
1099
|
existing.settlementId !== request.settlementId ||
|
|
@@ -826,6 +1110,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
826
1110
|
current,
|
|
827
1111
|
];
|
|
828
1112
|
}
|
|
1113
|
+
|
|
829
1114
|
return [
|
|
830
1115
|
success(
|
|
831
1116
|
ReservedSettlement.make({
|
|
@@ -840,6 +1125,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
840
1125
|
current,
|
|
841
1126
|
];
|
|
842
1127
|
}
|
|
1128
|
+
|
|
843
1129
|
const reservation: StoredReservation = {
|
|
844
1130
|
settlementId: request.settlementId,
|
|
845
1131
|
outcome: request.outcome,
|
|
@@ -847,10 +1133,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
847
1133
|
recordDigest: request.recordDigest,
|
|
848
1134
|
finalizedAtMillis: undefined,
|
|
849
1135
|
};
|
|
1136
|
+
|
|
850
1137
|
const row: SubmissionRow =
|
|
851
1138
|
STATE_RANK[stored.row.state] < STATE_RANK.terminalizing
|
|
852
1139
|
? { ...stored.row, state: "terminalizing" }
|
|
853
1140
|
: stored.row;
|
|
1141
|
+
|
|
854
1142
|
return [
|
|
855
1143
|
success(
|
|
856
1144
|
ReservedSettlement.make({
|
|
@@ -866,7 +1154,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
866
1154
|
];
|
|
867
1155
|
},
|
|
868
1156
|
);
|
|
1157
|
+
|
|
869
1158
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1159
|
+
|
|
870
1160
|
return decision.value;
|
|
871
1161
|
}),
|
|
872
1162
|
);
|
|
@@ -877,12 +1167,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
877
1167
|
Effect.gen(function* () {
|
|
878
1168
|
const request = yield* validate(SettlementFinalization, "finalizeSettlement", unvalidated);
|
|
879
1169
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1170
|
+
|
|
880
1171
|
const decision = yield* Ref.modify(
|
|
881
1172
|
state,
|
|
882
1173
|
(
|
|
883
1174
|
current,
|
|
884
1175
|
): readonly [Decision<Settlement, SettlementConflict | LedgerError>, LedgerState] => {
|
|
885
1176
|
const stored = current.submissions.get(request.submissionId);
|
|
1177
|
+
|
|
886
1178
|
if (stored === undefined) {
|
|
887
1179
|
return [
|
|
888
1180
|
failure(
|
|
@@ -892,6 +1184,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
892
1184
|
];
|
|
893
1185
|
}
|
|
894
1186
|
const reservation = stored.reservation;
|
|
1187
|
+
|
|
895
1188
|
if (reservation === undefined) {
|
|
896
1189
|
return [
|
|
897
1190
|
failure(
|
|
@@ -903,6 +1196,19 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
903
1196
|
current,
|
|
904
1197
|
];
|
|
905
1198
|
}
|
|
1199
|
+
const settlementFailure = settlementFailureFromRecord(reservation.record);
|
|
1200
|
+
|
|
1201
|
+
if ((reservation.outcome === "failed") !== (settlementFailure !== undefined)) {
|
|
1202
|
+
return [
|
|
1203
|
+
failure(
|
|
1204
|
+
ledgerError(
|
|
1205
|
+
"finalizeSettlement",
|
|
1206
|
+
`Settlement reservation for Submission ${request.submissionId} has contradictory failure evidence`,
|
|
1207
|
+
),
|
|
1208
|
+
),
|
|
1209
|
+
current,
|
|
1210
|
+
];
|
|
1211
|
+
}
|
|
906
1212
|
if (reservation.settlementId !== request.settlementId) {
|
|
907
1213
|
return [
|
|
908
1214
|
failure(
|
|
@@ -922,18 +1228,77 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
922
1228
|
settlementId: reservation.settlementId,
|
|
923
1229
|
receiptId: stored.row.receiptId,
|
|
924
1230
|
outcome: reservation.outcome,
|
|
1231
|
+
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
925
1232
|
settledAt: utc(reservation.finalizedAtMillis),
|
|
926
1233
|
}),
|
|
927
1234
|
),
|
|
928
1235
|
current,
|
|
929
1236
|
];
|
|
930
1237
|
}
|
|
931
|
-
|
|
1238
|
+
|
|
1239
|
+
const terminal =
|
|
1240
|
+
stored.row.workerAdmissionJson === undefined
|
|
1241
|
+
? undefined
|
|
1242
|
+
: workerTerminalFromRecord(toSnapshot(stored.row), reservation.record);
|
|
1243
|
+
|
|
1244
|
+
const latestId = current.latestByThread.get(stored.row.threadId);
|
|
1245
|
+
const latest = latestId === undefined ? undefined : current.submissions.get(latestId);
|
|
1246
|
+
|
|
1247
|
+
const pendingNewer =
|
|
1248
|
+
terminal === "completed" &&
|
|
1249
|
+
latest !== undefined &&
|
|
1250
|
+
latest.row.queueSequence > stored.row.queueSequence &&
|
|
1251
|
+
!(
|
|
1252
|
+
latest.joinedHostSubmissionId === stored.row.submissionId &&
|
|
1253
|
+
latest.inputApplied !== undefined
|
|
1254
|
+
);
|
|
1255
|
+
|
|
1256
|
+
let sealed = current;
|
|
1257
|
+
|
|
1258
|
+
if (
|
|
1259
|
+
terminal !== undefined &&
|
|
1260
|
+
!pendingNewer &&
|
|
1261
|
+
!current.stoppedWorkers.has(stored.row.threadId)
|
|
1262
|
+
) {
|
|
1263
|
+
const submissions = new Map(current.submissions);
|
|
1264
|
+
|
|
1265
|
+
for (const id of current.activeByThread.get(stored.row.threadId) ?? []) {
|
|
1266
|
+
const other = submissions.get(id);
|
|
1267
|
+
|
|
1268
|
+
if (other === undefined) continue;
|
|
1269
|
+
if (
|
|
1270
|
+
id !== stored.row.submissionId &&
|
|
1271
|
+
!(
|
|
1272
|
+
other.joinedHostSubmissionId === stored.row.submissionId &&
|
|
1273
|
+
other.inputApplied !== undefined
|
|
1274
|
+
) &&
|
|
1275
|
+
other.abortIntent === undefined
|
|
1276
|
+
) {
|
|
1277
|
+
submissions.set(id, {
|
|
1278
|
+
...other,
|
|
1279
|
+
abortIntent: AbortIntent.make({
|
|
1280
|
+
submissionId: id,
|
|
1281
|
+
author: stored.row.principal,
|
|
1282
|
+
reason: `Worker assignment ${terminal}`,
|
|
1283
|
+
requestedAt: utc(nowMillis),
|
|
1284
|
+
}),
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
sealed = {
|
|
1289
|
+
...current,
|
|
1290
|
+
submissions,
|
|
1291
|
+
stoppedWorkers: new Map(current.stoppedWorkers).set(stored.row.threadId, terminal),
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const next = withSubmission(sealed, {
|
|
932
1296
|
...stored,
|
|
933
1297
|
row: { ...stored.row, state: "settled", settledOutcome: reservation.outcome },
|
|
934
1298
|
ownership: undefined,
|
|
935
1299
|
reservation: { ...reservation, finalizedAtMillis: nowMillis },
|
|
936
1300
|
});
|
|
1301
|
+
|
|
937
1302
|
return [
|
|
938
1303
|
success(
|
|
939
1304
|
Settlement.make({
|
|
@@ -941,6 +1306,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
941
1306
|
settlementId: reservation.settlementId,
|
|
942
1307
|
receiptId: stored.row.receiptId,
|
|
943
1308
|
outcome: reservation.outcome,
|
|
1309
|
+
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
944
1310
|
settledAt: utc(nowMillis),
|
|
945
1311
|
}),
|
|
946
1312
|
),
|
|
@@ -948,17 +1314,85 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
948
1314
|
];
|
|
949
1315
|
},
|
|
950
1316
|
);
|
|
1317
|
+
|
|
951
1318
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1319
|
+
|
|
952
1320
|
return decision.value;
|
|
953
1321
|
}),
|
|
954
1322
|
);
|
|
955
1323
|
|
|
1324
|
+
const inspectWorker = Effect.fn("MemorySubmissionLedger.inspectWorker")(function* (
|
|
1325
|
+
threadId: ThreadId,
|
|
1326
|
+
) {
|
|
1327
|
+
const current = yield* Ref.get(state);
|
|
1328
|
+
const latestId = current.latestByThread.get(threadId);
|
|
1329
|
+
const latest = latestId === undefined ? undefined : current.submissions.get(latestId);
|
|
1330
|
+
|
|
1331
|
+
const active = [...(current.activeByThread.get(threadId) ?? [])]
|
|
1332
|
+
.flatMap((id) => {
|
|
1333
|
+
const row = current.submissions.get(id);
|
|
1334
|
+
|
|
1335
|
+
return row === undefined ? [] : [row];
|
|
1336
|
+
})
|
|
1337
|
+
.sort((a, b) => a.row.queueSequence - b.row.queueSequence)[0];
|
|
1338
|
+
|
|
1339
|
+
const terminal = current.stoppedWorkers.get(threadId);
|
|
1340
|
+
|
|
1341
|
+
return WorkerLedgerState.make({
|
|
1342
|
+
latest: latest === undefined ? null : toSnapshot(latest.row),
|
|
1343
|
+
active: active === undefined ? null : toSnapshot(active.row),
|
|
1344
|
+
stopped: current.stoppedWorkers.has(threadId),
|
|
1345
|
+
...(terminal === undefined ? {} : { terminal }),
|
|
1346
|
+
});
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
const stopWorker = Effect.fn("MemorySubmissionLedger.stopWorker")(function* (
|
|
1350
|
+
unvalidated: WorkerStopCommand,
|
|
1351
|
+
) {
|
|
1352
|
+
const request = yield* validate(WorkerStopCommand, "stopWorker", unvalidated);
|
|
1353
|
+
const now = yield* Clock.currentTimeMillis;
|
|
1354
|
+
|
|
1355
|
+
return yield* Ref.modify(state, (current) => {
|
|
1356
|
+
const submissions = new Map(current.submissions);
|
|
1357
|
+
let owned = 0;
|
|
1358
|
+
|
|
1359
|
+
for (const id of current.activeByThread.get(request.threadId) ?? []) {
|
|
1360
|
+
const stored = submissions.get(id);
|
|
1361
|
+
|
|
1362
|
+
if (stored === undefined) continue;
|
|
1363
|
+
if (stored.ownership !== undefined) owned++;
|
|
1364
|
+
if (stored.abortIntent === undefined)
|
|
1365
|
+
submissions.set(id, {
|
|
1366
|
+
...stored,
|
|
1367
|
+
abortIntent: AbortIntent.make({
|
|
1368
|
+
submissionId: id,
|
|
1369
|
+
author: request.author,
|
|
1370
|
+
reason: "Worker owner stopped the worker",
|
|
1371
|
+
requestedAt: utc(now),
|
|
1372
|
+
}),
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
return [
|
|
1377
|
+
owned,
|
|
1378
|
+
{
|
|
1379
|
+
...current,
|
|
1380
|
+
submissions,
|
|
1381
|
+
stoppedWorkers: current.stoppedWorkers.has(request.threadId)
|
|
1382
|
+
? current.stoppedWorkers
|
|
1383
|
+
: new Map(current.stoppedWorkers).set(request.threadId, undefined),
|
|
1384
|
+
},
|
|
1385
|
+
] as const;
|
|
1386
|
+
});
|
|
1387
|
+
});
|
|
1388
|
+
|
|
956
1389
|
const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fn(
|
|
957
1390
|
"MemorySubmissionLedger.requestAbort",
|
|
958
1391
|
)((unvalidated) =>
|
|
959
1392
|
Effect.gen(function* () {
|
|
960
1393
|
const request = yield* validate(AbortCommand, "requestAbort", unvalidated);
|
|
961
1394
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1395
|
+
|
|
962
1396
|
const decision = yield* Ref.modify(
|
|
963
1397
|
state,
|
|
964
1398
|
(
|
|
@@ -968,6 +1402,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
968
1402
|
LedgerState,
|
|
969
1403
|
] => {
|
|
970
1404
|
const stored = current.submissions.get(request.submissionId);
|
|
1405
|
+
|
|
971
1406
|
if (stored === undefined) {
|
|
972
1407
|
return [
|
|
973
1408
|
failure(ledgerError("requestAbort", `Unknown Submission ${request.submissionId}`)),
|
|
@@ -989,6 +1424,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
989
1424
|
current,
|
|
990
1425
|
];
|
|
991
1426
|
}
|
|
1427
|
+
|
|
992
1428
|
return [
|
|
993
1429
|
failure(
|
|
994
1430
|
JoinedToHost.make({
|
|
@@ -1011,6 +1447,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1011
1447
|
current,
|
|
1012
1448
|
];
|
|
1013
1449
|
}
|
|
1450
|
+
|
|
1014
1451
|
return [
|
|
1015
1452
|
failure(
|
|
1016
1453
|
SettlementConflict.make({
|
|
@@ -1022,16 +1459,20 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1022
1459
|
];
|
|
1023
1460
|
}
|
|
1024
1461
|
if (stored.abortIntent !== undefined) return [success(stored.abortIntent), current];
|
|
1462
|
+
|
|
1025
1463
|
const intent = AbortIntent.make({
|
|
1026
1464
|
submissionId: request.submissionId,
|
|
1027
1465
|
author: request.author,
|
|
1028
1466
|
reason: request.reason,
|
|
1029
1467
|
requestedAt: utc(nowMillis),
|
|
1030
1468
|
});
|
|
1469
|
+
|
|
1031
1470
|
return [success(intent), withSubmission(current, { ...stored, abortIntent: intent })];
|
|
1032
1471
|
},
|
|
1033
1472
|
);
|
|
1473
|
+
|
|
1034
1474
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1475
|
+
|
|
1035
1476
|
return decision.value;
|
|
1036
1477
|
}),
|
|
1037
1478
|
);
|
|
@@ -1041,6 +1482,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1041
1482
|
)((unvalidated) =>
|
|
1042
1483
|
Effect.gen(function* () {
|
|
1043
1484
|
const request = yield* validate(ClaimJoiningRequest, "claimJoining", unvalidated);
|
|
1485
|
+
|
|
1044
1486
|
const decision = yield* Ref.modify(
|
|
1045
1487
|
state,
|
|
1046
1488
|
(
|
|
@@ -1049,7 +1491,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1049
1491
|
Decision<ReadonlyArray<JoiningClaim>, OwnershipLost | LedgerError>,
|
|
1050
1492
|
LedgerState,
|
|
1051
1493
|
] => {
|
|
1494
|
+
if (current.stoppedWorkers.has(request.threadId)) return [success([]), current];
|
|
1495
|
+
|
|
1052
1496
|
const host = current.submissions.get(request.hostSubmissionId);
|
|
1497
|
+
|
|
1053
1498
|
if (host === undefined) {
|
|
1054
1499
|
return [
|
|
1055
1500
|
failure(
|
|
@@ -1058,29 +1503,32 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1058
1503
|
current,
|
|
1059
1504
|
];
|
|
1060
1505
|
}
|
|
1061
|
-
if (host.row.
|
|
1506
|
+
if (host.row.threadId !== request.threadId) {
|
|
1062
1507
|
return [
|
|
1063
1508
|
failure(
|
|
1064
1509
|
ledgerError(
|
|
1065
1510
|
"claimJoining",
|
|
1066
|
-
`Host Submission ${request.hostSubmissionId} does not belong to
|
|
1511
|
+
`Host Submission ${request.hostSubmissionId} does not belong to Thread ${request.threadId}`,
|
|
1067
1512
|
),
|
|
1068
1513
|
),
|
|
1069
1514
|
current,
|
|
1070
1515
|
];
|
|
1071
1516
|
}
|
|
1072
|
-
if (!ownsLane(host, request.ownershipToken)) {
|
|
1517
|
+
if (!ownsLane(current, host, request.ownershipToken)) {
|
|
1073
1518
|
return [failure(ownershipLost(current, host)), current];
|
|
1074
1519
|
}
|
|
1520
|
+
|
|
1075
1521
|
const later = [...current.submissions.values()]
|
|
1076
1522
|
.filter(
|
|
1077
1523
|
(stored) =>
|
|
1078
|
-
stored.row.
|
|
1524
|
+
stored.row.threadId === request.threadId &&
|
|
1079
1525
|
stored.row.queueSequence > host.row.queueSequence,
|
|
1080
1526
|
)
|
|
1081
1527
|
.sort((left, right) => left.row.queueSequence - right.row.queueSequence);
|
|
1528
|
+
|
|
1082
1529
|
const claims: Array<JoiningClaim> = [];
|
|
1083
1530
|
const submissions = new Map(current.submissions);
|
|
1531
|
+
|
|
1084
1532
|
for (const stored of later) {
|
|
1085
1533
|
if (claims.length >= request.maxCount) break;
|
|
1086
1534
|
// Rows already claimed by THIS host extend its contiguous prefix and are skipped;
|
|
@@ -1113,10 +1561,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1113
1561
|
}),
|
|
1114
1562
|
);
|
|
1115
1563
|
}
|
|
1564
|
+
|
|
1116
1565
|
return [success(claims), { ...current, submissions }];
|
|
1117
1566
|
},
|
|
1118
1567
|
);
|
|
1568
|
+
|
|
1119
1569
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1570
|
+
|
|
1120
1571
|
return decision.value;
|
|
1121
1572
|
}),
|
|
1122
1573
|
);
|
|
@@ -1126,10 +1577,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1126
1577
|
)((unvalidated) =>
|
|
1127
1578
|
Effect.gen(function* () {
|
|
1128
1579
|
const request = yield* validate(MarkJoinedRequest, "markJoined", unvalidated);
|
|
1580
|
+
|
|
1129
1581
|
const decision = yield* Ref.modify(
|
|
1130
1582
|
state,
|
|
1131
1583
|
(current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
|
|
1132
1584
|
const stored = current.submissions.get(request.submissionId);
|
|
1585
|
+
|
|
1133
1586
|
if (stored === undefined) {
|
|
1134
1587
|
return [
|
|
1135
1588
|
failure(ledgerError("markJoined", `Unknown Submission ${request.submissionId}`)),
|
|
@@ -1148,6 +1601,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1148
1601
|
];
|
|
1149
1602
|
}
|
|
1150
1603
|
const host = current.submissions.get(stored.joinedHostSubmissionId);
|
|
1604
|
+
|
|
1151
1605
|
if (host === undefined) {
|
|
1152
1606
|
return [
|
|
1153
1607
|
failure(
|
|
@@ -1161,7 +1615,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1161
1615
|
}
|
|
1162
1616
|
// The lane is host-owned: the presented token must own the HOST's ownership period,
|
|
1163
1617
|
// which also lets a later host Attempt repair a lost marker from history (DUR-016).
|
|
1164
|
-
if (!ownsLane(host, request.ownershipToken)) {
|
|
1618
|
+
if (!ownsLane(current, host, request.ownershipToken)) {
|
|
1165
1619
|
return [failure(ownershipLost(current, host)), current];
|
|
1166
1620
|
}
|
|
1167
1621
|
if (stored.inputApplied !== undefined) {
|
|
@@ -1171,6 +1625,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1171
1625
|
) {
|
|
1172
1626
|
return [success(undefined), current];
|
|
1173
1627
|
}
|
|
1628
|
+
|
|
1174
1629
|
return [
|
|
1175
1630
|
failure(
|
|
1176
1631
|
ledgerError(
|
|
@@ -1192,10 +1647,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1192
1647
|
current,
|
|
1193
1648
|
];
|
|
1194
1649
|
}
|
|
1650
|
+
|
|
1195
1651
|
const marker = InputAppliedMarker.make({
|
|
1196
1652
|
recordId: request.recordId,
|
|
1197
1653
|
sequence: request.sequence,
|
|
1198
1654
|
});
|
|
1655
|
+
|
|
1199
1656
|
return [
|
|
1200
1657
|
success(undefined),
|
|
1201
1658
|
withSubmission(current, {
|
|
@@ -1206,6 +1663,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1206
1663
|
];
|
|
1207
1664
|
},
|
|
1208
1665
|
);
|
|
1666
|
+
|
|
1209
1667
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1210
1668
|
}),
|
|
1211
1669
|
);
|
|
@@ -1215,10 +1673,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1215
1673
|
)((unvalidated) =>
|
|
1216
1674
|
Effect.gen(function* () {
|
|
1217
1675
|
const request = yield* validate(RevertJoiningRequest, "revertJoining", unvalidated);
|
|
1676
|
+
|
|
1218
1677
|
const decision = yield* Ref.modify(
|
|
1219
1678
|
state,
|
|
1220
1679
|
(current): readonly [Decision<void, LedgerError>, LedgerState] => {
|
|
1221
1680
|
const stored = current.submissions.get(request.submissionId);
|
|
1681
|
+
|
|
1222
1682
|
if (stored === undefined) {
|
|
1223
1683
|
return [
|
|
1224
1684
|
failure(ledgerError("revertJoining", `Unknown Submission ${request.submissionId}`)),
|
|
@@ -1228,6 +1688,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1228
1688
|
// Idempotent and recovery-only: only a still-`joining` Submission reverts; an
|
|
1229
1689
|
// already-joined (or already-reverted) Submission is a no-op (DUR-016).
|
|
1230
1690
|
if (stored.row.state !== "joining") return [success(undefined), current];
|
|
1691
|
+
|
|
1231
1692
|
return [
|
|
1232
1693
|
success(undefined),
|
|
1233
1694
|
withSubmission(current, {
|
|
@@ -1238,6 +1699,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1238
1699
|
];
|
|
1239
1700
|
},
|
|
1240
1701
|
);
|
|
1702
|
+
|
|
1241
1703
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1242
1704
|
}),
|
|
1243
1705
|
);
|
|
@@ -1248,6 +1710,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1248
1710
|
Effect.gen(function* () {
|
|
1249
1711
|
const request = yield* validate(SuspendRequest, "suspend", unvalidated);
|
|
1250
1712
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1713
|
+
|
|
1251
1714
|
const decision = yield* Ref.modify(
|
|
1252
1715
|
state,
|
|
1253
1716
|
(
|
|
@@ -1257,6 +1720,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1257
1720
|
LedgerState,
|
|
1258
1721
|
] => {
|
|
1259
1722
|
const stored = current.submissions.get(request.submissionId);
|
|
1723
|
+
|
|
1260
1724
|
if (stored === undefined) {
|
|
1261
1725
|
return [
|
|
1262
1726
|
failure(ledgerError("suspend", `Unknown Submission ${request.submissionId}`)),
|
|
@@ -1275,6 +1739,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1275
1739
|
current,
|
|
1276
1740
|
];
|
|
1277
1741
|
}
|
|
1742
|
+
|
|
1278
1743
|
return [
|
|
1279
1744
|
failure(
|
|
1280
1745
|
SettlementConflict.make({
|
|
@@ -1298,9 +1763,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1298
1763
|
current,
|
|
1299
1764
|
];
|
|
1300
1765
|
}
|
|
1301
|
-
if (!ownsLane(stored, request.ownershipToken)) {
|
|
1766
|
+
if (!ownsLane(current, stored, request.ownershipToken)) {
|
|
1302
1767
|
return [failure(ownershipLost(current, stored)), current];
|
|
1303
1768
|
}
|
|
1769
|
+
|
|
1304
1770
|
// A covering event that raced ahead of the suspend transaction (an approval decision,
|
|
1305
1771
|
// or a child settlement observed directly from the child's row in this single store)
|
|
1306
1772
|
// resumes the caller immediately WITHOUT releasing the lane (plan §2.6, spec §12).
|
|
@@ -1313,9 +1779,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1313
1779
|
(child) =>
|
|
1314
1780
|
current.submissions.get(child.childSubmissionId)?.row.state === "settled",
|
|
1315
1781
|
);
|
|
1782
|
+
|
|
1316
1783
|
if (alreadyCovered) {
|
|
1317
1784
|
return [success("resume-immediately" as const), current];
|
|
1318
1785
|
}
|
|
1786
|
+
|
|
1319
1787
|
return [
|
|
1320
1788
|
success("suspended" as const),
|
|
1321
1789
|
withSubmission(current, {
|
|
@@ -1327,7 +1795,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1327
1795
|
];
|
|
1328
1796
|
},
|
|
1329
1797
|
);
|
|
1798
|
+
|
|
1330
1799
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1800
|
+
|
|
1331
1801
|
return decision.value;
|
|
1332
1802
|
}),
|
|
1333
1803
|
);
|
|
@@ -1341,7 +1811,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1341
1811
|
"recordApprovalDecision",
|
|
1342
1812
|
unvalidated,
|
|
1343
1813
|
);
|
|
1814
|
+
|
|
1344
1815
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1816
|
+
|
|
1345
1817
|
const decision = yield* Ref.modify(
|
|
1346
1818
|
state,
|
|
1347
1819
|
(
|
|
@@ -1351,6 +1823,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1351
1823
|
LedgerState,
|
|
1352
1824
|
] => {
|
|
1353
1825
|
const stored = current.submissions.get(command.submissionId);
|
|
1826
|
+
|
|
1354
1827
|
if (stored === undefined) {
|
|
1355
1828
|
return [
|
|
1356
1829
|
failure(
|
|
@@ -1374,6 +1847,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1374
1847
|
current,
|
|
1375
1848
|
];
|
|
1376
1849
|
}
|
|
1850
|
+
|
|
1377
1851
|
return [
|
|
1378
1852
|
failure(
|
|
1379
1853
|
SettlementConflict.make({
|
|
@@ -1385,6 +1859,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1385
1859
|
];
|
|
1386
1860
|
}
|
|
1387
1861
|
const existing = stored.approvalDecisions.get(command.toolCallId);
|
|
1862
|
+
|
|
1388
1863
|
if (existing !== undefined) {
|
|
1389
1864
|
if (existing.decision !== command.decision) {
|
|
1390
1865
|
return [
|
|
@@ -1398,8 +1873,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1398
1873
|
current,
|
|
1399
1874
|
];
|
|
1400
1875
|
}
|
|
1876
|
+
|
|
1401
1877
|
return [success(existing), current];
|
|
1402
1878
|
}
|
|
1879
|
+
|
|
1403
1880
|
const intent = ApprovalDecisionIntent.make({
|
|
1404
1881
|
submissionId: command.submissionId,
|
|
1405
1882
|
toolCallId: command.toolCallId,
|
|
@@ -1408,10 +1885,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1408
1885
|
reason: command.reason,
|
|
1409
1886
|
decidedAt: utc(nowMillis),
|
|
1410
1887
|
});
|
|
1888
|
+
|
|
1411
1889
|
const approvalDecisions = new Map(stored.approvalDecisions).set(
|
|
1412
1890
|
command.toolCallId,
|
|
1413
1891
|
intent,
|
|
1414
1892
|
);
|
|
1893
|
+
|
|
1415
1894
|
// Once every pending call of an ApprovalPending suspension is decided, the lane
|
|
1416
1895
|
// wakes: suspended → input-applied (plan §2.6). A WaitingForChild suspension wakes
|
|
1417
1896
|
// only through recordChildSettled.
|
|
@@ -1422,6 +1901,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1422
1901
|
stored.suspension.reason.toolCallIds.every((toolCallId) =>
|
|
1423
1902
|
approvalDecisions.has(toolCallId),
|
|
1424
1903
|
);
|
|
1904
|
+
|
|
1425
1905
|
return [
|
|
1426
1906
|
success(intent),
|
|
1427
1907
|
withSubmission(current, {
|
|
@@ -1433,7 +1913,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1433
1913
|
];
|
|
1434
1914
|
},
|
|
1435
1915
|
);
|
|
1916
|
+
|
|
1436
1917
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1918
|
+
|
|
1437
1919
|
return decision.value;
|
|
1438
1920
|
}),
|
|
1439
1921
|
);
|
|
@@ -1443,10 +1925,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1443
1925
|
)((unvalidated) =>
|
|
1444
1926
|
Effect.gen(function* () {
|
|
1445
1927
|
const request = yield* validate(MarkUnknownRequest, "markUnknown", unvalidated);
|
|
1928
|
+
|
|
1446
1929
|
const decision = yield* Ref.modify(
|
|
1447
1930
|
state,
|
|
1448
1931
|
(current): readonly [Decision<void, SettlementConflict | LedgerError>, LedgerState] => {
|
|
1449
1932
|
const stored = current.submissions.get(request.submissionId);
|
|
1933
|
+
|
|
1450
1934
|
if (stored === undefined) {
|
|
1451
1935
|
return [
|
|
1452
1936
|
failure(ledgerError("markUnknown", `Unknown Submission ${request.submissionId}`)),
|
|
@@ -1465,6 +1949,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1465
1949
|
current,
|
|
1466
1950
|
];
|
|
1467
1951
|
}
|
|
1952
|
+
|
|
1468
1953
|
return [
|
|
1469
1954
|
failure(
|
|
1470
1955
|
SettlementConflict.make({
|
|
@@ -1492,10 +1977,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1492
1977
|
// set while the first recorded reason is kept.
|
|
1493
1978
|
const existing = stored.unknownMark;
|
|
1494
1979
|
const known = new Set(existing?.toolCallIds ?? []);
|
|
1980
|
+
|
|
1495
1981
|
const merged = [
|
|
1496
1982
|
...(existing?.toolCallIds ?? []),
|
|
1497
1983
|
...request.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
|
|
1498
1984
|
];
|
|
1985
|
+
|
|
1499
1986
|
return [
|
|
1500
1987
|
success(undefined),
|
|
1501
1988
|
withSubmission(current, {
|
|
@@ -1507,6 +1994,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1507
1994
|
];
|
|
1508
1995
|
},
|
|
1509
1996
|
);
|
|
1997
|
+
|
|
1510
1998
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1511
1999
|
}),
|
|
1512
2000
|
);
|
|
@@ -1519,15 +2007,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1519
2007
|
"recordUnknownResolution",
|
|
1520
2008
|
unvalidated,
|
|
1521
2009
|
);
|
|
2010
|
+
|
|
1522
2011
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1523
|
-
|
|
1524
|
-
command.resolution,
|
|
1525
|
-
).pipe(
|
|
1526
|
-
Effect.mapError((error) =>
|
|
1527
|
-
ledgerError("recordUnknownResolution", "Invalid unknown-outcome resolution", error),
|
|
1528
|
-
),
|
|
1529
|
-
);
|
|
1530
|
-
const resolutionJson = JSON.stringify(encodedResolution);
|
|
2012
|
+
|
|
1531
2013
|
const decision = yield* Ref.modify(
|
|
1532
2014
|
state,
|
|
1533
2015
|
(
|
|
@@ -1540,6 +2022,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1540
2022
|
LedgerState,
|
|
1541
2023
|
] => {
|
|
1542
2024
|
const stored = current.submissions.get(command.submissionId);
|
|
2025
|
+
|
|
1543
2026
|
if (stored === undefined) {
|
|
1544
2027
|
return [
|
|
1545
2028
|
failure(
|
|
@@ -1563,6 +2046,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1563
2046
|
current,
|
|
1564
2047
|
];
|
|
1565
2048
|
}
|
|
2049
|
+
|
|
1566
2050
|
return [
|
|
1567
2051
|
failure(
|
|
1568
2052
|
SettlementConflict.make({
|
|
@@ -1574,7 +2058,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1574
2058
|
];
|
|
1575
2059
|
}
|
|
1576
2060
|
const existing = stored.unknownResolutions.get(command.toolCallId);
|
|
1577
|
-
|
|
2061
|
+
|
|
2062
|
+
if (
|
|
2063
|
+
existing !== undefined &&
|
|
2064
|
+
!equivalentUnknownResolution(existing.intent.resolution, command.resolution)
|
|
2065
|
+
) {
|
|
1578
2066
|
return [
|
|
1579
2067
|
failure(
|
|
1580
2068
|
UnknownResolutionConflict.make({
|
|
@@ -1585,6 +2073,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1585
2073
|
current,
|
|
1586
2074
|
];
|
|
1587
2075
|
}
|
|
2076
|
+
|
|
1588
2077
|
const intent =
|
|
1589
2078
|
existing?.intent ??
|
|
1590
2079
|
UnknownResolutionIntent.make({
|
|
@@ -1595,13 +2084,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1595
2084
|
resolution: command.resolution,
|
|
1596
2085
|
resolvedAt: utc(nowMillis),
|
|
1597
2086
|
});
|
|
2087
|
+
|
|
1598
2088
|
const unknownResolutions =
|
|
1599
2089
|
existing !== undefined
|
|
1600
2090
|
? stored.unknownResolutions
|
|
1601
2091
|
: new Map(stored.unknownResolutions).set(command.toolCallId, {
|
|
1602
2092
|
intent,
|
|
1603
|
-
resolutionJson,
|
|
1604
2093
|
});
|
|
2094
|
+
|
|
1605
2095
|
// The lane reopens only when EVERY marked open call has a durable resolution
|
|
1606
2096
|
// intent: unknown → input-applied (DUR-017). Replays re-run the coverage check so
|
|
1607
2097
|
// a recovering caller can wake the lane idempotently.
|
|
@@ -1611,6 +2101,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1611
2101
|
stored.unknownMark.toolCallIds.every((toolCallId) =>
|
|
1612
2102
|
unknownResolutions.has(toolCallId),
|
|
1613
2103
|
);
|
|
2104
|
+
|
|
1614
2105
|
return [
|
|
1615
2106
|
success(intent),
|
|
1616
2107
|
withSubmission(current, {
|
|
@@ -1622,7 +2113,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1622
2113
|
];
|
|
1623
2114
|
},
|
|
1624
2115
|
);
|
|
2116
|
+
|
|
1625
2117
|
if (decision._tag === "failure") return yield* decision.error;
|
|
2118
|
+
|
|
1626
2119
|
return decision.value;
|
|
1627
2120
|
}),
|
|
1628
2121
|
);
|
|
@@ -1636,10 +2129,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1636
2129
|
"recordChildSettled",
|
|
1637
2130
|
unvalidated,
|
|
1638
2131
|
);
|
|
2132
|
+
|
|
1639
2133
|
const decision = yield* Ref.modify(
|
|
1640
2134
|
state,
|
|
1641
2135
|
(current): readonly [Decision<ChildSettledOutcome, LedgerError>, LedgerState] => {
|
|
1642
2136
|
const parent = current.submissions.get(request.parentSubmissionId);
|
|
2137
|
+
|
|
1643
2138
|
if (parent === undefined) {
|
|
1644
2139
|
return [
|
|
1645
2140
|
failure(
|
|
@@ -1651,10 +2146,18 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1651
2146
|
current,
|
|
1652
2147
|
];
|
|
1653
2148
|
}
|
|
1654
|
-
// The
|
|
1655
|
-
//
|
|
2149
|
+
// The caller may notify after the canonical Settlement append but before ledger
|
|
2150
|
+
// finalization. In a single store, an exact reservation plus `terminalizing` is the
|
|
2151
|
+
// narrow durable prefix that makes that ordering admissible; earlier states remain
|
|
2152
|
+
// a caller error.
|
|
1656
2153
|
const child = current.submissions.get(request.childSubmissionId);
|
|
1657
|
-
|
|
2154
|
+
|
|
2155
|
+
const announced =
|
|
2156
|
+
child !== undefined &&
|
|
2157
|
+
(child.row.state === "settled" ||
|
|
2158
|
+
(child.row.state === "terminalizing" && child.reservation !== undefined));
|
|
2159
|
+
|
|
2160
|
+
if (!announced) {
|
|
1658
2161
|
return [
|
|
1659
2162
|
failure(
|
|
1660
2163
|
ledgerError(
|
|
@@ -1673,16 +2176,24 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1673
2176
|
return [success("not-waiting" as const), current];
|
|
1674
2177
|
}
|
|
1675
2178
|
const children = parent.suspension.reason.children;
|
|
2179
|
+
|
|
1676
2180
|
if (!children.some((entry) => entry.childSubmissionId === request.childSubmissionId)) {
|
|
1677
2181
|
return [success("not-waiting" as const), current];
|
|
1678
2182
|
}
|
|
1679
|
-
|
|
1680
|
-
//
|
|
1681
|
-
// idempotently.
|
|
1682
|
-
const allSettled = children.every(
|
|
1683
|
-
|
|
1684
|
-
|
|
2183
|
+
|
|
2184
|
+
// Every listed child must be either finalized or canonically announced from the
|
|
2185
|
+
// exact terminalizing reservation. Replays re-run this coverage check idempotently.
|
|
2186
|
+
const allSettled = children.every((entry) => {
|
|
2187
|
+
const listed = current.submissions.get(entry.childSubmissionId);
|
|
2188
|
+
|
|
2189
|
+
return (
|
|
2190
|
+
listed?.row.state === "settled" ||
|
|
2191
|
+
(listed?.row.state === "terminalizing" && listed.reservation !== undefined)
|
|
2192
|
+
);
|
|
2193
|
+
});
|
|
2194
|
+
|
|
1685
2195
|
if (!allSettled) return [success("still-waiting" as const), current];
|
|
2196
|
+
|
|
1686
2197
|
return [
|
|
1687
2198
|
success("woken" as const),
|
|
1688
2199
|
withSubmission(current, {
|
|
@@ -1693,7 +2204,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1693
2204
|
];
|
|
1694
2205
|
},
|
|
1695
2206
|
);
|
|
2207
|
+
|
|
1696
2208
|
if (decision._tag === "failure") return yield* decision.error;
|
|
2209
|
+
|
|
1697
2210
|
return decision.value;
|
|
1698
2211
|
}),
|
|
1699
2212
|
);
|
|
@@ -1707,8 +2220,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1707
2220
|
"reserveChildBudget",
|
|
1708
2221
|
unvalidated,
|
|
1709
2222
|
);
|
|
2223
|
+
|
|
1710
2224
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1711
|
-
|
|
2225
|
+
|
|
1712
2226
|
const decision = yield* Ref.modify(
|
|
1713
2227
|
state,
|
|
1714
2228
|
(
|
|
@@ -1718,6 +2232,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1718
2232
|
LedgerState,
|
|
1719
2233
|
] => {
|
|
1720
2234
|
const existing = current.childReservations.get(request.reservationId);
|
|
2235
|
+
|
|
1721
2236
|
if (existing !== undefined) {
|
|
1722
2237
|
// Identical replays short-circuit before the fence, mirroring reserveSettlement:
|
|
1723
2238
|
// a replay creates nothing, so a recovering caller resumes rather than duplicates.
|
|
@@ -1725,7 +2240,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1725
2240
|
existing.parentSubmissionId === request.parentSubmissionId &&
|
|
1726
2241
|
existing.parentToolCallId === request.parentToolCallId &&
|
|
1727
2242
|
existing.allocationDigest === request.allocationDigest &&
|
|
1728
|
-
existing.
|
|
2243
|
+
equivalentPersistedJson(existing.allocation, request.allocation);
|
|
2244
|
+
|
|
1729
2245
|
if (!identical) {
|
|
1730
2246
|
return [
|
|
1731
2247
|
failure(
|
|
@@ -1739,6 +2255,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1739
2255
|
current,
|
|
1740
2256
|
];
|
|
1741
2257
|
}
|
|
2258
|
+
|
|
1742
2259
|
return [
|
|
1743
2260
|
success(
|
|
1744
2261
|
ReservedChildBudget.make({
|
|
@@ -1767,6 +2284,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1767
2284
|
}
|
|
1768
2285
|
}
|
|
1769
2286
|
const parent = current.submissions.get(request.parentSubmissionId);
|
|
2287
|
+
|
|
1770
2288
|
if (parent === undefined) {
|
|
1771
2289
|
return [
|
|
1772
2290
|
failure(
|
|
@@ -1780,9 +2298,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1780
2298
|
}
|
|
1781
2299
|
// Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
|
|
1782
2300
|
// parent Attempt can never create new reservation state.
|
|
1783
|
-
if (!ownsLane(parent, request.ownershipToken)) {
|
|
2301
|
+
if (!ownsLane(current, parent, request.ownershipToken)) {
|
|
1784
2302
|
return [failure(ownershipLost(current, parent)), current];
|
|
1785
2303
|
}
|
|
2304
|
+
|
|
1786
2305
|
const reservation: StoredChildReservation = {
|
|
1787
2306
|
reservationId: request.reservationId,
|
|
1788
2307
|
parentSubmissionId: request.parentSubmissionId,
|
|
@@ -1790,14 +2309,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1790
2309
|
childSubmissionId: undefined,
|
|
1791
2310
|
status: "reserved",
|
|
1792
2311
|
allocation: request.allocation,
|
|
1793
|
-
allocationJson,
|
|
1794
2312
|
allocationDigest: request.allocationDigest,
|
|
1795
2313
|
accounting: undefined,
|
|
1796
|
-
accountingJson: undefined,
|
|
1797
2314
|
reservedAtMillis: nowMillis,
|
|
1798
2315
|
releaseBeganAtMillis: undefined,
|
|
1799
2316
|
releasedAtMillis: undefined,
|
|
1800
2317
|
};
|
|
2318
|
+
|
|
1801
2319
|
return [
|
|
1802
2320
|
success(
|
|
1803
2321
|
ReservedChildBudget.make({
|
|
@@ -1809,7 +2327,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1809
2327
|
];
|
|
1810
2328
|
},
|
|
1811
2329
|
);
|
|
2330
|
+
|
|
1812
2331
|
if (decision._tag === "failure") return yield* decision.error;
|
|
2332
|
+
|
|
1813
2333
|
return decision.value;
|
|
1814
2334
|
}),
|
|
1815
2335
|
);
|
|
@@ -1822,6 +2342,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1822
2342
|
"attachChildToReservation",
|
|
1823
2343
|
unvalidated,
|
|
1824
2344
|
);
|
|
2345
|
+
|
|
1825
2346
|
const decision = yield* Ref.modify(
|
|
1826
2347
|
state,
|
|
1827
2348
|
(
|
|
@@ -1834,6 +2355,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1834
2355
|
LedgerState,
|
|
1835
2356
|
] => {
|
|
1836
2357
|
const reservation = current.childReservations.get(request.reservationId);
|
|
2358
|
+
|
|
1837
2359
|
if (reservation === undefined) {
|
|
1838
2360
|
return [
|
|
1839
2361
|
failure(
|
|
@@ -1850,6 +2372,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1850
2372
|
if (reservation.childSubmissionId === request.childSubmissionId) {
|
|
1851
2373
|
return [success(toReservationSnapshot(reservation)), current];
|
|
1852
2374
|
}
|
|
2375
|
+
|
|
1853
2376
|
return [
|
|
1854
2377
|
failure(
|
|
1855
2378
|
ChildReservationConflict.make({
|
|
@@ -1862,6 +2385,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1862
2385
|
];
|
|
1863
2386
|
}
|
|
1864
2387
|
const parent = current.submissions.get(reservation.parentSubmissionId);
|
|
2388
|
+
|
|
1865
2389
|
if (parent === undefined) {
|
|
1866
2390
|
return [
|
|
1867
2391
|
failure(
|
|
@@ -1873,7 +2397,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1873
2397
|
current,
|
|
1874
2398
|
];
|
|
1875
2399
|
}
|
|
1876
|
-
if (!ownsLane(parent, request.ownershipToken)) {
|
|
2400
|
+
if (!ownsLane(current, parent, request.ownershipToken)) {
|
|
1877
2401
|
return [failure(ownershipLost(current, parent)), current];
|
|
1878
2402
|
}
|
|
1879
2403
|
if (reservation.status !== "reserved") {
|
|
@@ -1901,17 +2425,21 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1901
2425
|
current,
|
|
1902
2426
|
];
|
|
1903
2427
|
}
|
|
2428
|
+
|
|
1904
2429
|
const attached: StoredChildReservation = {
|
|
1905
2430
|
...reservation,
|
|
1906
2431
|
childSubmissionId: request.childSubmissionId,
|
|
1907
2432
|
};
|
|
2433
|
+
|
|
1908
2434
|
return [
|
|
1909
2435
|
success(toReservationSnapshot(attached)),
|
|
1910
2436
|
withChildReservation(current, attached),
|
|
1911
2437
|
];
|
|
1912
2438
|
},
|
|
1913
2439
|
);
|
|
2440
|
+
|
|
1914
2441
|
if (decision._tag === "failure") return yield* decision.error;
|
|
2442
|
+
|
|
1915
2443
|
return decision.value;
|
|
1916
2444
|
}),
|
|
1917
2445
|
);
|
|
@@ -1924,8 +2452,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1924
2452
|
"beginChildBudgetRelease",
|
|
1925
2453
|
unvalidated,
|
|
1926
2454
|
);
|
|
2455
|
+
|
|
1927
2456
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1928
|
-
|
|
2457
|
+
|
|
1929
2458
|
const decision = yield* Ref.modify(
|
|
1930
2459
|
state,
|
|
1931
2460
|
(
|
|
@@ -1935,6 +2464,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1935
2464
|
LedgerState,
|
|
1936
2465
|
] => {
|
|
1937
2466
|
const reservation = current.childReservations.get(request.reservationId);
|
|
2467
|
+
|
|
1938
2468
|
if (reservation === undefined) {
|
|
1939
2469
|
return [
|
|
1940
2470
|
failure(
|
|
@@ -1949,9 +2479,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1949
2479
|
if (reservation.status !== "reserved") {
|
|
1950
2480
|
// The accounting decision was already frozen exactly once; an identical replay is
|
|
1951
2481
|
// a no-op and a divergent decision conflicts (spec §12 join step 6).
|
|
1952
|
-
if (
|
|
2482
|
+
if (
|
|
2483
|
+
reservation.accounting !== undefined &&
|
|
2484
|
+
equivalentPersistedJson(reservation.accounting, request.accounting)
|
|
2485
|
+
) {
|
|
1953
2486
|
return [success(toReservationSnapshot(reservation)), current];
|
|
1954
2487
|
}
|
|
2488
|
+
|
|
1955
2489
|
return [
|
|
1956
2490
|
failure(
|
|
1957
2491
|
ChildReservationConflict.make({
|
|
@@ -1964,20 +2498,23 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1964
2498
|
current,
|
|
1965
2499
|
];
|
|
1966
2500
|
}
|
|
2501
|
+
|
|
1967
2502
|
const frozen: StoredChildReservation = {
|
|
1968
2503
|
...reservation,
|
|
1969
2504
|
status: "releasePending",
|
|
1970
2505
|
accounting: request.accounting,
|
|
1971
|
-
accountingJson,
|
|
1972
2506
|
releaseBeganAtMillis: nowMillis,
|
|
1973
2507
|
};
|
|
2508
|
+
|
|
1974
2509
|
return [
|
|
1975
2510
|
success(toReservationSnapshot(frozen)),
|
|
1976
2511
|
withChildReservation(current, frozen),
|
|
1977
2512
|
];
|
|
1978
2513
|
},
|
|
1979
2514
|
);
|
|
2515
|
+
|
|
1980
2516
|
if (decision._tag === "failure") return yield* decision.error;
|
|
2517
|
+
|
|
1981
2518
|
return decision.value;
|
|
1982
2519
|
}),
|
|
1983
2520
|
);
|
|
@@ -1991,7 +2528,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1991
2528
|
"releaseChildBudget",
|
|
1992
2529
|
unvalidated,
|
|
1993
2530
|
);
|
|
2531
|
+
|
|
1994
2532
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
2533
|
+
|
|
1995
2534
|
const decision = yield* Ref.modify(
|
|
1996
2535
|
state,
|
|
1997
2536
|
(
|
|
@@ -2001,6 +2540,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2001
2540
|
LedgerState,
|
|
2002
2541
|
] => {
|
|
2003
2542
|
const reservation = current.childReservations.get(request.reservationId);
|
|
2543
|
+
|
|
2004
2544
|
if (reservation === undefined) {
|
|
2005
2545
|
return [
|
|
2006
2546
|
failure(
|
|
@@ -2030,18 +2570,22 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2030
2570
|
current,
|
|
2031
2571
|
];
|
|
2032
2572
|
}
|
|
2573
|
+
|
|
2033
2574
|
const released: StoredChildReservation = {
|
|
2034
2575
|
...reservation,
|
|
2035
2576
|
status: "released",
|
|
2036
2577
|
releasedAtMillis: nowMillis,
|
|
2037
2578
|
};
|
|
2579
|
+
|
|
2038
2580
|
return [
|
|
2039
2581
|
success(toReservationSnapshot(released)),
|
|
2040
2582
|
withChildReservation(current, released),
|
|
2041
2583
|
];
|
|
2042
2584
|
},
|
|
2043
2585
|
);
|
|
2586
|
+
|
|
2044
2587
|
if (decision._tag === "failure") return yield* decision.error;
|
|
2588
|
+
|
|
2045
2589
|
return decision.value;
|
|
2046
2590
|
}),
|
|
2047
2591
|
);
|
|
@@ -2052,18 +2596,43 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2052
2596
|
const snapshots = [...current.submissions.values()]
|
|
2053
2597
|
.filter((stored) => stored.row.state !== "settled")
|
|
2054
2598
|
.sort((left, right) =>
|
|
2055
|
-
left.row.
|
|
2599
|
+
left.row.threadId < right.row.threadId
|
|
2056
2600
|
? -1
|
|
2057
|
-
: left.row.
|
|
2601
|
+
: left.row.threadId > right.row.threadId
|
|
2058
2602
|
? 1
|
|
2059
2603
|
: left.row.queueSequence - right.row.queueSequence,
|
|
2060
2604
|
)
|
|
2061
|
-
.map((
|
|
2605
|
+
.map(({ row }) =>
|
|
2606
|
+
SubmissionWorkItem.make({
|
|
2607
|
+
submissionId: row.submissionId,
|
|
2608
|
+
threadId: row.threadId,
|
|
2609
|
+
queueSequence: row.queueSequence,
|
|
2610
|
+
principal: row.principal,
|
|
2611
|
+
idempotencyKey: row.idempotencyKey,
|
|
2612
|
+
deploymentId: row.deploymentId,
|
|
2613
|
+
receiptId: row.receiptId,
|
|
2614
|
+
state: row.state,
|
|
2615
|
+
}),
|
|
2616
|
+
);
|
|
2617
|
+
|
|
2062
2618
|
return Stream.fromIterable(snapshots);
|
|
2063
2619
|
}),
|
|
2064
2620
|
),
|
|
2065
2621
|
);
|
|
2066
2622
|
|
|
2623
|
+
const readAbortIntent: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
|
|
2624
|
+
"MemorySubmissionLedger.readAbortIntent",
|
|
2625
|
+
)(function* (unvalidated) {
|
|
2626
|
+
const request = yield* validate(AbortIntentRequest, "readAbortIntent", unvalidated);
|
|
2627
|
+
const stored = (yield* Ref.get(state)).submissions.get(request.submissionId);
|
|
2628
|
+
|
|
2629
|
+
if (stored === undefined) {
|
|
2630
|
+
return yield* ledgerError("readAbortIntent", `Unknown Submission ${request.submissionId}`);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
return stored.abortIntent;
|
|
2634
|
+
});
|
|
2635
|
+
|
|
2067
2636
|
const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
|
|
2068
2637
|
"MemorySubmissionLedger.loadRecoverySnapshot",
|
|
2069
2638
|
)((unvalidated) =>
|
|
@@ -2073,14 +2642,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2073
2642
|
"loadRecoverySnapshot",
|
|
2074
2643
|
unvalidated,
|
|
2075
2644
|
);
|
|
2645
|
+
|
|
2076
2646
|
const current = yield* Ref.get(state);
|
|
2077
2647
|
const stored = current.submissions.get(request.submissionId);
|
|
2648
|
+
|
|
2078
2649
|
if (stored === undefined) {
|
|
2079
2650
|
return yield* ledgerError(
|
|
2080
2651
|
"loadRecoverySnapshot",
|
|
2081
2652
|
`Unknown Submission ${request.submissionId}`,
|
|
2082
2653
|
);
|
|
2083
2654
|
}
|
|
2655
|
+
|
|
2084
2656
|
const joins = [...current.submissions.values()]
|
|
2085
2657
|
.filter((candidate) => candidate.joinedHostSubmissionId === request.submissionId)
|
|
2086
2658
|
.sort((left, right) => left.row.queueSequence - right.row.queueSequence)
|
|
@@ -2091,11 +2663,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2091
2663
|
hostSubmissionId: request.submissionId,
|
|
2092
2664
|
}),
|
|
2093
2665
|
);
|
|
2666
|
+
|
|
2094
2667
|
const byToolCallId = <A extends { readonly toolCallId: ToolCallId }>(
|
|
2095
2668
|
left: A,
|
|
2096
2669
|
right: A,
|
|
2097
2670
|
): number =>
|
|
2098
2671
|
left.toolCallId < right.toolCallId ? -1 : left.toolCallId > right.toolCallId ? 1 : 0;
|
|
2672
|
+
|
|
2099
2673
|
// Parent-side subagent view: this Submission's child budget reservations in parent Tool
|
|
2100
2674
|
// Call order, plus each attached child's current lane state (a disposable derived view;
|
|
2101
2675
|
// the canonical records stay the recovery truth, DUR-015).
|
|
@@ -2108,10 +2682,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2108
2682
|
? 1
|
|
2109
2683
|
: 0,
|
|
2110
2684
|
);
|
|
2685
|
+
|
|
2111
2686
|
const childAttachments: Array<ChildAttachmentSnapshot> = [];
|
|
2687
|
+
|
|
2112
2688
|
for (const reservation of childReservations) {
|
|
2113
2689
|
if (reservation.childSubmissionId === undefined) continue;
|
|
2114
2690
|
const child = current.submissions.get(reservation.childSubmissionId);
|
|
2691
|
+
|
|
2115
2692
|
if (child === undefined) continue;
|
|
2116
2693
|
childAttachments.push(
|
|
2117
2694
|
ChildAttachmentSnapshot.make({
|
|
@@ -2124,6 +2701,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2124
2701
|
}),
|
|
2125
2702
|
);
|
|
2126
2703
|
}
|
|
2704
|
+
|
|
2127
2705
|
return RecoverySnapshot.make({
|
|
2128
2706
|
submission: toSnapshot(stored.row),
|
|
2129
2707
|
joins,
|
|
@@ -2187,6 +2765,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2187
2765
|
reserveSettlement,
|
|
2188
2766
|
finalizeSettlement,
|
|
2189
2767
|
requestAbort,
|
|
2768
|
+
stopWorker,
|
|
2769
|
+
inspectWorker,
|
|
2190
2770
|
claimJoining,
|
|
2191
2771
|
markJoined,
|
|
2192
2772
|
revertJoining,
|
|
@@ -2201,6 +2781,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2201
2781
|
releaseChildBudget,
|
|
2202
2782
|
scanNonterminal,
|
|
2203
2783
|
loadRecoverySnapshot,
|
|
2784
|
+
readAbortIntent,
|
|
2204
2785
|
});
|
|
2205
2786
|
});
|
|
2206
2787
|
|