@effect-agent/storage-memory 0.1.0-beta.11 → 0.1.0-beta.110
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 +167 -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 +1058 -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 +457 -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 +327 -0
- package/src/MemoryScheduleStore.ts +328 -0
- package/src/MemorySemanticIndex.ts +357 -0
- package/src/{memory-ledger.ts → MemorySubmissionLedger.ts} +511 -107
- package/src/MemorySubscriptionStore.ts +1549 -0
- package/src/MemoryThreadStore.ts +916 -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,48 @@
|
|
|
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";
|
|
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";
|
|
10
36
|
import {
|
|
37
|
+
type ParentLinkage,
|
|
11
38
|
AbortCommand,
|
|
12
39
|
AdmissionAdmitted,
|
|
13
40
|
AdmissionConflict,
|
|
41
|
+
AdmissionPolicyError,
|
|
14
42
|
AdmissionIndeterminate,
|
|
15
43
|
AdmissionNotAdmitted,
|
|
16
44
|
AdmissionRequest,
|
|
45
|
+
SubmissionAdmissionFence,
|
|
17
46
|
AdmissionResult,
|
|
18
47
|
ApprovalConflict,
|
|
19
48
|
ApprovalDecisionCommand,
|
|
@@ -43,8 +72,6 @@ import {
|
|
|
43
72
|
OwnershipRenewal,
|
|
44
73
|
OwnershipSnapshot,
|
|
45
74
|
OwnershipToken,
|
|
46
|
-
ParentLinkage,
|
|
47
|
-
ProducerEpoch,
|
|
48
75
|
QueueSequence,
|
|
49
76
|
RecoverySnapshot,
|
|
50
77
|
RecoverySnapshotRequest,
|
|
@@ -59,7 +86,9 @@ import {
|
|
|
59
86
|
SettlementFinalization,
|
|
60
87
|
SettlementReservation,
|
|
61
88
|
SettlementReservationSnapshot,
|
|
89
|
+
settlementFailureFromRecord,
|
|
62
90
|
AbortIntent,
|
|
91
|
+
AbortIntentRequest,
|
|
63
92
|
SubmissionLedger,
|
|
64
93
|
SubmissionLookup,
|
|
65
94
|
SubmissionLookupByKey,
|
|
@@ -73,20 +102,12 @@ import {
|
|
|
73
102
|
type ChildReservationId,
|
|
74
103
|
type ChildReservationStatus,
|
|
75
104
|
type ChildSettledOutcome,
|
|
76
|
-
type DefinitionDigests,
|
|
77
|
-
type DeploymentId,
|
|
78
|
-
type Digest,
|
|
79
105
|
type IdempotencyKey,
|
|
80
|
-
type PersistedJson,
|
|
81
106
|
type Principal,
|
|
82
|
-
type ProducerId,
|
|
83
|
-
type RecordEnvelope,
|
|
84
|
-
type SettlementOutcome,
|
|
85
107
|
type SubmissionState,
|
|
86
108
|
type SuspensionOutcome,
|
|
87
109
|
type SuspensionReason,
|
|
88
|
-
} from "
|
|
89
|
-
import { Clock, DateTime, Duration, Effect, Layer, Option, Ref, Schema, Stream } from "effect";
|
|
110
|
+
} from "effect-agent/submission-ledger";
|
|
90
111
|
|
|
91
112
|
const MAX_SUBMISSIONS = 65_536;
|
|
92
113
|
|
|
@@ -110,7 +131,7 @@ const STATE_RANK: Record<SubmissionState, number> = {
|
|
|
110
131
|
|
|
111
132
|
interface SubmissionRow {
|
|
112
133
|
readonly submissionId: SubmissionId;
|
|
113
|
-
readonly
|
|
134
|
+
readonly threadId: ThreadId;
|
|
114
135
|
readonly queueSequence: QueueSequence;
|
|
115
136
|
readonly principal: Principal;
|
|
116
137
|
readonly idempotencyKey: IdempotencyKey;
|
|
@@ -126,6 +147,10 @@ interface SubmissionRow {
|
|
|
126
147
|
readonly readyAtMillis: number | undefined;
|
|
127
148
|
/** Immutable child-side lineage recorded at admission (spec §12 step 5). */
|
|
128
149
|
readonly parentLinkage: ParentLinkage | undefined;
|
|
150
|
+
readonly admissionGroup?: string;
|
|
151
|
+
readonly admissionFence?: AdmissionRequest["admissionFence"];
|
|
152
|
+
readonly workerAdmissionJson?: string;
|
|
153
|
+
readonly messageAdmissionJson?: string;
|
|
129
154
|
}
|
|
130
155
|
|
|
131
156
|
interface StoredOwnership {
|
|
@@ -156,8 +181,6 @@ interface StoredUnknownMark {
|
|
|
156
181
|
|
|
157
182
|
interface StoredUnknownResolution {
|
|
158
183
|
readonly intent: UnknownResolutionIntent;
|
|
159
|
-
/** Canonical JSON of the Schema-encoded resolution, for divergent re-resolution detection. */
|
|
160
|
-
readonly resolutionJson: string;
|
|
161
184
|
}
|
|
162
185
|
|
|
163
186
|
interface StoredSubmission {
|
|
@@ -176,13 +199,13 @@ interface StoredSubmission {
|
|
|
176
199
|
|
|
177
200
|
/**
|
|
178
201
|
* States in which `claim` never grants the head: the lane is host-owned (`joining`/`joined`)
|
|
179
|
-
* or durably
|
|
202
|
+
* or durably suspended rather than worker-claimable. Unknown heads are checked against abort
|
|
203
|
+
* intent separately: abort authorizes cleanup and settlement, never ordinary Tool replay.
|
|
180
204
|
*/
|
|
181
205
|
const BLOCKED_HEAD_STATES: ReadonlySet<SubmissionState> = new Set([
|
|
182
206
|
"joining",
|
|
183
207
|
"joined",
|
|
184
208
|
"suspended",
|
|
185
|
-
"unknown",
|
|
186
209
|
]);
|
|
187
210
|
|
|
188
211
|
interface LaneState {
|
|
@@ -198,12 +221,8 @@ interface StoredChildReservation {
|
|
|
198
221
|
readonly childSubmissionId: SubmissionId | undefined;
|
|
199
222
|
readonly status: ChildReservationStatus;
|
|
200
223
|
readonly allocation: PersistedJson;
|
|
201
|
-
/** Canonical JSON of the allocation, for divergent-replay detection. */
|
|
202
|
-
readonly allocationJson: string;
|
|
203
224
|
readonly allocationDigest: Digest;
|
|
204
225
|
readonly accounting: PersistedJson | undefined;
|
|
205
|
-
/** Canonical JSON of the frozen accounting decision, for divergent-freeze detection. */
|
|
206
|
-
readonly accountingJson: string | undefined;
|
|
207
226
|
readonly reservedAtMillis: number;
|
|
208
227
|
readonly releaseBeganAtMillis: number | undefined;
|
|
209
228
|
readonly releasedAtMillis: number | undefined;
|
|
@@ -212,7 +231,7 @@ interface StoredChildReservation {
|
|
|
212
231
|
interface LedgerState {
|
|
213
232
|
readonly submissions: ReadonlyMap<SubmissionId, StoredSubmission>;
|
|
214
233
|
readonly admissionIndex: ReadonlyMap<string, SubmissionId>;
|
|
215
|
-
readonly lanes: ReadonlyMap<
|
|
234
|
+
readonly lanes: ReadonlyMap<ThreadId, LaneState>;
|
|
216
235
|
readonly childReservations: ReadonlyMap<ChildReservationId, StoredChildReservation>;
|
|
217
236
|
readonly mintCounter: number;
|
|
218
237
|
}
|
|
@@ -247,19 +266,21 @@ const decodeAttemptId = Schema.decodeSync(AttemptId);
|
|
|
247
266
|
const decodeOwnershipToken = Schema.decodeSync(OwnershipToken);
|
|
248
267
|
const decodeQueueSequence = Schema.decodeSync(QueueSequence);
|
|
249
268
|
const decodeProducerEpoch = Schema.decodeSync(ProducerEpoch);
|
|
269
|
+
const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
|
|
270
|
+
const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
|
|
250
271
|
|
|
251
272
|
const utc = (millis: number): DateTime.Utc => DateTime.toUtc(DateTime.makeUnsafe(millis));
|
|
252
273
|
|
|
253
274
|
const admissionKey = (
|
|
254
|
-
|
|
275
|
+
threadId: ThreadId,
|
|
255
276
|
principal: Principal,
|
|
256
277
|
idempotencyKey: IdempotencyKey,
|
|
257
|
-
): string =>
|
|
278
|
+
): string => JSON.stringify([threadId, principal, idempotencyKey]);
|
|
258
279
|
|
|
259
280
|
const toSnapshot = (row: SubmissionRow): SubmissionSnapshot =>
|
|
260
281
|
SubmissionSnapshot.make({
|
|
261
282
|
submissionId: row.submissionId,
|
|
262
|
-
|
|
283
|
+
threadId: row.threadId,
|
|
263
284
|
queueSequence: row.queueSequence,
|
|
264
285
|
principal: row.principal,
|
|
265
286
|
idempotencyKey: row.idempotencyKey,
|
|
@@ -271,6 +292,22 @@ const toSnapshot = (row: SubmissionRow): SubmissionSnapshot =>
|
|
|
271
292
|
receiptId: row.receiptId,
|
|
272
293
|
state: row.state,
|
|
273
294
|
createdAt: utc(row.createdAtMillis),
|
|
295
|
+
...(row.admissionGroup === undefined ? {} : { admissionGroup: row.admissionGroup }),
|
|
296
|
+
...(row.admissionFence === undefined ? {} : { admissionFence: row.admissionFence }),
|
|
297
|
+
...(row.workerAdmissionJson === undefined
|
|
298
|
+
? {}
|
|
299
|
+
: {
|
|
300
|
+
workerAdmission: Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
|
|
301
|
+
row.workerAdmissionJson,
|
|
302
|
+
),
|
|
303
|
+
}),
|
|
304
|
+
...(row.messageAdmissionJson === undefined
|
|
305
|
+
? {}
|
|
306
|
+
: {
|
|
307
|
+
messageAdmission: Schema.decodeSync(Schema.fromJsonString(InputMessage))(
|
|
308
|
+
row.messageAdmissionJson,
|
|
309
|
+
),
|
|
310
|
+
}),
|
|
274
311
|
...(row.settledOutcome === undefined ? {} : { settledOutcome: row.settledOutcome }),
|
|
275
312
|
...(row.readyAtMillis === undefined ? {} : { readyAt: utc(row.readyAtMillis) }),
|
|
276
313
|
...(row.parentLinkage === undefined ? {} : { parentLinkage: row.parentLinkage }),
|
|
@@ -304,18 +341,24 @@ const sameParentLinkage = (
|
|
|
304
341
|
left.parentSubmissionId === right.parentSubmissionId &&
|
|
305
342
|
left.parentToolCallId === right.parentToolCallId;
|
|
306
343
|
|
|
307
|
-
const laneEpoch = (state: LedgerState,
|
|
308
|
-
state.lanes.get(
|
|
344
|
+
const laneEpoch = (state: LedgerState, threadId: ThreadId): number =>
|
|
345
|
+
state.lanes.get(threadId)?.producerEpoch ?? 0;
|
|
309
346
|
|
|
310
347
|
const ownershipLost = (state: LedgerState, stored: StoredSubmission): OwnershipLost =>
|
|
311
348
|
OwnershipLost.make({
|
|
312
349
|
submissionId: stored.row.submissionId,
|
|
313
|
-
actualEpoch: decodeProducerEpoch(laneEpoch(state, stored.row.
|
|
350
|
+
actualEpoch: decodeProducerEpoch(laneEpoch(state, stored.row.threadId)),
|
|
314
351
|
});
|
|
315
352
|
|
|
316
|
-
/**
|
|
317
|
-
const ownsLane = (
|
|
318
|
-
|
|
353
|
+
/** A retained row token cannot outlive a newer owner of the same Thread. */
|
|
354
|
+
const ownsLane = (
|
|
355
|
+
state: LedgerState,
|
|
356
|
+
stored: StoredSubmission,
|
|
357
|
+
ownershipToken: OwnershipToken,
|
|
358
|
+
): boolean =>
|
|
359
|
+
stored.ownership !== undefined &&
|
|
360
|
+
stored.ownership.ownershipToken === ownershipToken &&
|
|
361
|
+
stored.ownership.producerEpoch === laneEpoch(state, stored.row.threadId);
|
|
319
362
|
|
|
320
363
|
const withSubmission = (state: LedgerState, stored: StoredSubmission): LedgerState => ({
|
|
321
364
|
...state,
|
|
@@ -330,21 +373,25 @@ const withChildReservation = (
|
|
|
330
373
|
childReservations: new Map(state.childReservations).set(reservation.reservationId, reservation),
|
|
331
374
|
});
|
|
332
375
|
|
|
333
|
-
const findHead = (
|
|
334
|
-
state: LedgerState,
|
|
335
|
-
conversationId: ConversationId,
|
|
336
|
-
): StoredSubmission | undefined => {
|
|
376
|
+
const findHead = (state: LedgerState, threadId: ThreadId): StoredSubmission | undefined => {
|
|
337
377
|
let head: StoredSubmission | undefined;
|
|
378
|
+
|
|
338
379
|
for (const stored of state.submissions.values()) {
|
|
339
|
-
if (
|
|
380
|
+
if (
|
|
381
|
+
stored.row.threadId !== threadId ||
|
|
382
|
+
stored.row.state === "settled" ||
|
|
383
|
+
(stored.row.state === "unknown" && stored.abortIntent === undefined)
|
|
384
|
+
)
|
|
385
|
+
continue;
|
|
340
386
|
if (head === undefined || stored.row.queueSequence < head.row.queueSequence) head = stored;
|
|
341
387
|
}
|
|
388
|
+
|
|
342
389
|
return head;
|
|
343
390
|
};
|
|
344
391
|
|
|
345
392
|
/**
|
|
346
393
|
* Reference in-memory SubmissionLedger. It implements the full port contract — atomic idempotent
|
|
347
|
-
* admission, FIFO
|
|
394
|
+
* admission, eligible FIFO claims, producer-epoch fencing, Clock-driven ownership leases, idempotent
|
|
348
395
|
* settlement reservation/finalization, and durable abort intent — with every transition applied
|
|
349
396
|
* as one atomic `Ref.modify`, but its state does not survive the process (`non-durable`).
|
|
350
397
|
*
|
|
@@ -354,8 +401,8 @@ const findHead = (
|
|
|
354
401
|
* deterministically; no wall clock is consulted.
|
|
355
402
|
* - The ownership lease is pinned to `DEFAULT_OWNERSHIP_LEASE_DURATION` (D5); durable adapters
|
|
356
403
|
* own the configuration seam.
|
|
357
|
-
* -
|
|
358
|
-
*
|
|
404
|
+
* - Any live lease in the Thread blocks every new claim, including the same `producerId`.
|
|
405
|
+
* Unresolved unknown work is skipped only after ownership is released or expires.
|
|
359
406
|
* - Claiming advances `ready` to `running` and otherwise preserves the recorded state, so
|
|
360
407
|
* progress markers from an earlier Attempt survive a reclaim.
|
|
361
408
|
* - `renewOwnership` keeps the token stable (the port allows rotation); a replayed admission
|
|
@@ -385,6 +432,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
385
432
|
childReservations: new Map(),
|
|
386
433
|
mintCounter: 0,
|
|
387
434
|
});
|
|
435
|
+
|
|
436
|
+
const admissionFence = yield* SubmissionAdmissionFence;
|
|
388
437
|
const leaseMillis = Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION);
|
|
389
438
|
|
|
390
439
|
const capabilities = Effect.succeed(LedgerCapabilities.make({ durability: "non-durable" }));
|
|
@@ -393,23 +442,45 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
393
442
|
(unvalidated) =>
|
|
394
443
|
Effect.gen(function* () {
|
|
395
444
|
const request = yield* validate(AdmissionRequest, "admit", unvalidated);
|
|
445
|
+
|
|
446
|
+
const workerAdmissionJson =
|
|
447
|
+
request.workerAdmission === undefined
|
|
448
|
+
? undefined
|
|
449
|
+
: yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
450
|
+
request.workerAdmission,
|
|
451
|
+
).pipe(
|
|
452
|
+
Effect.mapError(() => ledgerError("admit", "Invalid worker admission metadata")),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const messageAdmissionJson =
|
|
456
|
+
request.messageAdmission === undefined
|
|
457
|
+
? undefined
|
|
458
|
+
: yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
|
|
459
|
+
request.messageAdmission,
|
|
460
|
+
).pipe(
|
|
461
|
+
Effect.mapError(() => ledgerError("admit", "Invalid message admission metadata")),
|
|
462
|
+
);
|
|
463
|
+
|
|
396
464
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
465
|
+
const services = yield* Effect.context<never>();
|
|
466
|
+
|
|
397
467
|
const decision = yield* Ref.modify(
|
|
398
468
|
state,
|
|
399
469
|
(
|
|
400
470
|
current,
|
|
401
471
|
): readonly [
|
|
402
|
-
|
|
472
|
+
(
|
|
473
|
+
| Decision<AdmissionResult, AdmissionConflict | AdmissionPolicyError | LedgerError>
|
|
474
|
+
| { readonly _tag: "cause"; readonly cause: Cause.Cause<AdmissionPolicyError> }
|
|
475
|
+
),
|
|
403
476
|
LedgerState,
|
|
404
477
|
] => {
|
|
405
|
-
const key = admissionKey(
|
|
406
|
-
request.conversationId,
|
|
407
|
-
request.principal,
|
|
408
|
-
request.idempotencyKey,
|
|
409
|
-
);
|
|
478
|
+
const key = admissionKey(request.threadId, request.principal, request.idempotencyKey);
|
|
410
479
|
const existingId = current.admissionIndex.get(key);
|
|
480
|
+
|
|
411
481
|
if (existingId !== undefined) {
|
|
412
482
|
const existing = current.submissions.get(existingId);
|
|
483
|
+
|
|
413
484
|
if (existing === undefined) {
|
|
414
485
|
return [
|
|
415
486
|
failure(
|
|
@@ -422,12 +493,33 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
422
493
|
// (or its absence): linkage is immutable lineage (spec §12 step 5, SUB-016).
|
|
423
494
|
if (
|
|
424
495
|
existing.row.inputDigest !== request.inputDigest ||
|
|
425
|
-
!sameParentLinkage(existing.row.parentLinkage, request.parentLinkage)
|
|
496
|
+
!sameParentLinkage(existing.row.parentLinkage, request.parentLinkage) ||
|
|
497
|
+
existing.row.admissionGroup !== request.admissionGroup ||
|
|
498
|
+
!Schema.toEquivalence(Schema.optional(WorkerAdmission))(
|
|
499
|
+
existing.row.workerAdmissionJson === undefined
|
|
500
|
+
? undefined
|
|
501
|
+
: Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
|
|
502
|
+
existing.row.workerAdmissionJson,
|
|
503
|
+
),
|
|
504
|
+
request.workerAdmission,
|
|
505
|
+
) ||
|
|
506
|
+
!Schema.toEquivalence(Schema.optional(InputMessage))(
|
|
507
|
+
existing.row.messageAdmissionJson === undefined
|
|
508
|
+
? undefined
|
|
509
|
+
: Schema.decodeSync(Schema.fromJsonString(InputMessage))(
|
|
510
|
+
existing.row.messageAdmissionJson,
|
|
511
|
+
),
|
|
512
|
+
request.messageAdmission,
|
|
513
|
+
) ||
|
|
514
|
+
!Schema.toEquivalence(Schema.optional(Schema.Json))(
|
|
515
|
+
existing.row.admissionFence,
|
|
516
|
+
request.admissionFence,
|
|
517
|
+
)
|
|
426
518
|
) {
|
|
427
519
|
return [
|
|
428
520
|
failure(
|
|
429
521
|
AdmissionConflict.make({
|
|
430
|
-
|
|
522
|
+
threadId: request.threadId,
|
|
431
523
|
principal: request.principal,
|
|
432
524
|
idempotencyKey: request.idempotencyKey,
|
|
433
525
|
existingInputDigest: existing.row.inputDigest,
|
|
@@ -437,6 +529,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
437
529
|
current,
|
|
438
530
|
];
|
|
439
531
|
}
|
|
532
|
+
|
|
440
533
|
return [
|
|
441
534
|
success(
|
|
442
535
|
AdmissionResult.make({
|
|
@@ -450,6 +543,58 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
450
543
|
current,
|
|
451
544
|
];
|
|
452
545
|
}
|
|
546
|
+
|
|
547
|
+
// A Thread's first admission fixes its worker origin before canonical materialization.
|
|
548
|
+
const first = [...current.submissions.values()].find(
|
|
549
|
+
({ row }) => row.threadId === request.threadId,
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
if (first !== undefined) {
|
|
553
|
+
const previous =
|
|
554
|
+
first.row.workerAdmissionJson === undefined
|
|
555
|
+
? undefined
|
|
556
|
+
: Schema.decodeSync(Schema.fromJsonString(WorkerAdmission))(
|
|
557
|
+
first.row.workerAdmissionJson,
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
if (
|
|
561
|
+
!Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
|
|
562
|
+
previous?.origin,
|
|
563
|
+
request.workerAdmission?.origin,
|
|
564
|
+
)
|
|
565
|
+
)
|
|
566
|
+
return [
|
|
567
|
+
failure(
|
|
568
|
+
AdmissionPolicyError.make({
|
|
569
|
+
reason: "refused",
|
|
570
|
+
code: "worker-origin-conflict",
|
|
571
|
+
}),
|
|
572
|
+
),
|
|
573
|
+
current,
|
|
574
|
+
];
|
|
575
|
+
}
|
|
576
|
+
// A memory policy and the ledger mutation share one synchronous critical section.
|
|
577
|
+
// An asynchronous policy cannot fence this Ref and therefore fails closed.
|
|
578
|
+
const checked = Effect.runSyncExitWith(services)(admissionFence.check(request));
|
|
579
|
+
|
|
580
|
+
if (Exit.isFailure(checked)) {
|
|
581
|
+
return [{ _tag: "cause", cause: checked.cause }, current];
|
|
582
|
+
}
|
|
583
|
+
if (
|
|
584
|
+
request.admissionGroup !== undefined &&
|
|
585
|
+
[...current.submissions.values()].some(
|
|
586
|
+
({ row }) =>
|
|
587
|
+
row.threadId === request.threadId &&
|
|
588
|
+
row.admissionGroup === request.admissionGroup &&
|
|
589
|
+
row.state !== "settled",
|
|
590
|
+
)
|
|
591
|
+
)
|
|
592
|
+
return [
|
|
593
|
+
failure(
|
|
594
|
+
AdmissionPolicyError.make({ reason: "occupied", code: "admission-group" }),
|
|
595
|
+
),
|
|
596
|
+
current,
|
|
597
|
+
];
|
|
453
598
|
if (current.submissions.size >= MAX_SUBMISSIONS) {
|
|
454
599
|
return [
|
|
455
600
|
failure(
|
|
@@ -458,14 +603,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
458
603
|
current,
|
|
459
604
|
];
|
|
460
605
|
}
|
|
461
|
-
|
|
606
|
+
|
|
607
|
+
const lane = current.lanes.get(request.threadId) ?? {
|
|
462
608
|
nextQueueSequence: 1,
|
|
463
609
|
producerEpoch: 0,
|
|
464
610
|
};
|
|
611
|
+
|
|
465
612
|
const mintCounter = current.mintCounter + 1;
|
|
613
|
+
|
|
466
614
|
const row: SubmissionRow = {
|
|
467
615
|
submissionId: decodeSubmissionId(`submission-memory-${mintCounter}`),
|
|
468
|
-
|
|
616
|
+
threadId: request.threadId,
|
|
469
617
|
queueSequence: decodeQueueSequence(lane.nextQueueSequence),
|
|
470
618
|
principal: request.principal,
|
|
471
619
|
idempotencyKey: request.idempotencyKey,
|
|
@@ -480,7 +628,16 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
480
628
|
createdAtMillis: nowMillis,
|
|
481
629
|
readyAtMillis: undefined,
|
|
482
630
|
parentLinkage: request.parentLinkage,
|
|
631
|
+
...(workerAdmissionJson === undefined ? {} : { workerAdmissionJson }),
|
|
632
|
+
...(messageAdmissionJson === undefined ? {} : { messageAdmissionJson }),
|
|
633
|
+
...(request.admissionGroup === undefined
|
|
634
|
+
? {}
|
|
635
|
+
: { admissionGroup: request.admissionGroup }),
|
|
636
|
+
...(request.admissionFence === undefined
|
|
637
|
+
? {}
|
|
638
|
+
: { admissionFence: request.admissionFence }),
|
|
483
639
|
};
|
|
640
|
+
|
|
484
641
|
const submissions = new Map(current.submissions).set(row.submissionId, {
|
|
485
642
|
row,
|
|
486
643
|
ownership: undefined,
|
|
@@ -493,11 +650,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
493
650
|
approvalDecisions: new Map<ToolCallId, ApprovalDecisionIntent>(),
|
|
494
651
|
unknownResolutions: new Map<ToolCallId, StoredUnknownResolution>(),
|
|
495
652
|
});
|
|
653
|
+
|
|
496
654
|
const admissionIndex = new Map(current.admissionIndex).set(key, row.submissionId);
|
|
497
|
-
|
|
655
|
+
|
|
656
|
+
const lanes = new Map(current.lanes).set(request.threadId, {
|
|
498
657
|
nextQueueSequence: lane.nextQueueSequence + 1,
|
|
499
658
|
producerEpoch: lane.producerEpoch,
|
|
500
659
|
});
|
|
660
|
+
|
|
501
661
|
return [
|
|
502
662
|
success(
|
|
503
663
|
AdmissionResult.make({
|
|
@@ -512,9 +672,26 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
512
672
|
];
|
|
513
673
|
},
|
|
514
674
|
);
|
|
675
|
+
|
|
515
676
|
if (decision._tag === "failure") return yield* decision.error;
|
|
677
|
+
if (decision._tag === "cause") {
|
|
678
|
+
for (const reason of decision.cause.reasons) {
|
|
679
|
+
if (Cause.isDieReason(reason) && Cause.isAsyncFiberError(reason.defect)) {
|
|
680
|
+
yield* Fiber.interrupt(reason.defect.fiber);
|
|
681
|
+
|
|
682
|
+
return yield* AdmissionPolicyError.make({
|
|
683
|
+
reason: "unavailable",
|
|
684
|
+
code: "synchronous-memory-policy-required",
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return yield* Effect.failCause(decision.cause);
|
|
690
|
+
}
|
|
691
|
+
|
|
516
692
|
return decision.value;
|
|
517
693
|
}),
|
|
694
|
+
Effect.uninterruptible,
|
|
518
695
|
);
|
|
519
696
|
|
|
520
697
|
const markReady: SubmissionLedger["Service"]["markReady"] = Effect.fn(
|
|
@@ -523,10 +700,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
523
700
|
Effect.gen(function* () {
|
|
524
701
|
const request = yield* validate(MarkReadyRequest, "markReady", unvalidated);
|
|
525
702
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
703
|
+
|
|
526
704
|
const decision = yield* Ref.modify(
|
|
527
705
|
state,
|
|
528
706
|
(current): readonly [Decision<void, LedgerError>, LedgerState] => {
|
|
529
707
|
const stored = current.submissions.get(request.submissionId);
|
|
708
|
+
|
|
530
709
|
if (stored === undefined) {
|
|
531
710
|
return [
|
|
532
711
|
failure(ledgerError("markReady", `Unknown Submission ${request.submissionId}`)),
|
|
@@ -534,6 +713,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
534
713
|
];
|
|
535
714
|
}
|
|
536
715
|
if (stored.row.state !== "admitted") return [success(undefined), current];
|
|
716
|
+
|
|
537
717
|
return [
|
|
538
718
|
success(undefined),
|
|
539
719
|
withSubmission(current, {
|
|
@@ -543,6 +723,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
543
723
|
];
|
|
544
724
|
},
|
|
545
725
|
);
|
|
726
|
+
|
|
546
727
|
if (decision._tag === "failure") return yield* decision.error;
|
|
547
728
|
}),
|
|
548
729
|
);
|
|
@@ -553,14 +734,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
553
734
|
Effect.gen(function* () {
|
|
554
735
|
const request = yield* validate(SubmissionLookup, "lookup", unvalidated);
|
|
555
736
|
const current = yield* Ref.get(state);
|
|
737
|
+
|
|
556
738
|
const submissionId =
|
|
557
739
|
request._tag === "SubmissionLookupById"
|
|
558
740
|
? request.submissionId
|
|
559
741
|
: current.admissionIndex.get(
|
|
560
|
-
admissionKey(request.
|
|
742
|
+
admissionKey(request.threadId, request.principal, request.idempotencyKey),
|
|
561
743
|
);
|
|
744
|
+
|
|
562
745
|
const stored =
|
|
563
746
|
submissionId === undefined ? undefined : current.submissions.get(submissionId);
|
|
747
|
+
|
|
564
748
|
return stored === undefined ? Option.none() : Option.some(toSnapshot(stored.row));
|
|
565
749
|
}),
|
|
566
750
|
);
|
|
@@ -570,20 +754,25 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
570
754
|
)((unvalidated) =>
|
|
571
755
|
Effect.gen(function* () {
|
|
572
756
|
const request = yield* validate(SubmissionLookupByKey, "resolveAdmission", unvalidated);
|
|
757
|
+
|
|
573
758
|
// Test-only fault seam: lets suites exercise the Indeterminate classification that a
|
|
574
759
|
// single strongly consistent store never produces on its own (SUB-031, P6 honesty).
|
|
575
760
|
if (options.resolveAdmissionFault !== undefined) {
|
|
576
761
|
const fault = yield* options.resolveAdmissionFault;
|
|
762
|
+
|
|
577
763
|
if (Option.isSome(fault)) {
|
|
578
764
|
return AdmissionIndeterminate.make({ reason: fault.value });
|
|
579
765
|
}
|
|
580
766
|
}
|
|
581
767
|
const current = yield* Ref.get(state);
|
|
768
|
+
|
|
582
769
|
const submissionId = current.admissionIndex.get(
|
|
583
|
-
admissionKey(request.
|
|
770
|
+
admissionKey(request.threadId, request.principal, request.idempotencyKey),
|
|
584
771
|
);
|
|
772
|
+
|
|
585
773
|
const stored =
|
|
586
774
|
submissionId === undefined ? undefined : current.submissions.get(submissionId);
|
|
775
|
+
|
|
587
776
|
return stored === undefined
|
|
588
777
|
? AdmissionNotAdmitted.make()
|
|
589
778
|
: AdmissionAdmitted.make({ submission: toSnapshot(stored.row) });
|
|
@@ -595,30 +784,34 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
595
784
|
Effect.gen(function* () {
|
|
596
785
|
const request = yield* validate(ClaimRequest, "claim", unvalidated);
|
|
597
786
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
787
|
+
|
|
598
788
|
const decision = yield* Ref.modify(
|
|
599
789
|
state,
|
|
600
790
|
(current): readonly [Decision<Option.Option<Claim>, LedgerError>, LedgerState] => {
|
|
601
|
-
const
|
|
791
|
+
for (const stored of current.submissions.values()) {
|
|
792
|
+
if (
|
|
793
|
+
stored.row.threadId === request.threadId &&
|
|
794
|
+
stored.ownership !== undefined &&
|
|
795
|
+
stored.ownership.leaseExpiresAtMillis > nowMillis
|
|
796
|
+
) {
|
|
797
|
+
return [success(Option.none()), current];
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const head = findHead(current, request.threadId);
|
|
801
|
+
|
|
602
802
|
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
803
|
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);
|
|
804
|
+
const lane = current.lanes.get(request.threadId);
|
|
805
|
+
|
|
614
806
|
if (lane === undefined) {
|
|
615
807
|
return [
|
|
616
|
-
failure(ledgerError("claim", "Claimable head without a
|
|
808
|
+
failure(ledgerError("claim", "Claimable head without a Thread lane")),
|
|
617
809
|
current,
|
|
618
810
|
];
|
|
619
811
|
}
|
|
620
812
|
const producerEpoch = decodeProducerEpoch(lane.producerEpoch + 1);
|
|
621
813
|
const mintCounter = current.mintCounter + 1;
|
|
814
|
+
|
|
622
815
|
const ownership: StoredOwnership = {
|
|
623
816
|
attemptId: decodeAttemptId(`attempt-memory-${mintCounter}`),
|
|
624
817
|
ownershipToken: decodeOwnershipToken(`ownership-memory-${mintCounter}`),
|
|
@@ -626,13 +819,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
626
819
|
ownerProducerId: request.producerId,
|
|
627
820
|
leaseExpiresAtMillis: nowMillis + leaseMillis,
|
|
628
821
|
};
|
|
822
|
+
|
|
629
823
|
const row: SubmissionRow =
|
|
630
824
|
head.row.state === "ready" ? { ...head.row, state: "running" } : head.row;
|
|
825
|
+
|
|
631
826
|
const next = withSubmission(current, { ...head, row, ownership });
|
|
632
|
-
|
|
827
|
+
|
|
828
|
+
const lanes = new Map(next.lanes).set(request.threadId, {
|
|
633
829
|
nextQueueSequence: lane.nextQueueSequence,
|
|
634
830
|
producerEpoch: lane.producerEpoch + 1,
|
|
635
831
|
});
|
|
832
|
+
|
|
636
833
|
return [
|
|
637
834
|
success(
|
|
638
835
|
Option.some(
|
|
@@ -650,7 +847,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
650
847
|
];
|
|
651
848
|
},
|
|
652
849
|
);
|
|
850
|
+
|
|
653
851
|
if (decision._tag === "failure") return yield* decision.error;
|
|
852
|
+
|
|
654
853
|
return decision.value;
|
|
655
854
|
}),
|
|
656
855
|
);
|
|
@@ -661,12 +860,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
661
860
|
Effect.gen(function* () {
|
|
662
861
|
const request = yield* validate(RenewOwnershipRequest, "renewOwnership", unvalidated);
|
|
663
862
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
863
|
+
|
|
664
864
|
const decision = yield* Ref.modify(
|
|
665
865
|
state,
|
|
666
866
|
(
|
|
667
867
|
current,
|
|
668
868
|
): readonly [Decision<OwnershipRenewal, OwnershipLost | LedgerError>, LedgerState] => {
|
|
669
869
|
const stored = current.submissions.get(request.submissionId);
|
|
870
|
+
|
|
670
871
|
if (stored === undefined) {
|
|
671
872
|
return [
|
|
672
873
|
failure(
|
|
@@ -675,13 +876,18 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
675
876
|
current,
|
|
676
877
|
];
|
|
677
878
|
}
|
|
678
|
-
if (
|
|
879
|
+
if (
|
|
880
|
+
stored.ownership === undefined ||
|
|
881
|
+
!ownsLane(current, stored, request.ownershipToken)
|
|
882
|
+
) {
|
|
679
883
|
return [failure(ownershipLost(current, stored)), current];
|
|
680
884
|
}
|
|
885
|
+
|
|
681
886
|
const ownership: StoredOwnership = {
|
|
682
887
|
...stored.ownership,
|
|
683
888
|
leaseExpiresAtMillis: nowMillis + leaseMillis,
|
|
684
889
|
};
|
|
890
|
+
|
|
685
891
|
return [
|
|
686
892
|
success(
|
|
687
893
|
OwnershipRenewal.make({
|
|
@@ -693,7 +899,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
693
899
|
];
|
|
694
900
|
},
|
|
695
901
|
);
|
|
902
|
+
|
|
696
903
|
if (decision._tag === "failure") return yield* decision.error;
|
|
904
|
+
|
|
697
905
|
return decision.value;
|
|
698
906
|
}),
|
|
699
907
|
);
|
|
@@ -703,10 +911,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
703
911
|
)((unvalidated) =>
|
|
704
912
|
Effect.gen(function* () {
|
|
705
913
|
const request = yield* validate(ReleaseOwnershipRequest, "releaseOwnership", unvalidated);
|
|
914
|
+
|
|
706
915
|
const decision = yield* Ref.modify(
|
|
707
916
|
state,
|
|
708
917
|
(current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
|
|
709
918
|
const stored = current.submissions.get(request.submissionId);
|
|
919
|
+
|
|
710
920
|
if (stored === undefined) {
|
|
711
921
|
return [
|
|
712
922
|
failure(
|
|
@@ -715,15 +925,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
715
925
|
current,
|
|
716
926
|
];
|
|
717
927
|
}
|
|
718
|
-
if (!ownsLane(stored, request.ownershipToken)) {
|
|
928
|
+
if (!ownsLane(current, stored, request.ownershipToken)) {
|
|
719
929
|
return [failure(ownershipLost(current, stored)), current];
|
|
720
930
|
}
|
|
931
|
+
|
|
721
932
|
return [
|
|
722
933
|
success(undefined),
|
|
723
934
|
withSubmission(current, { ...stored, ownership: undefined }),
|
|
724
935
|
];
|
|
725
936
|
},
|
|
726
937
|
);
|
|
938
|
+
|
|
727
939
|
if (decision._tag === "failure") return yield* decision.error;
|
|
728
940
|
}),
|
|
729
941
|
);
|
|
@@ -733,10 +945,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
733
945
|
)((unvalidated) =>
|
|
734
946
|
Effect.gen(function* () {
|
|
735
947
|
const request = yield* validate(MarkInputAppliedRequest, "markInputApplied", unvalidated);
|
|
948
|
+
|
|
736
949
|
const decision = yield* Ref.modify(
|
|
737
950
|
state,
|
|
738
951
|
(current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
|
|
739
952
|
const stored = current.submissions.get(request.submissionId);
|
|
953
|
+
|
|
740
954
|
if (stored === undefined) {
|
|
741
955
|
return [
|
|
742
956
|
failure(
|
|
@@ -745,23 +959,46 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
745
959
|
current,
|
|
746
960
|
];
|
|
747
961
|
}
|
|
748
|
-
if (!ownsLane(stored, request.ownershipToken)) {
|
|
962
|
+
if (!ownsLane(current, stored, request.ownershipToken)) {
|
|
749
963
|
return [failure(ownershipLost(current, stored)), current];
|
|
750
964
|
}
|
|
965
|
+
|
|
966
|
+
if (stored.inputApplied !== undefined) {
|
|
967
|
+
if (
|
|
968
|
+
stored.inputApplied.recordId === request.recordId &&
|
|
969
|
+
stored.inputApplied.sequence === request.sequence
|
|
970
|
+
) {
|
|
971
|
+
return [success(undefined), current];
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return [
|
|
975
|
+
failure(
|
|
976
|
+
ledgerError(
|
|
977
|
+
"markInputApplied",
|
|
978
|
+
`A different canonical input marker is already recorded for Submission ${request.submissionId}`,
|
|
979
|
+
),
|
|
980
|
+
),
|
|
981
|
+
current,
|
|
982
|
+
];
|
|
983
|
+
}
|
|
984
|
+
|
|
751
985
|
const marker = InputAppliedMarker.make({
|
|
752
986
|
recordId: request.recordId,
|
|
753
987
|
sequence: request.sequence,
|
|
754
988
|
});
|
|
989
|
+
|
|
755
990
|
const row: SubmissionRow =
|
|
756
991
|
STATE_RANK[stored.row.state] < STATE_RANK["input-applied"]
|
|
757
992
|
? { ...stored.row, state: "input-applied" }
|
|
758
993
|
: stored.row;
|
|
994
|
+
|
|
759
995
|
return [
|
|
760
996
|
success(undefined),
|
|
761
997
|
withSubmission(current, { ...stored, row, inputApplied: marker }),
|
|
762
998
|
];
|
|
763
999
|
},
|
|
764
1000
|
);
|
|
1001
|
+
|
|
765
1002
|
if (decision._tag === "failure") return yield* decision.error;
|
|
766
1003
|
}),
|
|
767
1004
|
);
|
|
@@ -771,6 +1008,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
771
1008
|
)((unvalidated) =>
|
|
772
1009
|
Effect.gen(function* () {
|
|
773
1010
|
const request = yield* validate(SettlementReservation, "reserveSettlement", unvalidated);
|
|
1011
|
+
|
|
774
1012
|
const decision = yield* Ref.modify(
|
|
775
1013
|
state,
|
|
776
1014
|
(
|
|
@@ -780,6 +1018,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
780
1018
|
LedgerState,
|
|
781
1019
|
] => {
|
|
782
1020
|
const stored = current.submissions.get(request.submissionId);
|
|
1021
|
+
|
|
783
1022
|
if (stored === undefined) {
|
|
784
1023
|
return [
|
|
785
1024
|
failure(
|
|
@@ -788,11 +1027,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
788
1027
|
current,
|
|
789
1028
|
];
|
|
790
1029
|
}
|
|
1030
|
+
|
|
791
1031
|
// A `joined` Submission settles WITH its host (plan §2.5) and its lane is never
|
|
792
1032
|
// worker-claimable, so no ownership token can exist for it: the recorded host
|
|
793
1033
|
// linkage authorizes the reservation and the presented token is not consulted.
|
|
794
1034
|
const joinedSettlement =
|
|
795
1035
|
stored.row.state === "joined" && stored.joinedHostSubmissionId !== undefined;
|
|
1036
|
+
|
|
796
1037
|
// P7 §7(c): an aborted, never-claimed, still-queued Submission likewise has no
|
|
797
1038
|
// live ownership to fence against — its durable abort intent authorizes exactly
|
|
798
1039
|
// its ABORTED settlement (`terminalizing` is the same pass's crash replay). Every
|
|
@@ -802,14 +1043,16 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
802
1043
|
stored.abortIntent !== undefined &&
|
|
803
1044
|
stored.ownership === undefined &&
|
|
804
1045
|
(stored.row.state === "ready" || stored.row.state === "terminalizing");
|
|
1046
|
+
|
|
805
1047
|
if (
|
|
806
1048
|
!joinedSettlement &&
|
|
807
1049
|
!queuedAbortSettlement &&
|
|
808
|
-
!ownsLane(stored, request.ownershipToken)
|
|
1050
|
+
!ownsLane(current, stored, request.ownershipToken)
|
|
809
1051
|
) {
|
|
810
1052
|
return [failure(ownershipLost(current, stored)), current];
|
|
811
1053
|
}
|
|
812
1054
|
const existing = stored.reservation;
|
|
1055
|
+
|
|
813
1056
|
if (existing !== undefined) {
|
|
814
1057
|
if (
|
|
815
1058
|
existing.settlementId !== request.settlementId ||
|
|
@@ -826,6 +1069,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
826
1069
|
current,
|
|
827
1070
|
];
|
|
828
1071
|
}
|
|
1072
|
+
|
|
829
1073
|
return [
|
|
830
1074
|
success(
|
|
831
1075
|
ReservedSettlement.make({
|
|
@@ -840,6 +1084,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
840
1084
|
current,
|
|
841
1085
|
];
|
|
842
1086
|
}
|
|
1087
|
+
|
|
843
1088
|
const reservation: StoredReservation = {
|
|
844
1089
|
settlementId: request.settlementId,
|
|
845
1090
|
outcome: request.outcome,
|
|
@@ -847,10 +1092,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
847
1092
|
recordDigest: request.recordDigest,
|
|
848
1093
|
finalizedAtMillis: undefined,
|
|
849
1094
|
};
|
|
1095
|
+
|
|
850
1096
|
const row: SubmissionRow =
|
|
851
1097
|
STATE_RANK[stored.row.state] < STATE_RANK.terminalizing
|
|
852
1098
|
? { ...stored.row, state: "terminalizing" }
|
|
853
1099
|
: stored.row;
|
|
1100
|
+
|
|
854
1101
|
return [
|
|
855
1102
|
success(
|
|
856
1103
|
ReservedSettlement.make({
|
|
@@ -866,7 +1113,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
866
1113
|
];
|
|
867
1114
|
},
|
|
868
1115
|
);
|
|
1116
|
+
|
|
869
1117
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1118
|
+
|
|
870
1119
|
return decision.value;
|
|
871
1120
|
}),
|
|
872
1121
|
);
|
|
@@ -877,12 +1126,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
877
1126
|
Effect.gen(function* () {
|
|
878
1127
|
const request = yield* validate(SettlementFinalization, "finalizeSettlement", unvalidated);
|
|
879
1128
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1129
|
+
|
|
880
1130
|
const decision = yield* Ref.modify(
|
|
881
1131
|
state,
|
|
882
1132
|
(
|
|
883
1133
|
current,
|
|
884
1134
|
): readonly [Decision<Settlement, SettlementConflict | LedgerError>, LedgerState] => {
|
|
885
1135
|
const stored = current.submissions.get(request.submissionId);
|
|
1136
|
+
|
|
886
1137
|
if (stored === undefined) {
|
|
887
1138
|
return [
|
|
888
1139
|
failure(
|
|
@@ -892,6 +1143,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
892
1143
|
];
|
|
893
1144
|
}
|
|
894
1145
|
const reservation = stored.reservation;
|
|
1146
|
+
|
|
895
1147
|
if (reservation === undefined) {
|
|
896
1148
|
return [
|
|
897
1149
|
failure(
|
|
@@ -903,6 +1155,19 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
903
1155
|
current,
|
|
904
1156
|
];
|
|
905
1157
|
}
|
|
1158
|
+
const settlementFailure = settlementFailureFromRecord(reservation.record);
|
|
1159
|
+
|
|
1160
|
+
if ((reservation.outcome === "failed") !== (settlementFailure !== undefined)) {
|
|
1161
|
+
return [
|
|
1162
|
+
failure(
|
|
1163
|
+
ledgerError(
|
|
1164
|
+
"finalizeSettlement",
|
|
1165
|
+
`Settlement reservation for Submission ${request.submissionId} has contradictory failure evidence`,
|
|
1166
|
+
),
|
|
1167
|
+
),
|
|
1168
|
+
current,
|
|
1169
|
+
];
|
|
1170
|
+
}
|
|
906
1171
|
if (reservation.settlementId !== request.settlementId) {
|
|
907
1172
|
return [
|
|
908
1173
|
failure(
|
|
@@ -922,18 +1187,21 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
922
1187
|
settlementId: reservation.settlementId,
|
|
923
1188
|
receiptId: stored.row.receiptId,
|
|
924
1189
|
outcome: reservation.outcome,
|
|
1190
|
+
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
925
1191
|
settledAt: utc(reservation.finalizedAtMillis),
|
|
926
1192
|
}),
|
|
927
1193
|
),
|
|
928
1194
|
current,
|
|
929
1195
|
];
|
|
930
1196
|
}
|
|
1197
|
+
|
|
931
1198
|
const next = withSubmission(current, {
|
|
932
1199
|
...stored,
|
|
933
1200
|
row: { ...stored.row, state: "settled", settledOutcome: reservation.outcome },
|
|
934
1201
|
ownership: undefined,
|
|
935
1202
|
reservation: { ...reservation, finalizedAtMillis: nowMillis },
|
|
936
1203
|
});
|
|
1204
|
+
|
|
937
1205
|
return [
|
|
938
1206
|
success(
|
|
939
1207
|
Settlement.make({
|
|
@@ -941,6 +1209,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
941
1209
|
settlementId: reservation.settlementId,
|
|
942
1210
|
receiptId: stored.row.receiptId,
|
|
943
1211
|
outcome: reservation.outcome,
|
|
1212
|
+
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
944
1213
|
settledAt: utc(nowMillis),
|
|
945
1214
|
}),
|
|
946
1215
|
),
|
|
@@ -948,7 +1217,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
948
1217
|
];
|
|
949
1218
|
},
|
|
950
1219
|
);
|
|
1220
|
+
|
|
951
1221
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1222
|
+
|
|
952
1223
|
return decision.value;
|
|
953
1224
|
}),
|
|
954
1225
|
);
|
|
@@ -959,6 +1230,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
959
1230
|
Effect.gen(function* () {
|
|
960
1231
|
const request = yield* validate(AbortCommand, "requestAbort", unvalidated);
|
|
961
1232
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1233
|
+
|
|
962
1234
|
const decision = yield* Ref.modify(
|
|
963
1235
|
state,
|
|
964
1236
|
(
|
|
@@ -968,6 +1240,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
968
1240
|
LedgerState,
|
|
969
1241
|
] => {
|
|
970
1242
|
const stored = current.submissions.get(request.submissionId);
|
|
1243
|
+
|
|
971
1244
|
if (stored === undefined) {
|
|
972
1245
|
return [
|
|
973
1246
|
failure(ledgerError("requestAbort", `Unknown Submission ${request.submissionId}`)),
|
|
@@ -989,6 +1262,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
989
1262
|
current,
|
|
990
1263
|
];
|
|
991
1264
|
}
|
|
1265
|
+
|
|
992
1266
|
return [
|
|
993
1267
|
failure(
|
|
994
1268
|
JoinedToHost.make({
|
|
@@ -1011,6 +1285,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1011
1285
|
current,
|
|
1012
1286
|
];
|
|
1013
1287
|
}
|
|
1288
|
+
|
|
1014
1289
|
return [
|
|
1015
1290
|
failure(
|
|
1016
1291
|
SettlementConflict.make({
|
|
@@ -1022,16 +1297,20 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1022
1297
|
];
|
|
1023
1298
|
}
|
|
1024
1299
|
if (stored.abortIntent !== undefined) return [success(stored.abortIntent), current];
|
|
1300
|
+
|
|
1025
1301
|
const intent = AbortIntent.make({
|
|
1026
1302
|
submissionId: request.submissionId,
|
|
1027
1303
|
author: request.author,
|
|
1028
1304
|
reason: request.reason,
|
|
1029
1305
|
requestedAt: utc(nowMillis),
|
|
1030
1306
|
});
|
|
1307
|
+
|
|
1031
1308
|
return [success(intent), withSubmission(current, { ...stored, abortIntent: intent })];
|
|
1032
1309
|
},
|
|
1033
1310
|
);
|
|
1311
|
+
|
|
1034
1312
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1313
|
+
|
|
1035
1314
|
return decision.value;
|
|
1036
1315
|
}),
|
|
1037
1316
|
);
|
|
@@ -1041,6 +1320,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1041
1320
|
)((unvalidated) =>
|
|
1042
1321
|
Effect.gen(function* () {
|
|
1043
1322
|
const request = yield* validate(ClaimJoiningRequest, "claimJoining", unvalidated);
|
|
1323
|
+
|
|
1044
1324
|
const decision = yield* Ref.modify(
|
|
1045
1325
|
state,
|
|
1046
1326
|
(
|
|
@@ -1050,6 +1330,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1050
1330
|
LedgerState,
|
|
1051
1331
|
] => {
|
|
1052
1332
|
const host = current.submissions.get(request.hostSubmissionId);
|
|
1333
|
+
|
|
1053
1334
|
if (host === undefined) {
|
|
1054
1335
|
return [
|
|
1055
1336
|
failure(
|
|
@@ -1058,29 +1339,32 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1058
1339
|
current,
|
|
1059
1340
|
];
|
|
1060
1341
|
}
|
|
1061
|
-
if (host.row.
|
|
1342
|
+
if (host.row.threadId !== request.threadId) {
|
|
1062
1343
|
return [
|
|
1063
1344
|
failure(
|
|
1064
1345
|
ledgerError(
|
|
1065
1346
|
"claimJoining",
|
|
1066
|
-
`Host Submission ${request.hostSubmissionId} does not belong to
|
|
1347
|
+
`Host Submission ${request.hostSubmissionId} does not belong to Thread ${request.threadId}`,
|
|
1067
1348
|
),
|
|
1068
1349
|
),
|
|
1069
1350
|
current,
|
|
1070
1351
|
];
|
|
1071
1352
|
}
|
|
1072
|
-
if (!ownsLane(host, request.ownershipToken)) {
|
|
1353
|
+
if (!ownsLane(current, host, request.ownershipToken)) {
|
|
1073
1354
|
return [failure(ownershipLost(current, host)), current];
|
|
1074
1355
|
}
|
|
1356
|
+
|
|
1075
1357
|
const later = [...current.submissions.values()]
|
|
1076
1358
|
.filter(
|
|
1077
1359
|
(stored) =>
|
|
1078
|
-
stored.row.
|
|
1360
|
+
stored.row.threadId === request.threadId &&
|
|
1079
1361
|
stored.row.queueSequence > host.row.queueSequence,
|
|
1080
1362
|
)
|
|
1081
1363
|
.sort((left, right) => left.row.queueSequence - right.row.queueSequence);
|
|
1364
|
+
|
|
1082
1365
|
const claims: Array<JoiningClaim> = [];
|
|
1083
1366
|
const submissions = new Map(current.submissions);
|
|
1367
|
+
|
|
1084
1368
|
for (const stored of later) {
|
|
1085
1369
|
if (claims.length >= request.maxCount) break;
|
|
1086
1370
|
// Rows already claimed by THIS host extend its contiguous prefix and are skipped;
|
|
@@ -1113,10 +1397,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1113
1397
|
}),
|
|
1114
1398
|
);
|
|
1115
1399
|
}
|
|
1400
|
+
|
|
1116
1401
|
return [success(claims), { ...current, submissions }];
|
|
1117
1402
|
},
|
|
1118
1403
|
);
|
|
1404
|
+
|
|
1119
1405
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1406
|
+
|
|
1120
1407
|
return decision.value;
|
|
1121
1408
|
}),
|
|
1122
1409
|
);
|
|
@@ -1126,10 +1413,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1126
1413
|
)((unvalidated) =>
|
|
1127
1414
|
Effect.gen(function* () {
|
|
1128
1415
|
const request = yield* validate(MarkJoinedRequest, "markJoined", unvalidated);
|
|
1416
|
+
|
|
1129
1417
|
const decision = yield* Ref.modify(
|
|
1130
1418
|
state,
|
|
1131
1419
|
(current): readonly [Decision<void, OwnershipLost | LedgerError>, LedgerState] => {
|
|
1132
1420
|
const stored = current.submissions.get(request.submissionId);
|
|
1421
|
+
|
|
1133
1422
|
if (stored === undefined) {
|
|
1134
1423
|
return [
|
|
1135
1424
|
failure(ledgerError("markJoined", `Unknown Submission ${request.submissionId}`)),
|
|
@@ -1148,6 +1437,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1148
1437
|
];
|
|
1149
1438
|
}
|
|
1150
1439
|
const host = current.submissions.get(stored.joinedHostSubmissionId);
|
|
1440
|
+
|
|
1151
1441
|
if (host === undefined) {
|
|
1152
1442
|
return [
|
|
1153
1443
|
failure(
|
|
@@ -1161,7 +1451,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1161
1451
|
}
|
|
1162
1452
|
// The lane is host-owned: the presented token must own the HOST's ownership period,
|
|
1163
1453
|
// which also lets a later host Attempt repair a lost marker from history (DUR-016).
|
|
1164
|
-
if (!ownsLane(host, request.ownershipToken)) {
|
|
1454
|
+
if (!ownsLane(current, host, request.ownershipToken)) {
|
|
1165
1455
|
return [failure(ownershipLost(current, host)), current];
|
|
1166
1456
|
}
|
|
1167
1457
|
if (stored.inputApplied !== undefined) {
|
|
@@ -1171,6 +1461,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1171
1461
|
) {
|
|
1172
1462
|
return [success(undefined), current];
|
|
1173
1463
|
}
|
|
1464
|
+
|
|
1174
1465
|
return [
|
|
1175
1466
|
failure(
|
|
1176
1467
|
ledgerError(
|
|
@@ -1192,10 +1483,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1192
1483
|
current,
|
|
1193
1484
|
];
|
|
1194
1485
|
}
|
|
1486
|
+
|
|
1195
1487
|
const marker = InputAppliedMarker.make({
|
|
1196
1488
|
recordId: request.recordId,
|
|
1197
1489
|
sequence: request.sequence,
|
|
1198
1490
|
});
|
|
1491
|
+
|
|
1199
1492
|
return [
|
|
1200
1493
|
success(undefined),
|
|
1201
1494
|
withSubmission(current, {
|
|
@@ -1206,6 +1499,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1206
1499
|
];
|
|
1207
1500
|
},
|
|
1208
1501
|
);
|
|
1502
|
+
|
|
1209
1503
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1210
1504
|
}),
|
|
1211
1505
|
);
|
|
@@ -1215,10 +1509,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1215
1509
|
)((unvalidated) =>
|
|
1216
1510
|
Effect.gen(function* () {
|
|
1217
1511
|
const request = yield* validate(RevertJoiningRequest, "revertJoining", unvalidated);
|
|
1512
|
+
|
|
1218
1513
|
const decision = yield* Ref.modify(
|
|
1219
1514
|
state,
|
|
1220
1515
|
(current): readonly [Decision<void, LedgerError>, LedgerState] => {
|
|
1221
1516
|
const stored = current.submissions.get(request.submissionId);
|
|
1517
|
+
|
|
1222
1518
|
if (stored === undefined) {
|
|
1223
1519
|
return [
|
|
1224
1520
|
failure(ledgerError("revertJoining", `Unknown Submission ${request.submissionId}`)),
|
|
@@ -1228,6 +1524,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1228
1524
|
// Idempotent and recovery-only: only a still-`joining` Submission reverts; an
|
|
1229
1525
|
// already-joined (or already-reverted) Submission is a no-op (DUR-016).
|
|
1230
1526
|
if (stored.row.state !== "joining") return [success(undefined), current];
|
|
1527
|
+
|
|
1231
1528
|
return [
|
|
1232
1529
|
success(undefined),
|
|
1233
1530
|
withSubmission(current, {
|
|
@@ -1238,6 +1535,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1238
1535
|
];
|
|
1239
1536
|
},
|
|
1240
1537
|
);
|
|
1538
|
+
|
|
1241
1539
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1242
1540
|
}),
|
|
1243
1541
|
);
|
|
@@ -1248,6 +1546,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1248
1546
|
Effect.gen(function* () {
|
|
1249
1547
|
const request = yield* validate(SuspendRequest, "suspend", unvalidated);
|
|
1250
1548
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1549
|
+
|
|
1251
1550
|
const decision = yield* Ref.modify(
|
|
1252
1551
|
state,
|
|
1253
1552
|
(
|
|
@@ -1257,6 +1556,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1257
1556
|
LedgerState,
|
|
1258
1557
|
] => {
|
|
1259
1558
|
const stored = current.submissions.get(request.submissionId);
|
|
1559
|
+
|
|
1260
1560
|
if (stored === undefined) {
|
|
1261
1561
|
return [
|
|
1262
1562
|
failure(ledgerError("suspend", `Unknown Submission ${request.submissionId}`)),
|
|
@@ -1275,6 +1575,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1275
1575
|
current,
|
|
1276
1576
|
];
|
|
1277
1577
|
}
|
|
1578
|
+
|
|
1278
1579
|
return [
|
|
1279
1580
|
failure(
|
|
1280
1581
|
SettlementConflict.make({
|
|
@@ -1298,9 +1599,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1298
1599
|
current,
|
|
1299
1600
|
];
|
|
1300
1601
|
}
|
|
1301
|
-
if (!ownsLane(stored, request.ownershipToken)) {
|
|
1602
|
+
if (!ownsLane(current, stored, request.ownershipToken)) {
|
|
1302
1603
|
return [failure(ownershipLost(current, stored)), current];
|
|
1303
1604
|
}
|
|
1605
|
+
|
|
1304
1606
|
// A covering event that raced ahead of the suspend transaction (an approval decision,
|
|
1305
1607
|
// or a child settlement observed directly from the child's row in this single store)
|
|
1306
1608
|
// resumes the caller immediately WITHOUT releasing the lane (plan §2.6, spec §12).
|
|
@@ -1313,9 +1615,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1313
1615
|
(child) =>
|
|
1314
1616
|
current.submissions.get(child.childSubmissionId)?.row.state === "settled",
|
|
1315
1617
|
);
|
|
1618
|
+
|
|
1316
1619
|
if (alreadyCovered) {
|
|
1317
1620
|
return [success("resume-immediately" as const), current];
|
|
1318
1621
|
}
|
|
1622
|
+
|
|
1319
1623
|
return [
|
|
1320
1624
|
success("suspended" as const),
|
|
1321
1625
|
withSubmission(current, {
|
|
@@ -1327,7 +1631,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1327
1631
|
];
|
|
1328
1632
|
},
|
|
1329
1633
|
);
|
|
1634
|
+
|
|
1330
1635
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1636
|
+
|
|
1331
1637
|
return decision.value;
|
|
1332
1638
|
}),
|
|
1333
1639
|
);
|
|
@@ -1341,7 +1647,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1341
1647
|
"recordApprovalDecision",
|
|
1342
1648
|
unvalidated,
|
|
1343
1649
|
);
|
|
1650
|
+
|
|
1344
1651
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1652
|
+
|
|
1345
1653
|
const decision = yield* Ref.modify(
|
|
1346
1654
|
state,
|
|
1347
1655
|
(
|
|
@@ -1351,6 +1659,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1351
1659
|
LedgerState,
|
|
1352
1660
|
] => {
|
|
1353
1661
|
const stored = current.submissions.get(command.submissionId);
|
|
1662
|
+
|
|
1354
1663
|
if (stored === undefined) {
|
|
1355
1664
|
return [
|
|
1356
1665
|
failure(
|
|
@@ -1374,6 +1683,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1374
1683
|
current,
|
|
1375
1684
|
];
|
|
1376
1685
|
}
|
|
1686
|
+
|
|
1377
1687
|
return [
|
|
1378
1688
|
failure(
|
|
1379
1689
|
SettlementConflict.make({
|
|
@@ -1385,6 +1695,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1385
1695
|
];
|
|
1386
1696
|
}
|
|
1387
1697
|
const existing = stored.approvalDecisions.get(command.toolCallId);
|
|
1698
|
+
|
|
1388
1699
|
if (existing !== undefined) {
|
|
1389
1700
|
if (existing.decision !== command.decision) {
|
|
1390
1701
|
return [
|
|
@@ -1398,8 +1709,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1398
1709
|
current,
|
|
1399
1710
|
];
|
|
1400
1711
|
}
|
|
1712
|
+
|
|
1401
1713
|
return [success(existing), current];
|
|
1402
1714
|
}
|
|
1715
|
+
|
|
1403
1716
|
const intent = ApprovalDecisionIntent.make({
|
|
1404
1717
|
submissionId: command.submissionId,
|
|
1405
1718
|
toolCallId: command.toolCallId,
|
|
@@ -1408,10 +1721,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1408
1721
|
reason: command.reason,
|
|
1409
1722
|
decidedAt: utc(nowMillis),
|
|
1410
1723
|
});
|
|
1724
|
+
|
|
1411
1725
|
const approvalDecisions = new Map(stored.approvalDecisions).set(
|
|
1412
1726
|
command.toolCallId,
|
|
1413
1727
|
intent,
|
|
1414
1728
|
);
|
|
1729
|
+
|
|
1415
1730
|
// Once every pending call of an ApprovalPending suspension is decided, the lane
|
|
1416
1731
|
// wakes: suspended → input-applied (plan §2.6). A WaitingForChild suspension wakes
|
|
1417
1732
|
// only through recordChildSettled.
|
|
@@ -1422,6 +1737,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1422
1737
|
stored.suspension.reason.toolCallIds.every((toolCallId) =>
|
|
1423
1738
|
approvalDecisions.has(toolCallId),
|
|
1424
1739
|
);
|
|
1740
|
+
|
|
1425
1741
|
return [
|
|
1426
1742
|
success(intent),
|
|
1427
1743
|
withSubmission(current, {
|
|
@@ -1433,7 +1749,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1433
1749
|
];
|
|
1434
1750
|
},
|
|
1435
1751
|
);
|
|
1752
|
+
|
|
1436
1753
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1754
|
+
|
|
1437
1755
|
return decision.value;
|
|
1438
1756
|
}),
|
|
1439
1757
|
);
|
|
@@ -1443,10 +1761,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1443
1761
|
)((unvalidated) =>
|
|
1444
1762
|
Effect.gen(function* () {
|
|
1445
1763
|
const request = yield* validate(MarkUnknownRequest, "markUnknown", unvalidated);
|
|
1764
|
+
|
|
1446
1765
|
const decision = yield* Ref.modify(
|
|
1447
1766
|
state,
|
|
1448
1767
|
(current): readonly [Decision<void, SettlementConflict | LedgerError>, LedgerState] => {
|
|
1449
1768
|
const stored = current.submissions.get(request.submissionId);
|
|
1769
|
+
|
|
1450
1770
|
if (stored === undefined) {
|
|
1451
1771
|
return [
|
|
1452
1772
|
failure(ledgerError("markUnknown", `Unknown Submission ${request.submissionId}`)),
|
|
@@ -1465,6 +1785,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1465
1785
|
current,
|
|
1466
1786
|
];
|
|
1467
1787
|
}
|
|
1788
|
+
|
|
1468
1789
|
return [
|
|
1469
1790
|
failure(
|
|
1470
1791
|
SettlementConflict.make({
|
|
@@ -1492,10 +1813,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1492
1813
|
// set while the first recorded reason is kept.
|
|
1493
1814
|
const existing = stored.unknownMark;
|
|
1494
1815
|
const known = new Set(existing?.toolCallIds ?? []);
|
|
1816
|
+
|
|
1495
1817
|
const merged = [
|
|
1496
1818
|
...(existing?.toolCallIds ?? []),
|
|
1497
1819
|
...request.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
|
|
1498
1820
|
];
|
|
1821
|
+
|
|
1499
1822
|
return [
|
|
1500
1823
|
success(undefined),
|
|
1501
1824
|
withSubmission(current, {
|
|
@@ -1507,6 +1830,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1507
1830
|
];
|
|
1508
1831
|
},
|
|
1509
1832
|
);
|
|
1833
|
+
|
|
1510
1834
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1511
1835
|
}),
|
|
1512
1836
|
);
|
|
@@ -1519,15 +1843,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1519
1843
|
"recordUnknownResolution",
|
|
1520
1844
|
unvalidated,
|
|
1521
1845
|
);
|
|
1846
|
+
|
|
1522
1847
|
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);
|
|
1848
|
+
|
|
1531
1849
|
const decision = yield* Ref.modify(
|
|
1532
1850
|
state,
|
|
1533
1851
|
(
|
|
@@ -1540,6 +1858,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1540
1858
|
LedgerState,
|
|
1541
1859
|
] => {
|
|
1542
1860
|
const stored = current.submissions.get(command.submissionId);
|
|
1861
|
+
|
|
1543
1862
|
if (stored === undefined) {
|
|
1544
1863
|
return [
|
|
1545
1864
|
failure(
|
|
@@ -1563,6 +1882,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1563
1882
|
current,
|
|
1564
1883
|
];
|
|
1565
1884
|
}
|
|
1885
|
+
|
|
1566
1886
|
return [
|
|
1567
1887
|
failure(
|
|
1568
1888
|
SettlementConflict.make({
|
|
@@ -1574,7 +1894,11 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1574
1894
|
];
|
|
1575
1895
|
}
|
|
1576
1896
|
const existing = stored.unknownResolutions.get(command.toolCallId);
|
|
1577
|
-
|
|
1897
|
+
|
|
1898
|
+
if (
|
|
1899
|
+
existing !== undefined &&
|
|
1900
|
+
!equivalentUnknownResolution(existing.intent.resolution, command.resolution)
|
|
1901
|
+
) {
|
|
1578
1902
|
return [
|
|
1579
1903
|
failure(
|
|
1580
1904
|
UnknownResolutionConflict.make({
|
|
@@ -1585,6 +1909,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1585
1909
|
current,
|
|
1586
1910
|
];
|
|
1587
1911
|
}
|
|
1912
|
+
|
|
1588
1913
|
const intent =
|
|
1589
1914
|
existing?.intent ??
|
|
1590
1915
|
UnknownResolutionIntent.make({
|
|
@@ -1595,13 +1920,14 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1595
1920
|
resolution: command.resolution,
|
|
1596
1921
|
resolvedAt: utc(nowMillis),
|
|
1597
1922
|
});
|
|
1923
|
+
|
|
1598
1924
|
const unknownResolutions =
|
|
1599
1925
|
existing !== undefined
|
|
1600
1926
|
? stored.unknownResolutions
|
|
1601
1927
|
: new Map(stored.unknownResolutions).set(command.toolCallId, {
|
|
1602
1928
|
intent,
|
|
1603
|
-
resolutionJson,
|
|
1604
1929
|
});
|
|
1930
|
+
|
|
1605
1931
|
// The lane reopens only when EVERY marked open call has a durable resolution
|
|
1606
1932
|
// intent: unknown → input-applied (DUR-017). Replays re-run the coverage check so
|
|
1607
1933
|
// a recovering caller can wake the lane idempotently.
|
|
@@ -1611,6 +1937,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1611
1937
|
stored.unknownMark.toolCallIds.every((toolCallId) =>
|
|
1612
1938
|
unknownResolutions.has(toolCallId),
|
|
1613
1939
|
);
|
|
1940
|
+
|
|
1614
1941
|
return [
|
|
1615
1942
|
success(intent),
|
|
1616
1943
|
withSubmission(current, {
|
|
@@ -1622,7 +1949,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1622
1949
|
];
|
|
1623
1950
|
},
|
|
1624
1951
|
);
|
|
1952
|
+
|
|
1625
1953
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1954
|
+
|
|
1626
1955
|
return decision.value;
|
|
1627
1956
|
}),
|
|
1628
1957
|
);
|
|
@@ -1636,10 +1965,12 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1636
1965
|
"recordChildSettled",
|
|
1637
1966
|
unvalidated,
|
|
1638
1967
|
);
|
|
1968
|
+
|
|
1639
1969
|
const decision = yield* Ref.modify(
|
|
1640
1970
|
state,
|
|
1641
1971
|
(current): readonly [Decision<ChildSettledOutcome, LedgerError>, LedgerState] => {
|
|
1642
1972
|
const parent = current.submissions.get(request.parentSubmissionId);
|
|
1973
|
+
|
|
1643
1974
|
if (parent === undefined) {
|
|
1644
1975
|
return [
|
|
1645
1976
|
failure(
|
|
@@ -1651,10 +1982,18 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1651
1982
|
current,
|
|
1652
1983
|
];
|
|
1653
1984
|
}
|
|
1654
|
-
// The
|
|
1655
|
-
//
|
|
1985
|
+
// The caller may notify after the canonical Settlement append but before ledger
|
|
1986
|
+
// finalization. In a single store, an exact reservation plus `terminalizing` is the
|
|
1987
|
+
// narrow durable prefix that makes that ordering admissible; earlier states remain
|
|
1988
|
+
// a caller error.
|
|
1656
1989
|
const child = current.submissions.get(request.childSubmissionId);
|
|
1657
|
-
|
|
1990
|
+
|
|
1991
|
+
const announced =
|
|
1992
|
+
child !== undefined &&
|
|
1993
|
+
(child.row.state === "settled" ||
|
|
1994
|
+
(child.row.state === "terminalizing" && child.reservation !== undefined));
|
|
1995
|
+
|
|
1996
|
+
if (!announced) {
|
|
1658
1997
|
return [
|
|
1659
1998
|
failure(
|
|
1660
1999
|
ledgerError(
|
|
@@ -1673,16 +2012,24 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1673
2012
|
return [success("not-waiting" as const), current];
|
|
1674
2013
|
}
|
|
1675
2014
|
const children = parent.suspension.reason.children;
|
|
2015
|
+
|
|
1676
2016
|
if (!children.some((entry) => entry.childSubmissionId === request.childSubmissionId)) {
|
|
1677
2017
|
return [success("not-waiting" as const), current];
|
|
1678
2018
|
}
|
|
1679
|
-
|
|
1680
|
-
//
|
|
1681
|
-
// idempotently.
|
|
1682
|
-
const allSettled = children.every(
|
|
1683
|
-
|
|
1684
|
-
|
|
2019
|
+
|
|
2020
|
+
// Every listed child must be either finalized or canonically announced from the
|
|
2021
|
+
// exact terminalizing reservation. Replays re-run this coverage check idempotently.
|
|
2022
|
+
const allSettled = children.every((entry) => {
|
|
2023
|
+
const listed = current.submissions.get(entry.childSubmissionId);
|
|
2024
|
+
|
|
2025
|
+
return (
|
|
2026
|
+
listed?.row.state === "settled" ||
|
|
2027
|
+
(listed?.row.state === "terminalizing" && listed.reservation !== undefined)
|
|
2028
|
+
);
|
|
2029
|
+
});
|
|
2030
|
+
|
|
1685
2031
|
if (!allSettled) return [success("still-waiting" as const), current];
|
|
2032
|
+
|
|
1686
2033
|
return [
|
|
1687
2034
|
success("woken" as const),
|
|
1688
2035
|
withSubmission(current, {
|
|
@@ -1693,7 +2040,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1693
2040
|
];
|
|
1694
2041
|
},
|
|
1695
2042
|
);
|
|
2043
|
+
|
|
1696
2044
|
if (decision._tag === "failure") return yield* decision.error;
|
|
2045
|
+
|
|
1697
2046
|
return decision.value;
|
|
1698
2047
|
}),
|
|
1699
2048
|
);
|
|
@@ -1707,8 +2056,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1707
2056
|
"reserveChildBudget",
|
|
1708
2057
|
unvalidated,
|
|
1709
2058
|
);
|
|
2059
|
+
|
|
1710
2060
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1711
|
-
|
|
2061
|
+
|
|
1712
2062
|
const decision = yield* Ref.modify(
|
|
1713
2063
|
state,
|
|
1714
2064
|
(
|
|
@@ -1718,6 +2068,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1718
2068
|
LedgerState,
|
|
1719
2069
|
] => {
|
|
1720
2070
|
const existing = current.childReservations.get(request.reservationId);
|
|
2071
|
+
|
|
1721
2072
|
if (existing !== undefined) {
|
|
1722
2073
|
// Identical replays short-circuit before the fence, mirroring reserveSettlement:
|
|
1723
2074
|
// a replay creates nothing, so a recovering caller resumes rather than duplicates.
|
|
@@ -1725,7 +2076,8 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1725
2076
|
existing.parentSubmissionId === request.parentSubmissionId &&
|
|
1726
2077
|
existing.parentToolCallId === request.parentToolCallId &&
|
|
1727
2078
|
existing.allocationDigest === request.allocationDigest &&
|
|
1728
|
-
existing.
|
|
2079
|
+
equivalentPersistedJson(existing.allocation, request.allocation);
|
|
2080
|
+
|
|
1729
2081
|
if (!identical) {
|
|
1730
2082
|
return [
|
|
1731
2083
|
failure(
|
|
@@ -1739,6 +2091,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1739
2091
|
current,
|
|
1740
2092
|
];
|
|
1741
2093
|
}
|
|
2094
|
+
|
|
1742
2095
|
return [
|
|
1743
2096
|
success(
|
|
1744
2097
|
ReservedChildBudget.make({
|
|
@@ -1767,6 +2120,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1767
2120
|
}
|
|
1768
2121
|
}
|
|
1769
2122
|
const parent = current.submissions.get(request.parentSubmissionId);
|
|
2123
|
+
|
|
1770
2124
|
if (parent === undefined) {
|
|
1771
2125
|
return [
|
|
1772
2126
|
failure(
|
|
@@ -1780,9 +2134,10 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1780
2134
|
}
|
|
1781
2135
|
// Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
|
|
1782
2136
|
// parent Attempt can never create new reservation state.
|
|
1783
|
-
if (!ownsLane(parent, request.ownershipToken)) {
|
|
2137
|
+
if (!ownsLane(current, parent, request.ownershipToken)) {
|
|
1784
2138
|
return [failure(ownershipLost(current, parent)), current];
|
|
1785
2139
|
}
|
|
2140
|
+
|
|
1786
2141
|
const reservation: StoredChildReservation = {
|
|
1787
2142
|
reservationId: request.reservationId,
|
|
1788
2143
|
parentSubmissionId: request.parentSubmissionId,
|
|
@@ -1790,14 +2145,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1790
2145
|
childSubmissionId: undefined,
|
|
1791
2146
|
status: "reserved",
|
|
1792
2147
|
allocation: request.allocation,
|
|
1793
|
-
allocationJson,
|
|
1794
2148
|
allocationDigest: request.allocationDigest,
|
|
1795
2149
|
accounting: undefined,
|
|
1796
|
-
accountingJson: undefined,
|
|
1797
2150
|
reservedAtMillis: nowMillis,
|
|
1798
2151
|
releaseBeganAtMillis: undefined,
|
|
1799
2152
|
releasedAtMillis: undefined,
|
|
1800
2153
|
};
|
|
2154
|
+
|
|
1801
2155
|
return [
|
|
1802
2156
|
success(
|
|
1803
2157
|
ReservedChildBudget.make({
|
|
@@ -1809,7 +2163,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1809
2163
|
];
|
|
1810
2164
|
},
|
|
1811
2165
|
);
|
|
2166
|
+
|
|
1812
2167
|
if (decision._tag === "failure") return yield* decision.error;
|
|
2168
|
+
|
|
1813
2169
|
return decision.value;
|
|
1814
2170
|
}),
|
|
1815
2171
|
);
|
|
@@ -1822,6 +2178,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1822
2178
|
"attachChildToReservation",
|
|
1823
2179
|
unvalidated,
|
|
1824
2180
|
);
|
|
2181
|
+
|
|
1825
2182
|
const decision = yield* Ref.modify(
|
|
1826
2183
|
state,
|
|
1827
2184
|
(
|
|
@@ -1834,6 +2191,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1834
2191
|
LedgerState,
|
|
1835
2192
|
] => {
|
|
1836
2193
|
const reservation = current.childReservations.get(request.reservationId);
|
|
2194
|
+
|
|
1837
2195
|
if (reservation === undefined) {
|
|
1838
2196
|
return [
|
|
1839
2197
|
failure(
|
|
@@ -1850,6 +2208,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1850
2208
|
if (reservation.childSubmissionId === request.childSubmissionId) {
|
|
1851
2209
|
return [success(toReservationSnapshot(reservation)), current];
|
|
1852
2210
|
}
|
|
2211
|
+
|
|
1853
2212
|
return [
|
|
1854
2213
|
failure(
|
|
1855
2214
|
ChildReservationConflict.make({
|
|
@@ -1862,6 +2221,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1862
2221
|
];
|
|
1863
2222
|
}
|
|
1864
2223
|
const parent = current.submissions.get(reservation.parentSubmissionId);
|
|
2224
|
+
|
|
1865
2225
|
if (parent === undefined) {
|
|
1866
2226
|
return [
|
|
1867
2227
|
failure(
|
|
@@ -1873,7 +2233,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1873
2233
|
current,
|
|
1874
2234
|
];
|
|
1875
2235
|
}
|
|
1876
|
-
if (!ownsLane(parent, request.ownershipToken)) {
|
|
2236
|
+
if (!ownsLane(current, parent, request.ownershipToken)) {
|
|
1877
2237
|
return [failure(ownershipLost(current, parent)), current];
|
|
1878
2238
|
}
|
|
1879
2239
|
if (reservation.status !== "reserved") {
|
|
@@ -1901,17 +2261,21 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1901
2261
|
current,
|
|
1902
2262
|
];
|
|
1903
2263
|
}
|
|
2264
|
+
|
|
1904
2265
|
const attached: StoredChildReservation = {
|
|
1905
2266
|
...reservation,
|
|
1906
2267
|
childSubmissionId: request.childSubmissionId,
|
|
1907
2268
|
};
|
|
2269
|
+
|
|
1908
2270
|
return [
|
|
1909
2271
|
success(toReservationSnapshot(attached)),
|
|
1910
2272
|
withChildReservation(current, attached),
|
|
1911
2273
|
];
|
|
1912
2274
|
},
|
|
1913
2275
|
);
|
|
2276
|
+
|
|
1914
2277
|
if (decision._tag === "failure") return yield* decision.error;
|
|
2278
|
+
|
|
1915
2279
|
return decision.value;
|
|
1916
2280
|
}),
|
|
1917
2281
|
);
|
|
@@ -1924,8 +2288,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1924
2288
|
"beginChildBudgetRelease",
|
|
1925
2289
|
unvalidated,
|
|
1926
2290
|
);
|
|
2291
|
+
|
|
1927
2292
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
1928
|
-
|
|
2293
|
+
|
|
1929
2294
|
const decision = yield* Ref.modify(
|
|
1930
2295
|
state,
|
|
1931
2296
|
(
|
|
@@ -1935,6 +2300,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1935
2300
|
LedgerState,
|
|
1936
2301
|
] => {
|
|
1937
2302
|
const reservation = current.childReservations.get(request.reservationId);
|
|
2303
|
+
|
|
1938
2304
|
if (reservation === undefined) {
|
|
1939
2305
|
return [
|
|
1940
2306
|
failure(
|
|
@@ -1949,9 +2315,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1949
2315
|
if (reservation.status !== "reserved") {
|
|
1950
2316
|
// The accounting decision was already frozen exactly once; an identical replay is
|
|
1951
2317
|
// a no-op and a divergent decision conflicts (spec §12 join step 6).
|
|
1952
|
-
if (
|
|
2318
|
+
if (
|
|
2319
|
+
reservation.accounting !== undefined &&
|
|
2320
|
+
equivalentPersistedJson(reservation.accounting, request.accounting)
|
|
2321
|
+
) {
|
|
1953
2322
|
return [success(toReservationSnapshot(reservation)), current];
|
|
1954
2323
|
}
|
|
2324
|
+
|
|
1955
2325
|
return [
|
|
1956
2326
|
failure(
|
|
1957
2327
|
ChildReservationConflict.make({
|
|
@@ -1964,20 +2334,23 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1964
2334
|
current,
|
|
1965
2335
|
];
|
|
1966
2336
|
}
|
|
2337
|
+
|
|
1967
2338
|
const frozen: StoredChildReservation = {
|
|
1968
2339
|
...reservation,
|
|
1969
2340
|
status: "releasePending",
|
|
1970
2341
|
accounting: request.accounting,
|
|
1971
|
-
accountingJson,
|
|
1972
2342
|
releaseBeganAtMillis: nowMillis,
|
|
1973
2343
|
};
|
|
2344
|
+
|
|
1974
2345
|
return [
|
|
1975
2346
|
success(toReservationSnapshot(frozen)),
|
|
1976
2347
|
withChildReservation(current, frozen),
|
|
1977
2348
|
];
|
|
1978
2349
|
},
|
|
1979
2350
|
);
|
|
2351
|
+
|
|
1980
2352
|
if (decision._tag === "failure") return yield* decision.error;
|
|
2353
|
+
|
|
1981
2354
|
return decision.value;
|
|
1982
2355
|
}),
|
|
1983
2356
|
);
|
|
@@ -1991,7 +2364,9 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
1991
2364
|
"releaseChildBudget",
|
|
1992
2365
|
unvalidated,
|
|
1993
2366
|
);
|
|
2367
|
+
|
|
1994
2368
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
2369
|
+
|
|
1995
2370
|
const decision = yield* Ref.modify(
|
|
1996
2371
|
state,
|
|
1997
2372
|
(
|
|
@@ -2001,6 +2376,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2001
2376
|
LedgerState,
|
|
2002
2377
|
] => {
|
|
2003
2378
|
const reservation = current.childReservations.get(request.reservationId);
|
|
2379
|
+
|
|
2004
2380
|
if (reservation === undefined) {
|
|
2005
2381
|
return [
|
|
2006
2382
|
failure(
|
|
@@ -2030,18 +2406,22 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2030
2406
|
current,
|
|
2031
2407
|
];
|
|
2032
2408
|
}
|
|
2409
|
+
|
|
2033
2410
|
const released: StoredChildReservation = {
|
|
2034
2411
|
...reservation,
|
|
2035
2412
|
status: "released",
|
|
2036
2413
|
releasedAtMillis: nowMillis,
|
|
2037
2414
|
};
|
|
2415
|
+
|
|
2038
2416
|
return [
|
|
2039
2417
|
success(toReservationSnapshot(released)),
|
|
2040
2418
|
withChildReservation(current, released),
|
|
2041
2419
|
];
|
|
2042
2420
|
},
|
|
2043
2421
|
);
|
|
2422
|
+
|
|
2044
2423
|
if (decision._tag === "failure") return yield* decision.error;
|
|
2424
|
+
|
|
2045
2425
|
return decision.value;
|
|
2046
2426
|
}),
|
|
2047
2427
|
);
|
|
@@ -2052,18 +2432,32 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2052
2432
|
const snapshots = [...current.submissions.values()]
|
|
2053
2433
|
.filter((stored) => stored.row.state !== "settled")
|
|
2054
2434
|
.sort((left, right) =>
|
|
2055
|
-
left.row.
|
|
2435
|
+
left.row.threadId < right.row.threadId
|
|
2056
2436
|
? -1
|
|
2057
|
-
: left.row.
|
|
2437
|
+
: left.row.threadId > right.row.threadId
|
|
2058
2438
|
? 1
|
|
2059
2439
|
: left.row.queueSequence - right.row.queueSequence,
|
|
2060
2440
|
)
|
|
2061
2441
|
.map((stored) => toSnapshot(stored.row));
|
|
2442
|
+
|
|
2062
2443
|
return Stream.fromIterable(snapshots);
|
|
2063
2444
|
}),
|
|
2064
2445
|
),
|
|
2065
2446
|
);
|
|
2066
2447
|
|
|
2448
|
+
const readAbortIntent: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
|
|
2449
|
+
"MemorySubmissionLedger.readAbortIntent",
|
|
2450
|
+
)(function* (unvalidated) {
|
|
2451
|
+
const request = yield* validate(AbortIntentRequest, "readAbortIntent", unvalidated);
|
|
2452
|
+
const stored = (yield* Ref.get(state)).submissions.get(request.submissionId);
|
|
2453
|
+
|
|
2454
|
+
if (stored === undefined) {
|
|
2455
|
+
return yield* ledgerError("readAbortIntent", `Unknown Submission ${request.submissionId}`);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
return stored.abortIntent;
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2067
2461
|
const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
|
|
2068
2462
|
"MemorySubmissionLedger.loadRecoverySnapshot",
|
|
2069
2463
|
)((unvalidated) =>
|
|
@@ -2073,14 +2467,17 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2073
2467
|
"loadRecoverySnapshot",
|
|
2074
2468
|
unvalidated,
|
|
2075
2469
|
);
|
|
2470
|
+
|
|
2076
2471
|
const current = yield* Ref.get(state);
|
|
2077
2472
|
const stored = current.submissions.get(request.submissionId);
|
|
2473
|
+
|
|
2078
2474
|
if (stored === undefined) {
|
|
2079
2475
|
return yield* ledgerError(
|
|
2080
2476
|
"loadRecoverySnapshot",
|
|
2081
2477
|
`Unknown Submission ${request.submissionId}`,
|
|
2082
2478
|
);
|
|
2083
2479
|
}
|
|
2480
|
+
|
|
2084
2481
|
const joins = [...current.submissions.values()]
|
|
2085
2482
|
.filter((candidate) => candidate.joinedHostSubmissionId === request.submissionId)
|
|
2086
2483
|
.sort((left, right) => left.row.queueSequence - right.row.queueSequence)
|
|
@@ -2091,11 +2488,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2091
2488
|
hostSubmissionId: request.submissionId,
|
|
2092
2489
|
}),
|
|
2093
2490
|
);
|
|
2491
|
+
|
|
2094
2492
|
const byToolCallId = <A extends { readonly toolCallId: ToolCallId }>(
|
|
2095
2493
|
left: A,
|
|
2096
2494
|
right: A,
|
|
2097
2495
|
): number =>
|
|
2098
2496
|
left.toolCallId < right.toolCallId ? -1 : left.toolCallId > right.toolCallId ? 1 : 0;
|
|
2497
|
+
|
|
2099
2498
|
// Parent-side subagent view: this Submission's child budget reservations in parent Tool
|
|
2100
2499
|
// Call order, plus each attached child's current lane state (a disposable derived view;
|
|
2101
2500
|
// the canonical records stay the recovery truth, DUR-015).
|
|
@@ -2108,10 +2507,13 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2108
2507
|
? 1
|
|
2109
2508
|
: 0,
|
|
2110
2509
|
);
|
|
2510
|
+
|
|
2111
2511
|
const childAttachments: Array<ChildAttachmentSnapshot> = [];
|
|
2512
|
+
|
|
2112
2513
|
for (const reservation of childReservations) {
|
|
2113
2514
|
if (reservation.childSubmissionId === undefined) continue;
|
|
2114
2515
|
const child = current.submissions.get(reservation.childSubmissionId);
|
|
2516
|
+
|
|
2115
2517
|
if (child === undefined) continue;
|
|
2116
2518
|
childAttachments.push(
|
|
2117
2519
|
ChildAttachmentSnapshot.make({
|
|
@@ -2124,6 +2526,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2124
2526
|
}),
|
|
2125
2527
|
);
|
|
2126
2528
|
}
|
|
2529
|
+
|
|
2127
2530
|
return RecoverySnapshot.make({
|
|
2128
2531
|
submission: toSnapshot(stored.row),
|
|
2129
2532
|
joins,
|
|
@@ -2201,6 +2604,7 @@ const makeSubmissionLedger = (options: MemorySubmissionLedgerOptions = {}) =>
|
|
|
2201
2604
|
releaseChildBudget,
|
|
2202
2605
|
scanNonterminal,
|
|
2203
2606
|
loadRecoverySnapshot,
|
|
2607
|
+
readAbortIntent,
|
|
2204
2608
|
});
|
|
2205
2609
|
});
|
|
2206
2610
|
|