@effect-agent/storage-cloudflare 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/DoMemoryStore.d.mts +29 -0
- package/dist/DoMemoryStore.mjs +55 -0
- package/dist/DoMemoryStore.mjs.map +1 -0
- package/dist/DoMessageDeliveryStore.d.mts +17 -0
- package/dist/DoMessageDeliveryStore.mjs +32 -0
- package/dist/DoMessageDeliveryStore.mjs.map +1 -0
- package/dist/DoScheduleStore.d.mts +36 -0
- package/dist/DoScheduleStore.mjs +388 -0
- package/dist/DoScheduleStore.mjs.map +1 -0
- package/dist/DoStorageConfig.d.mts +45 -0
- package/dist/DoStorageConfig.mjs +52 -0
- package/dist/DoStorageConfig.mjs.map +1 -0
- package/dist/DoStorageError-DjpuLHmX.d.mts +101 -0
- package/dist/DoStorageError.d.mts +2 -0
- package/dist/DoStorageError.mjs +167 -0
- package/dist/DoStorageError.mjs.map +1 -0
- package/dist/DoStorageFailpoint.d.mts +17 -0
- package/dist/DoStorageFailpoint.mjs +14 -0
- package/dist/DoStorageFailpoint.mjs.map +1 -0
- package/dist/DoStorageFailpointTesting.d.mts +33 -0
- package/dist/DoStorageFailpointTesting.mjs +35 -0
- package/dist/DoStorageFailpointTesting.mjs.map +1 -0
- package/dist/DoStorageVersion-DJwZzqel.d.mts +12 -0
- package/dist/DoStorageVersion.d.mts +2 -0
- package/dist/DoStorageVersion.mjs +8 -0
- package/dist/DoStorageVersion.mjs.map +1 -0
- package/dist/DoSubmissionLedger.d.mts +23 -0
- package/dist/DoSubmissionLedger.mjs +1939 -0
- package/dist/DoSubmissionLedger.mjs.map +1 -0
- package/dist/DoSubscriptionStore.d.mts +25 -0
- package/dist/DoSubscriptionStore.mjs +183 -0
- package/dist/DoSubscriptionStore.mjs.map +1 -0
- package/dist/DoThreadStore.d.mts +59 -0
- package/dist/DoThreadStore.mjs +499 -0
- package/dist/DoThreadStore.mjs.map +1 -0
- package/dist/MemoryProtocol.d.mts +38160 -0
- package/dist/MemoryProtocol.mjs +219 -0
- package/dist/MemoryProtocol.mjs.map +1 -0
- package/dist/PortProtocol.d.mts +5521 -0
- package/dist/PortProtocol.mjs +206 -0
- package/dist/PortProtocol.mjs.map +1 -0
- package/dist/PortRouting.d.mts +86 -0
- package/dist/PortRouting.mjs +461 -0
- package/dist/PortRouting.mjs.map +1 -0
- package/dist/do-journal-D65Iuln6.mjs +1119 -0
- package/dist/do-journal-D65Iuln6.mjs.map +1 -0
- package/dist/index.d.mts +14 -1525
- package/dist/index.mjs +14 -3695
- package/dist/migrations-Br6QqMIf.mjs +306 -0
- package/dist/migrations-Br6QqMIf.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -45
- package/src/DoMemoryStore.ts +56 -0
- package/src/DoMessageDeliveryStore.ts +53 -0
- package/src/DoScheduleStore.ts +622 -0
- package/src/{errors.ts → DoStorageError.ts} +13 -3
- package/src/DoStorageFailpoint.ts +20 -0
- package/src/{do-storage-failpoint.ts → DoStorageFailpointTesting.ts} +5 -18
- package/src/DoStorageVersion.ts +2 -0
- package/src/{do-ledger.ts → DoSubmissionLedger.ts} +1119 -262
- package/src/DoSubscriptionStore.ts +329 -0
- package/src/DoThreadStore.ts +1064 -0
- package/src/MemoryProtocol.ts +351 -0
- package/src/{port-protocol.ts → PortProtocol.ts} +103 -38
- package/src/{routing.ts → PortRouting.ts} +440 -264
- package/src/index.ts +13 -32
- package/src/internal/do-journal.ts +1789 -0
- package/src/internal/message-delivery-schema.ts +24 -0
- package/src/{migrations.ts → internal/migrations.ts} +72 -35
- package/src/internal/recovery-checkpoint-schema.ts +17 -0
- package/dist/index.mjs.map +0 -1
- package/src/do-conversation-store.ts +0 -850
- package/src/do-journal.ts +0 -1078
- /package/src/{do-storage-config.ts → DoStorageConfig.ts} +0 -0
|
@@ -1,18 +1,40 @@
|
|
|
1
|
+
import { BrowserCrypto } from "@effect/platform-browser";
|
|
2
|
+
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
3
|
+
import { Clock, Context, Crypto, DateTime, Effect, Layer, Option, Schema, Stream } from "effect";
|
|
4
|
+
import { EMPTY_TAIL_DIGEST } from "effect-agent/digest";
|
|
5
|
+
import { InputMessage } from "effect-agent/messaging";
|
|
6
|
+
import {
|
|
7
|
+
ApprovalDecision,
|
|
8
|
+
CanonicalSequence,
|
|
9
|
+
DefinitionDigests,
|
|
10
|
+
Digest,
|
|
11
|
+
PersistedJson,
|
|
12
|
+
WorkerAdmission,
|
|
13
|
+
ProducerEpoch,
|
|
14
|
+
RecordEnvelope,
|
|
15
|
+
SettlementOutcome,
|
|
16
|
+
} from "effect-agent/records";
|
|
1
17
|
import {
|
|
2
18
|
AbortCommand,
|
|
19
|
+
WorkerStopCommand,
|
|
20
|
+
WorkerLedgerState,
|
|
21
|
+
workerTerminalFromRecord,
|
|
3
22
|
AbortIntent,
|
|
23
|
+
AbortIntentRequest,
|
|
4
24
|
AdmissionAdmitted,
|
|
5
25
|
AdmissionConflict,
|
|
26
|
+
AdmissionFence,
|
|
27
|
+
AdmissionGroup,
|
|
28
|
+
AdmissionPolicyError,
|
|
6
29
|
AdmissionNotAdmitted,
|
|
7
30
|
AdmissionRequest,
|
|
31
|
+
SubmissionAdmissionFence,
|
|
8
32
|
AdmissionResult,
|
|
9
33
|
ApprovalConflict,
|
|
10
|
-
ApprovalDecision,
|
|
11
34
|
ApprovalDecisionCommand,
|
|
12
35
|
ApprovalDecisionIntent,
|
|
13
36
|
AttachChildToReservationRequest,
|
|
14
37
|
BeginChildBudgetReleaseRequest,
|
|
15
|
-
CanonicalSequence,
|
|
16
38
|
ChildAttachmentSnapshot,
|
|
17
39
|
ChildBudgetReservationRequest,
|
|
18
40
|
ChildBudgetReservationSnapshot,
|
|
@@ -22,9 +44,6 @@ import {
|
|
|
22
44
|
Claim,
|
|
23
45
|
ClaimJoiningRequest,
|
|
24
46
|
ClaimRequest,
|
|
25
|
-
DefinitionDigests,
|
|
26
|
-
Digest,
|
|
27
|
-
EMPTY_TAIL_DIGEST,
|
|
28
47
|
InputAppliedMarker,
|
|
29
48
|
JoinSnapshot,
|
|
30
49
|
JoinedToHost,
|
|
@@ -39,10 +58,7 @@ import {
|
|
|
39
58
|
OwnershipRenewal,
|
|
40
59
|
OwnershipSnapshot,
|
|
41
60
|
ParentLinkage,
|
|
42
|
-
PersistedJson,
|
|
43
|
-
ProducerEpoch,
|
|
44
61
|
QueueSequence,
|
|
45
|
-
RecordEnvelope,
|
|
46
62
|
RecoverySnapshot,
|
|
47
63
|
RecoverySnapshotRequest,
|
|
48
64
|
ReleaseChildBudgetRequest,
|
|
@@ -54,14 +70,15 @@ import {
|
|
|
54
70
|
Settlement,
|
|
55
71
|
SettlementConflict,
|
|
56
72
|
SettlementFinalization,
|
|
57
|
-
SettlementOutcome,
|
|
58
73
|
SettlementReservation,
|
|
59
74
|
SubmissionLedger,
|
|
60
75
|
SubmissionLookup,
|
|
61
76
|
SubmissionLookupByKey,
|
|
62
77
|
SubmissionSnapshot,
|
|
78
|
+
SubmissionWorkItem,
|
|
63
79
|
SubmissionState,
|
|
64
80
|
SettlementReservationSnapshot,
|
|
81
|
+
settlementFailureFromRecord,
|
|
65
82
|
SuspendRequest,
|
|
66
83
|
SuspensionReason,
|
|
67
84
|
SuspensionSnapshot,
|
|
@@ -72,28 +89,26 @@ import {
|
|
|
72
89
|
submissionAbortRecordId,
|
|
73
90
|
type ChildSettledOutcome,
|
|
74
91
|
type SuspensionOutcome,
|
|
75
|
-
} from "
|
|
76
|
-
import {
|
|
77
|
-
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
78
|
-
import { Clock, Context, Crypto, DateTime, Effect, Layer, Option, Schema, Stream } from "effect";
|
|
92
|
+
} from "effect-agent/submission-ledger";
|
|
93
|
+
import { ThreadStoreDiagnostic } from "effect-agent/thread-store";
|
|
79
94
|
import * as SqlClientService from "effect/unstable/sql/SqlClient";
|
|
80
95
|
import type { SqlError } from "effect/unstable/sql/SqlError";
|
|
81
96
|
|
|
82
|
-
import {
|
|
83
|
-
storageConfigLayer,
|
|
84
|
-
storageFailpointLayer,
|
|
85
|
-
type DoStorageInitializationError,
|
|
86
|
-
type DoStorageOptions,
|
|
87
|
-
} from "./do-conversation-store.ts";
|
|
88
|
-
import { decodeRows, initializeDoJournal } from "./do-journal.ts";
|
|
89
|
-
import { DoStorageConfig } from "./do-storage-config.ts";
|
|
90
|
-
import { DoStorageFailpoint } from "./do-storage-failpoint.ts";
|
|
97
|
+
import { DoStorageConfig } from "./DoStorageConfig.ts";
|
|
91
98
|
import {
|
|
92
99
|
DoLedgerError,
|
|
93
100
|
DoStorageCorruptionError,
|
|
94
101
|
DoStorageError,
|
|
95
102
|
type DoStorageFailpointLocation,
|
|
96
|
-
} from "./
|
|
103
|
+
} from "./DoStorageError.ts";
|
|
104
|
+
import { DoStorageFailpoint } from "./DoStorageFailpoint.ts";
|
|
105
|
+
import {
|
|
106
|
+
storageConfigLayer,
|
|
107
|
+
storageFailpointLayer,
|
|
108
|
+
type DoStorageInitializationError,
|
|
109
|
+
type DoStorageOptions,
|
|
110
|
+
} from "./DoThreadStore.ts";
|
|
111
|
+
import { decodeRows, initializeDoJournal } from "./internal/do-journal.ts";
|
|
97
112
|
|
|
98
113
|
type SubmissionId = SubmissionSnapshot["submissionId"];
|
|
99
114
|
|
|
@@ -107,11 +122,16 @@ const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
|
|
|
107
122
|
|
|
108
123
|
const SCAN_PAGE_SIZE = 256;
|
|
109
124
|
const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
|
|
125
|
+
const RESUME_IMMEDIATELY: SuspensionOutcome = "resume-immediately";
|
|
126
|
+
const SUSPENDED: SuspensionOutcome = "suspended";
|
|
127
|
+
const NOT_WAITING: ChildSettledOutcome = "not-waiting";
|
|
128
|
+
const STILL_WAITING: ChildSettledOutcome = "still-waiting";
|
|
129
|
+
const WOKEN: ChildSettledOutcome = "woken";
|
|
110
130
|
const MAX_IDENTIFIER_LENGTH = 1_024;
|
|
111
131
|
|
|
112
132
|
class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
|
|
113
133
|
submission_id: BoundedIdentifier,
|
|
114
|
-
|
|
134
|
+
thread_id: BoundedIdentifier,
|
|
115
135
|
queue_sequence: QueueSequence,
|
|
116
136
|
principal: BoundedIdentifier,
|
|
117
137
|
idempotency_key: BoundedIdentifier,
|
|
@@ -134,6 +154,10 @@ class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
|
|
|
134
154
|
unknown_tool_call_ids_json: Schema.NullOr(BoundedStoredText),
|
|
135
155
|
parent_submission_id: Schema.NullOr(BoundedIdentifier),
|
|
136
156
|
parent_tool_call_id: Schema.NullOr(BoundedIdentifier),
|
|
157
|
+
admission_group: Schema.NullOr(AdmissionGroup),
|
|
158
|
+
admission_fence_json: Schema.NullOr(BoundedStoredText),
|
|
159
|
+
worker_admission_json: Schema.NullOr(BoundedStoredText),
|
|
160
|
+
message_admission_json: Schema.NullOr(BoundedStoredText),
|
|
137
161
|
}) {}
|
|
138
162
|
|
|
139
163
|
class ChildReservationRow extends Schema.Class<ChildReservationRow>("ChildReservationRow")({
|
|
@@ -209,13 +233,22 @@ class MaxQueueSequenceRow extends Schema.Class<MaxQueueSequenceRow>("MaxQueueSeq
|
|
|
209
233
|
max_queue_sequence: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
210
234
|
}) {}
|
|
211
235
|
|
|
236
|
+
class AbortIntentLookupRow extends Schema.Class<AbortIntentLookupRow>("AbortIntentLookupRow")({
|
|
237
|
+
submission_id: BoundedIdentifier,
|
|
238
|
+
abort_submission_id: Schema.NullOr(BoundedIdentifier),
|
|
239
|
+
author: Schema.NullOr(BoundedIdentifier),
|
|
240
|
+
reason: Schema.NullOr(BoundedStoredText),
|
|
241
|
+
requested_at: Schema.NullOr(BoundedTimestamp),
|
|
242
|
+
canonical_record_id: Schema.NullOr(BoundedIdentifier),
|
|
243
|
+
}) {}
|
|
244
|
+
|
|
212
245
|
class CanonicalRecordIdRow extends Schema.Class<CanonicalRecordIdRow>("CanonicalRecordIdRow")({
|
|
213
246
|
record_id: BoundedIdentifier,
|
|
214
247
|
}) {}
|
|
215
248
|
|
|
216
249
|
const SUBMISSION_COLUMNS = `
|
|
217
250
|
submission_id,
|
|
218
|
-
|
|
251
|
+
thread_id,
|
|
219
252
|
queue_sequence,
|
|
220
253
|
principal,
|
|
221
254
|
idempotency_key,
|
|
@@ -237,7 +270,11 @@ const SUBMISSION_COLUMNS = `
|
|
|
237
270
|
unknown_reason,
|
|
238
271
|
unknown_tool_call_ids_json,
|
|
239
272
|
parent_submission_id,
|
|
240
|
-
parent_tool_call_id
|
|
273
|
+
parent_tool_call_id,
|
|
274
|
+
admission_group,
|
|
275
|
+
admission_fence_json,
|
|
276
|
+
worker_admission_json,
|
|
277
|
+
message_admission_json
|
|
241
278
|
`;
|
|
242
279
|
|
|
243
280
|
const CHILD_RESERVATION_COLUMNS = `
|
|
@@ -254,7 +291,7 @@ const CHILD_RESERVATION_COLUMNS = `
|
|
|
254
291
|
released_at
|
|
255
292
|
`;
|
|
256
293
|
|
|
257
|
-
/** The branded ToolCallId schema, reached through the
|
|
294
|
+
/** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
|
|
258
295
|
const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
|
|
259
296
|
const ToolCallIdList = Schema.Array(ToolCallIdSchema);
|
|
260
297
|
|
|
@@ -284,10 +321,15 @@ const decodeSuspensionSnapshot = Schema.decodeUnknownEffect(SuspensionSnapshot);
|
|
|
284
321
|
const decodeApprovalDecisionIntent = Schema.decodeUnknownEffect(ApprovalDecisionIntent);
|
|
285
322
|
const decodeUnknownResolutionIntent = Schema.decodeUnknownEffect(UnknownResolutionIntent);
|
|
286
323
|
const decodeParentLinkage = Schema.decodeUnknownEffect(ParentLinkage);
|
|
324
|
+
|
|
287
325
|
const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
|
|
288
326
|
ChildBudgetReservationSnapshot,
|
|
289
327
|
);
|
|
328
|
+
|
|
290
329
|
const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
|
|
330
|
+
const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
|
|
331
|
+
const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
|
|
332
|
+
const isDoStorageError = Schema.is(DoStorageError);
|
|
291
333
|
|
|
292
334
|
/** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
|
|
293
335
|
const internalFailure =
|
|
@@ -319,6 +361,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
319
361
|
const failpoint = yield* DoStorageFailpoint;
|
|
320
362
|
const sql = yield* SqlClientService.SqlClient;
|
|
321
363
|
const crypto = yield* Crypto.Crypto;
|
|
364
|
+
const admissionFence = yield* SubmissionAdmissionFence;
|
|
322
365
|
const journal = yield* initializeDoJournal(sql, failpoint.hit, config.maxStoredValueBytes);
|
|
323
366
|
|
|
324
367
|
const hitFailpoint = (
|
|
@@ -336,6 +379,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
336
379
|
A,
|
|
337
380
|
E extends
|
|
338
381
|
| AdmissionConflict
|
|
382
|
+
| AdmissionPolicyError
|
|
339
383
|
| ApprovalConflict
|
|
340
384
|
| ChildReservationConflict
|
|
341
385
|
| JoinedToHost
|
|
@@ -351,7 +395,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
351
395
|
.withWriteTransaction(operation)(effect)
|
|
352
396
|
.pipe(
|
|
353
397
|
Effect.mapError((error) =>
|
|
354
|
-
error
|
|
398
|
+
isDoStorageError(error) ? internalFailure(operation)(error) : error,
|
|
355
399
|
),
|
|
356
400
|
);
|
|
357
401
|
|
|
@@ -385,7 +429,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
385
429
|
FROM effect_agent_submissions
|
|
386
430
|
WHERE submission_id = ${submissionId}
|
|
387
431
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
432
|
+
|
|
388
433
|
const decoded = yield* decodeSubmissionRows(operation, submissionId, rows);
|
|
434
|
+
|
|
389
435
|
if (decoded.length > 1) {
|
|
390
436
|
return yield* corruptionFailure(
|
|
391
437
|
operation,
|
|
@@ -394,6 +440,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
394
440
|
"A submission primary key returned more than one row.",
|
|
395
441
|
);
|
|
396
442
|
}
|
|
443
|
+
|
|
397
444
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
398
445
|
});
|
|
399
446
|
|
|
@@ -402,12 +449,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
402
449
|
submissionId: string,
|
|
403
450
|
): Effect.fn.Return<SubmissionRow, LedgerError> {
|
|
404
451
|
const submission = yield* readSubmission(operation, submissionId);
|
|
452
|
+
|
|
405
453
|
if (Option.isNone(submission)) {
|
|
406
454
|
return yield* LedgerError.make({
|
|
407
455
|
operation,
|
|
408
456
|
message: `Unknown submission ${submissionId}.`,
|
|
409
457
|
});
|
|
410
458
|
}
|
|
459
|
+
|
|
411
460
|
return submission.value;
|
|
412
461
|
});
|
|
413
462
|
|
|
@@ -426,12 +475,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
426
475
|
FROM effect_agent_submission_ownership
|
|
427
476
|
WHERE submission_id = ${submissionId}
|
|
428
477
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
478
|
+
|
|
429
479
|
const decoded = yield* decodeRows(
|
|
430
480
|
Schema.Array(OwnershipRow),
|
|
431
481
|
"effect_agent_submission_ownership",
|
|
432
482
|
submissionId,
|
|
433
483
|
rows,
|
|
434
484
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
485
|
+
|
|
435
486
|
if (decoded.length > 1) {
|
|
436
487
|
return yield* corruptionFailure(
|
|
437
488
|
operation,
|
|
@@ -440,23 +491,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
440
491
|
"An ownership primary key returned more than one row.",
|
|
441
492
|
);
|
|
442
493
|
}
|
|
494
|
+
|
|
443
495
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
444
496
|
});
|
|
445
497
|
|
|
446
|
-
const
|
|
498
|
+
const threadEpoch = Effect.fn("DoSubmissionLedger.threadEpoch")(function* (
|
|
447
499
|
operation: string,
|
|
448
|
-
|
|
500
|
+
threadId: string,
|
|
449
501
|
): Effect.fn.Return<ProducerEpoch, LedgerError> {
|
|
450
|
-
const
|
|
451
|
-
.
|
|
502
|
+
const threads = yield* journal
|
|
503
|
+
.getThread(threadId)
|
|
452
504
|
.pipe(Effect.mapError(internalFailure(operation)));
|
|
453
|
-
|
|
505
|
+
|
|
506
|
+
return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
|
|
454
507
|
});
|
|
455
508
|
|
|
456
509
|
/**
|
|
457
510
|
* Verify inside the surrounding write transaction that the presented token still owns the
|
|
458
511
|
* Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
|
|
459
|
-
*
|
|
512
|
+
* Thread's current producer epoch (DUR-006).
|
|
460
513
|
*/
|
|
461
514
|
const requireOwnership = Effect.fn("DoSubmissionLedger.requireOwnership")(function* (
|
|
462
515
|
operation: string,
|
|
@@ -464,13 +517,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
464
517
|
ownershipToken: string,
|
|
465
518
|
): Effect.fn.Return<OwnershipRow, OwnershipLost | LedgerError> {
|
|
466
519
|
const ownership = yield* readOwnership(operation, submission.submission_id);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
520
|
+
const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
|
|
521
|
+
|
|
522
|
+
if (
|
|
523
|
+
Option.isNone(ownership) ||
|
|
524
|
+
ownership.value.ownership_token !== ownershipToken ||
|
|
525
|
+
ownership.value.producer_epoch !== actualEpoch
|
|
526
|
+
) {
|
|
527
|
+
const submissionId = yield* Schema.decodeEffect(SubmissionSnapshot.fields.submissionId)(
|
|
528
|
+
submission.submission_id,
|
|
529
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
530
|
+
|
|
472
531
|
return yield* OwnershipLost.make({ submissionId, actualEpoch });
|
|
473
532
|
}
|
|
533
|
+
|
|
474
534
|
return ownership.value;
|
|
475
535
|
});
|
|
476
536
|
|
|
@@ -479,26 +539,29 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
479
539
|
operation: string,
|
|
480
540
|
row: SubmissionRow,
|
|
481
541
|
): Effect.fn.Return<SubmissionSnapshot, LedgerError> {
|
|
542
|
+
const decodeFailure = (error: Schema.SchemaError) =>
|
|
543
|
+
internalFailure(operation)(
|
|
544
|
+
DoStorageCorruptionError.make({
|
|
545
|
+
table: "effect_agent_submissions",
|
|
546
|
+
rowKey: row.submission_id,
|
|
547
|
+
message: "Stored submission does not satisfy the ledger schema",
|
|
548
|
+
diagnostic: ThreadStoreDiagnostic.make({
|
|
549
|
+
causeTag: error._tag,
|
|
550
|
+
operation,
|
|
551
|
+
decoder: "SubmissionSnapshot",
|
|
552
|
+
issueTag: error.issue._tag,
|
|
553
|
+
}),
|
|
554
|
+
}),
|
|
555
|
+
);
|
|
556
|
+
|
|
482
557
|
const agentDigests = yield* parseStoredJsonText(row.agent_digests_json).pipe(
|
|
483
|
-
Effect.mapError(
|
|
484
|
-
corruptionFailure(
|
|
485
|
-
operation,
|
|
486
|
-
"effect_agent_submissions",
|
|
487
|
-
row.submission_id,
|
|
488
|
-
error.message,
|
|
489
|
-
),
|
|
490
|
-
),
|
|
558
|
+
Effect.mapError(decodeFailure),
|
|
491
559
|
);
|
|
560
|
+
|
|
492
561
|
const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
|
|
493
|
-
Effect.mapError(
|
|
494
|
-
corruptionFailure(
|
|
495
|
-
operation,
|
|
496
|
-
"effect_agent_submissions",
|
|
497
|
-
row.submission_id,
|
|
498
|
-
error.message,
|
|
499
|
-
),
|
|
500
|
-
),
|
|
562
|
+
Effect.mapError(decodeFailure),
|
|
501
563
|
);
|
|
564
|
+
|
|
502
565
|
if ((row.parent_submission_id === null) !== (row.parent_tool_call_id === null)) {
|
|
503
566
|
return yield* corruptionFailure(
|
|
504
567
|
operation,
|
|
@@ -507,9 +570,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
507
570
|
"A parent linkage must record both the parent Submission and the parent Tool Call.",
|
|
508
571
|
);
|
|
509
572
|
}
|
|
573
|
+
|
|
510
574
|
return yield* decodeSubmissionSnapshotUnknown({
|
|
511
575
|
submissionId: row.submission_id,
|
|
512
|
-
|
|
576
|
+
threadId: row.thread_id,
|
|
513
577
|
queueSequence: row.queue_sequence,
|
|
514
578
|
principal: row.principal,
|
|
515
579
|
idempotencyKey: row.idempotency_key,
|
|
@@ -521,6 +585,28 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
521
585
|
receiptId: row.receipt_id,
|
|
522
586
|
state: row.state,
|
|
523
587
|
createdAt: row.created_at,
|
|
588
|
+
...(row.admission_group === null ? {} : { admissionGroup: row.admission_group }),
|
|
589
|
+
...(row.worker_admission_json === null
|
|
590
|
+
? {}
|
|
591
|
+
: {
|
|
592
|
+
workerAdmission: yield* parseStoredJsonText(row.worker_admission_json).pipe(
|
|
593
|
+
Effect.mapError(decodeFailure),
|
|
594
|
+
),
|
|
595
|
+
}),
|
|
596
|
+
...(row.message_admission_json === null
|
|
597
|
+
? {}
|
|
598
|
+
: {
|
|
599
|
+
messageAdmission: yield* parseStoredJsonText(row.message_admission_json).pipe(
|
|
600
|
+
Effect.mapError(decodeFailure),
|
|
601
|
+
),
|
|
602
|
+
}),
|
|
603
|
+
...(row.admission_fence_json === null
|
|
604
|
+
? {}
|
|
605
|
+
: {
|
|
606
|
+
admissionFence: yield* parseStoredJsonText(row.admission_fence_json).pipe(
|
|
607
|
+
Effect.mapError(decodeFailure),
|
|
608
|
+
),
|
|
609
|
+
}),
|
|
524
610
|
...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
|
|
525
611
|
...(row.ready_at === null ? {} : { readyAt: row.ready_at }),
|
|
526
612
|
...(row.parent_submission_id === null || row.parent_tool_call_id === null
|
|
@@ -531,16 +617,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
531
617
|
parentToolCallId: row.parent_tool_call_id,
|
|
532
618
|
},
|
|
533
619
|
}),
|
|
534
|
-
}).pipe(
|
|
535
|
-
Effect.mapError((error) =>
|
|
536
|
-
corruptionFailure(
|
|
537
|
-
operation,
|
|
538
|
-
"effect_agent_submissions",
|
|
539
|
-
row.submission_id,
|
|
540
|
-
error.message,
|
|
541
|
-
),
|
|
542
|
-
),
|
|
543
|
-
);
|
|
620
|
+
}).pipe(Effect.mapError(decodeFailure));
|
|
544
621
|
},
|
|
545
622
|
);
|
|
546
623
|
|
|
@@ -561,12 +638,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
561
638
|
FROM effect_agent_settlement_reservations
|
|
562
639
|
WHERE submission_id = ${submissionId}
|
|
563
640
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
641
|
+
|
|
564
642
|
const decoded = yield* decodeRows(
|
|
565
643
|
Schema.Array(ReservationRow),
|
|
566
644
|
"effect_agent_settlement_reservations",
|
|
567
645
|
submissionId,
|
|
568
646
|
rows,
|
|
569
647
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
648
|
+
|
|
570
649
|
if (decoded.length > 1) {
|
|
571
650
|
return yield* corruptionFailure(
|
|
572
651
|
operation,
|
|
@@ -575,6 +654,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
575
654
|
"A settlement reservation primary key returned more than one row.",
|
|
576
655
|
);
|
|
577
656
|
}
|
|
657
|
+
|
|
578
658
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
579
659
|
});
|
|
580
660
|
|
|
@@ -592,12 +672,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
592
672
|
FROM effect_agent_abort_intents
|
|
593
673
|
WHERE submission_id = ${submissionId}
|
|
594
674
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
675
|
+
|
|
595
676
|
const decoded = yield* decodeRows(
|
|
596
677
|
Schema.Array(AbortIntentRow),
|
|
597
678
|
"effect_agent_abort_intents",
|
|
598
679
|
submissionId,
|
|
599
680
|
rows,
|
|
600
681
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
682
|
+
|
|
601
683
|
if (decoded.length > 1) {
|
|
602
684
|
return yield* corruptionFailure(
|
|
603
685
|
operation,
|
|
@@ -606,6 +688,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
606
688
|
"An abort intent primary key returned more than one row.",
|
|
607
689
|
);
|
|
608
690
|
}
|
|
691
|
+
|
|
609
692
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
610
693
|
});
|
|
611
694
|
|
|
@@ -629,6 +712,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
629
712
|
WHERE parent_submission_id = ${parentSubmissionId}
|
|
630
713
|
ORDER BY child_submission_id ASC
|
|
631
714
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
715
|
+
|
|
632
716
|
return yield* decodeRows(
|
|
633
717
|
Schema.Array(ChildSettlementMarkerRow),
|
|
634
718
|
"effect_agent_child_settlements",
|
|
@@ -651,6 +735,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
651
735
|
): Effect.fn.Return<boolean, LedgerError> {
|
|
652
736
|
if (markerChildren.has(childSubmissionId)) return true;
|
|
653
737
|
const childRow = yield* readSubmission(operation, childSubmissionId);
|
|
738
|
+
|
|
654
739
|
return Option.isSome(childRow) && childRow.value.state === "settled";
|
|
655
740
|
});
|
|
656
741
|
|
|
@@ -671,7 +756,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
671
756
|
FROM effect_agent_child_reservations
|
|
672
757
|
WHERE reservation_id = ${reservationId}
|
|
673
758
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
759
|
+
|
|
674
760
|
const decoded = yield* decodeChildReservationRows(operation, reservationId, rows);
|
|
761
|
+
|
|
675
762
|
if (decoded.length > 1) {
|
|
676
763
|
return yield* corruptionFailure(
|
|
677
764
|
operation,
|
|
@@ -680,6 +767,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
680
767
|
"A child reservation primary key returned more than one row.",
|
|
681
768
|
);
|
|
682
769
|
}
|
|
770
|
+
|
|
683
771
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
684
772
|
});
|
|
685
773
|
|
|
@@ -695,11 +783,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
695
783
|
WHERE parent_submission_id = ${parentSubmissionId}
|
|
696
784
|
AND parent_tool_call_id = ${parentToolCallId}
|
|
697
785
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
786
|
+
|
|
698
787
|
const decoded = yield* decodeChildReservationRows(
|
|
699
788
|
operation,
|
|
700
789
|
`${parentSubmissionId}/${parentToolCallId}`,
|
|
701
790
|
rows,
|
|
702
791
|
);
|
|
792
|
+
|
|
703
793
|
if (decoded.length > 1) {
|
|
704
794
|
return yield* corruptionFailure(
|
|
705
795
|
operation,
|
|
@@ -708,6 +798,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
708
798
|
"A parent Tool Call returned more than one child reservation.",
|
|
709
799
|
);
|
|
710
800
|
}
|
|
801
|
+
|
|
711
802
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
712
803
|
},
|
|
713
804
|
);
|
|
@@ -725,13 +816,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
725
816
|
row.reservation_id,
|
|
726
817
|
error.message,
|
|
727
818
|
);
|
|
819
|
+
|
|
728
820
|
const allocation = yield* parseStoredJsonText(row.allocation_json).pipe(
|
|
729
821
|
Effect.mapError(rowFailure),
|
|
730
822
|
);
|
|
823
|
+
|
|
731
824
|
const accounting =
|
|
732
825
|
row.accounting_json === null
|
|
733
826
|
? undefined
|
|
734
827
|
: yield* parseStoredJsonText(row.accounting_json).pipe(Effect.mapError(rowFailure));
|
|
828
|
+
|
|
735
829
|
return yield* decodeChildReservationSnapshotUnknown({
|
|
736
830
|
reservationId: row.reservation_id,
|
|
737
831
|
parentSubmissionId: row.parent_submission_id,
|
|
@@ -763,6 +857,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
763
857
|
WHERE submission_id = ${submissionId}
|
|
764
858
|
ORDER BY tool_call_id ASC
|
|
765
859
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
860
|
+
|
|
766
861
|
return yield* decodeRows(
|
|
767
862
|
Schema.Array(ApprovalDecisionRow),
|
|
768
863
|
"effect_agent_approval_decisions",
|
|
@@ -810,6 +905,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
810
905
|
WHERE submission_id = ${submissionId}
|
|
811
906
|
ORDER BY tool_call_id ASC
|
|
812
907
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
908
|
+
|
|
813
909
|
return yield* decodeRows(
|
|
814
910
|
Schema.Array(UnknownResolutionRow),
|
|
815
911
|
"effect_agent_unknown_resolutions",
|
|
@@ -834,6 +930,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
834
930
|
),
|
|
835
931
|
),
|
|
836
932
|
);
|
|
933
|
+
|
|
837
934
|
return yield* decodeUnknownResolutionIntent({
|
|
838
935
|
submissionId: row.submission_id,
|
|
839
936
|
toolCallId: row.tool_call_id,
|
|
@@ -860,6 +957,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
860
957
|
submission: SubmissionRow,
|
|
861
958
|
): Effect.fn.Return<ReadonlyArray<typeof ToolCallIdSchema.Type>, LedgerError> {
|
|
862
959
|
if (submission.unknown_tool_call_ids_json === null) return [];
|
|
960
|
+
|
|
863
961
|
return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(
|
|
864
962
|
Effect.mapError((error) =>
|
|
865
963
|
corruptionFailure(
|
|
@@ -880,22 +978,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
880
978
|
*/
|
|
881
979
|
const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (
|
|
882
980
|
operation: string,
|
|
883
|
-
|
|
981
|
+
threadId: string,
|
|
884
982
|
submissionId: SubmissionId,
|
|
885
983
|
): Effect.fn.Return<string | undefined, LedgerError> {
|
|
886
984
|
const recordId = submissionAbortRecordId(submissionId);
|
|
985
|
+
|
|
887
986
|
const rows = yield* sql<Record<string, unknown>>`
|
|
888
987
|
SELECT record_id
|
|
889
988
|
FROM effect_agent_canonical_records
|
|
890
|
-
WHERE
|
|
989
|
+
WHERE thread_id = ${threadId}
|
|
891
990
|
AND record_id = ${recordId}
|
|
892
991
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
992
|
+
|
|
893
993
|
const decoded = yield* decodeRows(
|
|
894
994
|
Schema.Array(CanonicalRecordIdRow),
|
|
895
995
|
"effect_agent_canonical_records",
|
|
896
|
-
`${
|
|
996
|
+
`${threadId}/${recordId}`,
|
|
897
997
|
rows,
|
|
898
998
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
999
|
+
|
|
899
1000
|
return decoded.length === 0 ? undefined : recordId;
|
|
900
1001
|
});
|
|
901
1002
|
|
|
@@ -907,9 +1008,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
907
1008
|
): Effect.fn.Return<AbortIntent, LedgerError> {
|
|
908
1009
|
const canonicalRecordId = yield* canonicalAbortRecordId(
|
|
909
1010
|
operation,
|
|
910
|
-
submission.
|
|
1011
|
+
submission.thread_id,
|
|
911
1012
|
submissionId,
|
|
912
1013
|
);
|
|
1014
|
+
|
|
913
1015
|
return yield* decodeAbortIntent({
|
|
914
1016
|
submissionId: row.submission_id,
|
|
915
1017
|
author: row.author,
|
|
@@ -938,47 +1040,92 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
938
1040
|
const admit: SubmissionLedger["Service"]["admit"] = Effect.fn("DoSubmissionLedger.admit")(
|
|
939
1041
|
function* (request: AdmissionRequest) {
|
|
940
1042
|
const operation = "ledger admit";
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1043
|
+
|
|
1044
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AdmissionRequest))(request).pipe(
|
|
1045
|
+
Effect.mapError(internalFailure(operation)),
|
|
1046
|
+
);
|
|
1047
|
+
|
|
944
1048
|
const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(
|
|
945
1049
|
Effect.mapError(internalFailure(operation)),
|
|
946
1050
|
);
|
|
1051
|
+
|
|
947
1052
|
// The platform's ~2 MB per-value bound, refused typed BEFORE any durable mutation
|
|
948
1053
|
// (resource-limits gate; oversized payloads are the designed R2 overflow path).
|
|
949
1054
|
yield* journal
|
|
950
1055
|
.checkValueBound(operation, inputJson)
|
|
951
1056
|
.pipe(Effect.mapError(internalFailure(operation)));
|
|
1057
|
+
|
|
1058
|
+
const workerAdmissionJson =
|
|
1059
|
+
validated.workerAdmission === undefined
|
|
1060
|
+
? null
|
|
1061
|
+
: yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1062
|
+
validated.workerAdmission,
|
|
1063
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1064
|
+
|
|
1065
|
+
if (
|
|
1066
|
+
workerAdmissionJson !== null &&
|
|
1067
|
+
new TextEncoder().encode(workerAdmissionJson).byteLength > config.maxStoredValueBytes
|
|
1068
|
+
) {
|
|
1069
|
+
return yield* LedgerError.make({
|
|
1070
|
+
operation,
|
|
1071
|
+
message: "Worker admission metadata exceeds the stored value bound",
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const messageAdmissionJson =
|
|
1076
|
+
validated.messageAdmission === undefined
|
|
1077
|
+
? null
|
|
1078
|
+
: yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
|
|
1079
|
+
validated.messageAdmission,
|
|
1080
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1081
|
+
|
|
1082
|
+
if (
|
|
1083
|
+
messageAdmissionJson !== null &&
|
|
1084
|
+
new TextEncoder().encode(messageAdmissionJson).byteLength > config.maxStoredValueBytes
|
|
1085
|
+
) {
|
|
1086
|
+
return yield* LedgerError.make({
|
|
1087
|
+
operation,
|
|
1088
|
+
message: "Message admission metadata exceeds the stored value bound",
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
952
1092
|
const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
|
|
953
1093
|
Effect.mapError(internalFailure(operation)),
|
|
954
1094
|
);
|
|
955
|
-
|
|
1095
|
+
|
|
1096
|
+
// Routable Submission identity (D-P6-5): `{uuidv7}:{threadId}`. The cross-DO
|
|
956
1097
|
// routing layer parses ITS OWN minted format (split at the first ":") to address
|
|
957
|
-
// submissionId-only operations to the owning
|
|
1098
|
+
// submissionId-only operations to the owning Thread Object; the id stays opaque
|
|
958
1099
|
// to every other component, exactly like DN's `submission-{uuid}` prefix.
|
|
959
|
-
const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.
|
|
1100
|
+
const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.threadId}`;
|
|
1101
|
+
|
|
960
1102
|
if (mintedSubmissionId.length > MAX_IDENTIFIER_LENGTH) {
|
|
961
1103
|
return yield* LedgerError.make({
|
|
962
1104
|
operation,
|
|
963
1105
|
message:
|
|
964
1106
|
`A routable Submission identity of ${mintedSubmissionId.length} characters exceeds ` +
|
|
965
|
-
`the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the
|
|
1107
|
+
`the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Thread identity.`,
|
|
966
1108
|
});
|
|
967
1109
|
}
|
|
968
1110
|
const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
|
|
1111
|
+
|
|
969
1112
|
yield* hitFailpoint("ledger:admit:before", operation);
|
|
1113
|
+
|
|
970
1114
|
const result = yield* inWriteTransaction(
|
|
971
1115
|
operation,
|
|
972
1116
|
Effect.gen(function* () {
|
|
973
|
-
const keyRowKey = `${validated.
|
|
1117
|
+
const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
|
|
1118
|
+
|
|
974
1119
|
const existingRows = yield* sql<Record<string, unknown>>`
|
|
975
1120
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
976
1121
|
FROM effect_agent_submissions
|
|
977
|
-
WHERE
|
|
1122
|
+
WHERE thread_id = ${validated.threadId}
|
|
978
1123
|
AND principal = ${validated.principal}
|
|
979
1124
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
980
1125
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1126
|
+
|
|
981
1127
|
const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
|
|
1128
|
+
|
|
982
1129
|
if (existing.length > 1) {
|
|
983
1130
|
return yield* corruptionFailure(
|
|
984
1131
|
operation,
|
|
@@ -996,15 +1143,61 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
996
1143
|
existing[0].parent_tool_call_id === null
|
|
997
1144
|
: existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId &&
|
|
998
1145
|
existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
|
|
1146
|
+
|
|
999
1147
|
if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
|
|
1000
1148
|
return yield* AdmissionConflict.make({
|
|
1001
|
-
|
|
1149
|
+
threadId: validated.threadId,
|
|
1002
1150
|
principal: validated.principal,
|
|
1003
1151
|
idempotencyKey: validated.idempotencyKey,
|
|
1004
1152
|
existingInputDigest: existing[0].input_digest,
|
|
1005
1153
|
attemptedInputDigest: validated.inputDigest,
|
|
1006
1154
|
});
|
|
1007
1155
|
}
|
|
1156
|
+
|
|
1157
|
+
const retainedWorkerAdmission =
|
|
1158
|
+
existing[0].worker_admission_json === null
|
|
1159
|
+
? undefined
|
|
1160
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1161
|
+
existing[0].worker_admission_json,
|
|
1162
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1163
|
+
|
|
1164
|
+
const retainedInputMessage =
|
|
1165
|
+
existing[0].message_admission_json === null
|
|
1166
|
+
? undefined
|
|
1167
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(InputMessage))(
|
|
1168
|
+
existing[0].message_admission_json,
|
|
1169
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1170
|
+
|
|
1171
|
+
const retainedFence =
|
|
1172
|
+
existing[0].admission_fence_json === null
|
|
1173
|
+
? undefined
|
|
1174
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(AdmissionFence))(
|
|
1175
|
+
existing[0].admission_fence_json,
|
|
1176
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1177
|
+
|
|
1178
|
+
if (
|
|
1179
|
+
(existing[0].admission_group ?? undefined) !== validated.admissionGroup ||
|
|
1180
|
+
!Schema.toEquivalence(Schema.optional(WorkerAdmission))(
|
|
1181
|
+
retainedWorkerAdmission,
|
|
1182
|
+
validated.workerAdmission,
|
|
1183
|
+
) ||
|
|
1184
|
+
!Schema.toEquivalence(Schema.optional(InputMessage))(
|
|
1185
|
+
retainedInputMessage,
|
|
1186
|
+
validated.messageAdmission,
|
|
1187
|
+
) ||
|
|
1188
|
+
!Schema.toEquivalence(Schema.optional(AdmissionFence))(
|
|
1189
|
+
retainedFence,
|
|
1190
|
+
validated.admissionFence,
|
|
1191
|
+
)
|
|
1192
|
+
)
|
|
1193
|
+
return yield* AdmissionConflict.make({
|
|
1194
|
+
threadId: validated.threadId,
|
|
1195
|
+
principal: validated.principal,
|
|
1196
|
+
idempotencyKey: validated.idempotencyKey,
|
|
1197
|
+
existingInputDigest: existing[0].input_digest,
|
|
1198
|
+
attemptedInputDigest: validated.inputDigest,
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1008
1201
|
return yield* decodeAdmissionResult({
|
|
1009
1202
|
submissionId: existing[0].submission_id,
|
|
1010
1203
|
receiptId: existing[0].receipt_id,
|
|
@@ -1014,26 +1207,86 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1014
1207
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1015
1208
|
}
|
|
1016
1209
|
|
|
1210
|
+
const stopped =
|
|
1211
|
+
yield* sql`SELECT thread_id FROM effect_agent_worker_stops WHERE thread_id = ${validated.threadId}`.pipe(
|
|
1212
|
+
Effect.mapError(sqlFailure(operation)),
|
|
1213
|
+
);
|
|
1214
|
+
|
|
1215
|
+
if (stopped.length > 0)
|
|
1216
|
+
return yield* AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" });
|
|
1217
|
+
|
|
1218
|
+
// The first accepted input fixes ordinary/worker lane identity atomically with admission.
|
|
1219
|
+
// Canonical origin materialization can lag admission; a log scan cannot fence that race.
|
|
1220
|
+
const firstRows = yield* sql<Record<string, unknown>>`
|
|
1221
|
+
SELECT worker_admission_json FROM effect_agent_submissions
|
|
1222
|
+
WHERE thread_id=${validated.threadId} ORDER BY queue_sequence LIMIT 1
|
|
1223
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1224
|
+
|
|
1225
|
+
const first = yield* Schema.decodeUnknownEffect(
|
|
1226
|
+
Schema.Array(
|
|
1227
|
+
Schema.Struct({
|
|
1228
|
+
worker_admission_json: Schema.NullOr(BoundedStoredText),
|
|
1229
|
+
}),
|
|
1230
|
+
),
|
|
1231
|
+
)(firstRows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1232
|
+
|
|
1233
|
+
if (first[0] !== undefined) {
|
|
1234
|
+
const previous =
|
|
1235
|
+
first[0].worker_admission_json === null
|
|
1236
|
+
? undefined
|
|
1237
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1238
|
+
first[0].worker_admission_json,
|
|
1239
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1240
|
+
|
|
1241
|
+
if (
|
|
1242
|
+
!Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
|
|
1243
|
+
previous?.origin,
|
|
1244
|
+
validated.workerAdmission?.origin,
|
|
1245
|
+
)
|
|
1246
|
+
)
|
|
1247
|
+
return yield* AdmissionPolicyError.make({
|
|
1248
|
+
reason: "refused",
|
|
1249
|
+
code: "worker-origin-conflict",
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
yield* admissionFence.check(validated);
|
|
1254
|
+
if (validated.admissionGroup !== undefined) {
|
|
1255
|
+
const occupied = yield* sql<Record<string, unknown>>`
|
|
1256
|
+
SELECT submission_id FROM effect_agent_submissions
|
|
1257
|
+
WHERE thread_id=${validated.threadId} AND admission_group=${validated.admissionGroup} AND state<>'settled' LIMIT 1
|
|
1258
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1259
|
+
|
|
1260
|
+
if (occupied.length > 0)
|
|
1261
|
+
return yield* AdmissionPolicyError.make({
|
|
1262
|
+
reason: "occupied",
|
|
1263
|
+
code: "admission-group",
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1017
1267
|
const maxRows = yield* sql<Record<string, unknown>>`
|
|
1018
1268
|
SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
|
|
1019
1269
|
FROM effect_agent_submissions
|
|
1020
|
-
WHERE
|
|
1270
|
+
WHERE thread_id = ${validated.threadId}
|
|
1021
1271
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1272
|
+
|
|
1022
1273
|
const decodedMax = yield* decodeRows(
|
|
1023
1274
|
Schema.Array(MaxQueueSequenceRow),
|
|
1024
1275
|
"effect_agent_submissions",
|
|
1025
|
-
validated.
|
|
1276
|
+
validated.threadId,
|
|
1026
1277
|
maxRows,
|
|
1027
1278
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1279
|
+
|
|
1028
1280
|
const queueSequence = yield* decodeQueueSequence(
|
|
1029
1281
|
(decodedMax[0]?.max_queue_sequence ?? 0) + 1,
|
|
1030
1282
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1283
|
+
|
|
1031
1284
|
const now = yield* currentInstant;
|
|
1032
1285
|
|
|
1033
1286
|
yield* sql`
|
|
1034
1287
|
INSERT INTO effect_agent_submissions (
|
|
1035
1288
|
submission_id,
|
|
1036
|
-
|
|
1289
|
+
thread_id,
|
|
1037
1290
|
queue_sequence,
|
|
1038
1291
|
principal,
|
|
1039
1292
|
idempotency_key,
|
|
@@ -1046,10 +1299,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1046
1299
|
state,
|
|
1047
1300
|
created_at,
|
|
1048
1301
|
parent_submission_id,
|
|
1049
|
-
parent_tool_call_id
|
|
1302
|
+
parent_tool_call_id,
|
|
1303
|
+
admission_group,
|
|
1304
|
+
admission_fence_json,
|
|
1305
|
+
worker_admission_json,
|
|
1306
|
+
message_admission_json
|
|
1050
1307
|
) VALUES (
|
|
1051
1308
|
${mintedSubmissionId},
|
|
1052
|
-
${validated.
|
|
1309
|
+
${validated.threadId},
|
|
1053
1310
|
${queueSequence},
|
|
1054
1311
|
${validated.principal},
|
|
1055
1312
|
${validated.idempotencyKey},
|
|
@@ -1062,7 +1319,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1062
1319
|
'admitted',
|
|
1063
1320
|
${now.iso},
|
|
1064
1321
|
${validated.parentLinkage?.parentSubmissionId ?? null},
|
|
1065
|
-
${validated.parentLinkage?.parentToolCallId ?? null}
|
|
1322
|
+
${validated.parentLinkage?.parentToolCallId ?? null},
|
|
1323
|
+
${validated.admissionGroup ?? null},
|
|
1324
|
+
${validated.admissionFence === undefined ? null : JSON.stringify(validated.admissionFence)},
|
|
1325
|
+
${workerAdmissionJson},
|
|
1326
|
+
${messageAdmissionJson}
|
|
1066
1327
|
)
|
|
1067
1328
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1068
1329
|
|
|
@@ -1075,7 +1336,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1075
1336
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1076
1337
|
}),
|
|
1077
1338
|
);
|
|
1339
|
+
|
|
1078
1340
|
yield* hitFailpoint("ledger:admit:after", operation);
|
|
1341
|
+
|
|
1079
1342
|
return result;
|
|
1080
1343
|
},
|
|
1081
1344
|
);
|
|
@@ -1084,16 +1347,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1084
1347
|
"DoSubmissionLedger.markReady",
|
|
1085
1348
|
)(function* (request: MarkReadyRequest) {
|
|
1086
1349
|
const operation = "ledger mark ready";
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1350
|
+
|
|
1351
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkReadyRequest))(request).pipe(
|
|
1352
|
+
Effect.mapError(internalFailure(operation)),
|
|
1353
|
+
);
|
|
1354
|
+
|
|
1090
1355
|
yield* hitFailpoint("ledger:mark-ready:before", operation);
|
|
1091
1356
|
yield* inWriteTransaction(
|
|
1092
1357
|
operation,
|
|
1093
1358
|
Effect.gen(function* () {
|
|
1094
1359
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1360
|
+
|
|
1095
1361
|
if (submission.state !== "admitted") return;
|
|
1096
1362
|
const now = yield* currentInstant;
|
|
1363
|
+
|
|
1097
1364
|
yield* sql`
|
|
1098
1365
|
UPDATE effect_agent_submissions
|
|
1099
1366
|
SET state = 'ready', ready_at = ${now.iso}
|
|
@@ -1107,40 +1374,48 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1107
1374
|
const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn("DoSubmissionLedger.lookup")(
|
|
1108
1375
|
function* (request: SubmissionLookup) {
|
|
1109
1376
|
const operation = "ledger lookup";
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1377
|
+
|
|
1378
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookup))(request).pipe(
|
|
1379
|
+
Effect.mapError(internalFailure(operation)),
|
|
1380
|
+
);
|
|
1381
|
+
|
|
1113
1382
|
if (validated._tag === "SubmissionLookupById") {
|
|
1114
1383
|
const row = yield* readSubmission(operation, validated.submissionId);
|
|
1384
|
+
|
|
1115
1385
|
if (Option.isNone(row)) return Option.none();
|
|
1386
|
+
|
|
1116
1387
|
return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
|
|
1117
1388
|
}
|
|
1389
|
+
|
|
1118
1390
|
const rows = yield* sql<Record<string, unknown>>`
|
|
1119
1391
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1120
1392
|
FROM effect_agent_submissions
|
|
1121
|
-
WHERE
|
|
1393
|
+
WHERE thread_id = ${validated.threadId}
|
|
1122
1394
|
AND principal = ${validated.principal}
|
|
1123
1395
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
1124
1396
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1397
|
+
|
|
1125
1398
|
const decoded = yield* decodeSubmissionRows(
|
|
1126
1399
|
operation,
|
|
1127
|
-
`${validated.
|
|
1400
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1128
1401
|
rows,
|
|
1129
1402
|
);
|
|
1403
|
+
|
|
1130
1404
|
if (decoded.length > 1) {
|
|
1131
1405
|
return yield* corruptionFailure(
|
|
1132
1406
|
operation,
|
|
1133
1407
|
"effect_agent_submissions",
|
|
1134
|
-
`${validated.
|
|
1408
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1135
1409
|
"An admission idempotency key returned more than one row.",
|
|
1136
1410
|
);
|
|
1137
1411
|
}
|
|
1138
1412
|
if (decoded.length === 0) return Option.none();
|
|
1413
|
+
|
|
1139
1414
|
return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
|
|
1140
1415
|
},
|
|
1141
1416
|
);
|
|
1142
1417
|
|
|
1143
|
-
// This LOCAL facet is the authoritative owner of every
|
|
1418
|
+
// This LOCAL facet is the authoritative owner of every Thread stored in this Durable
|
|
1144
1419
|
// Object, so the key-scoped read IS the admission truth and the tri-state degenerates to
|
|
1145
1420
|
// NotAdmitted or Admitted (SUB-031). `AdmissionIndeterminate` becomes real one layer out:
|
|
1146
1421
|
// the WP2 routed decorator answers it when the OWNING Durable Object is unreachable.
|
|
@@ -1148,30 +1423,35 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1148
1423
|
"DoSubmissionLedger.resolveAdmission",
|
|
1149
1424
|
)(function* (request: SubmissionLookupByKey) {
|
|
1150
1425
|
const operation = "ledger resolve admission";
|
|
1151
|
-
|
|
1426
|
+
|
|
1427
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookupByKey))(
|
|
1152
1428
|
request,
|
|
1153
1429
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1430
|
+
|
|
1154
1431
|
const rows = yield* sql<Record<string, unknown>>`
|
|
1155
1432
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1156
1433
|
FROM effect_agent_submissions
|
|
1157
|
-
WHERE
|
|
1434
|
+
WHERE thread_id = ${validated.threadId}
|
|
1158
1435
|
AND principal = ${validated.principal}
|
|
1159
1436
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
1160
1437
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1438
|
+
|
|
1161
1439
|
const decoded = yield* decodeSubmissionRows(
|
|
1162
1440
|
operation,
|
|
1163
|
-
`${validated.
|
|
1441
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1164
1442
|
rows,
|
|
1165
1443
|
);
|
|
1444
|
+
|
|
1166
1445
|
if (decoded.length > 1) {
|
|
1167
1446
|
return yield* corruptionFailure(
|
|
1168
1447
|
operation,
|
|
1169
1448
|
"effect_agent_submissions",
|
|
1170
|
-
`${validated.
|
|
1449
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1171
1450
|
"An admission idempotency key returned more than one row.",
|
|
1172
1451
|
);
|
|
1173
1452
|
}
|
|
1174
1453
|
if (decoded.length === 0) return AdmissionNotAdmitted.make();
|
|
1454
|
+
|
|
1175
1455
|
return AdmissionAdmitted.make({
|
|
1176
1456
|
submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
|
|
1177
1457
|
});
|
|
@@ -1180,71 +1460,103 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1180
1460
|
const claim: SubmissionLedger["Service"]["claim"] = Effect.fn("DoSubmissionLedger.claim")(
|
|
1181
1461
|
function* (request: ClaimRequest) {
|
|
1182
1462
|
const operation = "ledger claim";
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1463
|
+
|
|
1464
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
|
|
1465
|
+
Effect.mapError(internalFailure(operation)),
|
|
1466
|
+
);
|
|
1467
|
+
|
|
1186
1468
|
const attemptId = `attempt-${yield* mintUuid(operation)}`;
|
|
1187
1469
|
const ownershipToken = `owner-${yield* mintUuid(operation)}`;
|
|
1470
|
+
|
|
1188
1471
|
yield* hitFailpoint("ledger:claim:before", operation);
|
|
1472
|
+
|
|
1189
1473
|
const claimed = yield* inWriteTransaction(
|
|
1190
1474
|
operation,
|
|
1191
1475
|
Effect.gen(function* () {
|
|
1476
|
+
const now = yield* currentInstant;
|
|
1477
|
+
|
|
1478
|
+
// Ownership belongs to the whole Thread, including unknown work skipped below.
|
|
1479
|
+
// Stored lease instants are normalized UTC strings, so the latest expiry covers
|
|
1480
|
+
// every live lease. This check and the epoch grant share one write transaction.
|
|
1481
|
+
const ownershipRows = yield* sql<Record<string, unknown>>`
|
|
1482
|
+
SELECT ownership.*
|
|
1483
|
+
FROM effect_agent_submission_ownership AS ownership
|
|
1484
|
+
JOIN effect_agent_submissions AS submission
|
|
1485
|
+
ON submission.submission_id = ownership.submission_id
|
|
1486
|
+
WHERE submission.thread_id = ${validated.threadId}
|
|
1487
|
+
ORDER BY ownership.lease_expires_at DESC
|
|
1488
|
+
LIMIT 1
|
|
1489
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1490
|
+
|
|
1491
|
+
const ownership = yield* decodeRows(
|
|
1492
|
+
Schema.Array(OwnershipRow),
|
|
1493
|
+
"effect_agent_submission_ownership",
|
|
1494
|
+
validated.threadId,
|
|
1495
|
+
ownershipRows,
|
|
1496
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1497
|
+
|
|
1498
|
+
if (ownership.length > 0) {
|
|
1499
|
+
const expiresAt = yield* timestampMillis(
|
|
1500
|
+
operation,
|
|
1501
|
+
ownership[0].submission_id,
|
|
1502
|
+
)(ownership[0].lease_expires_at);
|
|
1503
|
+
|
|
1504
|
+
if (expiresAt > now.millis) return Option.none<Claim>();
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1192
1507
|
const headRows = yield* sql<Record<string, unknown>>`
|
|
1193
1508
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1194
1509
|
FROM effect_agent_submissions
|
|
1195
|
-
WHERE
|
|
1510
|
+
WHERE thread_id = ${validated.threadId}
|
|
1196
1511
|
AND state <> 'settled'
|
|
1512
|
+
AND (
|
|
1513
|
+
state <> 'unknown' OR EXISTS (
|
|
1514
|
+
SELECT 1 FROM effect_agent_abort_intents
|
|
1515
|
+
WHERE submission_id = effect_agent_submissions.submission_id
|
|
1516
|
+
)
|
|
1517
|
+
)
|
|
1197
1518
|
ORDER BY queue_sequence ASC
|
|
1198
1519
|
LIMIT 1
|
|
1199
1520
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1200
|
-
|
|
1521
|
+
|
|
1522
|
+
const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
|
|
1523
|
+
|
|
1201
1524
|
if (heads.length === 0) return Option.none<Claim>();
|
|
1202
1525
|
const head = heads[0];
|
|
1203
1526
|
|
|
1204
|
-
//
|
|
1205
|
-
//
|
|
1206
|
-
// skipped past the blocked head (DUR-004).
|
|
1527
|
+
// Approval/delegation suspension and joined work retain their queue barrier.
|
|
1528
|
+
// Unknown work with an abort intent is selected for cleanup without Tool replay.
|
|
1207
1529
|
if (
|
|
1208
1530
|
head.state === "joining" ||
|
|
1209
1531
|
head.state === "joined" ||
|
|
1210
1532
|
head.state === "suspended" ||
|
|
1211
|
-
head.state === "unknown"
|
|
1533
|
+
(head.state === "unknown" &&
|
|
1534
|
+
Option.isNone(yield* readAbortIntent(operation, head.submission_id)))
|
|
1212
1535
|
) {
|
|
1213
1536
|
return Option.none<Claim>();
|
|
1214
1537
|
}
|
|
1215
1538
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
if (Option.isSome(ownership)) {
|
|
1219
|
-
const expiresAt = yield* timestampMillis(
|
|
1220
|
-
operation,
|
|
1221
|
-
head.submission_id,
|
|
1222
|
-
)(ownership.value.lease_expires_at);
|
|
1223
|
-
// A live lease blocks every new claim; expiry alone only revokes the liveness
|
|
1224
|
-
// assumption — correctness stays with producer-epoch fencing (D5). In DC a live
|
|
1225
|
-
// lease under another token can only come from an evicted incarnation.
|
|
1226
|
-
if (expiresAt > now.millis) return Option.none<Claim>();
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
// Bump the Conversation's producer epoch atomically with the claim so every stale
|
|
1230
|
-
// Attempt is fenced out of canonical appends (DUR-006). A Conversation that was
|
|
1539
|
+
// Bump the Thread's producer epoch atomically with the claim so every stale
|
|
1540
|
+
// Attempt is fenced out of canonical appends (DUR-006). A Thread that was
|
|
1231
1541
|
// never materialized (eviction between admission and materialization) is created
|
|
1232
1542
|
// here so recovery can claim first and re-materialize idempotently at this epoch.
|
|
1233
|
-
const
|
|
1234
|
-
.
|
|
1543
|
+
const threads = yield* journal
|
|
1544
|
+
.getThread(head.thread_id)
|
|
1235
1545
|
.pipe(Effect.mapError(internalFailure(operation)));
|
|
1546
|
+
|
|
1236
1547
|
let producerEpoch: number;
|
|
1237
|
-
|
|
1548
|
+
|
|
1549
|
+
if (threads.length === 0) {
|
|
1238
1550
|
producerEpoch = 1;
|
|
1239
1551
|
yield* sql`
|
|
1240
|
-
INSERT INTO
|
|
1241
|
-
|
|
1552
|
+
INSERT INTO effect_agent_threads (
|
|
1553
|
+
thread_id,
|
|
1242
1554
|
created_at,
|
|
1243
1555
|
tail_sequence,
|
|
1244
1556
|
tail_digest,
|
|
1245
1557
|
producer_epoch
|
|
1246
1558
|
) VALUES (
|
|
1247
|
-
${head.
|
|
1559
|
+
${head.thread_id},
|
|
1248
1560
|
${now.iso},
|
|
1249
1561
|
0,
|
|
1250
1562
|
${EMPTY_TAIL_DIGEST},
|
|
@@ -1252,15 +1564,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1252
1564
|
)
|
|
1253
1565
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1254
1566
|
} else {
|
|
1255
|
-
producerEpoch =
|
|
1567
|
+
producerEpoch = threads[0].producer_epoch + 1;
|
|
1256
1568
|
yield* sql`
|
|
1257
|
-
UPDATE
|
|
1569
|
+
UPDATE effect_agent_threads
|
|
1258
1570
|
SET producer_epoch = ${producerEpoch}
|
|
1259
|
-
WHERE
|
|
1571
|
+
WHERE thread_id = ${head.thread_id}
|
|
1260
1572
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1261
1573
|
}
|
|
1262
1574
|
|
|
1263
1575
|
const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
|
|
1576
|
+
|
|
1264
1577
|
yield* sql`
|
|
1265
1578
|
INSERT INTO effect_agent_submission_ownership (
|
|
1266
1579
|
submission_id,
|
|
@@ -1289,14 +1602,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1289
1602
|
INSERT INTO effect_agent_attempts (
|
|
1290
1603
|
attempt_id,
|
|
1291
1604
|
submission_id,
|
|
1292
|
-
|
|
1605
|
+
thread_id,
|
|
1293
1606
|
owner_producer_id,
|
|
1294
1607
|
producer_epoch,
|
|
1295
1608
|
claimed_at
|
|
1296
1609
|
) VALUES (
|
|
1297
1610
|
${attemptId},
|
|
1298
1611
|
${head.submission_id},
|
|
1299
|
-
${head.
|
|
1612
|
+
${head.thread_id},
|
|
1300
1613
|
${validated.producerId},
|
|
1301
1614
|
${producerEpoch},
|
|
1302
1615
|
${now.iso}
|
|
@@ -1321,6 +1634,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1321
1634
|
),
|
|
1322
1635
|
),
|
|
1323
1636
|
);
|
|
1637
|
+
|
|
1324
1638
|
return Option.some(
|
|
1325
1639
|
yield* decodeClaim({
|
|
1326
1640
|
submissionId: head.submission_id,
|
|
@@ -1333,7 +1647,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1333
1647
|
);
|
|
1334
1648
|
}),
|
|
1335
1649
|
);
|
|
1650
|
+
|
|
1336
1651
|
yield* hitFailpoint("ledger:claim:after", operation);
|
|
1652
|
+
|
|
1337
1653
|
return claimed;
|
|
1338
1654
|
},
|
|
1339
1655
|
);
|
|
@@ -1342,29 +1658,37 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1342
1658
|
"DoSubmissionLedger.renewOwnership",
|
|
1343
1659
|
)(function* (request: RenewOwnershipRequest) {
|
|
1344
1660
|
const operation = "ledger renew ownership";
|
|
1345
|
-
|
|
1661
|
+
|
|
1662
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(RenewOwnershipRequest))(
|
|
1346
1663
|
request,
|
|
1347
1664
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1665
|
+
|
|
1348
1666
|
yield* hitFailpoint("ledger:renew:before", operation);
|
|
1667
|
+
|
|
1349
1668
|
const renewal = yield* inWriteTransaction(
|
|
1350
1669
|
operation,
|
|
1351
1670
|
Effect.gen(function* () {
|
|
1352
1671
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1672
|
+
|
|
1353
1673
|
yield* requireOwnership(operation, submission, validated.ownershipToken);
|
|
1354
1674
|
const now = yield* currentInstant;
|
|
1355
1675
|
const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
|
|
1676
|
+
|
|
1356
1677
|
yield* sql`
|
|
1357
1678
|
UPDATE effect_agent_submission_ownership
|
|
1358
1679
|
SET lease_expires_at = ${leaseExpiresAt}
|
|
1359
1680
|
WHERE submission_id = ${validated.submissionId}
|
|
1360
1681
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1682
|
+
|
|
1361
1683
|
return yield* decodeOwnershipRenewal({
|
|
1362
1684
|
ownershipToken: validated.ownershipToken,
|
|
1363
1685
|
leaseExpiresAt,
|
|
1364
1686
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1365
1687
|
}),
|
|
1366
1688
|
);
|
|
1689
|
+
|
|
1367
1690
|
yield* hitFailpoint("ledger:renew:after", operation);
|
|
1691
|
+
|
|
1368
1692
|
return renewal;
|
|
1369
1693
|
});
|
|
1370
1694
|
|
|
@@ -1372,14 +1696,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1372
1696
|
"DoSubmissionLedger.releaseOwnership",
|
|
1373
1697
|
)(function* (request: ReleaseOwnershipRequest) {
|
|
1374
1698
|
const operation = "ledger release ownership";
|
|
1375
|
-
|
|
1699
|
+
|
|
1700
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseOwnershipRequest))(
|
|
1376
1701
|
request,
|
|
1377
1702
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1703
|
+
|
|
1378
1704
|
yield* hitFailpoint("ledger:release:before", operation);
|
|
1379
1705
|
yield* inWriteTransaction(
|
|
1380
1706
|
operation,
|
|
1381
1707
|
Effect.gen(function* () {
|
|
1382
1708
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1709
|
+
|
|
1383
1710
|
yield* requireOwnership(operation, submission, validated.ownershipToken);
|
|
1384
1711
|
yield* sql`
|
|
1385
1712
|
DELETE FROM effect_agent_submission_ownership
|
|
@@ -1401,14 +1728,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1401
1728
|
"DoSubmissionLedger.markInputApplied",
|
|
1402
1729
|
)(function* (request: MarkInputAppliedRequest) {
|
|
1403
1730
|
const operation = "ledger mark input applied";
|
|
1404
|
-
|
|
1731
|
+
|
|
1732
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
|
|
1405
1733
|
request,
|
|
1406
1734
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1735
|
+
|
|
1407
1736
|
yield* hitFailpoint("ledger:mark-input-applied:before", operation);
|
|
1408
1737
|
yield* inWriteTransaction(
|
|
1409
1738
|
operation,
|
|
1410
1739
|
Effect.gen(function* () {
|
|
1411
1740
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1741
|
+
|
|
1412
1742
|
yield* requireOwnership(operation, submission, validated.ownershipToken);
|
|
1413
1743
|
if (submission.input_applied_record_id !== null) {
|
|
1414
1744
|
if (
|
|
@@ -1417,6 +1747,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1417
1747
|
) {
|
|
1418
1748
|
return;
|
|
1419
1749
|
}
|
|
1750
|
+
|
|
1420
1751
|
return yield* corruptionFailure(
|
|
1421
1752
|
operation,
|
|
1422
1753
|
"effect_agent_submissions",
|
|
@@ -1444,34 +1775,41 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1444
1775
|
"DoSubmissionLedger.reserveSettlement",
|
|
1445
1776
|
)(function* (request: SettlementReservation) {
|
|
1446
1777
|
const operation = "ledger reserve settlement";
|
|
1447
|
-
|
|
1778
|
+
|
|
1779
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SettlementReservation))(
|
|
1448
1780
|
request,
|
|
1449
1781
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1782
|
+
|
|
1450
1783
|
const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(
|
|
1451
1784
|
Effect.mapError(internalFailure(operation)),
|
|
1452
1785
|
);
|
|
1786
|
+
|
|
1453
1787
|
// The reserved record is appended canonically later; refuse over-bound payloads typed
|
|
1454
1788
|
// before the reservation row exists.
|
|
1455
1789
|
yield* journal
|
|
1456
1790
|
.checkValueBound(operation, recordJson)
|
|
1457
1791
|
.pipe(Effect.mapError(internalFailure(operation)));
|
|
1458
1792
|
yield* hitFailpoint("ledger:reserve-settlement:before", operation);
|
|
1793
|
+
|
|
1459
1794
|
const reserved = yield* inWriteTransaction(
|
|
1460
1795
|
operation,
|
|
1461
1796
|
Effect.gen(function* () {
|
|
1462
1797
|
const existing = yield* readReservation(operation, validated.submissionId);
|
|
1798
|
+
|
|
1463
1799
|
if (Option.isSome(existing)) {
|
|
1464
1800
|
const identical =
|
|
1465
1801
|
existing.value.settlement_id === validated.settlementId &&
|
|
1466
1802
|
existing.value.outcome === validated.outcome &&
|
|
1467
1803
|
existing.value.record_digest === validated.recordDigest &&
|
|
1468
1804
|
existing.value.record_json === recordJson;
|
|
1805
|
+
|
|
1469
1806
|
if (!identical) {
|
|
1470
1807
|
return yield* SettlementConflict.make({
|
|
1471
1808
|
submissionId: validated.submissionId,
|
|
1472
1809
|
existingOutcome: existing.value.outcome,
|
|
1473
1810
|
});
|
|
1474
1811
|
}
|
|
1812
|
+
|
|
1475
1813
|
const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(
|
|
1476
1814
|
Effect.mapError((error) =>
|
|
1477
1815
|
corruptionFailure(
|
|
@@ -1482,6 +1820,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1482
1820
|
),
|
|
1483
1821
|
),
|
|
1484
1822
|
);
|
|
1823
|
+
|
|
1485
1824
|
return ReservedSettlement.make({
|
|
1486
1825
|
submissionId: validated.submissionId,
|
|
1487
1826
|
settlementId: validated.settlementId,
|
|
@@ -1493,6 +1832,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1493
1832
|
}
|
|
1494
1833
|
|
|
1495
1834
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1835
|
+
|
|
1496
1836
|
if (submission.state === "settled") {
|
|
1497
1837
|
if (submission.settled_outcome === null) {
|
|
1498
1838
|
return yield* corruptionFailure(
|
|
@@ -1502,6 +1842,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1502
1842
|
"A settled Submission carries no terminal outcome.",
|
|
1503
1843
|
);
|
|
1504
1844
|
}
|
|
1845
|
+
|
|
1505
1846
|
return yield* SettlementConflict.make({
|
|
1506
1847
|
submissionId: validated.submissionId,
|
|
1507
1848
|
existingOutcome: submission.settled_outcome,
|
|
@@ -1516,13 +1857,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1516
1857
|
// ABORTED settlement (`terminalizing` is the same pass's crash replay). Every other
|
|
1517
1858
|
// reservation stays fenced by the target lane's live ownership.
|
|
1518
1859
|
let queuedAbortSettlement = false;
|
|
1860
|
+
|
|
1519
1861
|
if (
|
|
1520
1862
|
validated.outcome === "aborted" &&
|
|
1521
1863
|
(submission.state === "ready" || submission.state === "terminalizing")
|
|
1522
1864
|
) {
|
|
1523
1865
|
const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
|
|
1866
|
+
|
|
1524
1867
|
if (Option.isSome(abortIntent)) {
|
|
1525
1868
|
const ownership = yield* readOwnership(operation, validated.submissionId);
|
|
1869
|
+
|
|
1526
1870
|
queuedAbortSettlement = Option.isNone(ownership);
|
|
1527
1871
|
}
|
|
1528
1872
|
}
|
|
@@ -1531,6 +1875,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1531
1875
|
}
|
|
1532
1876
|
}
|
|
1533
1877
|
const now = yield* currentInstant;
|
|
1878
|
+
|
|
1534
1879
|
yield* sql`
|
|
1535
1880
|
INSERT INTO effect_agent_settlement_reservations (
|
|
1536
1881
|
submission_id,
|
|
@@ -1555,6 +1900,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1555
1900
|
SET state = 'terminalizing'
|
|
1556
1901
|
WHERE submission_id = ${validated.submissionId}
|
|
1557
1902
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1903
|
+
|
|
1558
1904
|
return ReservedSettlement.make({
|
|
1559
1905
|
submissionId: validated.submissionId,
|
|
1560
1906
|
settlementId: validated.settlementId,
|
|
@@ -1565,56 +1911,197 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1565
1911
|
});
|
|
1566
1912
|
}),
|
|
1567
1913
|
);
|
|
1914
|
+
|
|
1568
1915
|
yield* hitFailpoint("ledger:reserve-settlement:after", operation);
|
|
1916
|
+
|
|
1569
1917
|
return reserved;
|
|
1570
1918
|
});
|
|
1571
1919
|
|
|
1920
|
+
const validateFinalization = Effect.fn("DoSubmissionLedger.validateFinalization")(function* (
|
|
1921
|
+
validated: SettlementFinalization,
|
|
1922
|
+
reservation: Option.Option<ReservationRow>,
|
|
1923
|
+
) {
|
|
1924
|
+
const operation = "ledger finalize settlement";
|
|
1925
|
+
|
|
1926
|
+
if (Option.isNone(reservation)) {
|
|
1927
|
+
return yield* LedgerError.make({
|
|
1928
|
+
operation,
|
|
1929
|
+
message: `No settlement reservation exists for submission ${validated.submissionId}.`,
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
if (reservation.value.settlement_id !== validated.settlementId) {
|
|
1933
|
+
return yield* SettlementConflict.make({
|
|
1934
|
+
submissionId: validated.submissionId,
|
|
1935
|
+
existingOutcome: reservation.value.outcome,
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
const reservationRecord = yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(
|
|
1940
|
+
Effect.mapError((error) =>
|
|
1941
|
+
corruptionFailure(
|
|
1942
|
+
operation,
|
|
1943
|
+
"effect_agent_settlement_reservations",
|
|
1944
|
+
validated.submissionId,
|
|
1945
|
+
error.message,
|
|
1946
|
+
),
|
|
1947
|
+
),
|
|
1948
|
+
);
|
|
1949
|
+
|
|
1950
|
+
const settlementFailure = settlementFailureFromRecord(reservationRecord);
|
|
1951
|
+
|
|
1952
|
+
if ((reservation.value.outcome === "failed") !== (settlementFailure !== undefined)) {
|
|
1953
|
+
return yield* corruptionFailure(
|
|
1954
|
+
operation,
|
|
1955
|
+
"effect_agent_settlement_reservations",
|
|
1956
|
+
validated.submissionId,
|
|
1957
|
+
"The reserved outcome and canonical failure diagnostic disagree.",
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
return { reservation: reservation.value, reservationRecord, settlementFailure };
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
const replayFinalization = Effect.fn("DoSubmissionLedger.replayFinalization")(function* (
|
|
1965
|
+
validated: SettlementFinalization,
|
|
1966
|
+
submission: SubmissionRow,
|
|
1967
|
+
{ reservation, settlementFailure }: Effect.Success<ReturnType<typeof validateFinalization>>,
|
|
1968
|
+
) {
|
|
1969
|
+
const operation = "ledger finalize settlement";
|
|
1970
|
+
|
|
1971
|
+
if (reservation.finalized_at === null) {
|
|
1972
|
+
return yield* corruptionFailure(
|
|
1973
|
+
operation,
|
|
1974
|
+
"effect_agent_settlement_reservations",
|
|
1975
|
+
validated.submissionId,
|
|
1976
|
+
"A settled Submission's reservation carries no finalization timestamp.",
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
return yield* decodeSettlement({
|
|
1981
|
+
submissionId: validated.submissionId,
|
|
1982
|
+
settlementId: validated.settlementId,
|
|
1983
|
+
receiptId: submission.receipt_id,
|
|
1984
|
+
outcome: reservation.outcome,
|
|
1985
|
+
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
1986
|
+
settledAt: reservation.finalized_at,
|
|
1987
|
+
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1572
1990
|
const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
|
|
1573
1991
|
"DoSubmissionLedger.finalizeSettlement",
|
|
1574
1992
|
)(function* (request: SettlementFinalization) {
|
|
1575
1993
|
const operation = "ledger finalize settlement";
|
|
1576
|
-
|
|
1994
|
+
|
|
1995
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SettlementFinalization))(
|
|
1577
1996
|
request,
|
|
1578
1997
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1998
|
+
|
|
1579
1999
|
yield* hitFailpoint("ledger:finalize-settlement:before", operation);
|
|
2000
|
+
|
|
2001
|
+
// A single statement captures the settled row and its immutable reservation together.
|
|
2002
|
+
// A miss does not authorize finalization: the write path re-reads under its transaction.
|
|
2003
|
+
const replayRows = yield* sql<Record<string, unknown>>`
|
|
2004
|
+
SELECT submission.*, reservation.settlement_id, reservation.outcome,
|
|
2005
|
+
reservation.record_id, reservation.record_json, reservation.record_digest,
|
|
2006
|
+
reservation.reserved_at, reservation.finalized_at
|
|
2007
|
+
FROM effect_agent_submissions AS submission
|
|
2008
|
+
INNER JOIN effect_agent_settlement_reservations AS reservation
|
|
2009
|
+
ON reservation.submission_id = submission.submission_id
|
|
2010
|
+
WHERE submission.submission_id = ${validated.submissionId} AND submission.state = 'settled'
|
|
2011
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2012
|
+
|
|
2013
|
+
if (replayRows.length > 1) {
|
|
2014
|
+
return yield* corruptionFailure(
|
|
2015
|
+
operation,
|
|
2016
|
+
"effect_agent_submissions",
|
|
2017
|
+
validated.submissionId,
|
|
2018
|
+
"A submission primary key returned more than one row.",
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
const replayRow = replayRows[0];
|
|
2022
|
+
|
|
2023
|
+
if (replayRow !== undefined) {
|
|
2024
|
+
const reservations = yield* decodeRows(
|
|
2025
|
+
Schema.Array(ReservationRow),
|
|
2026
|
+
"effect_agent_settlement_reservations",
|
|
2027
|
+
validated.submissionId,
|
|
2028
|
+
replayRows,
|
|
2029
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2030
|
+
|
|
2031
|
+
const state = yield* validateFinalization(validated, Option.fromUndefinedOr(reservations[0]));
|
|
2032
|
+
|
|
2033
|
+
const submission = yield* Schema.decodeUnknownEffect(SubmissionRow)(replayRow).pipe(
|
|
2034
|
+
Effect.mapError((error) =>
|
|
2035
|
+
corruptionFailure(
|
|
2036
|
+
operation,
|
|
2037
|
+
"effect_agent_submissions",
|
|
2038
|
+
validated.submissionId,
|
|
2039
|
+
error.message,
|
|
2040
|
+
),
|
|
2041
|
+
),
|
|
2042
|
+
);
|
|
2043
|
+
|
|
2044
|
+
const settlement = yield* replayFinalization(validated, submission, state);
|
|
2045
|
+
|
|
2046
|
+
yield* hitFailpoint("ledger:finalize-settlement:after", operation);
|
|
2047
|
+
|
|
2048
|
+
return settlement;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
1580
2051
|
const settlement = yield* inWriteTransaction(
|
|
1581
2052
|
operation,
|
|
1582
2053
|
Effect.gen(function* () {
|
|
1583
|
-
const
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
}
|
|
1590
|
-
if (reservation.value.settlement_id !== validated.settlementId) {
|
|
1591
|
-
return yield* SettlementConflict.make({
|
|
1592
|
-
submissionId: validated.submissionId,
|
|
1593
|
-
existingOutcome: reservation.value.outcome,
|
|
1594
|
-
});
|
|
1595
|
-
}
|
|
2054
|
+
const state = yield* validateFinalization(
|
|
2055
|
+
validated,
|
|
2056
|
+
yield* readReservation(operation, validated.submissionId),
|
|
2057
|
+
);
|
|
2058
|
+
|
|
2059
|
+
const { reservation, reservationRecord, settlementFailure } = state;
|
|
1596
2060
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
2061
|
+
|
|
2062
|
+
if (submission.state === "settled")
|
|
2063
|
+
return yield* replayFinalization(validated, submission, state);
|
|
2064
|
+
const now = yield* currentInstant;
|
|
2065
|
+
|
|
2066
|
+
const terminal =
|
|
2067
|
+
submission.worker_admission_json === null
|
|
2068
|
+
? undefined
|
|
2069
|
+
: workerTerminalFromRecord(
|
|
2070
|
+
yield* decodeSubmissionSnapshot(operation, submission),
|
|
2071
|
+
reservationRecord,
|
|
2072
|
+
);
|
|
2073
|
+
|
|
2074
|
+
if (terminal !== undefined) {
|
|
2075
|
+
// Admission and finalization serialize here. An accepted correction that this Run
|
|
2076
|
+
// has not applied vetoes its completion, including admission after RunCompleted.
|
|
2077
|
+
const pending =
|
|
2078
|
+
terminal === "completed"
|
|
2079
|
+
? yield* sql`SELECT submission_id FROM effect_agent_submissions
|
|
2080
|
+
WHERE thread_id = ${submission.thread_id} AND queue_sequence > ${submission.queue_sequence}
|
|
2081
|
+
AND queue_sequence = (SELECT MAX(queue_sequence) FROM effect_agent_submissions WHERE thread_id = ${submission.thread_id})
|
|
2082
|
+
AND (joined_host_submission_id IS NULL OR joined_host_submission_id <> ${submission.submission_id}
|
|
2083
|
+
OR input_applied_record_id IS NULL) LIMIT 1`.pipe(
|
|
2084
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2085
|
+
)
|
|
2086
|
+
: [];
|
|
2087
|
+
|
|
2088
|
+
if (pending.length === 0) {
|
|
2089
|
+
yield* sql`INSERT OR IGNORE INTO effect_agent_worker_stops (thread_id, terminal)
|
|
2090
|
+
VALUES (${submission.thread_id}, ${terminal})`.pipe(
|
|
2091
|
+
Effect.mapError(sqlFailure(operation)),
|
|
1604
2092
|
);
|
|
2093
|
+
yield* sql`INSERT OR IGNORE INTO effect_agent_abort_intents (submission_id, author, reason, requested_at)
|
|
2094
|
+
SELECT submission_id, ${submission.principal}, ${`Worker assignment ${terminal}`}, ${now.iso}
|
|
2095
|
+
FROM effect_agent_submissions WHERE thread_id = ${submission.thread_id} AND state <> 'settled'
|
|
2096
|
+
AND submission_id <> ${submission.submission_id}
|
|
2097
|
+
AND (joined_host_submission_id IS NULL OR joined_host_submission_id <> ${submission.submission_id}
|
|
2098
|
+
OR input_applied_record_id IS NULL)`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1605
2099
|
}
|
|
1606
|
-
return yield* decodeSettlement({
|
|
1607
|
-
submissionId: validated.submissionId,
|
|
1608
|
-
settlementId: validated.settlementId,
|
|
1609
|
-
receiptId: submission.receipt_id,
|
|
1610
|
-
outcome: reservation.value.outcome,
|
|
1611
|
-
settledAt: reservation.value.finalized_at,
|
|
1612
|
-
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1613
2100
|
}
|
|
1614
|
-
|
|
2101
|
+
|
|
1615
2102
|
yield* sql`
|
|
1616
2103
|
UPDATE effect_agent_submissions
|
|
1617
|
-
SET state = 'settled', settled_outcome = ${reservation.
|
|
2104
|
+
SET state = 'settled', settled_outcome = ${reservation.outcome}
|
|
1618
2105
|
WHERE submission_id = ${validated.submissionId}
|
|
1619
2106
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1620
2107
|
yield* sql`
|
|
@@ -1626,31 +2113,117 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1626
2113
|
DELETE FROM effect_agent_submission_ownership
|
|
1627
2114
|
WHERE submission_id = ${validated.submissionId}
|
|
1628
2115
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2116
|
+
|
|
1629
2117
|
return yield* decodeSettlement({
|
|
1630
2118
|
submissionId: validated.submissionId,
|
|
1631
2119
|
settlementId: validated.settlementId,
|
|
1632
2120
|
receiptId: submission.receipt_id,
|
|
1633
|
-
outcome: reservation.
|
|
2121
|
+
outcome: reservation.outcome,
|
|
2122
|
+
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
1634
2123
|
settledAt: now.iso,
|
|
1635
2124
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1636
2125
|
}),
|
|
1637
2126
|
);
|
|
2127
|
+
|
|
1638
2128
|
yield* hitFailpoint("ledger:finalize-settlement:after", operation);
|
|
2129
|
+
|
|
1639
2130
|
return settlement;
|
|
1640
2131
|
});
|
|
1641
2132
|
|
|
2133
|
+
const inspectWorker = Effect.fn("DoSubmissionLedger.inspectWorker")(function* (
|
|
2134
|
+
threadId: SubmissionSnapshot["threadId"],
|
|
2135
|
+
) {
|
|
2136
|
+
const operation = "inspect worker";
|
|
2137
|
+
|
|
2138
|
+
return yield* sql
|
|
2139
|
+
.withTransaction(
|
|
2140
|
+
Effect.gen(function* () {
|
|
2141
|
+
const read = Effect.fnUntraced(function* (active: boolean) {
|
|
2142
|
+
const rows =
|
|
2143
|
+
yield* sql`SELECT ${sql.literal(SUBMISSION_COLUMNS)} FROM effect_agent_submissions
|
|
2144
|
+
WHERE thread_id = ${threadId} ${active ? sql`AND state <> 'settled'` : sql``}
|
|
2145
|
+
ORDER BY queue_sequence ${active ? sql`ASC` : sql`DESC`} LIMIT 1`.pipe(
|
|
2146
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2147
|
+
);
|
|
2148
|
+
|
|
2149
|
+
const decoded = yield* decodeSubmissionRows(operation, threadId, rows);
|
|
2150
|
+
|
|
2151
|
+
return decoded[0] === undefined
|
|
2152
|
+
? null
|
|
2153
|
+
: yield* decodeSubmissionSnapshot(operation, decoded[0]);
|
|
2154
|
+
});
|
|
2155
|
+
|
|
2156
|
+
const latest = yield* read(false);
|
|
2157
|
+
const active = yield* read(true);
|
|
2158
|
+
|
|
2159
|
+
const stops =
|
|
2160
|
+
yield* sql`SELECT terminal FROM effect_agent_worker_stops WHERE thread_id = ${threadId}`.pipe(
|
|
2161
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2162
|
+
);
|
|
2163
|
+
|
|
2164
|
+
return yield* Schema.decodeUnknownEffect(Schema.toType(WorkerLedgerState))({
|
|
2165
|
+
latest,
|
|
2166
|
+
active,
|
|
2167
|
+
stopped: stops.length > 0,
|
|
2168
|
+
...(stops[0] === undefined || stops[0].terminal === null
|
|
2169
|
+
? {}
|
|
2170
|
+
: { terminal: stops[0].terminal }),
|
|
2171
|
+
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
2172
|
+
}),
|
|
2173
|
+
)
|
|
2174
|
+
.pipe(Effect.catchTag("SqlError", (cause) => sqlFailure(operation)(cause)));
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
const stopWorker = Effect.fn("DoSubmissionLedger.stopWorker")(function* (
|
|
2178
|
+
request: WorkerStopCommand,
|
|
2179
|
+
) {
|
|
2180
|
+
const operation = "ledger stop worker";
|
|
2181
|
+
|
|
2182
|
+
const validated = yield* Schema.decodeEffect(WorkerStopCommand)(request).pipe(
|
|
2183
|
+
Effect.mapError(internalFailure(operation)),
|
|
2184
|
+
);
|
|
2185
|
+
|
|
2186
|
+
return yield* inWriteTransaction(
|
|
2187
|
+
operation,
|
|
2188
|
+
Effect.gen(function* () {
|
|
2189
|
+
const now = yield* currentInstant;
|
|
2190
|
+
|
|
2191
|
+
yield* sql`INSERT OR IGNORE INTO effect_agent_worker_stops (thread_id) VALUES (${validated.threadId})`.pipe(
|
|
2192
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2193
|
+
);
|
|
2194
|
+
yield* sql`INSERT OR IGNORE INTO effect_agent_abort_intents (submission_id, author, reason, requested_at)
|
|
2195
|
+
SELECT submission_id, ${validated.author}, 'Worker owner stopped the worker', ${now.iso}
|
|
2196
|
+
FROM effect_agent_submissions WHERE thread_id = ${validated.threadId} AND state <> 'settled'`.pipe(
|
|
2197
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2198
|
+
);
|
|
2199
|
+
|
|
2200
|
+
const rows = yield* sql`SELECT o.submission_id FROM effect_agent_submission_ownership o
|
|
2201
|
+
JOIN effect_agent_submissions s ON s.submission_id = o.submission_id
|
|
2202
|
+
WHERE s.thread_id = ${validated.threadId} AND s.state <> 'settled'`.pipe(
|
|
2203
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2204
|
+
);
|
|
2205
|
+
|
|
2206
|
+
return rows.length;
|
|
2207
|
+
}),
|
|
2208
|
+
);
|
|
2209
|
+
});
|
|
2210
|
+
|
|
1642
2211
|
const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fn(
|
|
1643
2212
|
"DoSubmissionLedger.requestAbort",
|
|
1644
2213
|
)(function* (request: AbortCommand) {
|
|
1645
2214
|
const operation = "ledger request abort";
|
|
1646
|
-
|
|
2215
|
+
|
|
2216
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AbortCommand))(request).pipe(
|
|
1647
2217
|
Effect.mapError(internalFailure(operation)),
|
|
1648
2218
|
);
|
|
2219
|
+
|
|
1649
2220
|
yield* hitFailpoint("ledger:request-abort:before", operation);
|
|
2221
|
+
|
|
1650
2222
|
const intent = yield* inWriteTransaction(
|
|
1651
2223
|
operation,
|
|
1652
2224
|
Effect.gen(function* () {
|
|
1653
2225
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2226
|
+
|
|
1654
2227
|
if (submission.state === "settled") {
|
|
1655
2228
|
if (submission.settled_outcome === null) {
|
|
1656
2229
|
return yield* corruptionFailure(
|
|
@@ -1660,6 +2233,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1660
2233
|
"A settled Submission carries no terminal outcome.",
|
|
1661
2234
|
);
|
|
1662
2235
|
}
|
|
2236
|
+
|
|
1663
2237
|
return yield* SettlementConflict.make({
|
|
1664
2238
|
submissionId: validated.submissionId,
|
|
1665
2239
|
existingOutcome: submission.settled_outcome,
|
|
@@ -1677,15 +2251,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1677
2251
|
"A joined Submission carries no host linkage.",
|
|
1678
2252
|
);
|
|
1679
2253
|
}
|
|
2254
|
+
|
|
1680
2255
|
const hostSubmissionId = yield* decodeSubmissionId(
|
|
1681
2256
|
submission.joined_host_submission_id,
|
|
1682
2257
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2258
|
+
|
|
1683
2259
|
return yield* JoinedToHost.make({
|
|
1684
2260
|
submissionId: validated.submissionId,
|
|
1685
2261
|
hostSubmissionId,
|
|
1686
2262
|
});
|
|
1687
2263
|
}
|
|
1688
2264
|
const existing = yield* readAbortIntent(operation, validated.submissionId);
|
|
2265
|
+
|
|
1689
2266
|
if (Option.isSome(existing)) {
|
|
1690
2267
|
return yield* abortIntentFromRow(
|
|
1691
2268
|
operation,
|
|
@@ -1695,6 +2272,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1695
2272
|
);
|
|
1696
2273
|
}
|
|
1697
2274
|
const now = yield* currentInstant;
|
|
2275
|
+
|
|
1698
2276
|
yield* sql`
|
|
1699
2277
|
INSERT INTO effect_agent_abort_intents (
|
|
1700
2278
|
submission_id,
|
|
@@ -1708,11 +2286,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1708
2286
|
${now.iso}
|
|
1709
2287
|
)
|
|
1710
2288
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2289
|
+
|
|
1711
2290
|
const canonicalRecordId = yield* canonicalAbortRecordId(
|
|
1712
2291
|
operation,
|
|
1713
|
-
submission.
|
|
2292
|
+
submission.thread_id,
|
|
1714
2293
|
validated.submissionId,
|
|
1715
2294
|
);
|
|
2295
|
+
|
|
1716
2296
|
return yield* decodeAbortIntent({
|
|
1717
2297
|
submissionId: validated.submissionId,
|
|
1718
2298
|
author: validated.author,
|
|
@@ -1722,7 +2302,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1722
2302
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1723
2303
|
}),
|
|
1724
2304
|
);
|
|
2305
|
+
|
|
1725
2306
|
yield* hitFailpoint("ledger:request-abort:after", operation);
|
|
2307
|
+
|
|
1726
2308
|
return intent;
|
|
1727
2309
|
});
|
|
1728
2310
|
|
|
@@ -1730,31 +2312,45 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1730
2312
|
"DoSubmissionLedger.claimJoining",
|
|
1731
2313
|
)(function* (request: ClaimJoiningRequest) {
|
|
1732
2314
|
const operation = "ledger claim joining";
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
2315
|
+
|
|
2316
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(
|
|
2317
|
+
Effect.mapError(internalFailure(operation)),
|
|
2318
|
+
);
|
|
2319
|
+
|
|
1736
2320
|
yield* hitFailpoint("ledger:claim-joining:before", operation);
|
|
2321
|
+
|
|
1737
2322
|
const claims = yield* inWriteTransaction(
|
|
1738
2323
|
operation,
|
|
1739
2324
|
Effect.gen(function* () {
|
|
1740
2325
|
const host = yield* requireSubmission(operation, validated.hostSubmissionId);
|
|
1741
|
-
|
|
2326
|
+
|
|
2327
|
+
if (host.thread_id !== validated.threadId) {
|
|
1742
2328
|
return yield* LedgerError.make({
|
|
1743
2329
|
operation,
|
|
1744
|
-
message: `Host submission ${validated.hostSubmissionId} does not belong to
|
|
2330
|
+
message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
|
|
1745
2331
|
});
|
|
1746
2332
|
}
|
|
1747
2333
|
// The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
|
|
1748
2334
|
yield* requireOwnership(operation, host, validated.ownershipToken);
|
|
2335
|
+
|
|
2336
|
+
const stopped =
|
|
2337
|
+
yield* sql`SELECT thread_id FROM effect_agent_worker_stops WHERE thread_id = ${validated.threadId}`.pipe(
|
|
2338
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2339
|
+
);
|
|
2340
|
+
|
|
2341
|
+
if (stopped.length > 0) return [];
|
|
2342
|
+
|
|
1749
2343
|
const laterRows = yield* sql<Record<string, unknown>>`
|
|
1750
2344
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1751
2345
|
FROM effect_agent_submissions
|
|
1752
|
-
WHERE
|
|
2346
|
+
WHERE thread_id = ${validated.threadId}
|
|
1753
2347
|
AND queue_sequence > ${host.queue_sequence}
|
|
1754
2348
|
ORDER BY queue_sequence ASC
|
|
1755
2349
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1756
|
-
|
|
2350
|
+
|
|
2351
|
+
const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
|
|
1757
2352
|
const claimed: Array<JoiningClaim> = [];
|
|
2353
|
+
|
|
1758
2354
|
for (const row of later) {
|
|
1759
2355
|
if (claimed.length >= validated.maxCount) break;
|
|
1760
2356
|
// Rows already claimed by THIS host extend its contiguous prefix and are skipped;
|
|
@@ -1777,6 +2373,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1777
2373
|
SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
|
|
1778
2374
|
WHERE submission_id = ${row.submission_id}
|
|
1779
2375
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2376
|
+
|
|
1780
2377
|
const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
|
|
1781
2378
|
Effect.mapError((error) =>
|
|
1782
2379
|
corruptionFailure(
|
|
@@ -1787,6 +2384,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1787
2384
|
),
|
|
1788
2385
|
),
|
|
1789
2386
|
);
|
|
2387
|
+
|
|
1790
2388
|
claimed.push(
|
|
1791
2389
|
yield* decodeJoiningClaim({
|
|
1792
2390
|
submissionId: row.submission_id,
|
|
@@ -1795,10 +2393,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1795
2393
|
}).pipe(Effect.mapError(internalFailure(operation))),
|
|
1796
2394
|
);
|
|
1797
2395
|
}
|
|
1798
|
-
|
|
2396
|
+
|
|
2397
|
+
return claimed;
|
|
1799
2398
|
}),
|
|
1800
2399
|
);
|
|
2400
|
+
|
|
1801
2401
|
yield* hitFailpoint("ledger:claim-joining:after", operation);
|
|
2402
|
+
|
|
1802
2403
|
return claims;
|
|
1803
2404
|
});
|
|
1804
2405
|
|
|
@@ -1806,14 +2407,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1806
2407
|
"DoSubmissionLedger.markJoined",
|
|
1807
2408
|
)(function* (request: MarkJoinedRequest) {
|
|
1808
2409
|
const operation = "ledger mark joined";
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
2410
|
+
|
|
2411
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkJoinedRequest))(request).pipe(
|
|
2412
|
+
Effect.mapError(internalFailure(operation)),
|
|
2413
|
+
);
|
|
2414
|
+
|
|
1812
2415
|
yield* hitFailpoint("ledger:mark-joined:before", operation);
|
|
1813
2416
|
yield* inWriteTransaction(
|
|
1814
2417
|
operation,
|
|
1815
2418
|
Effect.gen(function* () {
|
|
1816
2419
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2420
|
+
|
|
1817
2421
|
if (submission.joined_host_submission_id === null) {
|
|
1818
2422
|
return yield* LedgerError.make({
|
|
1819
2423
|
operation,
|
|
@@ -1821,6 +2425,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1821
2425
|
});
|
|
1822
2426
|
}
|
|
1823
2427
|
const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
|
|
2428
|
+
|
|
1824
2429
|
// The lane is host-owned: the presented token must own the HOST's ownership period,
|
|
1825
2430
|
// which also lets a later host Attempt repair a lost marker from history (DUR-016).
|
|
1826
2431
|
yield* requireOwnership(operation, host, validated.ownershipToken);
|
|
@@ -1831,6 +2436,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1831
2436
|
) {
|
|
1832
2437
|
return;
|
|
1833
2438
|
}
|
|
2439
|
+
|
|
1834
2440
|
return yield* corruptionFailure(
|
|
1835
2441
|
operation,
|
|
1836
2442
|
"effect_agent_submissions",
|
|
@@ -1861,14 +2467,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1861
2467
|
"DoSubmissionLedger.revertJoining",
|
|
1862
2468
|
)(function* (request: RevertJoiningRequest) {
|
|
1863
2469
|
const operation = "ledger revert joining";
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
2470
|
+
|
|
2471
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(RevertJoiningRequest))(request).pipe(
|
|
2472
|
+
Effect.mapError(internalFailure(operation)),
|
|
2473
|
+
);
|
|
2474
|
+
|
|
1867
2475
|
yield* hitFailpoint("ledger:revert-joining:before", operation);
|
|
1868
2476
|
yield* inWriteTransaction(
|
|
1869
2477
|
operation,
|
|
1870
2478
|
Effect.gen(function* () {
|
|
1871
2479
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2480
|
+
|
|
1872
2481
|
// Idempotent and recovery-only: only a still-`joining` Submission reverts; an
|
|
1873
2482
|
// already-joined (or already-reverted) Submission is a no-op (DUR-016).
|
|
1874
2483
|
if (submission.state !== "joining") return;
|
|
@@ -1885,17 +2494,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1885
2494
|
const suspend: SubmissionLedger["Service"]["suspend"] = Effect.fn("DoSubmissionLedger.suspend")(
|
|
1886
2495
|
function* (request: SuspendRequest) {
|
|
1887
2496
|
const operation = "ledger suspend";
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2497
|
+
|
|
2498
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SuspendRequest))(request).pipe(
|
|
2499
|
+
Effect.mapError(internalFailure(operation)),
|
|
2500
|
+
);
|
|
2501
|
+
|
|
1891
2502
|
const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
|
|
1892
2503
|
Effect.mapError(internalFailure(operation)),
|
|
1893
2504
|
);
|
|
2505
|
+
|
|
1894
2506
|
yield* hitFailpoint("ledger:suspend:before", operation);
|
|
2507
|
+
|
|
1895
2508
|
const outcome = yield* inWriteTransaction(
|
|
1896
2509
|
operation,
|
|
1897
2510
|
Effect.gen(function* () {
|
|
1898
2511
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2512
|
+
|
|
1899
2513
|
if (submission.state === "settled") {
|
|
1900
2514
|
if (submission.settled_outcome === null) {
|
|
1901
2515
|
return yield* corruptionFailure(
|
|
@@ -1905,6 +2519,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1905
2519
|
"A settled Submission carries no terminal outcome.",
|
|
1906
2520
|
);
|
|
1907
2521
|
}
|
|
2522
|
+
|
|
1908
2523
|
return yield* SettlementConflict.make({
|
|
1909
2524
|
submissionId: validated.submissionId,
|
|
1910
2525
|
existingOutcome: submission.settled_outcome,
|
|
@@ -1913,6 +2528,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1913
2528
|
// An exact terminal outcome is already reserved (DUR-011); suspension would
|
|
1914
2529
|
// contradict it, so the reservation wins.
|
|
1915
2530
|
const reservation = yield* readReservation(operation, validated.submissionId);
|
|
2531
|
+
|
|
1916
2532
|
if (Option.isSome(reservation)) {
|
|
1917
2533
|
return yield* SettlementConflict.make({
|
|
1918
2534
|
submissionId: validated.submissionId,
|
|
@@ -1923,35 +2539,39 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1923
2539
|
// A covering event that raced ahead of the suspend transaction resumes the caller
|
|
1924
2540
|
// immediately WITHOUT releasing the lane (plan §2.6, §12). For WaitingForChild the
|
|
1925
2541
|
// covering evidence is EITHER a locally settled child row OR a durable cross-store
|
|
1926
|
-
// notification marker: parent and child
|
|
2542
|
+
// notification marker: parent and child Threads live in different Durable
|
|
1927
2543
|
// Objects, and the port contract requires that a child settlement reported (via
|
|
1928
2544
|
// `recordChildSettled` → marker) before this suspend commits is observed here.
|
|
1929
2545
|
if (validated.reason._tag === "ApprovalPending") {
|
|
1930
2546
|
const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
1931
2547
|
const decided = new Set(decisions.map((row) => row.tool_call_id));
|
|
2548
|
+
|
|
1932
2549
|
if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
|
|
1933
|
-
return
|
|
2550
|
+
return RESUME_IMMEDIATELY;
|
|
1934
2551
|
}
|
|
1935
2552
|
} else {
|
|
1936
2553
|
const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
|
|
1937
2554
|
const markerChildren = new Set(markers.map((row) => row.child_submission_id));
|
|
1938
2555
|
let allSettled = true;
|
|
2556
|
+
|
|
1939
2557
|
for (const child of validated.reason.children) {
|
|
1940
2558
|
const settled = yield* childProvablySettled(
|
|
1941
2559
|
operation,
|
|
1942
2560
|
markerChildren,
|
|
1943
2561
|
child.childSubmissionId,
|
|
1944
2562
|
);
|
|
2563
|
+
|
|
1945
2564
|
if (!settled) {
|
|
1946
2565
|
allSettled = false;
|
|
1947
2566
|
break;
|
|
1948
2567
|
}
|
|
1949
2568
|
}
|
|
1950
2569
|
if (allSettled) {
|
|
1951
|
-
return
|
|
2570
|
+
return RESUME_IMMEDIATELY;
|
|
1952
2571
|
}
|
|
1953
2572
|
}
|
|
1954
2573
|
const now = yield* currentInstant;
|
|
2574
|
+
|
|
1955
2575
|
yield* sql`
|
|
1956
2576
|
UPDATE effect_agent_submissions
|
|
1957
2577
|
SET
|
|
@@ -1966,10 +2586,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1966
2586
|
DELETE FROM effect_agent_submission_ownership
|
|
1967
2587
|
WHERE submission_id = ${validated.submissionId}
|
|
1968
2588
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1969
|
-
|
|
2589
|
+
|
|
2590
|
+
return SUSPENDED;
|
|
1970
2591
|
}),
|
|
1971
2592
|
);
|
|
2593
|
+
|
|
1972
2594
|
yield* hitFailpoint("ledger:suspend:after", operation);
|
|
2595
|
+
|
|
1973
2596
|
return outcome;
|
|
1974
2597
|
},
|
|
1975
2598
|
);
|
|
@@ -1985,6 +2608,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1985
2608
|
submission: SubmissionRow,
|
|
1986
2609
|
): Effect.fn.Return<void, LedgerError> {
|
|
1987
2610
|
if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
|
|
2611
|
+
|
|
1988
2612
|
const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
|
|
1989
2613
|
submission.suspended_reason_json,
|
|
1990
2614
|
).pipe(
|
|
@@ -1997,9 +2621,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1997
2621
|
),
|
|
1998
2622
|
),
|
|
1999
2623
|
);
|
|
2624
|
+
|
|
2000
2625
|
if (reason._tag !== "ApprovalPending") return;
|
|
2001
2626
|
const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
|
|
2002
2627
|
const decided = new Set(decisions.map((row) => row.tool_call_id));
|
|
2628
|
+
|
|
2003
2629
|
if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
|
|
2004
2630
|
yield* sql`
|
|
2005
2631
|
UPDATE effect_agent_submissions
|
|
@@ -2015,14 +2641,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2015
2641
|
"DoSubmissionLedger.recordApprovalDecision",
|
|
2016
2642
|
)(function* (command: ApprovalDecisionCommand) {
|
|
2017
2643
|
const operation = "ledger record approval decision";
|
|
2018
|
-
|
|
2644
|
+
|
|
2645
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ApprovalDecisionCommand))(
|
|
2019
2646
|
command,
|
|
2020
2647
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2648
|
+
|
|
2021
2649
|
yield* hitFailpoint("ledger:approval-decision:before", operation);
|
|
2650
|
+
|
|
2022
2651
|
const intent = yield* inWriteTransaction(
|
|
2023
2652
|
operation,
|
|
2024
2653
|
Effect.gen(function* () {
|
|
2025
2654
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2655
|
+
|
|
2026
2656
|
if (submission.state === "settled") {
|
|
2027
2657
|
if (submission.settled_outcome === null) {
|
|
2028
2658
|
return yield* corruptionFailure(
|
|
@@ -2032,6 +2662,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2032
2662
|
"A settled Submission carries no terminal outcome.",
|
|
2033
2663
|
);
|
|
2034
2664
|
}
|
|
2665
|
+
|
|
2035
2666
|
return yield* SettlementConflict.make({
|
|
2036
2667
|
submissionId: validated.submissionId,
|
|
2037
2668
|
existingOutcome: submission.settled_outcome,
|
|
@@ -2039,6 +2670,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2039
2670
|
}
|
|
2040
2671
|
const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
2041
2672
|
const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
|
|
2673
|
+
|
|
2042
2674
|
if (existing !== undefined) {
|
|
2043
2675
|
// Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
|
|
2044
2676
|
// the recorded intent unchanged; a divergent re-decision conflicts.
|
|
@@ -2049,9 +2681,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2049
2681
|
existingDecision: existing.decision,
|
|
2050
2682
|
});
|
|
2051
2683
|
}
|
|
2684
|
+
|
|
2052
2685
|
return yield* approvalIntentFromRow(operation, existing);
|
|
2053
2686
|
}
|
|
2054
2687
|
const now = yield* currentInstant;
|
|
2688
|
+
|
|
2055
2689
|
yield* sql`
|
|
2056
2690
|
INSERT INTO effect_agent_approval_decisions (
|
|
2057
2691
|
submission_id,
|
|
@@ -2070,6 +2704,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2070
2704
|
)
|
|
2071
2705
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2072
2706
|
yield* wakeSuspendedIfCovered(operation, submission);
|
|
2707
|
+
|
|
2073
2708
|
return yield* decodeApprovalDecisionIntent({
|
|
2074
2709
|
submissionId: validated.submissionId,
|
|
2075
2710
|
toolCallId: validated.toolCallId,
|
|
@@ -2080,7 +2715,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2080
2715
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
2081
2716
|
}),
|
|
2082
2717
|
);
|
|
2718
|
+
|
|
2083
2719
|
yield* hitFailpoint("ledger:approval-decision:after", operation);
|
|
2720
|
+
|
|
2084
2721
|
return intent;
|
|
2085
2722
|
});
|
|
2086
2723
|
|
|
@@ -2088,14 +2725,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2088
2725
|
"DoSubmissionLedger.markUnknown",
|
|
2089
2726
|
)(function* (request: MarkUnknownRequest) {
|
|
2090
2727
|
const operation = "ledger mark unknown";
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2728
|
+
|
|
2729
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkUnknownRequest))(request).pipe(
|
|
2730
|
+
Effect.mapError(internalFailure(operation)),
|
|
2731
|
+
);
|
|
2732
|
+
|
|
2094
2733
|
yield* hitFailpoint("ledger:mark-unknown:before", operation);
|
|
2095
2734
|
yield* inWriteTransaction(
|
|
2096
2735
|
operation,
|
|
2097
2736
|
Effect.gen(function* () {
|
|
2098
2737
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2738
|
+
|
|
2099
2739
|
if (submission.state === "settled") {
|
|
2100
2740
|
if (submission.settled_outcome === null) {
|
|
2101
2741
|
return yield* corruptionFailure(
|
|
@@ -2105,6 +2745,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2105
2745
|
"A settled Submission carries no terminal outcome.",
|
|
2106
2746
|
);
|
|
2107
2747
|
}
|
|
2748
|
+
|
|
2108
2749
|
return yield* SettlementConflict.make({
|
|
2109
2750
|
submissionId: validated.submissionId,
|
|
2110
2751
|
existingOutcome: submission.settled_outcome,
|
|
@@ -2113,6 +2754,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2113
2754
|
// A reserved exact outcome wins over a late Unknown marking (DUR-011); the recovery
|
|
2114
2755
|
// classifier orders reservation ahead of MarkUnknown for the same reason.
|
|
2115
2756
|
const reservation = yield* readReservation(operation, validated.submissionId);
|
|
2757
|
+
|
|
2116
2758
|
if (Option.isSome(reservation)) {
|
|
2117
2759
|
return yield* SettlementConflict.make({
|
|
2118
2760
|
submissionId: validated.submissionId,
|
|
@@ -2123,13 +2765,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2123
2765
|
// set while the first recorded reason is kept.
|
|
2124
2766
|
const existingIds = yield* storedUnknownToolCallIds(operation, submission);
|
|
2125
2767
|
const known = new Set(existingIds);
|
|
2768
|
+
|
|
2126
2769
|
const merged = [
|
|
2127
2770
|
...existingIds,
|
|
2128
2771
|
...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
|
|
2129
2772
|
];
|
|
2773
|
+
|
|
2130
2774
|
const idsJson = yield* encodeToolCallIdsText(merged).pipe(
|
|
2131
2775
|
Effect.mapError(internalFailure(operation)),
|
|
2132
2776
|
);
|
|
2777
|
+
|
|
2133
2778
|
yield* sql`
|
|
2134
2779
|
UPDATE effect_agent_submissions
|
|
2135
2780
|
SET
|
|
@@ -2147,17 +2792,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2147
2792
|
"DoSubmissionLedger.recordUnknownResolution",
|
|
2148
2793
|
)(function* (command: UnknownResolutionCommand) {
|
|
2149
2794
|
const operation = "ledger record unknown resolution";
|
|
2150
|
-
|
|
2795
|
+
|
|
2796
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(UnknownResolutionCommand))(
|
|
2151
2797
|
command,
|
|
2152
2798
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2799
|
+
|
|
2153
2800
|
const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
|
|
2154
2801
|
Effect.mapError(internalFailure(operation)),
|
|
2155
2802
|
);
|
|
2803
|
+
|
|
2156
2804
|
yield* hitFailpoint("ledger:unknown-resolution:before", operation);
|
|
2805
|
+
|
|
2157
2806
|
const intent = yield* inWriteTransaction(
|
|
2158
2807
|
operation,
|
|
2159
2808
|
Effect.gen(function* () {
|
|
2160
2809
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2810
|
+
|
|
2161
2811
|
if (submission.state === "settled") {
|
|
2162
2812
|
if (submission.settled_outcome === null) {
|
|
2163
2813
|
return yield* corruptionFailure(
|
|
@@ -2167,6 +2817,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2167
2817
|
"A settled Submission carries no terminal outcome.",
|
|
2168
2818
|
);
|
|
2169
2819
|
}
|
|
2820
|
+
|
|
2170
2821
|
return yield* SettlementConflict.make({
|
|
2171
2822
|
submissionId: validated.submissionId,
|
|
2172
2823
|
existingOutcome: submission.settled_outcome,
|
|
@@ -2174,19 +2825,30 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2174
2825
|
}
|
|
2175
2826
|
const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
2176
2827
|
const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
|
|
2177
|
-
|
|
2828
|
+
|
|
2829
|
+
const existingIntent =
|
|
2830
|
+
existing === undefined
|
|
2831
|
+
? undefined
|
|
2832
|
+
: yield* unknownResolutionIntentFromRow(operation, existing);
|
|
2833
|
+
|
|
2834
|
+
if (
|
|
2835
|
+
existingIntent !== undefined &&
|
|
2836
|
+
!equivalentUnknownResolution(existingIntent.resolution, validated.resolution)
|
|
2837
|
+
) {
|
|
2178
2838
|
return yield* UnknownResolutionConflict.make({
|
|
2179
2839
|
submissionId: validated.submissionId,
|
|
2180
2840
|
toolCallId: validated.toolCallId,
|
|
2181
2841
|
});
|
|
2182
2842
|
}
|
|
2183
2843
|
let resolved: UnknownResolutionIntent;
|
|
2184
|
-
|
|
2844
|
+
|
|
2845
|
+
if (existingIntent !== undefined) {
|
|
2185
2846
|
// Idempotent replay of the recorded intent (author/reason may differ; the stored
|
|
2186
2847
|
// audit fields win, exactly like requestAbort).
|
|
2187
|
-
resolved =
|
|
2848
|
+
resolved = existingIntent;
|
|
2188
2849
|
} else {
|
|
2189
2850
|
const now = yield* currentInstant;
|
|
2851
|
+
|
|
2190
2852
|
yield* sql`
|
|
2191
2853
|
INSERT INTO effect_agent_unknown_resolutions (
|
|
2192
2854
|
submission_id,
|
|
@@ -2204,9 +2866,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2204
2866
|
${now.iso}
|
|
2205
2867
|
)
|
|
2206
2868
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2869
|
+
|
|
2207
2870
|
const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
|
|
2208
2871
|
Effect.mapError(internalFailure(operation)),
|
|
2209
2872
|
);
|
|
2873
|
+
|
|
2210
2874
|
resolved = yield* decodeUnknownResolutionIntent({
|
|
2211
2875
|
submissionId: validated.submissionId,
|
|
2212
2876
|
toolCallId: validated.toolCallId,
|
|
@@ -2223,6 +2887,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2223
2887
|
const markedIds = yield* storedUnknownToolCallIds(operation, submission);
|
|
2224
2888
|
const covering = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
2225
2889
|
const coveredIds = new Set(covering.map((row) => row.tool_call_id));
|
|
2890
|
+
|
|
2226
2891
|
if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) {
|
|
2227
2892
|
yield* sql`
|
|
2228
2893
|
UPDATE effect_agent_submissions
|
|
@@ -2234,10 +2899,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2234
2899
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2235
2900
|
}
|
|
2236
2901
|
}
|
|
2902
|
+
|
|
2237
2903
|
return resolved;
|
|
2238
2904
|
}),
|
|
2239
2905
|
);
|
|
2906
|
+
|
|
2240
2907
|
yield* hitFailpoint("ledger:unknown-resolution:after", operation);
|
|
2908
|
+
|
|
2241
2909
|
return intent;
|
|
2242
2910
|
});
|
|
2243
2911
|
|
|
@@ -2245,21 +2913,31 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2245
2913
|
"DoSubmissionLedger.recordChildSettled",
|
|
2246
2914
|
)(function* (request: ChildSettledNotification) {
|
|
2247
2915
|
const operation = "ledger record child settled";
|
|
2248
|
-
|
|
2916
|
+
|
|
2917
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ChildSettledNotification))(
|
|
2249
2918
|
request,
|
|
2250
2919
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2920
|
+
|
|
2251
2921
|
yield* hitFailpoint("ledger:child-settled:before", operation);
|
|
2922
|
+
|
|
2252
2923
|
const outcome = yield* inWriteTransaction(
|
|
2253
2924
|
operation,
|
|
2254
2925
|
Effect.gen(function* () {
|
|
2255
2926
|
const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
|
|
2256
2927
|
// The child's canonical Settlement is the authority for this wake. When the child's
|
|
2257
|
-
// row lives in THIS store (single-store latitude, and every conformance lane),
|
|
2258
|
-
//
|
|
2259
|
-
//
|
|
2260
|
-
//
|
|
2928
|
+
// row lives in THIS store (single-store latitude, and every conformance lane), either a
|
|
2929
|
+
// finalized row or an exact terminalizing reservation admits the notification: the
|
|
2930
|
+
// runtime calls only after the canonical append and before ledger finalization. When the
|
|
2931
|
+
// row does not live here — the normal cross-DO case — the routed notification from the
|
|
2932
|
+
// child's owning Durable Object is the settlement evidence this store records durably.
|
|
2261
2933
|
const child = yield* readSubmission(operation, validated.childSubmissionId);
|
|
2262
|
-
|
|
2934
|
+
const childReservation = yield* readReservation(operation, validated.childSubmissionId);
|
|
2935
|
+
|
|
2936
|
+
if (
|
|
2937
|
+
Option.isSome(child) &&
|
|
2938
|
+
child.value.state !== "settled" &&
|
|
2939
|
+
!(child.value.state === "terminalizing" && Option.isSome(childReservation))
|
|
2940
|
+
) {
|
|
2263
2941
|
return yield* LedgerError.make({
|
|
2264
2942
|
operation,
|
|
2265
2943
|
message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`,
|
|
@@ -2270,6 +2948,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2270
2948
|
// before the parent's suspend transaction is observed by that suspend's covering
|
|
2271
2949
|
// check, even when the parent is not (or not yet) suspended.
|
|
2272
2950
|
const now = yield* currentInstant;
|
|
2951
|
+
|
|
2273
2952
|
yield* sql`
|
|
2274
2953
|
INSERT INTO effect_agent_child_settlements (
|
|
2275
2954
|
parent_submission_id,
|
|
@@ -2279,15 +2958,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2279
2958
|
) VALUES (
|
|
2280
2959
|
${validated.parentSubmissionId},
|
|
2281
2960
|
${validated.childSubmissionId},
|
|
2282
|
-
${
|
|
2961
|
+
${
|
|
2962
|
+
Option.isSome(child) && child.value.state === "settled"
|
|
2963
|
+
? child.value.settled_outcome
|
|
2964
|
+
: Option.isSome(childReservation)
|
|
2965
|
+
? childReservation.value.outcome
|
|
2966
|
+
: null
|
|
2967
|
+
},
|
|
2283
2968
|
${now.iso}
|
|
2284
2969
|
)
|
|
2285
2970
|
ON CONFLICT (parent_submission_id, child_submission_id) DO NOTHING
|
|
2286
2971
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2287
2972
|
|
|
2288
2973
|
if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
|
|
2289
|
-
return
|
|
2974
|
+
return NOT_WAITING;
|
|
2290
2975
|
}
|
|
2976
|
+
|
|
2291
2977
|
const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
|
|
2292
2978
|
parent.suspended_reason_json,
|
|
2293
2979
|
).pipe(
|
|
@@ -2300,27 +2986,30 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2300
2986
|
),
|
|
2301
2987
|
),
|
|
2302
2988
|
);
|
|
2989
|
+
|
|
2303
2990
|
if (reason._tag !== "WaitingForChild") {
|
|
2304
|
-
return
|
|
2991
|
+
return NOT_WAITING;
|
|
2305
2992
|
}
|
|
2306
2993
|
if (
|
|
2307
2994
|
!reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
|
|
2308
2995
|
) {
|
|
2309
|
-
return
|
|
2996
|
+
return NOT_WAITING;
|
|
2310
2997
|
}
|
|
2311
2998
|
// The parent wakes exactly when EVERY listed child is provably settled — from its
|
|
2312
2999
|
// local row or a recorded marker (spec §12 step 10); replays re-run the coverage
|
|
2313
3000
|
// check so a recovering caller wakes the lane idempotently.
|
|
2314
3001
|
const markers = yield* readChildSettlementMarkers(operation, validated.parentSubmissionId);
|
|
2315
3002
|
const markerChildren = new Set(markers.map((row) => row.child_submission_id));
|
|
3003
|
+
|
|
2316
3004
|
for (const entry of reason.children) {
|
|
2317
3005
|
const settled = yield* childProvablySettled(
|
|
2318
3006
|
operation,
|
|
2319
3007
|
markerChildren,
|
|
2320
3008
|
entry.childSubmissionId,
|
|
2321
3009
|
);
|
|
3010
|
+
|
|
2322
3011
|
if (!settled) {
|
|
2323
|
-
return
|
|
3012
|
+
return STILL_WAITING;
|
|
2324
3013
|
}
|
|
2325
3014
|
}
|
|
2326
3015
|
yield* sql`
|
|
@@ -2331,10 +3020,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2331
3020
|
suspended_at = NULL
|
|
2332
3021
|
WHERE submission_id = ${validated.parentSubmissionId}
|
|
2333
3022
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2334
|
-
|
|
3023
|
+
|
|
3024
|
+
return WOKEN;
|
|
2335
3025
|
}),
|
|
2336
3026
|
);
|
|
3027
|
+
|
|
2337
3028
|
yield* hitFailpoint("ledger:child-settled:after", operation);
|
|
3029
|
+
|
|
2338
3030
|
return outcome;
|
|
2339
3031
|
});
|
|
2340
3032
|
|
|
@@ -2342,25 +3034,36 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2342
3034
|
"DoSubmissionLedger.reserveChildBudget",
|
|
2343
3035
|
)(function* (request: ChildBudgetReservationRequest) {
|
|
2344
3036
|
const operation = "ledger reserve child budget";
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
3037
|
+
|
|
3038
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ChildBudgetReservationRequest))(
|
|
3039
|
+
request,
|
|
3040
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3041
|
+
|
|
2348
3042
|
const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
|
|
2349
3043
|
Effect.mapError(internalFailure(operation)),
|
|
2350
3044
|
);
|
|
3045
|
+
|
|
2351
3046
|
yield* hitFailpoint("ledger:child-reservation:before", operation);
|
|
3047
|
+
|
|
2352
3048
|
const reserved = yield* inWriteTransaction(
|
|
2353
3049
|
operation,
|
|
2354
3050
|
Effect.gen(function* () {
|
|
2355
3051
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
3052
|
+
|
|
2356
3053
|
if (Option.isSome(existing)) {
|
|
3054
|
+
const existingSnapshot = yield* childReservationSnapshotFromRow(
|
|
3055
|
+
operation,
|
|
3056
|
+
existing.value,
|
|
3057
|
+
);
|
|
3058
|
+
|
|
2357
3059
|
// Identical replays short-circuit before the fence, mirroring reserveSettlement: a
|
|
2358
3060
|
// replay creates nothing, so a recovering caller resumes rather than duplicates.
|
|
2359
3061
|
const identical =
|
|
2360
3062
|
existing.value.parent_submission_id === validated.parentSubmissionId &&
|
|
2361
3063
|
existing.value.parent_tool_call_id === validated.parentToolCallId &&
|
|
2362
3064
|
existing.value.allocation_digest === validated.allocationDigest &&
|
|
2363
|
-
|
|
3065
|
+
equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
|
|
3066
|
+
|
|
2364
3067
|
if (!identical) {
|
|
2365
3068
|
return yield* ChildReservationConflict.make({
|
|
2366
3069
|
reservationId: validated.reservationId,
|
|
@@ -2369,16 +3072,19 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2369
3072
|
"A reservation with this identity exists with a different parent Tool Call or allocation.",
|
|
2370
3073
|
});
|
|
2371
3074
|
}
|
|
3075
|
+
|
|
2372
3076
|
return ReservedChildBudget.make({
|
|
2373
|
-
reservation:
|
|
3077
|
+
reservation: existingSnapshot,
|
|
2374
3078
|
replayed: true,
|
|
2375
3079
|
});
|
|
2376
3080
|
}
|
|
3081
|
+
|
|
2377
3082
|
const collision = yield* readChildReservationForCall(
|
|
2378
3083
|
operation,
|
|
2379
3084
|
validated.parentSubmissionId,
|
|
2380
3085
|
validated.parentToolCallId,
|
|
2381
3086
|
);
|
|
3087
|
+
|
|
2382
3088
|
if (Option.isSome(collision)) {
|
|
2383
3089
|
return yield* ChildReservationConflict.make({
|
|
2384
3090
|
reservationId: validated.reservationId,
|
|
@@ -2387,10 +3093,12 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2387
3093
|
});
|
|
2388
3094
|
}
|
|
2389
3095
|
const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
|
|
3096
|
+
|
|
2390
3097
|
// Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
|
|
2391
3098
|
// parent Attempt can never create new reservation state.
|
|
2392
3099
|
yield* requireOwnership(operation, parent, validated.ownershipToken);
|
|
2393
3100
|
const now = yield* currentInstant;
|
|
3101
|
+
|
|
2394
3102
|
yield* sql`
|
|
2395
3103
|
INSERT INTO effect_agent_child_reservations (
|
|
2396
3104
|
reservation_id,
|
|
@@ -2411,6 +3119,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2411
3119
|
)
|
|
2412
3120
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2413
3121
|
const inserted = yield* readChildReservation(operation, validated.reservationId);
|
|
3122
|
+
|
|
2414
3123
|
if (Option.isNone(inserted)) {
|
|
2415
3124
|
return yield* corruptionFailure(
|
|
2416
3125
|
operation,
|
|
@@ -2419,13 +3128,16 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2419
3128
|
"An inserted child reservation row is missing inside its own transaction.",
|
|
2420
3129
|
);
|
|
2421
3130
|
}
|
|
3131
|
+
|
|
2422
3132
|
return ReservedChildBudget.make({
|
|
2423
3133
|
reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
|
|
2424
3134
|
replayed: false,
|
|
2425
3135
|
});
|
|
2426
3136
|
}),
|
|
2427
3137
|
);
|
|
3138
|
+
|
|
2428
3139
|
yield* hitFailpoint("ledger:child-reservation:after", operation);
|
|
3140
|
+
|
|
2429
3141
|
return reserved;
|
|
2430
3142
|
});
|
|
2431
3143
|
|
|
@@ -2434,14 +3146,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2434
3146
|
request: AttachChildToReservationRequest,
|
|
2435
3147
|
) {
|
|
2436
3148
|
const operation = "ledger attach child to reservation";
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
3149
|
+
|
|
3150
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AttachChildToReservationRequest))(
|
|
3151
|
+
request,
|
|
3152
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3153
|
+
|
|
2440
3154
|
yield* hitFailpoint("ledger:child-attach:before", operation);
|
|
3155
|
+
|
|
2441
3156
|
const attached = yield* inWriteTransaction(
|
|
2442
3157
|
operation,
|
|
2443
3158
|
Effect.gen(function* () {
|
|
2444
3159
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
3160
|
+
|
|
2445
3161
|
if (Option.isNone(existing)) {
|
|
2446
3162
|
return yield* LedgerError.make({
|
|
2447
3163
|
operation,
|
|
@@ -2453,6 +3169,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2453
3169
|
if (existing.value.child_submission_id === validated.childSubmissionId) {
|
|
2454
3170
|
return yield* childReservationSnapshotFromRow(operation, existing.value);
|
|
2455
3171
|
}
|
|
3172
|
+
|
|
2456
3173
|
return yield* ChildReservationConflict.make({
|
|
2457
3174
|
reservationId: validated.reservationId,
|
|
2458
3175
|
status: existing.value.status,
|
|
@@ -2460,6 +3177,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2460
3177
|
});
|
|
2461
3178
|
}
|
|
2462
3179
|
const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
|
|
3180
|
+
|
|
2463
3181
|
yield* requireOwnership(operation, parent, validated.ownershipToken);
|
|
2464
3182
|
if (existing.value.status !== "reserved") {
|
|
2465
3183
|
return yield* ChildReservationConflict.make({
|
|
@@ -2478,6 +3196,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2478
3196
|
WHERE reservation_id = ${validated.reservationId}
|
|
2479
3197
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2480
3198
|
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
3199
|
+
|
|
2481
3200
|
if (Option.isNone(updated)) {
|
|
2482
3201
|
return yield* corruptionFailure(
|
|
2483
3202
|
operation,
|
|
@@ -2486,10 +3205,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2486
3205
|
"An updated child reservation row is missing inside its own transaction.",
|
|
2487
3206
|
);
|
|
2488
3207
|
}
|
|
3208
|
+
|
|
2489
3209
|
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
2490
3210
|
}),
|
|
2491
3211
|
);
|
|
3212
|
+
|
|
2492
3213
|
yield* hitFailpoint("ledger:child-attach:after", operation);
|
|
3214
|
+
|
|
2493
3215
|
return attached;
|
|
2494
3216
|
});
|
|
2495
3217
|
|
|
@@ -2497,17 +3219,22 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2497
3219
|
"DoSubmissionLedger.beginChildBudgetRelease",
|
|
2498
3220
|
)(function* (request: BeginChildBudgetReleaseRequest) {
|
|
2499
3221
|
const operation = "ledger begin child budget release";
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
3222
|
+
|
|
3223
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(BeginChildBudgetReleaseRequest))(
|
|
3224
|
+
request,
|
|
3225
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3226
|
+
|
|
2503
3227
|
const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
|
|
2504
3228
|
Effect.mapError(internalFailure(operation)),
|
|
2505
3229
|
);
|
|
3230
|
+
|
|
2506
3231
|
yield* hitFailpoint("ledger:child-release-pending:before", operation);
|
|
3232
|
+
|
|
2507
3233
|
const frozen = yield* inWriteTransaction(
|
|
2508
3234
|
operation,
|
|
2509
3235
|
Effect.gen(function* () {
|
|
2510
3236
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
3237
|
+
|
|
2511
3238
|
if (Option.isNone(existing)) {
|
|
2512
3239
|
return yield* LedgerError.make({
|
|
2513
3240
|
operation,
|
|
@@ -2515,11 +3242,20 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2515
3242
|
});
|
|
2516
3243
|
}
|
|
2517
3244
|
if (existing.value.status !== "reserved") {
|
|
3245
|
+
const existingSnapshot = yield* childReservationSnapshotFromRow(
|
|
3246
|
+
operation,
|
|
3247
|
+
existing.value,
|
|
3248
|
+
);
|
|
3249
|
+
|
|
2518
3250
|
// The accounting decision was already frozen exactly once; an identical replay is a
|
|
2519
3251
|
// no-op and a divergent decision conflicts (spec §12 join step 6).
|
|
2520
|
-
if (
|
|
2521
|
-
|
|
3252
|
+
if (
|
|
3253
|
+
existingSnapshot.accounting !== undefined &&
|
|
3254
|
+
equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
|
|
3255
|
+
) {
|
|
3256
|
+
return existingSnapshot;
|
|
2522
3257
|
}
|
|
3258
|
+
|
|
2523
3259
|
return yield* ChildReservationConflict.make({
|
|
2524
3260
|
reservationId: validated.reservationId,
|
|
2525
3261
|
status: existing.value.status,
|
|
@@ -2527,6 +3263,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2527
3263
|
});
|
|
2528
3264
|
}
|
|
2529
3265
|
const now = yield* currentInstant;
|
|
3266
|
+
|
|
2530
3267
|
yield* sql`
|
|
2531
3268
|
UPDATE effect_agent_child_reservations
|
|
2532
3269
|
SET
|
|
@@ -2536,6 +3273,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2536
3273
|
WHERE reservation_id = ${validated.reservationId}
|
|
2537
3274
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2538
3275
|
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
3276
|
+
|
|
2539
3277
|
if (Option.isNone(updated)) {
|
|
2540
3278
|
return yield* corruptionFailure(
|
|
2541
3279
|
operation,
|
|
@@ -2544,10 +3282,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2544
3282
|
"An updated child reservation row is missing inside its own transaction.",
|
|
2545
3283
|
);
|
|
2546
3284
|
}
|
|
3285
|
+
|
|
2547
3286
|
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
2548
3287
|
}),
|
|
2549
3288
|
);
|
|
3289
|
+
|
|
2550
3290
|
yield* hitFailpoint("ledger:child-release-pending:after", operation);
|
|
3291
|
+
|
|
2551
3292
|
return frozen;
|
|
2552
3293
|
});
|
|
2553
3294
|
|
|
@@ -2555,14 +3296,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2555
3296
|
"DoSubmissionLedger.releaseChildBudget",
|
|
2556
3297
|
)(function* (request: ReleaseChildBudgetRequest) {
|
|
2557
3298
|
const operation = "ledger release child budget";
|
|
2558
|
-
|
|
3299
|
+
|
|
3300
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseChildBudgetRequest))(
|
|
2559
3301
|
request,
|
|
2560
3302
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3303
|
+
|
|
2561
3304
|
yield* hitFailpoint("ledger:child-release:before", operation);
|
|
3305
|
+
|
|
2562
3306
|
const released = yield* inWriteTransaction(
|
|
2563
3307
|
operation,
|
|
2564
3308
|
Effect.gen(function* () {
|
|
2565
3309
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
3310
|
+
|
|
2566
3311
|
if (Option.isNone(existing)) {
|
|
2567
3312
|
return yield* LedgerError.make({
|
|
2568
3313
|
operation,
|
|
@@ -2582,12 +3327,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2582
3327
|
});
|
|
2583
3328
|
}
|
|
2584
3329
|
const now = yield* currentInstant;
|
|
3330
|
+
|
|
2585
3331
|
yield* sql`
|
|
2586
3332
|
UPDATE effect_agent_child_reservations
|
|
2587
3333
|
SET status = 'released', released_at = ${now.iso}
|
|
2588
3334
|
WHERE reservation_id = ${validated.reservationId}
|
|
2589
3335
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2590
3336
|
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
3337
|
+
|
|
2591
3338
|
if (Option.isNone(updated)) {
|
|
2592
3339
|
return yield* corruptionFailure(
|
|
2593
3340
|
operation,
|
|
@@ -2596,76 +3343,159 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2596
3343
|
"An updated child reservation row is missing inside its own transaction.",
|
|
2597
3344
|
);
|
|
2598
3345
|
}
|
|
3346
|
+
|
|
2599
3347
|
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
2600
3348
|
}),
|
|
2601
3349
|
);
|
|
3350
|
+
|
|
2602
3351
|
yield* hitFailpoint("ledger:child-release:after", operation);
|
|
3352
|
+
|
|
2603
3353
|
return released;
|
|
2604
3354
|
});
|
|
2605
3355
|
|
|
2606
3356
|
interface ScanCursor {
|
|
2607
|
-
readonly
|
|
3357
|
+
readonly threadId: string;
|
|
2608
3358
|
readonly queueSequence: number;
|
|
2609
3359
|
}
|
|
2610
3360
|
|
|
2611
3361
|
const scanPage = Effect.fn("DoSubmissionLedger.scanPage")(function* (
|
|
2612
3362
|
cursor: ScanCursor | undefined,
|
|
2613
3363
|
): Effect.fn.Return<
|
|
2614
|
-
readonly [ReadonlyArray<
|
|
3364
|
+
readonly [ReadonlyArray<SubmissionWorkItem>, Option.Option<ScanCursor | undefined>],
|
|
2615
3365
|
LedgerError
|
|
2616
3366
|
> {
|
|
2617
3367
|
const operation = "ledger scan nonterminal";
|
|
3368
|
+
|
|
2618
3369
|
const rows = yield* (
|
|
2619
3370
|
cursor === undefined
|
|
2620
3371
|
? sql<Record<string, unknown>>`
|
|
2621
|
-
SELECT
|
|
3372
|
+
SELECT submission_id AS "submissionId", thread_id AS "threadId",
|
|
3373
|
+
queue_sequence AS "queueSequence", principal, idempotency_key AS "idempotencyKey",
|
|
3374
|
+
deployment_id AS "deploymentId", receipt_id AS "receiptId", state
|
|
2622
3375
|
FROM effect_agent_submissions
|
|
2623
3376
|
WHERE state <> 'settled'
|
|
2624
|
-
ORDER BY
|
|
3377
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
2625
3378
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
2626
3379
|
`
|
|
2627
3380
|
: sql<Record<string, unknown>>`
|
|
2628
|
-
SELECT
|
|
3381
|
+
SELECT submission_id AS "submissionId", thread_id AS "threadId",
|
|
3382
|
+
queue_sequence AS "queueSequence", principal, idempotency_key AS "idempotencyKey",
|
|
3383
|
+
deployment_id AS "deploymentId", receipt_id AS "receiptId", state
|
|
2629
3384
|
FROM effect_agent_submissions
|
|
2630
3385
|
WHERE state <> 'settled'
|
|
2631
|
-
AND (
|
|
2632
|
-
|
|
2633
|
-
OR (
|
|
2634
|
-
conversation_id = ${cursor.conversationId}
|
|
2635
|
-
AND queue_sequence > ${cursor.queueSequence}
|
|
2636
|
-
)
|
|
2637
|
-
)
|
|
2638
|
-
ORDER BY conversation_id ASC, queue_sequence ASC
|
|
3386
|
+
AND (thread_id, queue_sequence) > (${cursor.threadId}, ${cursor.queueSequence})
|
|
3387
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
2639
3388
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
2640
3389
|
`
|
|
2641
3390
|
).pipe(Effect.mapError(sqlFailure(operation)));
|
|
2642
|
-
|
|
2643
|
-
const
|
|
2644
|
-
|
|
2645
|
-
|
|
3391
|
+
|
|
3392
|
+
const decoded = yield* decodeRows(
|
|
3393
|
+
Schema.Array(SubmissionWorkItem),
|
|
3394
|
+
"effect_agent_submissions",
|
|
3395
|
+
"nonterminal_scan",
|
|
3396
|
+
rows,
|
|
3397
|
+
"SubmissionWorkItem",
|
|
3398
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3399
|
+
|
|
2646
3400
|
const last = decoded[decoded.length - 1];
|
|
3401
|
+
|
|
2647
3402
|
const next: Option.Option<ScanCursor | undefined> =
|
|
2648
3403
|
last === undefined || decoded.length < SCAN_PAGE_SIZE
|
|
2649
3404
|
? Option.none()
|
|
2650
3405
|
: Option.some({
|
|
2651
|
-
|
|
2652
|
-
queueSequence: last.
|
|
3406
|
+
threadId: last.threadId,
|
|
3407
|
+
queueSequence: last.queueSequence,
|
|
2653
3408
|
});
|
|
2654
|
-
|
|
3409
|
+
|
|
3410
|
+
return [decoded, next] as const;
|
|
2655
3411
|
});
|
|
2656
3412
|
|
|
2657
|
-
const scanNonterminal: Stream.Stream<
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
3413
|
+
const scanNonterminal: Stream.Stream<SubmissionWorkItem, LedgerError> = Stream.paginate<
|
|
3414
|
+
ScanCursor | undefined,
|
|
3415
|
+
SubmissionWorkItem,
|
|
3416
|
+
LedgerError
|
|
3417
|
+
>(undefined, scanPage);
|
|
3418
|
+
|
|
3419
|
+
const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
|
|
3420
|
+
"DoSubmissionLedger.readAbortIntentForSubmission",
|
|
3421
|
+
)(function* (request) {
|
|
3422
|
+
const operation = "ledger read abort intent";
|
|
3423
|
+
|
|
3424
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
|
|
3425
|
+
Effect.mapError(internalFailure(operation)),
|
|
3426
|
+
);
|
|
3427
|
+
|
|
3428
|
+
const recordId = submissionAbortRecordId(validated.submissionId);
|
|
3429
|
+
|
|
3430
|
+
const rows = yield* sql<Record<string, unknown>>`
|
|
3431
|
+
SELECT
|
|
3432
|
+
submission.submission_id,
|
|
3433
|
+
abort.submission_id AS abort_submission_id,
|
|
3434
|
+
abort.author,
|
|
3435
|
+
abort.reason,
|
|
3436
|
+
abort.requested_at,
|
|
3437
|
+
canonical.record_id AS canonical_record_id
|
|
3438
|
+
FROM effect_agent_submissions AS submission
|
|
3439
|
+
LEFT JOIN effect_agent_abort_intents AS abort
|
|
3440
|
+
ON abort.submission_id = submission.submission_id
|
|
3441
|
+
LEFT JOIN effect_agent_canonical_records AS canonical
|
|
3442
|
+
ON canonical.thread_id = submission.thread_id
|
|
3443
|
+
AND canonical.record_id = ${recordId}
|
|
3444
|
+
WHERE submission.submission_id = ${validated.submissionId}
|
|
3445
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
3446
|
+
|
|
3447
|
+
const decoded = yield* decodeRows(
|
|
3448
|
+
Schema.Array(AbortIntentLookupRow),
|
|
3449
|
+
"effect_agent_abort_intents",
|
|
3450
|
+
validated.submissionId,
|
|
3451
|
+
rows,
|
|
3452
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3453
|
+
|
|
3454
|
+
if (decoded.length === 0) {
|
|
3455
|
+
return yield* LedgerError.make({
|
|
3456
|
+
operation,
|
|
3457
|
+
message: `Unknown submission ${validated.submissionId}.`,
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
if (decoded.length !== 1) {
|
|
3461
|
+
return yield* corruptionFailure(
|
|
3462
|
+
operation,
|
|
3463
|
+
"effect_agent_abort_intents",
|
|
3464
|
+
validated.submissionId,
|
|
3465
|
+
"An abort intent lookup returned more than one row.",
|
|
3466
|
+
);
|
|
3467
|
+
}
|
|
3468
|
+
const row = decoded[0];
|
|
3469
|
+
|
|
3470
|
+
if (row.abort_submission_id === null) return undefined;
|
|
3471
|
+
|
|
3472
|
+
return yield* decodeAbortIntent({
|
|
3473
|
+
submissionId: row.abort_submission_id,
|
|
3474
|
+
author: row.author,
|
|
3475
|
+
reason: row.reason,
|
|
3476
|
+
requestedAt: row.requested_at,
|
|
3477
|
+
...(row.canonical_record_id === null ? {} : { canonicalRecordId: row.canonical_record_id }),
|
|
3478
|
+
}).pipe(
|
|
3479
|
+
Effect.mapError((error) =>
|
|
3480
|
+
corruptionFailure(
|
|
3481
|
+
operation,
|
|
3482
|
+
"effect_agent_abort_intents",
|
|
3483
|
+
validated.submissionId,
|
|
3484
|
+
error.message,
|
|
3485
|
+
),
|
|
3486
|
+
),
|
|
3487
|
+
);
|
|
3488
|
+
});
|
|
2661
3489
|
|
|
2662
3490
|
const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
|
|
2663
3491
|
"DoSubmissionLedger.loadRecoverySnapshot",
|
|
2664
3492
|
)(function* (request: RecoverySnapshotRequest) {
|
|
2665
3493
|
const operation = "ledger load recovery snapshot";
|
|
2666
|
-
|
|
3494
|
+
|
|
3495
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
|
|
2667
3496
|
request,
|
|
2668
3497
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3498
|
+
|
|
2669
3499
|
return yield* sql
|
|
2670
3500
|
.withTransaction(
|
|
2671
3501
|
Effect.gen(function* () {
|
|
@@ -2674,6 +3504,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2674
3504
|
|
|
2675
3505
|
let ownership: OwnershipSnapshot | undefined;
|
|
2676
3506
|
const ownershipRow = yield* readOwnership(operation, validated.submissionId);
|
|
3507
|
+
|
|
2677
3508
|
if (Option.isSome(ownershipRow)) {
|
|
2678
3509
|
ownership = yield* decodeOwnershipSnapshot({
|
|
2679
3510
|
attemptId: ownershipRow.value.attempt_id,
|
|
@@ -2684,6 +3515,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2684
3515
|
}
|
|
2685
3516
|
|
|
2686
3517
|
let inputApplied: InputAppliedMarker | undefined;
|
|
3518
|
+
|
|
2687
3519
|
if (
|
|
2688
3520
|
submissionRow.input_applied_record_id !== null &&
|
|
2689
3521
|
submissionRow.input_applied_sequence !== null
|
|
@@ -2696,6 +3528,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2696
3528
|
|
|
2697
3529
|
let reservation: SettlementReservationSnapshot | undefined;
|
|
2698
3530
|
const reservationRow = yield* readReservation(operation, validated.submissionId);
|
|
3531
|
+
|
|
2699
3532
|
if (Option.isSome(reservationRow)) {
|
|
2700
3533
|
const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(
|
|
2701
3534
|
Effect.mapError((error) =>
|
|
@@ -2707,9 +3540,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2707
3540
|
),
|
|
2708
3541
|
),
|
|
2709
3542
|
);
|
|
2710
|
-
|
|
3543
|
+
|
|
3544
|
+
const settlementId = yield* Schema.decodeEffect(
|
|
2711
3545
|
SettlementReservationSnapshot.fields.settlementId,
|
|
2712
3546
|
)(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
|
|
3547
|
+
|
|
2713
3548
|
reservation = SettlementReservationSnapshot.make({
|
|
2714
3549
|
settlementId,
|
|
2715
3550
|
outcome: reservationRow.value.outcome,
|
|
@@ -2721,6 +3556,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2721
3556
|
|
|
2722
3557
|
let abortIntent: AbortIntent | undefined;
|
|
2723
3558
|
const abortRow = yield* readAbortIntent(operation, validated.submissionId);
|
|
3559
|
+
|
|
2724
3560
|
if (Option.isSome(abortRow)) {
|
|
2725
3561
|
abortIntent = yield* abortIntentFromRow(
|
|
2726
3562
|
operation,
|
|
@@ -2738,11 +3574,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2738
3574
|
WHERE joined_host_submission_id = ${validated.submissionId}
|
|
2739
3575
|
ORDER BY queue_sequence ASC
|
|
2740
3576
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
3577
|
+
|
|
2741
3578
|
const joinSubmissions = yield* decodeSubmissionRows(
|
|
2742
3579
|
operation,
|
|
2743
3580
|
validated.submissionId,
|
|
2744
3581
|
joinRows,
|
|
2745
3582
|
);
|
|
3583
|
+
|
|
2746
3584
|
const joins = yield* Effect.forEach(joinSubmissions, (row) =>
|
|
2747
3585
|
decodeJoinSnapshot({
|
|
2748
3586
|
submissionId: row.submission_id,
|
|
@@ -2752,6 +3590,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2752
3590
|
);
|
|
2753
3591
|
|
|
2754
3592
|
let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
|
|
3593
|
+
|
|
2755
3594
|
if (submissionRow.joined_host_submission_id !== null) {
|
|
2756
3595
|
hostSubmissionId = yield* decodeSubmissionId(
|
|
2757
3596
|
submissionRow.joined_host_submission_id,
|
|
@@ -2759,6 +3598,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2759
3598
|
}
|
|
2760
3599
|
|
|
2761
3600
|
let suspension: SuspensionSnapshot | undefined;
|
|
3601
|
+
|
|
2762
3602
|
if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
|
|
2763
3603
|
const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(
|
|
2764
3604
|
Effect.mapError((error) =>
|
|
@@ -2770,6 +3610,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2770
3610
|
),
|
|
2771
3611
|
),
|
|
2772
3612
|
);
|
|
3613
|
+
|
|
2773
3614
|
suspension = yield* decodeSuspensionSnapshot({
|
|
2774
3615
|
reason,
|
|
2775
3616
|
suspendedAt: submissionRow.suspended_at,
|
|
@@ -2786,11 +3627,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2786
3627
|
}
|
|
2787
3628
|
|
|
2788
3629
|
const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
3630
|
+
|
|
2789
3631
|
const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
|
|
2790
3632
|
approvalIntentFromRow(operation, row),
|
|
2791
3633
|
);
|
|
2792
3634
|
|
|
2793
3635
|
const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
3636
|
+
|
|
2794
3637
|
const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
|
|
2795
3638
|
unknownResolutionIntentFromRow(operation, row),
|
|
2796
3639
|
);
|
|
@@ -2807,20 +3650,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2807
3650
|
WHERE parent_submission_id = ${validated.submissionId}
|
|
2808
3651
|
ORDER BY parent_tool_call_id ASC
|
|
2809
3652
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
3653
|
+
|
|
2810
3654
|
const decodedChildReservations = yield* decodeChildReservationRows(
|
|
2811
3655
|
operation,
|
|
2812
3656
|
validated.submissionId,
|
|
2813
3657
|
childReservationRows,
|
|
2814
3658
|
);
|
|
3659
|
+
|
|
2815
3660
|
const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
|
|
2816
3661
|
childReservationSnapshotFromRow(operation, row),
|
|
2817
3662
|
);
|
|
3663
|
+
|
|
2818
3664
|
const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
|
|
2819
3665
|
const markersByChild = new Map(markers.map((row) => [row.child_submission_id, row]));
|
|
2820
3666
|
const childAttachments: Array<ChildAttachmentSnapshot> = [];
|
|
3667
|
+
|
|
2821
3668
|
for (const row of decodedChildReservations) {
|
|
2822
3669
|
if (row.child_submission_id === null) continue;
|
|
2823
3670
|
const child = yield* readSubmission(operation, row.child_submission_id);
|
|
3671
|
+
|
|
2824
3672
|
if (Option.isSome(child)) {
|
|
2825
3673
|
childAttachments.push(
|
|
2826
3674
|
yield* decodeChildAttachmentSnapshot({
|
|
@@ -2835,6 +3683,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2835
3683
|
continue;
|
|
2836
3684
|
}
|
|
2837
3685
|
const marker = markersByChild.get(row.child_submission_id);
|
|
3686
|
+
|
|
2838
3687
|
if (marker === undefined) continue;
|
|
2839
3688
|
childAttachments.push(
|
|
2840
3689
|
yield* decodeChildAttachmentSnapshot({
|
|
@@ -2847,6 +3696,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2847
3696
|
}
|
|
2848
3697
|
|
|
2849
3698
|
let parentLinkage: ParentLinkage | undefined;
|
|
3699
|
+
|
|
2850
3700
|
if (
|
|
2851
3701
|
submissionRow.parent_submission_id !== null &&
|
|
2852
3702
|
submissionRow.parent_tool_call_id !== null
|
|
@@ -2892,6 +3742,8 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2892
3742
|
reserveSettlement,
|
|
2893
3743
|
finalizeSettlement,
|
|
2894
3744
|
requestAbort,
|
|
3745
|
+
stopWorker,
|
|
3746
|
+
inspectWorker,
|
|
2895
3747
|
claimJoining,
|
|
2896
3748
|
markJoined,
|
|
2897
3749
|
revertJoining,
|
|
@@ -2906,6 +3758,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2906
3758
|
releaseChildBudget,
|
|
2907
3759
|
scanNonterminal,
|
|
2908
3760
|
loadRecoverySnapshot,
|
|
3761
|
+
readAbortIntent: readAbortIntentForSubmission,
|
|
2909
3762
|
}),
|
|
2910
3763
|
);
|
|
2911
3764
|
});
|
|
@@ -2923,18 +3776,22 @@ export const submissionLedgerLayer: Layer.Layer<
|
|
|
2923
3776
|
|
|
2924
3777
|
/**
|
|
2925
3778
|
* A composition-root convenience Layer for the durable Submission Ledger. Point it at the
|
|
2926
|
-
* same `ctx.storage` as the
|
|
3779
|
+
* same `ctx.storage` as the ThreadStore so claims fence the same producer epochs.
|
|
2927
3780
|
*/
|
|
2928
3781
|
export const ledgerLayer = (
|
|
2929
3782
|
options: DoStorageOptions,
|
|
2930
3783
|
): Layer.Layer<SubmissionLedger, DoStorageInitializationError> =>
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
3784
|
+
Layer.unwrap(
|
|
3785
|
+
Effect.map(DoStorageConfig, (config) =>
|
|
3786
|
+
submissionLedgerLayer.pipe(
|
|
3787
|
+
Layer.provide(
|
|
3788
|
+
Layer.mergeAll(
|
|
3789
|
+
Layer.succeed(DoStorageConfig)(config),
|
|
3790
|
+
storageFailpointLayer(options),
|
|
3791
|
+
SqliteClient.layer({ storage: options.storage }),
|
|
3792
|
+
BrowserCrypto.layer,
|
|
3793
|
+
),
|
|
3794
|
+
),
|
|
2938
3795
|
),
|
|
2939
3796
|
),
|
|
2940
|
-
);
|
|
3797
|
+
).pipe(Layer.provide(storageConfigLayer(options)));
|