@effect-agent/storage-sqlite 0.1.0-beta.9 → 0.1.0-beta.90
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/SqliteActivityStore.d.mts +14 -0
- package/dist/SqliteActivityStore.mjs +310 -0
- package/dist/SqliteActivityStore.mjs.map +1 -0
- package/dist/SqliteMessageDeliveryStore.d.mts +14 -0
- package/dist/SqliteMessageDeliveryStore.mjs +24 -0
- package/dist/SqliteMessageDeliveryStore.mjs.map +1 -0
- package/dist/SqliteScheduleStore.d.mts +14 -0
- package/dist/SqliteScheduleStore.mjs +255 -0
- package/dist/SqliteScheduleStore.mjs.map +1 -0
- package/dist/SqliteStorageConfig.d.mts +34 -0
- package/dist/SqliteStorageConfig.mjs +39 -0
- package/dist/SqliteStorageConfig.mjs.map +1 -0
- package/dist/SqliteStorageError-DVWeaWLm.d.mts +86 -0
- package/dist/SqliteStorageError.d.mts +2 -0
- package/dist/SqliteStorageError.mjs +150 -0
- package/dist/SqliteStorageError.mjs.map +1 -0
- package/dist/SqliteStorageFailpoint.d.mts +17 -0
- package/dist/SqliteStorageFailpoint.mjs +14 -0
- package/dist/SqliteStorageFailpoint.mjs.map +1 -0
- package/dist/SqliteStorageFailpointTesting.d.mts +15 -0
- package/dist/SqliteStorageFailpointTesting.mjs +19 -0
- package/dist/SqliteStorageFailpointTesting.mjs.map +1 -0
- package/dist/SqliteStorageVersion-BqmqX0CI.d.mts +11 -0
- package/dist/SqliteStorageVersion.d.mts +2 -0
- package/dist/SqliteStorageVersion.mjs +8 -0
- package/dist/SqliteStorageVersion.mjs.map +1 -0
- package/dist/SqliteSubmissionLedger.d.mts +23 -0
- package/dist/SqliteSubmissionLedger.mjs +1788 -0
- package/dist/SqliteSubmissionLedger.mjs.map +1 -0
- package/dist/SqliteSubscriptionStore.d.mts +13 -0
- package/dist/SqliteSubscriptionStore.mjs +28 -0
- package/dist/SqliteSubscriptionStore.mjs.map +1 -0
- package/dist/SqliteThreadStore.d.mts +49 -0
- package/dist/SqliteThreadStore.mjs +460 -0
- package/dist/SqliteThreadStore.mjs.map +1 -0
- package/dist/index.d.mts +11 -193
- package/dist/index.mjs +11 -3082
- package/dist/migrations-Dy5v0HGe.mjs +346 -0
- package/dist/migrations-Dy5v0HGe.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/sqlite-journal-CBOQDySe.mjs +998 -0
- package/dist/sqlite-journal-CBOQDySe.mjs.map +1 -0
- package/package.json +1 -41
- package/src/SqliteActivityStore.ts +556 -0
- package/src/SqliteMessageDeliveryStore.ts +42 -0
- package/src/SqliteScheduleStore.ts +404 -0
- package/src/{sqlite-storage-config.ts → SqliteStorageConfig.ts} +1 -1
- package/src/{errors.ts → SqliteStorageError.ts} +10 -3
- package/src/SqliteStorageFailpoint.ts +23 -0
- package/src/{sqlite-storage-failpoint.ts → SqliteStorageFailpointTesting.ts} +7 -18
- package/src/SqliteStorageVersion.ts +2 -0
- package/src/{sqlite-ledger.ts → SqliteSubmissionLedger.ts} +912 -212
- package/src/SqliteSubscriptionStore.ts +55 -0
- package/src/SqliteThreadStore.ts +999 -0
- package/src/index.ts +10 -6
- package/src/internal/message-delivery-schema.ts +24 -0
- package/src/{migrations.ts → internal/migrations.ts} +156 -79
- package/src/internal/recovery-checkpoint-schema.ts +17 -0
- package/src/{sqlite-journal.ts → internal/sqlite-journal.ts} +751 -241
- package/dist/index.mjs.map +0 -1
- package/src/sqlite-conversation-store.ts +0 -841
|
@@ -1,18 +1,37 @@
|
|
|
1
|
+
import { NodeCrypto } from "@effect/platform-node";
|
|
2
|
+
import { SqliteClient } from "@effect/sql-sqlite-node";
|
|
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,
|
|
3
19
|
AbortIntent,
|
|
20
|
+
AbortIntentRequest,
|
|
4
21
|
AdmissionAdmitted,
|
|
5
22
|
AdmissionConflict,
|
|
23
|
+
AdmissionFence,
|
|
24
|
+
AdmissionGroup,
|
|
25
|
+
AdmissionPolicyError,
|
|
6
26
|
AdmissionNotAdmitted,
|
|
7
27
|
AdmissionRequest,
|
|
28
|
+
SubmissionAdmissionFence,
|
|
8
29
|
AdmissionResult,
|
|
9
30
|
ApprovalConflict,
|
|
10
|
-
ApprovalDecision,
|
|
11
31
|
ApprovalDecisionCommand,
|
|
12
32
|
ApprovalDecisionIntent,
|
|
13
33
|
AttachChildToReservationRequest,
|
|
14
34
|
BeginChildBudgetReleaseRequest,
|
|
15
|
-
CanonicalSequence,
|
|
16
35
|
ChildAttachmentSnapshot,
|
|
17
36
|
ChildBudgetReservationRequest,
|
|
18
37
|
ChildBudgetReservationSnapshot,
|
|
@@ -22,9 +41,6 @@ import {
|
|
|
22
41
|
Claim,
|
|
23
42
|
ClaimJoiningRequest,
|
|
24
43
|
ClaimRequest,
|
|
25
|
-
DefinitionDigests,
|
|
26
|
-
Digest,
|
|
27
|
-
EMPTY_TAIL_DIGEST,
|
|
28
44
|
InputAppliedMarker,
|
|
29
45
|
JoinSnapshot,
|
|
30
46
|
JoinedToHost,
|
|
@@ -39,10 +55,7 @@ import {
|
|
|
39
55
|
OwnershipRenewal,
|
|
40
56
|
OwnershipSnapshot,
|
|
41
57
|
ParentLinkage,
|
|
42
|
-
PersistedJson,
|
|
43
|
-
ProducerEpoch,
|
|
44
58
|
QueueSequence,
|
|
45
|
-
RecordEnvelope,
|
|
46
59
|
RecoverySnapshot,
|
|
47
60
|
RecoverySnapshotRequest,
|
|
48
61
|
ReleaseChildBudgetRequest,
|
|
@@ -54,7 +67,6 @@ import {
|
|
|
54
67
|
Settlement,
|
|
55
68
|
SettlementConflict,
|
|
56
69
|
SettlementFinalization,
|
|
57
|
-
SettlementOutcome,
|
|
58
70
|
SettlementReservation,
|
|
59
71
|
SubmissionLedger,
|
|
60
72
|
SubmissionLookup,
|
|
@@ -62,6 +74,7 @@ import {
|
|
|
62
74
|
SubmissionSnapshot,
|
|
63
75
|
SubmissionState,
|
|
64
76
|
SettlementReservationSnapshot,
|
|
77
|
+
settlementFailureFromRecord,
|
|
65
78
|
SuspendRequest,
|
|
66
79
|
SuspensionReason,
|
|
67
80
|
SuspensionSnapshot,
|
|
@@ -72,29 +85,26 @@ import {
|
|
|
72
85
|
submissionAbortRecordId,
|
|
73
86
|
type ChildSettledOutcome,
|
|
74
87
|
type SuspensionOutcome,
|
|
75
|
-
} from "
|
|
76
|
-
import { NodeCrypto } from "@effect/platform-node";
|
|
77
|
-
import { SqliteClient } from "@effect/sql-sqlite-node";
|
|
78
|
-
import { Clock, Context, Crypto, DateTime, Effect, Layer, Option, Schema, Stream } from "effect";
|
|
88
|
+
} from "effect-agent/submission-ledger";
|
|
79
89
|
import * as SqlClientService from "effect/unstable/sql/SqlClient";
|
|
80
90
|
import type { SqlError } from "effect/unstable/sql/SqlError";
|
|
81
91
|
|
|
92
|
+
import { decodeRows, initializeSqliteJournal } from "./internal/sqlite-journal.ts";
|
|
93
|
+
import { SqliteStorageConfig } from "./SqliteStorageConfig.ts";
|
|
82
94
|
import {
|
|
83
95
|
SqliteLedgerError,
|
|
84
96
|
SqliteStorageCorruptionError,
|
|
85
97
|
SqliteStorageError,
|
|
86
98
|
SqliteWriteContention,
|
|
87
99
|
type SqliteStorageFailpointLocation,
|
|
88
|
-
} from "./
|
|
100
|
+
} from "./SqliteStorageError.ts";
|
|
101
|
+
import { SqliteStorageFailpoint } from "./SqliteStorageFailpoint.ts";
|
|
89
102
|
import {
|
|
90
103
|
storageConfigLayer,
|
|
91
104
|
storageFailpointLayer,
|
|
92
105
|
type SqliteStorageInitializationError,
|
|
93
106
|
type SqliteStorageOptions,
|
|
94
|
-
} from "./
|
|
95
|
-
import { decodeRows, initializeSqliteJournal } from "./sqlite-journal.ts";
|
|
96
|
-
import { SqliteStorageConfig } from "./sqlite-storage-config.ts";
|
|
97
|
-
import { SqliteStorageFailpoint } from "./sqlite-storage-failpoint.ts";
|
|
107
|
+
} from "./SqliteThreadStore.ts";
|
|
98
108
|
|
|
99
109
|
type SubmissionId = SubmissionSnapshot["submissionId"];
|
|
100
110
|
|
|
@@ -104,10 +114,15 @@ const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
|
|
|
104
114
|
|
|
105
115
|
const SCAN_PAGE_SIZE = 256;
|
|
106
116
|
const EPOCH_ZERO = Schema.decodeSync(ProducerEpoch)(0);
|
|
117
|
+
const RESUME_IMMEDIATELY: SuspensionOutcome = "resume-immediately";
|
|
118
|
+
const SUSPENDED: SuspensionOutcome = "suspended";
|
|
119
|
+
const NOT_WAITING: ChildSettledOutcome = "not-waiting";
|
|
120
|
+
const STILL_WAITING: ChildSettledOutcome = "still-waiting";
|
|
121
|
+
const WOKEN: ChildSettledOutcome = "woken";
|
|
107
122
|
|
|
108
123
|
class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
|
|
109
124
|
submission_id: BoundedIdentifier,
|
|
110
|
-
|
|
125
|
+
thread_id: BoundedIdentifier,
|
|
111
126
|
queue_sequence: QueueSequence,
|
|
112
127
|
principal: BoundedIdentifier,
|
|
113
128
|
idempotency_key: BoundedIdentifier,
|
|
@@ -130,6 +145,10 @@ class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
|
|
|
130
145
|
unknown_tool_call_ids_json: Schema.NullOr(BoundedStoredText),
|
|
131
146
|
parent_submission_id: Schema.NullOr(BoundedIdentifier),
|
|
132
147
|
parent_tool_call_id: Schema.NullOr(BoundedIdentifier),
|
|
148
|
+
admission_group: Schema.NullOr(AdmissionGroup),
|
|
149
|
+
admission_fence_json: Schema.NullOr(BoundedStoredText),
|
|
150
|
+
worker_admission_json: Schema.NullOr(BoundedStoredText),
|
|
151
|
+
message_admission_json: Schema.NullOr(BoundedStoredText),
|
|
133
152
|
}) {}
|
|
134
153
|
|
|
135
154
|
class ChildReservationRow extends Schema.Class<ChildReservationRow>("ChildReservationRow")({
|
|
@@ -196,13 +215,22 @@ class MaxQueueSequenceRow extends Schema.Class<MaxQueueSequenceRow>("MaxQueueSeq
|
|
|
196
215
|
max_queue_sequence: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
197
216
|
}) {}
|
|
198
217
|
|
|
218
|
+
class AbortIntentLookupRow extends Schema.Class<AbortIntentLookupRow>("AbortIntentLookupRow")({
|
|
219
|
+
submission_id: BoundedIdentifier,
|
|
220
|
+
abort_submission_id: Schema.NullOr(BoundedIdentifier),
|
|
221
|
+
author: Schema.NullOr(BoundedIdentifier),
|
|
222
|
+
reason: Schema.NullOr(BoundedStoredText),
|
|
223
|
+
requested_at: Schema.NullOr(BoundedTimestamp),
|
|
224
|
+
canonical_record_id: Schema.NullOr(BoundedIdentifier),
|
|
225
|
+
}) {}
|
|
226
|
+
|
|
199
227
|
class CanonicalRecordIdRow extends Schema.Class<CanonicalRecordIdRow>("CanonicalRecordIdRow")({
|
|
200
228
|
record_id: BoundedIdentifier,
|
|
201
229
|
}) {}
|
|
202
230
|
|
|
203
231
|
const SUBMISSION_COLUMNS = `
|
|
204
232
|
submission_id,
|
|
205
|
-
|
|
233
|
+
thread_id,
|
|
206
234
|
queue_sequence,
|
|
207
235
|
principal,
|
|
208
236
|
idempotency_key,
|
|
@@ -224,7 +252,11 @@ const SUBMISSION_COLUMNS = `
|
|
|
224
252
|
unknown_reason,
|
|
225
253
|
unknown_tool_call_ids_json,
|
|
226
254
|
parent_submission_id,
|
|
227
|
-
parent_tool_call_id
|
|
255
|
+
parent_tool_call_id,
|
|
256
|
+
admission_group,
|
|
257
|
+
admission_fence_json,
|
|
258
|
+
worker_admission_json,
|
|
259
|
+
message_admission_json
|
|
228
260
|
`;
|
|
229
261
|
|
|
230
262
|
const CHILD_RESERVATION_COLUMNS = `
|
|
@@ -241,7 +273,7 @@ const CHILD_RESERVATION_COLUMNS = `
|
|
|
241
273
|
released_at
|
|
242
274
|
`;
|
|
243
275
|
|
|
244
|
-
/** The branded ToolCallId schema, reached through the
|
|
276
|
+
/** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
|
|
245
277
|
const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
|
|
246
278
|
const ToolCallIdList = Schema.Array(ToolCallIdSchema);
|
|
247
279
|
|
|
@@ -271,10 +303,18 @@ const decodeSuspensionSnapshot = Schema.decodeUnknownEffect(SuspensionSnapshot);
|
|
|
271
303
|
const decodeApprovalDecisionIntent = Schema.decodeUnknownEffect(ApprovalDecisionIntent);
|
|
272
304
|
const decodeUnknownResolutionIntent = Schema.decodeUnknownEffect(UnknownResolutionIntent);
|
|
273
305
|
const decodeParentLinkage = Schema.decodeUnknownEffect(ParentLinkage);
|
|
306
|
+
|
|
274
307
|
const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
|
|
275
308
|
ChildBudgetReservationSnapshot,
|
|
276
309
|
);
|
|
310
|
+
|
|
277
311
|
const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
|
|
312
|
+
const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
|
|
313
|
+
const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
|
|
314
|
+
|
|
315
|
+
const isSqliteTransactionFailure = Schema.is(
|
|
316
|
+
Schema.Union([SqliteStorageError, SqliteWriteContention]),
|
|
317
|
+
);
|
|
278
318
|
|
|
279
319
|
/** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
|
|
280
320
|
const internalFailure =
|
|
@@ -298,6 +338,7 @@ const sqlFailure =
|
|
|
298
338
|
operation,
|
|
299
339
|
message: error.message,
|
|
300
340
|
});
|
|
341
|
+
|
|
301
342
|
return internalFailure(operation)(internal);
|
|
302
343
|
};
|
|
303
344
|
|
|
@@ -309,7 +350,8 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
309
350
|
const failpoint = yield* SqliteStorageFailpoint;
|
|
310
351
|
const sql = yield* SqlClientService.SqlClient;
|
|
311
352
|
const crypto = yield* Crypto.Crypto;
|
|
312
|
-
const
|
|
353
|
+
const admissionFence = yield* SubmissionAdmissionFence;
|
|
354
|
+
const journal = yield* initializeSqliteJournal();
|
|
313
355
|
|
|
314
356
|
const hitFailpoint = (
|
|
315
357
|
location: SqliteStorageFailpointLocation,
|
|
@@ -327,6 +369,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
327
369
|
A,
|
|
328
370
|
E extends
|
|
329
371
|
| AdmissionConflict
|
|
372
|
+
| AdmissionPolicyError
|
|
330
373
|
| ApprovalConflict
|
|
331
374
|
| ChildReservationConflict
|
|
332
375
|
| JoinedToHost
|
|
@@ -342,9 +385,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
342
385
|
.withWriteTransaction(operation)(effect)
|
|
343
386
|
.pipe(
|
|
344
387
|
Effect.mapError((error) =>
|
|
345
|
-
error
|
|
346
|
-
? internalFailure(operation)(error)
|
|
347
|
-
: error,
|
|
388
|
+
isSqliteTransactionFailure(error) ? internalFailure(operation)(error) : error,
|
|
348
389
|
),
|
|
349
390
|
);
|
|
350
391
|
|
|
@@ -381,7 +422,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
381
422
|
FROM effect_agent_submissions
|
|
382
423
|
WHERE submission_id = ${submissionId}
|
|
383
424
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
425
|
+
|
|
384
426
|
const decoded = yield* decodeSubmissionRows(operation, submissionId, rows);
|
|
427
|
+
|
|
385
428
|
if (decoded.length > 1) {
|
|
386
429
|
return yield* corruptionFailure(
|
|
387
430
|
operation,
|
|
@@ -390,6 +433,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
390
433
|
"A submission primary key returned more than one row.",
|
|
391
434
|
);
|
|
392
435
|
}
|
|
436
|
+
|
|
393
437
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
394
438
|
});
|
|
395
439
|
|
|
@@ -398,12 +442,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
398
442
|
submissionId: string,
|
|
399
443
|
): Effect.fn.Return<SubmissionRow, LedgerError> {
|
|
400
444
|
const submission = yield* readSubmission(operation, submissionId);
|
|
445
|
+
|
|
401
446
|
if (Option.isNone(submission)) {
|
|
402
447
|
return yield* LedgerError.make({
|
|
403
448
|
operation,
|
|
404
449
|
message: `Unknown submission ${submissionId}.`,
|
|
405
450
|
});
|
|
406
451
|
}
|
|
452
|
+
|
|
407
453
|
return submission.value;
|
|
408
454
|
});
|
|
409
455
|
|
|
@@ -422,12 +468,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
422
468
|
FROM effect_agent_submission_ownership
|
|
423
469
|
WHERE submission_id = ${submissionId}
|
|
424
470
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
471
|
+
|
|
425
472
|
const decoded = yield* decodeRows(
|
|
426
473
|
Schema.Array(OwnershipRow),
|
|
427
474
|
"effect_agent_submission_ownership",
|
|
428
475
|
submissionId,
|
|
429
476
|
rows,
|
|
430
477
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
478
|
+
|
|
431
479
|
if (decoded.length > 1) {
|
|
432
480
|
return yield* corruptionFailure(
|
|
433
481
|
operation,
|
|
@@ -436,23 +484,25 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
436
484
|
"An ownership primary key returned more than one row.",
|
|
437
485
|
);
|
|
438
486
|
}
|
|
487
|
+
|
|
439
488
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
440
489
|
});
|
|
441
490
|
|
|
442
|
-
const
|
|
491
|
+
const threadEpoch = Effect.fn("SqliteSubmissionLedger.threadEpoch")(function* (
|
|
443
492
|
operation: string,
|
|
444
|
-
|
|
493
|
+
threadId: string,
|
|
445
494
|
): Effect.fn.Return<ProducerEpoch, LedgerError> {
|
|
446
|
-
const
|
|
447
|
-
.
|
|
495
|
+
const threads = yield* journal
|
|
496
|
+
.getThread(threadId)
|
|
448
497
|
.pipe(Effect.mapError(internalFailure(operation)));
|
|
449
|
-
|
|
498
|
+
|
|
499
|
+
return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
|
|
450
500
|
});
|
|
451
501
|
|
|
452
502
|
/**
|
|
453
503
|
* Verify inside the surrounding write transaction that the presented token still owns the
|
|
454
504
|
* Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
|
|
455
|
-
*
|
|
505
|
+
* Thread's current producer epoch (DUR-006).
|
|
456
506
|
*/
|
|
457
507
|
const requireOwnership = Effect.fn("SqliteSubmissionLedger.requireOwnership")(function* (
|
|
458
508
|
operation: string,
|
|
@@ -460,13 +510,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
460
510
|
ownershipToken: string,
|
|
461
511
|
): Effect.fn.Return<OwnershipRow, OwnershipLost | LedgerError> {
|
|
462
512
|
const ownership = yield* readOwnership(operation, submission.submission_id);
|
|
513
|
+
|
|
463
514
|
if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
|
|
464
|
-
const actualEpoch = yield*
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
515
|
+
const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
|
|
516
|
+
|
|
517
|
+
const submissionId = yield* Schema.decodeEffect(SubmissionSnapshot.fields.submissionId)(
|
|
518
|
+
submission.submission_id,
|
|
519
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
520
|
+
|
|
468
521
|
return yield* OwnershipLost.make({ submissionId, actualEpoch });
|
|
469
522
|
}
|
|
523
|
+
|
|
470
524
|
return ownership.value;
|
|
471
525
|
});
|
|
472
526
|
|
|
@@ -485,6 +539,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
485
539
|
),
|
|
486
540
|
),
|
|
487
541
|
);
|
|
542
|
+
|
|
488
543
|
const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
|
|
489
544
|
Effect.mapError((error) =>
|
|
490
545
|
corruptionFailure(
|
|
@@ -495,6 +550,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
495
550
|
),
|
|
496
551
|
),
|
|
497
552
|
);
|
|
553
|
+
|
|
498
554
|
if ((row.parent_submission_id === null) !== (row.parent_tool_call_id === null)) {
|
|
499
555
|
return yield* corruptionFailure(
|
|
500
556
|
operation,
|
|
@@ -503,9 +559,10 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
503
559
|
"A parent linkage must record both the parent Submission and the parent Tool Call.",
|
|
504
560
|
);
|
|
505
561
|
}
|
|
562
|
+
|
|
506
563
|
return yield* decodeSubmissionSnapshotUnknown({
|
|
507
564
|
submissionId: row.submission_id,
|
|
508
|
-
|
|
565
|
+
threadId: row.thread_id,
|
|
509
566
|
queueSequence: row.queue_sequence,
|
|
510
567
|
principal: row.principal,
|
|
511
568
|
idempotencyKey: row.idempotency_key,
|
|
@@ -517,6 +574,28 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
517
574
|
receiptId: row.receipt_id,
|
|
518
575
|
state: row.state,
|
|
519
576
|
createdAt: row.created_at,
|
|
577
|
+
...(row.admission_group === null ? {} : { admissionGroup: row.admission_group }),
|
|
578
|
+
...(row.worker_admission_json === null
|
|
579
|
+
? {}
|
|
580
|
+
: {
|
|
581
|
+
workerAdmission: yield* parseStoredJsonText(row.worker_admission_json).pipe(
|
|
582
|
+
Effect.mapError(internalFailure(operation)),
|
|
583
|
+
),
|
|
584
|
+
}),
|
|
585
|
+
...(row.message_admission_json === null
|
|
586
|
+
? {}
|
|
587
|
+
: {
|
|
588
|
+
messageAdmission: yield* parseStoredJsonText(row.message_admission_json).pipe(
|
|
589
|
+
Effect.mapError(internalFailure(operation)),
|
|
590
|
+
),
|
|
591
|
+
}),
|
|
592
|
+
...(row.admission_fence_json === null
|
|
593
|
+
? {}
|
|
594
|
+
: {
|
|
595
|
+
admissionFence: yield* parseStoredJsonText(row.admission_fence_json).pipe(
|
|
596
|
+
Effect.mapError(internalFailure(operation)),
|
|
597
|
+
),
|
|
598
|
+
}),
|
|
520
599
|
...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
|
|
521
600
|
...(row.ready_at === null ? {} : { readyAt: row.ready_at }),
|
|
522
601
|
...(row.parent_submission_id === null || row.parent_tool_call_id === null
|
|
@@ -557,12 +636,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
557
636
|
FROM effect_agent_settlement_reservations
|
|
558
637
|
WHERE submission_id = ${submissionId}
|
|
559
638
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
639
|
+
|
|
560
640
|
const decoded = yield* decodeRows(
|
|
561
641
|
Schema.Array(ReservationRow),
|
|
562
642
|
"effect_agent_settlement_reservations",
|
|
563
643
|
submissionId,
|
|
564
644
|
rows,
|
|
565
645
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
646
|
+
|
|
566
647
|
if (decoded.length > 1) {
|
|
567
648
|
return yield* corruptionFailure(
|
|
568
649
|
operation,
|
|
@@ -571,6 +652,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
571
652
|
"A settlement reservation primary key returned more than one row.",
|
|
572
653
|
);
|
|
573
654
|
}
|
|
655
|
+
|
|
574
656
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
575
657
|
});
|
|
576
658
|
|
|
@@ -588,12 +670,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
588
670
|
FROM effect_agent_abort_intents
|
|
589
671
|
WHERE submission_id = ${submissionId}
|
|
590
672
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
673
|
+
|
|
591
674
|
const decoded = yield* decodeRows(
|
|
592
675
|
Schema.Array(AbortIntentRow),
|
|
593
676
|
"effect_agent_abort_intents",
|
|
594
677
|
submissionId,
|
|
595
678
|
rows,
|
|
596
679
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
680
|
+
|
|
597
681
|
if (decoded.length > 1) {
|
|
598
682
|
return yield* corruptionFailure(
|
|
599
683
|
operation,
|
|
@@ -602,6 +686,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
602
686
|
"An abort intent primary key returned more than one row.",
|
|
603
687
|
);
|
|
604
688
|
}
|
|
689
|
+
|
|
605
690
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
606
691
|
});
|
|
607
692
|
|
|
@@ -622,7 +707,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
622
707
|
FROM effect_agent_child_reservations
|
|
623
708
|
WHERE reservation_id = ${reservationId}
|
|
624
709
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
710
|
+
|
|
625
711
|
const decoded = yield* decodeChildReservationRows(operation, reservationId, rows);
|
|
712
|
+
|
|
626
713
|
if (decoded.length > 1) {
|
|
627
714
|
return yield* corruptionFailure(
|
|
628
715
|
operation,
|
|
@@ -631,6 +718,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
631
718
|
"A child reservation primary key returned more than one row.",
|
|
632
719
|
);
|
|
633
720
|
}
|
|
721
|
+
|
|
634
722
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
635
723
|
});
|
|
636
724
|
|
|
@@ -647,11 +735,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
647
735
|
WHERE parent_submission_id = ${parentSubmissionId}
|
|
648
736
|
AND parent_tool_call_id = ${parentToolCallId}
|
|
649
737
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
738
|
+
|
|
650
739
|
const decoded = yield* decodeChildReservationRows(
|
|
651
740
|
operation,
|
|
652
741
|
`${parentSubmissionId}/${parentToolCallId}`,
|
|
653
742
|
rows,
|
|
654
743
|
);
|
|
744
|
+
|
|
655
745
|
if (decoded.length > 1) {
|
|
656
746
|
return yield* corruptionFailure(
|
|
657
747
|
operation,
|
|
@@ -660,6 +750,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
660
750
|
"A parent Tool Call returned more than one child reservation.",
|
|
661
751
|
);
|
|
662
752
|
}
|
|
753
|
+
|
|
663
754
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
664
755
|
});
|
|
665
756
|
|
|
@@ -676,13 +767,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
676
767
|
row.reservation_id,
|
|
677
768
|
error.message,
|
|
678
769
|
);
|
|
770
|
+
|
|
679
771
|
const allocation = yield* parseStoredJsonText(row.allocation_json).pipe(
|
|
680
772
|
Effect.mapError(rowFailure),
|
|
681
773
|
);
|
|
774
|
+
|
|
682
775
|
const accounting =
|
|
683
776
|
row.accounting_json === null
|
|
684
777
|
? undefined
|
|
685
778
|
: yield* parseStoredJsonText(row.accounting_json).pipe(Effect.mapError(rowFailure));
|
|
779
|
+
|
|
686
780
|
return yield* decodeChildReservationSnapshotUnknown({
|
|
687
781
|
reservationId: row.reservation_id,
|
|
688
782
|
parentSubmissionId: row.parent_submission_id,
|
|
@@ -715,6 +809,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
715
809
|
WHERE submission_id = ${submissionId}
|
|
716
810
|
ORDER BY tool_call_id ASC
|
|
717
811
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
812
|
+
|
|
718
813
|
return yield* decodeRows(
|
|
719
814
|
Schema.Array(ApprovalDecisionRow),
|
|
720
815
|
"effect_agent_approval_decisions",
|
|
@@ -766,6 +861,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
766
861
|
WHERE submission_id = ${submissionId}
|
|
767
862
|
ORDER BY tool_call_id ASC
|
|
768
863
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
864
|
+
|
|
769
865
|
return yield* decodeRows(
|
|
770
866
|
Schema.Array(UnknownResolutionRow),
|
|
771
867
|
"effect_agent_unknown_resolutions",
|
|
@@ -791,6 +887,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
791
887
|
),
|
|
792
888
|
),
|
|
793
889
|
);
|
|
890
|
+
|
|
794
891
|
return yield* decodeUnknownResolutionIntent({
|
|
795
892
|
submissionId: row.submission_id,
|
|
796
893
|
toolCallId: row.tool_call_id,
|
|
@@ -817,6 +914,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
817
914
|
submission: SubmissionRow,
|
|
818
915
|
): Effect.fn.Return<ReadonlyArray<typeof ToolCallIdSchema.Type>, LedgerError> {
|
|
819
916
|
if (submission.unknown_tool_call_ids_json === null) return [];
|
|
917
|
+
|
|
820
918
|
return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(
|
|
821
919
|
Effect.mapError((error) =>
|
|
822
920
|
corruptionFailure(
|
|
@@ -838,22 +936,25 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
838
936
|
const canonicalAbortRecordId = Effect.fn("SqliteSubmissionLedger.canonicalAbortRecordId")(
|
|
839
937
|
function* (
|
|
840
938
|
operation: string,
|
|
841
|
-
|
|
939
|
+
threadId: string,
|
|
842
940
|
submissionId: SubmissionId,
|
|
843
941
|
): Effect.fn.Return<string | undefined, LedgerError> {
|
|
844
942
|
const recordId = submissionAbortRecordId(submissionId);
|
|
943
|
+
|
|
845
944
|
const rows = yield* sql<Record<string, unknown>>`
|
|
846
945
|
SELECT record_id
|
|
847
946
|
FROM effect_agent_canonical_records
|
|
848
|
-
WHERE
|
|
947
|
+
WHERE thread_id = ${threadId}
|
|
849
948
|
AND record_id = ${recordId}
|
|
850
949
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
950
|
+
|
|
851
951
|
const decoded = yield* decodeRows(
|
|
852
952
|
Schema.Array(CanonicalRecordIdRow),
|
|
853
953
|
"effect_agent_canonical_records",
|
|
854
|
-
`${
|
|
954
|
+
`${threadId}/${recordId}`,
|
|
855
955
|
rows,
|
|
856
956
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
957
|
+
|
|
857
958
|
return decoded.length === 0 ? undefined : recordId;
|
|
858
959
|
},
|
|
859
960
|
);
|
|
@@ -866,9 +967,10 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
866
967
|
): Effect.fn.Return<AbortIntent, LedgerError> {
|
|
867
968
|
const canonicalRecordId = yield* canonicalAbortRecordId(
|
|
868
969
|
operation,
|
|
869
|
-
submission.
|
|
970
|
+
submission.thread_id,
|
|
870
971
|
submissionId,
|
|
871
972
|
);
|
|
973
|
+
|
|
872
974
|
return yield* decodeAbortIntent({
|
|
873
975
|
submissionId: row.submission_id,
|
|
874
976
|
author: row.author,
|
|
@@ -892,30 +994,73 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
892
994
|
const admit: SubmissionLedger["Service"]["admit"] = Effect.fn("SqliteSubmissionLedger.admit")(
|
|
893
995
|
function* (request: AdmissionRequest) {
|
|
894
996
|
const operation = "ledger admit";
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
997
|
+
|
|
998
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AdmissionRequest))(request).pipe(
|
|
999
|
+
Effect.mapError(internalFailure(operation)),
|
|
1000
|
+
);
|
|
1001
|
+
|
|
898
1002
|
const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(
|
|
899
1003
|
Effect.mapError(internalFailure(operation)),
|
|
900
1004
|
);
|
|
1005
|
+
|
|
1006
|
+
const workerAdmissionJson =
|
|
1007
|
+
validated.workerAdmission === undefined
|
|
1008
|
+
? null
|
|
1009
|
+
: yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1010
|
+
validated.workerAdmission,
|
|
1011
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1012
|
+
|
|
1013
|
+
if (
|
|
1014
|
+
workerAdmissionJson !== null &&
|
|
1015
|
+
new TextEncoder().encode(workerAdmissionJson).byteLength > 16 * 1024 * 1024
|
|
1016
|
+
) {
|
|
1017
|
+
return yield* LedgerError.make({
|
|
1018
|
+
operation,
|
|
1019
|
+
message: "Worker admission metadata exceeds the stored value bound",
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const messageAdmissionJson =
|
|
1024
|
+
validated.messageAdmission === undefined
|
|
1025
|
+
? null
|
|
1026
|
+
: yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
|
|
1027
|
+
validated.messageAdmission,
|
|
1028
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1029
|
+
|
|
1030
|
+
if (
|
|
1031
|
+
messageAdmissionJson !== null &&
|
|
1032
|
+
new TextEncoder().encode(messageAdmissionJson).byteLength > 16 * 1024 * 1024
|
|
1033
|
+
) {
|
|
1034
|
+
return yield* LedgerError.make({
|
|
1035
|
+
operation,
|
|
1036
|
+
message: "Message admission metadata exceeds the stored value bound",
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
901
1040
|
const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
|
|
902
1041
|
Effect.mapError(internalFailure(operation)),
|
|
903
1042
|
);
|
|
1043
|
+
|
|
904
1044
|
const mintedSubmissionId = yield* mintIdentifier("submission", operation);
|
|
905
1045
|
const mintedReceiptId = yield* mintIdentifier("receipt", operation);
|
|
1046
|
+
|
|
906
1047
|
yield* hitFailpoint("ledger:admit:before", operation);
|
|
1048
|
+
|
|
907
1049
|
const result = yield* inWriteTransaction(
|
|
908
1050
|
operation,
|
|
909
1051
|
Effect.gen(function* () {
|
|
910
|
-
const keyRowKey = `${validated.
|
|
1052
|
+
const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
|
|
1053
|
+
|
|
911
1054
|
const existingRows = yield* sql<Record<string, unknown>>`
|
|
912
1055
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
913
1056
|
FROM effect_agent_submissions
|
|
914
|
-
WHERE
|
|
1057
|
+
WHERE thread_id = ${validated.threadId}
|
|
915
1058
|
AND principal = ${validated.principal}
|
|
916
1059
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
917
1060
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1061
|
+
|
|
918
1062
|
const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
|
|
1063
|
+
|
|
919
1064
|
if (existing.length > 1) {
|
|
920
1065
|
return yield* corruptionFailure(
|
|
921
1066
|
operation,
|
|
@@ -933,15 +1078,61 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
933
1078
|
existing[0].parent_tool_call_id === null
|
|
934
1079
|
: existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId &&
|
|
935
1080
|
existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
|
|
1081
|
+
|
|
936
1082
|
if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
|
|
937
1083
|
return yield* AdmissionConflict.make({
|
|
938
|
-
|
|
1084
|
+
threadId: validated.threadId,
|
|
939
1085
|
principal: validated.principal,
|
|
940
1086
|
idempotencyKey: validated.idempotencyKey,
|
|
941
1087
|
existingInputDigest: existing[0].input_digest,
|
|
942
1088
|
attemptedInputDigest: validated.inputDigest,
|
|
943
1089
|
});
|
|
944
1090
|
}
|
|
1091
|
+
|
|
1092
|
+
const retainedWorkerAdmission =
|
|
1093
|
+
existing[0].worker_admission_json === null
|
|
1094
|
+
? undefined
|
|
1095
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1096
|
+
existing[0].worker_admission_json,
|
|
1097
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1098
|
+
|
|
1099
|
+
const retainedInputMessage =
|
|
1100
|
+
existing[0].message_admission_json === null
|
|
1101
|
+
? undefined
|
|
1102
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(InputMessage))(
|
|
1103
|
+
existing[0].message_admission_json,
|
|
1104
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1105
|
+
|
|
1106
|
+
const retainedFence =
|
|
1107
|
+
existing[0].admission_fence_json === null
|
|
1108
|
+
? undefined
|
|
1109
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(AdmissionFence))(
|
|
1110
|
+
existing[0].admission_fence_json,
|
|
1111
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1112
|
+
|
|
1113
|
+
if (
|
|
1114
|
+
(existing[0].admission_group ?? undefined) !== validated.admissionGroup ||
|
|
1115
|
+
!Schema.toEquivalence(Schema.optional(WorkerAdmission))(
|
|
1116
|
+
retainedWorkerAdmission,
|
|
1117
|
+
validated.workerAdmission,
|
|
1118
|
+
) ||
|
|
1119
|
+
!Schema.toEquivalence(Schema.optional(InputMessage))(
|
|
1120
|
+
retainedInputMessage,
|
|
1121
|
+
validated.messageAdmission,
|
|
1122
|
+
) ||
|
|
1123
|
+
!Schema.toEquivalence(Schema.optional(AdmissionFence))(
|
|
1124
|
+
retainedFence,
|
|
1125
|
+
validated.admissionFence,
|
|
1126
|
+
)
|
|
1127
|
+
)
|
|
1128
|
+
return yield* AdmissionConflict.make({
|
|
1129
|
+
threadId: validated.threadId,
|
|
1130
|
+
principal: validated.principal,
|
|
1131
|
+
idempotencyKey: validated.idempotencyKey,
|
|
1132
|
+
existingInputDigest: existing[0].input_digest,
|
|
1133
|
+
attemptedInputDigest: validated.inputDigest,
|
|
1134
|
+
});
|
|
1135
|
+
|
|
945
1136
|
return yield* decodeAdmissionResult({
|
|
946
1137
|
submissionId: existing[0].submission_id,
|
|
947
1138
|
receiptId: existing[0].receipt_id,
|
|
@@ -951,26 +1142,78 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
951
1142
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
952
1143
|
}
|
|
953
1144
|
|
|
1145
|
+
// The first accepted input fixes ordinary/worker lane identity atomically with admission.
|
|
1146
|
+
// Canonical origin materialization can lag admission; a log scan cannot fence that race.
|
|
1147
|
+
const firstRows = yield* sql<Record<string, unknown>>`
|
|
1148
|
+
SELECT worker_admission_json FROM effect_agent_submissions
|
|
1149
|
+
WHERE thread_id=${validated.threadId} ORDER BY queue_sequence LIMIT 1
|
|
1150
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1151
|
+
|
|
1152
|
+
const first = yield* Schema.decodeUnknownEffect(
|
|
1153
|
+
Schema.Array(
|
|
1154
|
+
Schema.Struct({
|
|
1155
|
+
worker_admission_json: Schema.NullOr(BoundedStoredText),
|
|
1156
|
+
}),
|
|
1157
|
+
),
|
|
1158
|
+
)(firstRows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1159
|
+
|
|
1160
|
+
if (first[0] !== undefined) {
|
|
1161
|
+
const previous =
|
|
1162
|
+
first[0].worker_admission_json === null
|
|
1163
|
+
? undefined
|
|
1164
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1165
|
+
first[0].worker_admission_json,
|
|
1166
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1167
|
+
|
|
1168
|
+
if (
|
|
1169
|
+
!Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
|
|
1170
|
+
previous?.origin,
|
|
1171
|
+
validated.workerAdmission?.origin,
|
|
1172
|
+
)
|
|
1173
|
+
)
|
|
1174
|
+
return yield* AdmissionPolicyError.make({
|
|
1175
|
+
reason: "refused",
|
|
1176
|
+
code: "worker-origin-conflict",
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
yield* admissionFence.check(validated);
|
|
1181
|
+
if (validated.admissionGroup !== undefined) {
|
|
1182
|
+
const occupied = yield* sql<Record<string, unknown>>`
|
|
1183
|
+
SELECT submission_id FROM effect_agent_submissions
|
|
1184
|
+
WHERE thread_id=${validated.threadId} AND admission_group=${validated.admissionGroup} AND state<>'settled' LIMIT 1
|
|
1185
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1186
|
+
|
|
1187
|
+
if (occupied.length > 0)
|
|
1188
|
+
return yield* AdmissionPolicyError.make({
|
|
1189
|
+
reason: "occupied",
|
|
1190
|
+
code: "admission-group",
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
|
|
954
1194
|
const maxRows = yield* sql<Record<string, unknown>>`
|
|
955
1195
|
SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
|
|
956
1196
|
FROM effect_agent_submissions
|
|
957
|
-
WHERE
|
|
1197
|
+
WHERE thread_id = ${validated.threadId}
|
|
958
1198
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1199
|
+
|
|
959
1200
|
const decodedMax = yield* decodeRows(
|
|
960
1201
|
Schema.Array(MaxQueueSequenceRow),
|
|
961
1202
|
"effect_agent_submissions",
|
|
962
|
-
validated.
|
|
1203
|
+
validated.threadId,
|
|
963
1204
|
maxRows,
|
|
964
1205
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1206
|
+
|
|
965
1207
|
const queueSequence = yield* decodeQueueSequence(
|
|
966
1208
|
(decodedMax[0]?.max_queue_sequence ?? 0) + 1,
|
|
967
1209
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1210
|
+
|
|
968
1211
|
const now = yield* currentInstant;
|
|
969
1212
|
|
|
970
1213
|
yield* sql`
|
|
971
1214
|
INSERT INTO effect_agent_submissions (
|
|
972
1215
|
submission_id,
|
|
973
|
-
|
|
1216
|
+
thread_id,
|
|
974
1217
|
queue_sequence,
|
|
975
1218
|
principal,
|
|
976
1219
|
idempotency_key,
|
|
@@ -983,10 +1226,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
983
1226
|
state,
|
|
984
1227
|
created_at,
|
|
985
1228
|
parent_submission_id,
|
|
986
|
-
parent_tool_call_id
|
|
1229
|
+
parent_tool_call_id,
|
|
1230
|
+
admission_group,
|
|
1231
|
+
admission_fence_json,
|
|
1232
|
+
worker_admission_json,
|
|
1233
|
+
message_admission_json
|
|
987
1234
|
) VALUES (
|
|
988
1235
|
${mintedSubmissionId},
|
|
989
|
-
${validated.
|
|
1236
|
+
${validated.threadId},
|
|
990
1237
|
${queueSequence},
|
|
991
1238
|
${validated.principal},
|
|
992
1239
|
${validated.idempotencyKey},
|
|
@@ -999,7 +1246,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
999
1246
|
'admitted',
|
|
1000
1247
|
${now.iso},
|
|
1001
1248
|
${validated.parentLinkage?.parentSubmissionId ?? null},
|
|
1002
|
-
${validated.parentLinkage?.parentToolCallId ?? null}
|
|
1249
|
+
${validated.parentLinkage?.parentToolCallId ?? null},
|
|
1250
|
+
${validated.admissionGroup ?? null},
|
|
1251
|
+
${validated.admissionFence === undefined ? null : JSON.stringify(validated.admissionFence)},
|
|
1252
|
+
${workerAdmissionJson},
|
|
1253
|
+
${messageAdmissionJson}
|
|
1003
1254
|
)
|
|
1004
1255
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1005
1256
|
|
|
@@ -1012,7 +1263,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1012
1263
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1013
1264
|
}),
|
|
1014
1265
|
);
|
|
1266
|
+
|
|
1015
1267
|
yield* hitFailpoint("ledger:admit:after", operation);
|
|
1268
|
+
|
|
1016
1269
|
return result;
|
|
1017
1270
|
},
|
|
1018
1271
|
);
|
|
@@ -1021,16 +1274,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1021
1274
|
"SqliteSubmissionLedger.markReady",
|
|
1022
1275
|
)(function* (request: MarkReadyRequest) {
|
|
1023
1276
|
const operation = "ledger mark ready";
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1277
|
+
|
|
1278
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkReadyRequest))(request).pipe(
|
|
1279
|
+
Effect.mapError(internalFailure(operation)),
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1027
1282
|
yield* hitFailpoint("ledger:mark-ready:before", operation);
|
|
1028
1283
|
yield* inWriteTransaction(
|
|
1029
1284
|
operation,
|
|
1030
1285
|
Effect.gen(function* () {
|
|
1031
1286
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1287
|
+
|
|
1032
1288
|
if (submission.state !== "admitted") return;
|
|
1033
1289
|
const now = yield* currentInstant;
|
|
1290
|
+
|
|
1034
1291
|
yield* sql`
|
|
1035
1292
|
UPDATE effect_agent_submissions
|
|
1036
1293
|
SET state = 'ready', ready_at = ${now.iso}
|
|
@@ -1044,35 +1301,43 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1044
1301
|
const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn("SqliteSubmissionLedger.lookup")(
|
|
1045
1302
|
function* (request: SubmissionLookup) {
|
|
1046
1303
|
const operation = "ledger lookup";
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1304
|
+
|
|
1305
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookup))(request).pipe(
|
|
1306
|
+
Effect.mapError(internalFailure(operation)),
|
|
1307
|
+
);
|
|
1308
|
+
|
|
1050
1309
|
if (validated._tag === "SubmissionLookupById") {
|
|
1051
1310
|
const row = yield* readSubmission(operation, validated.submissionId);
|
|
1311
|
+
|
|
1052
1312
|
if (Option.isNone(row)) return Option.none();
|
|
1313
|
+
|
|
1053
1314
|
return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
|
|
1054
1315
|
}
|
|
1316
|
+
|
|
1055
1317
|
const rows = yield* sql<Record<string, unknown>>`
|
|
1056
1318
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1057
1319
|
FROM effect_agent_submissions
|
|
1058
|
-
WHERE
|
|
1320
|
+
WHERE thread_id = ${validated.threadId}
|
|
1059
1321
|
AND principal = ${validated.principal}
|
|
1060
1322
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
1061
1323
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1324
|
+
|
|
1062
1325
|
const decoded = yield* decodeSubmissionRows(
|
|
1063
1326
|
operation,
|
|
1064
|
-
`${validated.
|
|
1327
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1065
1328
|
rows,
|
|
1066
1329
|
);
|
|
1330
|
+
|
|
1067
1331
|
if (decoded.length > 1) {
|
|
1068
1332
|
return yield* corruptionFailure(
|
|
1069
1333
|
operation,
|
|
1070
1334
|
"effect_agent_submissions",
|
|
1071
|
-
`${validated.
|
|
1335
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1072
1336
|
"An admission idempotency key returned more than one row.",
|
|
1073
1337
|
);
|
|
1074
1338
|
}
|
|
1075
1339
|
if (decoded.length === 0) return Option.none();
|
|
1340
|
+
|
|
1076
1341
|
return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
|
|
1077
1342
|
},
|
|
1078
1343
|
);
|
|
@@ -1084,30 +1349,35 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1084
1349
|
"SqliteSubmissionLedger.resolveAdmission",
|
|
1085
1350
|
)(function* (request: SubmissionLookupByKey) {
|
|
1086
1351
|
const operation = "ledger resolve admission";
|
|
1087
|
-
|
|
1352
|
+
|
|
1353
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookupByKey))(
|
|
1088
1354
|
request,
|
|
1089
1355
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1356
|
+
|
|
1090
1357
|
const rows = yield* sql<Record<string, unknown>>`
|
|
1091
1358
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1092
1359
|
FROM effect_agent_submissions
|
|
1093
|
-
WHERE
|
|
1360
|
+
WHERE thread_id = ${validated.threadId}
|
|
1094
1361
|
AND principal = ${validated.principal}
|
|
1095
1362
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
1096
1363
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1364
|
+
|
|
1097
1365
|
const decoded = yield* decodeSubmissionRows(
|
|
1098
1366
|
operation,
|
|
1099
|
-
`${validated.
|
|
1367
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1100
1368
|
rows,
|
|
1101
1369
|
);
|
|
1370
|
+
|
|
1102
1371
|
if (decoded.length > 1) {
|
|
1103
1372
|
return yield* corruptionFailure(
|
|
1104
1373
|
operation,
|
|
1105
1374
|
"effect_agent_submissions",
|
|
1106
|
-
`${validated.
|
|
1375
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1107
1376
|
"An admission idempotency key returned more than one row.",
|
|
1108
1377
|
);
|
|
1109
1378
|
}
|
|
1110
1379
|
if (decoded.length === 0) return AdmissionNotAdmitted.make();
|
|
1380
|
+
|
|
1111
1381
|
return AdmissionAdmitted.make({
|
|
1112
1382
|
submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
|
|
1113
1383
|
});
|
|
@@ -1116,70 +1386,81 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1116
1386
|
const claim: SubmissionLedger["Service"]["claim"] = Effect.fn("SqliteSubmissionLedger.claim")(
|
|
1117
1387
|
function* (request: ClaimRequest) {
|
|
1118
1388
|
const operation = "ledger claim";
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1389
|
+
|
|
1390
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
|
|
1391
|
+
Effect.mapError(internalFailure(operation)),
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1122
1394
|
const attemptId = yield* mintIdentifier("attempt", operation);
|
|
1123
1395
|
const ownershipToken = yield* mintIdentifier("owner", operation);
|
|
1396
|
+
|
|
1124
1397
|
yield* hitFailpoint("ledger:claim:before", operation);
|
|
1398
|
+
|
|
1125
1399
|
const claimed = yield* inWriteTransaction(
|
|
1126
1400
|
operation,
|
|
1127
1401
|
Effect.gen(function* () {
|
|
1128
1402
|
const headRows = yield* sql<Record<string, unknown>>`
|
|
1129
1403
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1130
1404
|
FROM effect_agent_submissions
|
|
1131
|
-
WHERE
|
|
1405
|
+
WHERE thread_id = ${validated.threadId}
|
|
1132
1406
|
AND state <> 'settled'
|
|
1133
1407
|
ORDER BY queue_sequence ASC
|
|
1134
1408
|
LIMIT 1
|
|
1135
1409
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1136
|
-
|
|
1410
|
+
|
|
1411
|
+
const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
|
|
1412
|
+
|
|
1137
1413
|
if (heads.length === 0) return Option.none<Claim>();
|
|
1138
1414
|
const head = heads[0];
|
|
1139
1415
|
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
//
|
|
1416
|
+
// Unknown work is claimable only for a durably requested abort: the coordinator
|
|
1417
|
+
// cleans up children and settles without replaying ordinary Tools. Read the intent
|
|
1418
|
+
// in this claim transaction; retain uncertainty evidence and all ownership fencing.
|
|
1143
1419
|
if (
|
|
1144
1420
|
head.state === "joining" ||
|
|
1145
1421
|
head.state === "joined" ||
|
|
1146
1422
|
head.state === "suspended" ||
|
|
1147
|
-
head.state === "unknown"
|
|
1423
|
+
(head.state === "unknown" &&
|
|
1424
|
+
Option.isNone(yield* readAbortIntent(operation, head.submission_id)))
|
|
1148
1425
|
) {
|
|
1149
1426
|
return Option.none<Claim>();
|
|
1150
1427
|
}
|
|
1151
1428
|
|
|
1152
1429
|
const now = yield* currentInstant;
|
|
1153
1430
|
const ownership = yield* readOwnership(operation, head.submission_id);
|
|
1431
|
+
|
|
1154
1432
|
if (Option.isSome(ownership)) {
|
|
1155
1433
|
const expiresAt = yield* timestampMillis(
|
|
1156
1434
|
operation,
|
|
1157
1435
|
head.submission_id,
|
|
1158
1436
|
)(ownership.value.lease_expires_at);
|
|
1437
|
+
|
|
1159
1438
|
// A live lease blocks every new claim; expiry alone only revokes the liveness
|
|
1160
1439
|
// assumption — correctness stays with producer-epoch fencing (D5).
|
|
1161
1440
|
if (expiresAt > now.millis) return Option.none<Claim>();
|
|
1162
1441
|
}
|
|
1163
1442
|
|
|
1164
|
-
// Bump the
|
|
1165
|
-
// Attempt is fenced out of canonical appends (DUR-006). A
|
|
1443
|
+
// Bump the Thread's producer epoch atomically with the claim so every stale
|
|
1444
|
+
// Attempt is fenced out of canonical appends (DUR-006). A Thread that was
|
|
1166
1445
|
// never materialized (crash between admission and materialization) is created here
|
|
1167
1446
|
// so recovery can claim first and re-materialize idempotently at this epoch.
|
|
1168
|
-
const
|
|
1169
|
-
.
|
|
1447
|
+
const threads = yield* journal
|
|
1448
|
+
.getThread(head.thread_id)
|
|
1170
1449
|
.pipe(Effect.mapError(internalFailure(operation)));
|
|
1450
|
+
|
|
1171
1451
|
let producerEpoch: number;
|
|
1172
|
-
|
|
1452
|
+
|
|
1453
|
+
if (threads.length === 0) {
|
|
1173
1454
|
producerEpoch = 1;
|
|
1174
1455
|
yield* sql`
|
|
1175
|
-
INSERT INTO
|
|
1176
|
-
|
|
1456
|
+
INSERT INTO effect_agent_threads (
|
|
1457
|
+
thread_id,
|
|
1177
1458
|
created_at,
|
|
1178
1459
|
tail_sequence,
|
|
1179
1460
|
tail_digest,
|
|
1180
1461
|
producer_epoch
|
|
1181
1462
|
) VALUES (
|
|
1182
|
-
${head.
|
|
1463
|
+
${head.thread_id},
|
|
1183
1464
|
${now.iso},
|
|
1184
1465
|
0,
|
|
1185
1466
|
${EMPTY_TAIL_DIGEST},
|
|
@@ -1187,15 +1468,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1187
1468
|
)
|
|
1188
1469
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1189
1470
|
} else {
|
|
1190
|
-
producerEpoch =
|
|
1471
|
+
producerEpoch = threads[0].producer_epoch + 1;
|
|
1191
1472
|
yield* sql`
|
|
1192
|
-
UPDATE
|
|
1473
|
+
UPDATE effect_agent_threads
|
|
1193
1474
|
SET producer_epoch = ${producerEpoch}
|
|
1194
|
-
WHERE
|
|
1475
|
+
WHERE thread_id = ${head.thread_id}
|
|
1195
1476
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1196
1477
|
}
|
|
1197
1478
|
|
|
1198
1479
|
const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
|
|
1480
|
+
|
|
1199
1481
|
yield* sql`
|
|
1200
1482
|
INSERT INTO effect_agent_submission_ownership (
|
|
1201
1483
|
submission_id,
|
|
@@ -1224,14 +1506,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1224
1506
|
INSERT INTO effect_agent_attempts (
|
|
1225
1507
|
attempt_id,
|
|
1226
1508
|
submission_id,
|
|
1227
|
-
|
|
1509
|
+
thread_id,
|
|
1228
1510
|
owner_producer_id,
|
|
1229
1511
|
producer_epoch,
|
|
1230
1512
|
claimed_at
|
|
1231
1513
|
) VALUES (
|
|
1232
1514
|
${attemptId},
|
|
1233
1515
|
${head.submission_id},
|
|
1234
|
-
${head.
|
|
1516
|
+
${head.thread_id},
|
|
1235
1517
|
${validated.producerId},
|
|
1236
1518
|
${producerEpoch},
|
|
1237
1519
|
${now.iso}
|
|
@@ -1256,6 +1538,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1256
1538
|
),
|
|
1257
1539
|
),
|
|
1258
1540
|
);
|
|
1541
|
+
|
|
1259
1542
|
return Option.some(
|
|
1260
1543
|
yield* decodeClaim({
|
|
1261
1544
|
submissionId: head.submission_id,
|
|
@@ -1268,7 +1551,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1268
1551
|
);
|
|
1269
1552
|
}),
|
|
1270
1553
|
);
|
|
1554
|
+
|
|
1271
1555
|
yield* hitFailpoint("ledger:claim:after", operation);
|
|
1556
|
+
|
|
1272
1557
|
return claimed;
|
|
1273
1558
|
},
|
|
1274
1559
|
);
|
|
@@ -1277,29 +1562,37 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1277
1562
|
"SqliteSubmissionLedger.renewOwnership",
|
|
1278
1563
|
)(function* (request: RenewOwnershipRequest) {
|
|
1279
1564
|
const operation = "ledger renew ownership";
|
|
1280
|
-
|
|
1565
|
+
|
|
1566
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(RenewOwnershipRequest))(
|
|
1281
1567
|
request,
|
|
1282
1568
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1569
|
+
|
|
1283
1570
|
yield* hitFailpoint("ledger:renew:before", operation);
|
|
1571
|
+
|
|
1284
1572
|
const renewal = yield* inWriteTransaction(
|
|
1285
1573
|
operation,
|
|
1286
1574
|
Effect.gen(function* () {
|
|
1287
1575
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1576
|
+
|
|
1288
1577
|
yield* requireOwnership(operation, submission, validated.ownershipToken);
|
|
1289
1578
|
const now = yield* currentInstant;
|
|
1290
1579
|
const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
|
|
1580
|
+
|
|
1291
1581
|
yield* sql`
|
|
1292
1582
|
UPDATE effect_agent_submission_ownership
|
|
1293
1583
|
SET lease_expires_at = ${leaseExpiresAt}
|
|
1294
1584
|
WHERE submission_id = ${validated.submissionId}
|
|
1295
1585
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1586
|
+
|
|
1296
1587
|
return yield* decodeOwnershipRenewal({
|
|
1297
1588
|
ownershipToken: validated.ownershipToken,
|
|
1298
1589
|
leaseExpiresAt,
|
|
1299
1590
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1300
1591
|
}),
|
|
1301
1592
|
);
|
|
1593
|
+
|
|
1302
1594
|
yield* hitFailpoint("ledger:renew:after", operation);
|
|
1595
|
+
|
|
1303
1596
|
return renewal;
|
|
1304
1597
|
});
|
|
1305
1598
|
|
|
@@ -1307,14 +1600,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1307
1600
|
"SqliteSubmissionLedger.releaseOwnership",
|
|
1308
1601
|
)(function* (request: ReleaseOwnershipRequest) {
|
|
1309
1602
|
const operation = "ledger release ownership";
|
|
1310
|
-
|
|
1603
|
+
|
|
1604
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseOwnershipRequest))(
|
|
1311
1605
|
request,
|
|
1312
1606
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1607
|
+
|
|
1313
1608
|
yield* hitFailpoint("ledger:release:before", operation);
|
|
1314
1609
|
yield* inWriteTransaction(
|
|
1315
1610
|
operation,
|
|
1316
1611
|
Effect.gen(function* () {
|
|
1317
1612
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1613
|
+
|
|
1318
1614
|
yield* requireOwnership(operation, submission, validated.ownershipToken);
|
|
1319
1615
|
yield* sql`
|
|
1320
1616
|
DELETE FROM effect_agent_submission_ownership
|
|
@@ -1336,14 +1632,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1336
1632
|
"SqliteSubmissionLedger.markInputApplied",
|
|
1337
1633
|
)(function* (request: MarkInputAppliedRequest) {
|
|
1338
1634
|
const operation = "ledger mark input applied";
|
|
1339
|
-
|
|
1635
|
+
|
|
1636
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
|
|
1340
1637
|
request,
|
|
1341
1638
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1639
|
+
|
|
1342
1640
|
yield* hitFailpoint("ledger:mark-input-applied:before", operation);
|
|
1343
1641
|
yield* inWriteTransaction(
|
|
1344
1642
|
operation,
|
|
1345
1643
|
Effect.gen(function* () {
|
|
1346
1644
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1645
|
+
|
|
1347
1646
|
yield* requireOwnership(operation, submission, validated.ownershipToken);
|
|
1348
1647
|
if (submission.input_applied_record_id !== null) {
|
|
1349
1648
|
if (
|
|
@@ -1352,6 +1651,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1352
1651
|
) {
|
|
1353
1652
|
return;
|
|
1354
1653
|
}
|
|
1654
|
+
|
|
1355
1655
|
return yield* corruptionFailure(
|
|
1356
1656
|
operation,
|
|
1357
1657
|
"effect_agent_submissions",
|
|
@@ -1379,29 +1679,36 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1379
1679
|
"SqliteSubmissionLedger.reserveSettlement",
|
|
1380
1680
|
)(function* (request: SettlementReservation) {
|
|
1381
1681
|
const operation = "ledger reserve settlement";
|
|
1382
|
-
|
|
1682
|
+
|
|
1683
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SettlementReservation))(
|
|
1383
1684
|
request,
|
|
1384
1685
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1686
|
+
|
|
1385
1687
|
const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(
|
|
1386
1688
|
Effect.mapError(internalFailure(operation)),
|
|
1387
1689
|
);
|
|
1690
|
+
|
|
1388
1691
|
yield* hitFailpoint("ledger:reserve-settlement:before", operation);
|
|
1692
|
+
|
|
1389
1693
|
const reserved = yield* inWriteTransaction(
|
|
1390
1694
|
operation,
|
|
1391
1695
|
Effect.gen(function* () {
|
|
1392
1696
|
const existing = yield* readReservation(operation, validated.submissionId);
|
|
1697
|
+
|
|
1393
1698
|
if (Option.isSome(existing)) {
|
|
1394
1699
|
const identical =
|
|
1395
1700
|
existing.value.settlement_id === validated.settlementId &&
|
|
1396
1701
|
existing.value.outcome === validated.outcome &&
|
|
1397
1702
|
existing.value.record_digest === validated.recordDigest &&
|
|
1398
1703
|
existing.value.record_json === recordJson;
|
|
1704
|
+
|
|
1399
1705
|
if (!identical) {
|
|
1400
1706
|
return yield* SettlementConflict.make({
|
|
1401
1707
|
submissionId: validated.submissionId,
|
|
1402
1708
|
existingOutcome: existing.value.outcome,
|
|
1403
1709
|
});
|
|
1404
1710
|
}
|
|
1711
|
+
|
|
1405
1712
|
const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(
|
|
1406
1713
|
Effect.mapError((error) =>
|
|
1407
1714
|
corruptionFailure(
|
|
@@ -1412,6 +1719,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1412
1719
|
),
|
|
1413
1720
|
),
|
|
1414
1721
|
);
|
|
1722
|
+
|
|
1415
1723
|
return ReservedSettlement.make({
|
|
1416
1724
|
submissionId: validated.submissionId,
|
|
1417
1725
|
settlementId: validated.settlementId,
|
|
@@ -1423,6 +1731,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1423
1731
|
}
|
|
1424
1732
|
|
|
1425
1733
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1734
|
+
|
|
1426
1735
|
if (submission.state === "settled") {
|
|
1427
1736
|
if (submission.settled_outcome === null) {
|
|
1428
1737
|
return yield* corruptionFailure(
|
|
@@ -1432,6 +1741,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1432
1741
|
"A settled Submission carries no terminal outcome.",
|
|
1433
1742
|
);
|
|
1434
1743
|
}
|
|
1744
|
+
|
|
1435
1745
|
return yield* SettlementConflict.make({
|
|
1436
1746
|
submissionId: validated.submissionId,
|
|
1437
1747
|
existingOutcome: submission.settled_outcome,
|
|
@@ -1446,13 +1756,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1446
1756
|
// ABORTED settlement (`terminalizing` is the same pass's crash replay). Every other
|
|
1447
1757
|
// reservation stays fenced by the target lane's live ownership.
|
|
1448
1758
|
let queuedAbortSettlement = false;
|
|
1759
|
+
|
|
1449
1760
|
if (
|
|
1450
1761
|
validated.outcome === "aborted" &&
|
|
1451
1762
|
(submission.state === "ready" || submission.state === "terminalizing")
|
|
1452
1763
|
) {
|
|
1453
1764
|
const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
|
|
1765
|
+
|
|
1454
1766
|
if (Option.isSome(abortIntent)) {
|
|
1455
1767
|
const ownership = yield* readOwnership(operation, validated.submissionId);
|
|
1768
|
+
|
|
1456
1769
|
queuedAbortSettlement = Option.isNone(ownership);
|
|
1457
1770
|
}
|
|
1458
1771
|
}
|
|
@@ -1461,6 +1774,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1461
1774
|
}
|
|
1462
1775
|
}
|
|
1463
1776
|
const now = yield* currentInstant;
|
|
1777
|
+
|
|
1464
1778
|
yield* sql`
|
|
1465
1779
|
INSERT INTO effect_agent_settlement_reservations (
|
|
1466
1780
|
submission_id,
|
|
@@ -1485,6 +1799,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1485
1799
|
SET state = 'terminalizing'
|
|
1486
1800
|
WHERE submission_id = ${validated.submissionId}
|
|
1487
1801
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1802
|
+
|
|
1488
1803
|
return ReservedSettlement.make({
|
|
1489
1804
|
submissionId: validated.submissionId,
|
|
1490
1805
|
settlementId: validated.settlementId,
|
|
@@ -1495,56 +1810,161 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1495
1810
|
});
|
|
1496
1811
|
}),
|
|
1497
1812
|
);
|
|
1813
|
+
|
|
1498
1814
|
yield* hitFailpoint("ledger:reserve-settlement:after", operation);
|
|
1815
|
+
|
|
1499
1816
|
return reserved;
|
|
1500
1817
|
});
|
|
1501
1818
|
|
|
1819
|
+
const validateFinalization = Effect.fn("SqliteSubmissionLedger.validateFinalization")(function* (
|
|
1820
|
+
validated: SettlementFinalization,
|
|
1821
|
+
reservation: Option.Option<ReservationRow>,
|
|
1822
|
+
) {
|
|
1823
|
+
const operation = "ledger finalize settlement";
|
|
1824
|
+
|
|
1825
|
+
if (Option.isNone(reservation)) {
|
|
1826
|
+
return yield* LedgerError.make({
|
|
1827
|
+
operation,
|
|
1828
|
+
message: `No settlement reservation exists for submission ${validated.submissionId}.`,
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
if (reservation.value.settlement_id !== validated.settlementId) {
|
|
1832
|
+
return yield* SettlementConflict.make({
|
|
1833
|
+
submissionId: validated.submissionId,
|
|
1834
|
+
existingOutcome: reservation.value.outcome,
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
const reservationRecord = yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(
|
|
1839
|
+
Effect.mapError((error) =>
|
|
1840
|
+
corruptionFailure(
|
|
1841
|
+
operation,
|
|
1842
|
+
"effect_agent_settlement_reservations",
|
|
1843
|
+
validated.submissionId,
|
|
1844
|
+
error.message,
|
|
1845
|
+
),
|
|
1846
|
+
),
|
|
1847
|
+
);
|
|
1848
|
+
|
|
1849
|
+
const settlementFailure = settlementFailureFromRecord(reservationRecord);
|
|
1850
|
+
|
|
1851
|
+
if ((reservation.value.outcome === "failed") !== (settlementFailure !== undefined)) {
|
|
1852
|
+
return yield* corruptionFailure(
|
|
1853
|
+
operation,
|
|
1854
|
+
"effect_agent_settlement_reservations",
|
|
1855
|
+
validated.submissionId,
|
|
1856
|
+
"The reserved outcome and canonical failure diagnostic disagree.",
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
return { reservation: reservation.value, settlementFailure };
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
const replayFinalization = Effect.fn("SqliteSubmissionLedger.replayFinalization")(function* (
|
|
1864
|
+
validated: SettlementFinalization,
|
|
1865
|
+
submission: SubmissionRow,
|
|
1866
|
+
{ reservation, settlementFailure }: Effect.Success<ReturnType<typeof validateFinalization>>,
|
|
1867
|
+
) {
|
|
1868
|
+
const operation = "ledger finalize settlement";
|
|
1869
|
+
|
|
1870
|
+
if (reservation.finalized_at === null) {
|
|
1871
|
+
return yield* corruptionFailure(
|
|
1872
|
+
operation,
|
|
1873
|
+
"effect_agent_settlement_reservations",
|
|
1874
|
+
validated.submissionId,
|
|
1875
|
+
"A settled Submission's reservation carries no finalization timestamp.",
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
return yield* decodeSettlement({
|
|
1880
|
+
submissionId: validated.submissionId,
|
|
1881
|
+
settlementId: validated.settlementId,
|
|
1882
|
+
receiptId: submission.receipt_id,
|
|
1883
|
+
outcome: reservation.outcome,
|
|
1884
|
+
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
1885
|
+
settledAt: reservation.finalized_at,
|
|
1886
|
+
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1502
1889
|
const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
|
|
1503
1890
|
"SqliteSubmissionLedger.finalizeSettlement",
|
|
1504
1891
|
)(function* (request: SettlementFinalization) {
|
|
1505
1892
|
const operation = "ledger finalize settlement";
|
|
1506
|
-
|
|
1893
|
+
|
|
1894
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SettlementFinalization))(
|
|
1507
1895
|
request,
|
|
1508
1896
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1897
|
+
|
|
1509
1898
|
yield* hitFailpoint("ledger:finalize-settlement:before", operation);
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1899
|
+
|
|
1900
|
+
// A single statement captures the settled row and its immutable reservation together.
|
|
1901
|
+
// A miss does not authorize finalization: the write path re-reads under its transaction.
|
|
1902
|
+
const replayRows = yield* sql<Record<string, unknown>>`
|
|
1903
|
+
SELECT submission.*, reservation.settlement_id, reservation.outcome,
|
|
1904
|
+
reservation.record_id, reservation.record_json, reservation.record_digest,
|
|
1905
|
+
reservation.reserved_at, reservation.finalized_at
|
|
1906
|
+
FROM effect_agent_submissions AS submission
|
|
1907
|
+
INNER JOIN effect_agent_settlement_reservations AS reservation
|
|
1908
|
+
ON reservation.submission_id = submission.submission_id
|
|
1909
|
+
WHERE submission.submission_id = ${validated.submissionId} AND submission.state = 'settled'
|
|
1910
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1911
|
+
|
|
1912
|
+
if (replayRows.length > 1) {
|
|
1913
|
+
return yield* corruptionFailure(
|
|
1914
|
+
operation,
|
|
1915
|
+
"effect_agent_submissions",
|
|
1916
|
+
validated.submissionId,
|
|
1917
|
+
"A submission primary key returned more than one row.",
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
const replayRow = replayRows[0];
|
|
1921
|
+
|
|
1922
|
+
if (replayRow !== undefined) {
|
|
1923
|
+
const reservations = yield* decodeRows(
|
|
1924
|
+
Schema.Array(ReservationRow),
|
|
1925
|
+
"effect_agent_settlement_reservations",
|
|
1926
|
+
validated.submissionId,
|
|
1927
|
+
replayRows,
|
|
1928
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1929
|
+
|
|
1930
|
+
const state = yield* validateFinalization(validated, Option.fromUndefinedOr(reservations[0]));
|
|
1931
|
+
|
|
1932
|
+
const submission = yield* Schema.decodeUnknownEffect(SubmissionRow)(replayRow).pipe(
|
|
1933
|
+
Effect.mapError((error) =>
|
|
1934
|
+
corruptionFailure(
|
|
1516
1935
|
operation,
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1936
|
+
"effect_agent_submissions",
|
|
1937
|
+
validated.submissionId,
|
|
1938
|
+
error.message,
|
|
1939
|
+
),
|
|
1940
|
+
),
|
|
1941
|
+
);
|
|
1942
|
+
|
|
1943
|
+
const settlement = yield* replayFinalization(validated, submission, state);
|
|
1944
|
+
|
|
1945
|
+
yield* hitFailpoint("ledger:finalize-settlement:after", operation);
|
|
1946
|
+
|
|
1947
|
+
return settlement;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
const settlement = yield* inWriteTransaction(
|
|
1951
|
+
operation,
|
|
1952
|
+
Effect.gen(function* () {
|
|
1953
|
+
const state = yield* validateFinalization(
|
|
1954
|
+
validated,
|
|
1955
|
+
yield* readReservation(operation, validated.submissionId),
|
|
1956
|
+
);
|
|
1957
|
+
|
|
1958
|
+
const { reservation, settlementFailure } = state;
|
|
1526
1959
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
operation,
|
|
1531
|
-
"effect_agent_settlement_reservations",
|
|
1532
|
-
validated.submissionId,
|
|
1533
|
-
"A settled Submission's reservation carries no finalization timestamp.",
|
|
1534
|
-
);
|
|
1535
|
-
}
|
|
1536
|
-
return yield* decodeSettlement({
|
|
1537
|
-
submissionId: validated.submissionId,
|
|
1538
|
-
settlementId: validated.settlementId,
|
|
1539
|
-
receiptId: submission.receipt_id,
|
|
1540
|
-
outcome: reservation.value.outcome,
|
|
1541
|
-
settledAt: reservation.value.finalized_at,
|
|
1542
|
-
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1543
|
-
}
|
|
1960
|
+
|
|
1961
|
+
if (submission.state === "settled")
|
|
1962
|
+
return yield* replayFinalization(validated, submission, state);
|
|
1544
1963
|
const now = yield* currentInstant;
|
|
1964
|
+
|
|
1545
1965
|
yield* sql`
|
|
1546
1966
|
UPDATE effect_agent_submissions
|
|
1547
|
-
SET state = 'settled', settled_outcome = ${reservation.
|
|
1967
|
+
SET state = 'settled', settled_outcome = ${reservation.outcome}
|
|
1548
1968
|
WHERE submission_id = ${validated.submissionId}
|
|
1549
1969
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1550
1970
|
yield* sql`
|
|
@@ -1556,16 +1976,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1556
1976
|
DELETE FROM effect_agent_submission_ownership
|
|
1557
1977
|
WHERE submission_id = ${validated.submissionId}
|
|
1558
1978
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1979
|
+
|
|
1559
1980
|
return yield* decodeSettlement({
|
|
1560
1981
|
submissionId: validated.submissionId,
|
|
1561
1982
|
settlementId: validated.settlementId,
|
|
1562
1983
|
receiptId: submission.receipt_id,
|
|
1563
|
-
outcome: reservation.
|
|
1984
|
+
outcome: reservation.outcome,
|
|
1985
|
+
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
1564
1986
|
settledAt: now.iso,
|
|
1565
1987
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1566
1988
|
}),
|
|
1567
1989
|
);
|
|
1990
|
+
|
|
1568
1991
|
yield* hitFailpoint("ledger:finalize-settlement:after", operation);
|
|
1992
|
+
|
|
1569
1993
|
return settlement;
|
|
1570
1994
|
});
|
|
1571
1995
|
|
|
@@ -1573,14 +1997,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1573
1997
|
"SqliteSubmissionLedger.requestAbort",
|
|
1574
1998
|
)(function* (request: AbortCommand) {
|
|
1575
1999
|
const operation = "ledger request abort";
|
|
1576
|
-
|
|
2000
|
+
|
|
2001
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AbortCommand))(request).pipe(
|
|
1577
2002
|
Effect.mapError(internalFailure(operation)),
|
|
1578
2003
|
);
|
|
2004
|
+
|
|
1579
2005
|
yield* hitFailpoint("ledger:request-abort:before", operation);
|
|
2006
|
+
|
|
1580
2007
|
const intent = yield* inWriteTransaction(
|
|
1581
2008
|
operation,
|
|
1582
2009
|
Effect.gen(function* () {
|
|
1583
2010
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2011
|
+
|
|
1584
2012
|
if (submission.state === "settled") {
|
|
1585
2013
|
if (submission.settled_outcome === null) {
|
|
1586
2014
|
return yield* corruptionFailure(
|
|
@@ -1590,6 +2018,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1590
2018
|
"A settled Submission carries no terminal outcome.",
|
|
1591
2019
|
);
|
|
1592
2020
|
}
|
|
2021
|
+
|
|
1593
2022
|
return yield* SettlementConflict.make({
|
|
1594
2023
|
submissionId: validated.submissionId,
|
|
1595
2024
|
existingOutcome: submission.settled_outcome,
|
|
@@ -1607,15 +2036,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1607
2036
|
"A joined Submission carries no host linkage.",
|
|
1608
2037
|
);
|
|
1609
2038
|
}
|
|
2039
|
+
|
|
1610
2040
|
const hostSubmissionId = yield* decodeSubmissionId(
|
|
1611
2041
|
submission.joined_host_submission_id,
|
|
1612
2042
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2043
|
+
|
|
1613
2044
|
return yield* JoinedToHost.make({
|
|
1614
2045
|
submissionId: validated.submissionId,
|
|
1615
2046
|
hostSubmissionId,
|
|
1616
2047
|
});
|
|
1617
2048
|
}
|
|
1618
2049
|
const existing = yield* readAbortIntent(operation, validated.submissionId);
|
|
2050
|
+
|
|
1619
2051
|
if (Option.isSome(existing)) {
|
|
1620
2052
|
return yield* abortIntentFromRow(
|
|
1621
2053
|
operation,
|
|
@@ -1625,6 +2057,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1625
2057
|
);
|
|
1626
2058
|
}
|
|
1627
2059
|
const now = yield* currentInstant;
|
|
2060
|
+
|
|
1628
2061
|
yield* sql`
|
|
1629
2062
|
INSERT INTO effect_agent_abort_intents (
|
|
1630
2063
|
submission_id,
|
|
@@ -1638,11 +2071,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1638
2071
|
${now.iso}
|
|
1639
2072
|
)
|
|
1640
2073
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2074
|
+
|
|
1641
2075
|
const canonicalRecordId = yield* canonicalAbortRecordId(
|
|
1642
2076
|
operation,
|
|
1643
|
-
submission.
|
|
2077
|
+
submission.thread_id,
|
|
1644
2078
|
validated.submissionId,
|
|
1645
2079
|
);
|
|
2080
|
+
|
|
1646
2081
|
return yield* decodeAbortIntent({
|
|
1647
2082
|
submissionId: validated.submissionId,
|
|
1648
2083
|
author: validated.author,
|
|
@@ -1652,7 +2087,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1652
2087
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1653
2088
|
}),
|
|
1654
2089
|
);
|
|
2090
|
+
|
|
1655
2091
|
yield* hitFailpoint("ledger:request-abort:after", operation);
|
|
2092
|
+
|
|
1656
2093
|
return intent;
|
|
1657
2094
|
});
|
|
1658
2095
|
|
|
@@ -1660,31 +2097,38 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1660
2097
|
"SqliteSubmissionLedger.claimJoining",
|
|
1661
2098
|
)(function* (request: ClaimJoiningRequest) {
|
|
1662
2099
|
const operation = "ledger claim joining";
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
2100
|
+
|
|
2101
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(
|
|
2102
|
+
Effect.mapError(internalFailure(operation)),
|
|
2103
|
+
);
|
|
2104
|
+
|
|
1666
2105
|
yield* hitFailpoint("ledger:claim-joining:before", operation);
|
|
2106
|
+
|
|
1667
2107
|
const claims = yield* inWriteTransaction(
|
|
1668
2108
|
operation,
|
|
1669
2109
|
Effect.gen(function* () {
|
|
1670
2110
|
const host = yield* requireSubmission(operation, validated.hostSubmissionId);
|
|
1671
|
-
|
|
2111
|
+
|
|
2112
|
+
if (host.thread_id !== validated.threadId) {
|
|
1672
2113
|
return yield* LedgerError.make({
|
|
1673
2114
|
operation,
|
|
1674
|
-
message: `Host submission ${validated.hostSubmissionId} does not belong to
|
|
2115
|
+
message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
|
|
1675
2116
|
});
|
|
1676
2117
|
}
|
|
1677
2118
|
// The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
|
|
1678
2119
|
yield* requireOwnership(operation, host, validated.ownershipToken);
|
|
2120
|
+
|
|
1679
2121
|
const laterRows = yield* sql<Record<string, unknown>>`
|
|
1680
2122
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1681
2123
|
FROM effect_agent_submissions
|
|
1682
|
-
WHERE
|
|
2124
|
+
WHERE thread_id = ${validated.threadId}
|
|
1683
2125
|
AND queue_sequence > ${host.queue_sequence}
|
|
1684
2126
|
ORDER BY queue_sequence ASC
|
|
1685
2127
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1686
|
-
|
|
2128
|
+
|
|
2129
|
+
const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
|
|
1687
2130
|
const claimed: Array<JoiningClaim> = [];
|
|
2131
|
+
|
|
1688
2132
|
for (const row of later) {
|
|
1689
2133
|
if (claimed.length >= validated.maxCount) break;
|
|
1690
2134
|
// Rows already claimed by THIS host extend its contiguous prefix and are skipped;
|
|
@@ -1707,6 +2151,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1707
2151
|
SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
|
|
1708
2152
|
WHERE submission_id = ${row.submission_id}
|
|
1709
2153
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2154
|
+
|
|
1710
2155
|
const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
|
|
1711
2156
|
Effect.mapError((error) =>
|
|
1712
2157
|
corruptionFailure(
|
|
@@ -1717,6 +2162,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1717
2162
|
),
|
|
1718
2163
|
),
|
|
1719
2164
|
);
|
|
2165
|
+
|
|
1720
2166
|
claimed.push(
|
|
1721
2167
|
yield* decodeJoiningClaim({
|
|
1722
2168
|
submissionId: row.submission_id,
|
|
@@ -1725,10 +2171,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1725
2171
|
}).pipe(Effect.mapError(internalFailure(operation))),
|
|
1726
2172
|
);
|
|
1727
2173
|
}
|
|
1728
|
-
|
|
2174
|
+
|
|
2175
|
+
return claimed;
|
|
1729
2176
|
}),
|
|
1730
2177
|
);
|
|
2178
|
+
|
|
1731
2179
|
yield* hitFailpoint("ledger:claim-joining:after", operation);
|
|
2180
|
+
|
|
1732
2181
|
return claims;
|
|
1733
2182
|
});
|
|
1734
2183
|
|
|
@@ -1736,14 +2185,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1736
2185
|
"SqliteSubmissionLedger.markJoined",
|
|
1737
2186
|
)(function* (request: MarkJoinedRequest) {
|
|
1738
2187
|
const operation = "ledger mark joined";
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
2188
|
+
|
|
2189
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkJoinedRequest))(request).pipe(
|
|
2190
|
+
Effect.mapError(internalFailure(operation)),
|
|
2191
|
+
);
|
|
2192
|
+
|
|
1742
2193
|
yield* hitFailpoint("ledger:mark-joined:before", operation);
|
|
1743
2194
|
yield* inWriteTransaction(
|
|
1744
2195
|
operation,
|
|
1745
2196
|
Effect.gen(function* () {
|
|
1746
2197
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2198
|
+
|
|
1747
2199
|
if (submission.joined_host_submission_id === null) {
|
|
1748
2200
|
return yield* LedgerError.make({
|
|
1749
2201
|
operation,
|
|
@@ -1751,6 +2203,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1751
2203
|
});
|
|
1752
2204
|
}
|
|
1753
2205
|
const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
|
|
2206
|
+
|
|
1754
2207
|
// The lane is host-owned: the presented token must own the HOST's ownership period,
|
|
1755
2208
|
// which also lets a later host Attempt repair a lost marker from history (DUR-016).
|
|
1756
2209
|
yield* requireOwnership(operation, host, validated.ownershipToken);
|
|
@@ -1761,6 +2214,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1761
2214
|
) {
|
|
1762
2215
|
return;
|
|
1763
2216
|
}
|
|
2217
|
+
|
|
1764
2218
|
return yield* corruptionFailure(
|
|
1765
2219
|
operation,
|
|
1766
2220
|
"effect_agent_submissions",
|
|
@@ -1791,14 +2245,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1791
2245
|
"SqliteSubmissionLedger.revertJoining",
|
|
1792
2246
|
)(function* (request: RevertJoiningRequest) {
|
|
1793
2247
|
const operation = "ledger revert joining";
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2248
|
+
|
|
2249
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(RevertJoiningRequest))(request).pipe(
|
|
2250
|
+
Effect.mapError(internalFailure(operation)),
|
|
2251
|
+
);
|
|
2252
|
+
|
|
1797
2253
|
yield* hitFailpoint("ledger:revert-joining:before", operation);
|
|
1798
2254
|
yield* inWriteTransaction(
|
|
1799
2255
|
operation,
|
|
1800
2256
|
Effect.gen(function* () {
|
|
1801
2257
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2258
|
+
|
|
1802
2259
|
// Idempotent and recovery-only: only a still-`joining` Submission reverts; an
|
|
1803
2260
|
// already-joined (or already-reverted) Submission is a no-op (DUR-016).
|
|
1804
2261
|
if (submission.state !== "joining") return;
|
|
@@ -1816,17 +2273,22 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1816
2273
|
"SqliteSubmissionLedger.suspend",
|
|
1817
2274
|
)(function* (request: SuspendRequest) {
|
|
1818
2275
|
const operation = "ledger suspend";
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2276
|
+
|
|
2277
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SuspendRequest))(request).pipe(
|
|
2278
|
+
Effect.mapError(internalFailure(operation)),
|
|
2279
|
+
);
|
|
2280
|
+
|
|
1822
2281
|
const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
|
|
1823
2282
|
Effect.mapError(internalFailure(operation)),
|
|
1824
2283
|
);
|
|
2284
|
+
|
|
1825
2285
|
yield* hitFailpoint("ledger:suspend:before", operation);
|
|
2286
|
+
|
|
1826
2287
|
const outcome = yield* inWriteTransaction(
|
|
1827
2288
|
operation,
|
|
1828
2289
|
Effect.gen(function* () {
|
|
1829
2290
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2291
|
+
|
|
1830
2292
|
if (submission.state === "settled") {
|
|
1831
2293
|
if (submission.settled_outcome === null) {
|
|
1832
2294
|
return yield* corruptionFailure(
|
|
@@ -1836,6 +2298,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1836
2298
|
"A settled Submission carries no terminal outcome.",
|
|
1837
2299
|
);
|
|
1838
2300
|
}
|
|
2301
|
+
|
|
1839
2302
|
return yield* SettlementConflict.make({
|
|
1840
2303
|
submissionId: validated.submissionId,
|
|
1841
2304
|
existingOutcome: submission.settled_outcome,
|
|
@@ -1844,6 +2307,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1844
2307
|
// An exact terminal outcome is already reserved (DUR-011); suspension would
|
|
1845
2308
|
// contradict it, so the reservation wins.
|
|
1846
2309
|
const reservation = yield* readReservation(operation, validated.submissionId);
|
|
2310
|
+
|
|
1847
2311
|
if (Option.isSome(reservation)) {
|
|
1848
2312
|
return yield* SettlementConflict.make({
|
|
1849
2313
|
submissionId: validated.submissionId,
|
|
@@ -1857,23 +2321,27 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1857
2321
|
if (validated.reason._tag === "ApprovalPending") {
|
|
1858
2322
|
const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
1859
2323
|
const decided = new Set(decisions.map((row) => row.tool_call_id));
|
|
2324
|
+
|
|
1860
2325
|
if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
|
|
1861
|
-
return
|
|
2326
|
+
return RESUME_IMMEDIATELY;
|
|
1862
2327
|
}
|
|
1863
2328
|
} else {
|
|
1864
2329
|
let allSettled = true;
|
|
2330
|
+
|
|
1865
2331
|
for (const child of validated.reason.children) {
|
|
1866
2332
|
const childRow = yield* readSubmission(operation, child.childSubmissionId);
|
|
2333
|
+
|
|
1867
2334
|
if (Option.isNone(childRow) || childRow.value.state !== "settled") {
|
|
1868
2335
|
allSettled = false;
|
|
1869
2336
|
break;
|
|
1870
2337
|
}
|
|
1871
2338
|
}
|
|
1872
2339
|
if (allSettled) {
|
|
1873
|
-
return
|
|
2340
|
+
return RESUME_IMMEDIATELY;
|
|
1874
2341
|
}
|
|
1875
2342
|
}
|
|
1876
2343
|
const now = yield* currentInstant;
|
|
2344
|
+
|
|
1877
2345
|
yield* sql`
|
|
1878
2346
|
UPDATE effect_agent_submissions
|
|
1879
2347
|
SET
|
|
@@ -1888,10 +2356,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1888
2356
|
DELETE FROM effect_agent_submission_ownership
|
|
1889
2357
|
WHERE submission_id = ${validated.submissionId}
|
|
1890
2358
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1891
|
-
|
|
2359
|
+
|
|
2360
|
+
return SUSPENDED;
|
|
1892
2361
|
}),
|
|
1893
2362
|
);
|
|
2363
|
+
|
|
1894
2364
|
yield* hitFailpoint("ledger:suspend:after", operation);
|
|
2365
|
+
|
|
1895
2366
|
return outcome;
|
|
1896
2367
|
});
|
|
1897
2368
|
|
|
@@ -1904,6 +2375,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1904
2375
|
const wakeSuspendedIfCovered = Effect.fn("SqliteSubmissionLedger.wakeSuspendedIfCovered")(
|
|
1905
2376
|
function* (operation: string, submission: SubmissionRow): Effect.fn.Return<void, LedgerError> {
|
|
1906
2377
|
if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
|
|
2378
|
+
|
|
1907
2379
|
const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
|
|
1908
2380
|
submission.suspended_reason_json,
|
|
1909
2381
|
).pipe(
|
|
@@ -1916,9 +2388,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1916
2388
|
),
|
|
1917
2389
|
),
|
|
1918
2390
|
);
|
|
2391
|
+
|
|
1919
2392
|
if (reason._tag !== "ApprovalPending") return;
|
|
1920
2393
|
const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
|
|
1921
2394
|
const decided = new Set(decisions.map((row) => row.tool_call_id));
|
|
2395
|
+
|
|
1922
2396
|
if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
|
|
1923
2397
|
yield* sql`
|
|
1924
2398
|
UPDATE effect_agent_submissions
|
|
@@ -1935,14 +2409,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1935
2409
|
"SqliteSubmissionLedger.recordApprovalDecision",
|
|
1936
2410
|
)(function* (command: ApprovalDecisionCommand) {
|
|
1937
2411
|
const operation = "ledger record approval decision";
|
|
1938
|
-
|
|
2412
|
+
|
|
2413
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ApprovalDecisionCommand))(
|
|
1939
2414
|
command,
|
|
1940
2415
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2416
|
+
|
|
1941
2417
|
yield* hitFailpoint("ledger:approval-decision:before", operation);
|
|
2418
|
+
|
|
1942
2419
|
const intent = yield* inWriteTransaction(
|
|
1943
2420
|
operation,
|
|
1944
2421
|
Effect.gen(function* () {
|
|
1945
2422
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2423
|
+
|
|
1946
2424
|
if (submission.state === "settled") {
|
|
1947
2425
|
if (submission.settled_outcome === null) {
|
|
1948
2426
|
return yield* corruptionFailure(
|
|
@@ -1952,6 +2430,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1952
2430
|
"A settled Submission carries no terminal outcome.",
|
|
1953
2431
|
);
|
|
1954
2432
|
}
|
|
2433
|
+
|
|
1955
2434
|
return yield* SettlementConflict.make({
|
|
1956
2435
|
submissionId: validated.submissionId,
|
|
1957
2436
|
existingOutcome: submission.settled_outcome,
|
|
@@ -1959,6 +2438,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1959
2438
|
}
|
|
1960
2439
|
const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
1961
2440
|
const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
|
|
2441
|
+
|
|
1962
2442
|
if (existing !== undefined) {
|
|
1963
2443
|
// Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
|
|
1964
2444
|
// the recorded intent unchanged; a divergent re-decision conflicts.
|
|
@@ -1969,9 +2449,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1969
2449
|
existingDecision: existing.decision,
|
|
1970
2450
|
});
|
|
1971
2451
|
}
|
|
2452
|
+
|
|
1972
2453
|
return yield* approvalIntentFromRow(operation, existing);
|
|
1973
2454
|
}
|
|
1974
2455
|
const now = yield* currentInstant;
|
|
2456
|
+
|
|
1975
2457
|
yield* sql`
|
|
1976
2458
|
INSERT INTO effect_agent_approval_decisions (
|
|
1977
2459
|
submission_id,
|
|
@@ -1990,6 +2472,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1990
2472
|
)
|
|
1991
2473
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1992
2474
|
yield* wakeSuspendedIfCovered(operation, submission);
|
|
2475
|
+
|
|
1993
2476
|
return yield* decodeApprovalDecisionIntent({
|
|
1994
2477
|
submissionId: validated.submissionId,
|
|
1995
2478
|
toolCallId: validated.toolCallId,
|
|
@@ -2000,7 +2483,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2000
2483
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
2001
2484
|
}),
|
|
2002
2485
|
);
|
|
2486
|
+
|
|
2003
2487
|
yield* hitFailpoint("ledger:approval-decision:after", operation);
|
|
2488
|
+
|
|
2004
2489
|
return intent;
|
|
2005
2490
|
});
|
|
2006
2491
|
|
|
@@ -2008,14 +2493,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2008
2493
|
"SqliteSubmissionLedger.markUnknown",
|
|
2009
2494
|
)(function* (request: MarkUnknownRequest) {
|
|
2010
2495
|
const operation = "ledger mark unknown";
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2496
|
+
|
|
2497
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkUnknownRequest))(request).pipe(
|
|
2498
|
+
Effect.mapError(internalFailure(operation)),
|
|
2499
|
+
);
|
|
2500
|
+
|
|
2014
2501
|
yield* hitFailpoint("ledger:mark-unknown:before", operation);
|
|
2015
2502
|
yield* inWriteTransaction(
|
|
2016
2503
|
operation,
|
|
2017
2504
|
Effect.gen(function* () {
|
|
2018
2505
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2506
|
+
|
|
2019
2507
|
if (submission.state === "settled") {
|
|
2020
2508
|
if (submission.settled_outcome === null) {
|
|
2021
2509
|
return yield* corruptionFailure(
|
|
@@ -2025,6 +2513,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2025
2513
|
"A settled Submission carries no terminal outcome.",
|
|
2026
2514
|
);
|
|
2027
2515
|
}
|
|
2516
|
+
|
|
2028
2517
|
return yield* SettlementConflict.make({
|
|
2029
2518
|
submissionId: validated.submissionId,
|
|
2030
2519
|
existingOutcome: submission.settled_outcome,
|
|
@@ -2033,6 +2522,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2033
2522
|
// A reserved exact outcome wins over a late Unknown marking (DUR-011); the recovery
|
|
2034
2523
|
// classifier orders reservation ahead of MarkUnknown for the same reason.
|
|
2035
2524
|
const reservation = yield* readReservation(operation, validated.submissionId);
|
|
2525
|
+
|
|
2036
2526
|
if (Option.isSome(reservation)) {
|
|
2037
2527
|
return yield* SettlementConflict.make({
|
|
2038
2528
|
submissionId: validated.submissionId,
|
|
@@ -2043,13 +2533,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2043
2533
|
// set while the first recorded reason is kept.
|
|
2044
2534
|
const existingIds = yield* storedUnknownToolCallIds(operation, submission);
|
|
2045
2535
|
const known = new Set(existingIds);
|
|
2536
|
+
|
|
2046
2537
|
const merged = [
|
|
2047
2538
|
...existingIds,
|
|
2048
2539
|
...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
|
|
2049
2540
|
];
|
|
2541
|
+
|
|
2050
2542
|
const idsJson = yield* encodeToolCallIdsText(merged).pipe(
|
|
2051
2543
|
Effect.mapError(internalFailure(operation)),
|
|
2052
2544
|
);
|
|
2545
|
+
|
|
2053
2546
|
yield* sql`
|
|
2054
2547
|
UPDATE effect_agent_submissions
|
|
2055
2548
|
SET
|
|
@@ -2067,17 +2560,22 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2067
2560
|
"SqliteSubmissionLedger.recordUnknownResolution",
|
|
2068
2561
|
)(function* (command: UnknownResolutionCommand) {
|
|
2069
2562
|
const operation = "ledger record unknown resolution";
|
|
2070
|
-
|
|
2563
|
+
|
|
2564
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(UnknownResolutionCommand))(
|
|
2071
2565
|
command,
|
|
2072
2566
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2567
|
+
|
|
2073
2568
|
const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
|
|
2074
2569
|
Effect.mapError(internalFailure(operation)),
|
|
2075
2570
|
);
|
|
2571
|
+
|
|
2076
2572
|
yield* hitFailpoint("ledger:unknown-resolution:before", operation);
|
|
2573
|
+
|
|
2077
2574
|
const intent = yield* inWriteTransaction(
|
|
2078
2575
|
operation,
|
|
2079
2576
|
Effect.gen(function* () {
|
|
2080
2577
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2578
|
+
|
|
2081
2579
|
if (submission.state === "settled") {
|
|
2082
2580
|
if (submission.settled_outcome === null) {
|
|
2083
2581
|
return yield* corruptionFailure(
|
|
@@ -2087,6 +2585,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2087
2585
|
"A settled Submission carries no terminal outcome.",
|
|
2088
2586
|
);
|
|
2089
2587
|
}
|
|
2588
|
+
|
|
2090
2589
|
return yield* SettlementConflict.make({
|
|
2091
2590
|
submissionId: validated.submissionId,
|
|
2092
2591
|
existingOutcome: submission.settled_outcome,
|
|
@@ -2094,19 +2593,30 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2094
2593
|
}
|
|
2095
2594
|
const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
2096
2595
|
const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
|
|
2097
|
-
|
|
2596
|
+
|
|
2597
|
+
const existingIntent =
|
|
2598
|
+
existing === undefined
|
|
2599
|
+
? undefined
|
|
2600
|
+
: yield* unknownResolutionIntentFromRow(operation, existing);
|
|
2601
|
+
|
|
2602
|
+
if (
|
|
2603
|
+
existingIntent !== undefined &&
|
|
2604
|
+
!equivalentUnknownResolution(existingIntent.resolution, validated.resolution)
|
|
2605
|
+
) {
|
|
2098
2606
|
return yield* UnknownResolutionConflict.make({
|
|
2099
2607
|
submissionId: validated.submissionId,
|
|
2100
2608
|
toolCallId: validated.toolCallId,
|
|
2101
2609
|
});
|
|
2102
2610
|
}
|
|
2103
2611
|
let resolved: UnknownResolutionIntent;
|
|
2104
|
-
|
|
2612
|
+
|
|
2613
|
+
if (existingIntent !== undefined) {
|
|
2105
2614
|
// Idempotent replay of the recorded intent (author/reason may differ; the stored
|
|
2106
2615
|
// audit fields win, exactly like requestAbort).
|
|
2107
|
-
resolved =
|
|
2616
|
+
resolved = existingIntent;
|
|
2108
2617
|
} else {
|
|
2109
2618
|
const now = yield* currentInstant;
|
|
2619
|
+
|
|
2110
2620
|
yield* sql`
|
|
2111
2621
|
INSERT INTO effect_agent_unknown_resolutions (
|
|
2112
2622
|
submission_id,
|
|
@@ -2124,9 +2634,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2124
2634
|
${now.iso}
|
|
2125
2635
|
)
|
|
2126
2636
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2637
|
+
|
|
2127
2638
|
const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
|
|
2128
2639
|
Effect.mapError(internalFailure(operation)),
|
|
2129
2640
|
);
|
|
2641
|
+
|
|
2130
2642
|
resolved = yield* decodeUnknownResolutionIntent({
|
|
2131
2643
|
submissionId: validated.submissionId,
|
|
2132
2644
|
toolCallId: validated.toolCallId,
|
|
@@ -2143,6 +2655,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2143
2655
|
const markedIds = yield* storedUnknownToolCallIds(operation, submission);
|
|
2144
2656
|
const covering = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
2145
2657
|
const coveredIds = new Set(covering.map((row) => row.tool_call_id));
|
|
2658
|
+
|
|
2146
2659
|
if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) {
|
|
2147
2660
|
yield* sql`
|
|
2148
2661
|
UPDATE effect_agent_submissions
|
|
@@ -2154,10 +2667,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2154
2667
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2155
2668
|
}
|
|
2156
2669
|
}
|
|
2670
|
+
|
|
2157
2671
|
return resolved;
|
|
2158
2672
|
}),
|
|
2159
2673
|
);
|
|
2674
|
+
|
|
2160
2675
|
yield* hitFailpoint("ledger:unknown-resolution:after", operation);
|
|
2676
|
+
|
|
2161
2677
|
return intent;
|
|
2162
2678
|
});
|
|
2163
2679
|
|
|
@@ -2165,26 +2681,38 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2165
2681
|
"SqliteSubmissionLedger.recordChildSettled",
|
|
2166
2682
|
)(function* (request: ChildSettledNotification) {
|
|
2167
2683
|
const operation = "ledger record child settled";
|
|
2168
|
-
|
|
2684
|
+
|
|
2685
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ChildSettledNotification))(
|
|
2169
2686
|
request,
|
|
2170
2687
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2688
|
+
|
|
2171
2689
|
yield* hitFailpoint("ledger:child-settled:before", operation);
|
|
2690
|
+
|
|
2172
2691
|
const outcome = yield* inWriteTransaction(
|
|
2173
2692
|
operation,
|
|
2174
2693
|
Effect.gen(function* () {
|
|
2175
2694
|
const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
|
|
2176
|
-
// The
|
|
2177
|
-
//
|
|
2695
|
+
// The caller may notify after the canonical Settlement append but before ledger
|
|
2696
|
+
// finalization. An exact reservation plus `terminalizing` is the narrow durable prefix
|
|
2697
|
+
// that makes that ordering admissible; earlier states remain a caller error.
|
|
2178
2698
|
const child = yield* readSubmission(operation, validated.childSubmissionId);
|
|
2179
|
-
|
|
2699
|
+
const childReservation = yield* readReservation(operation, validated.childSubmissionId);
|
|
2700
|
+
|
|
2701
|
+
const announced =
|
|
2702
|
+
Option.isSome(child) &&
|
|
2703
|
+
(child.value.state === "settled" ||
|
|
2704
|
+
(child.value.state === "terminalizing" && Option.isSome(childReservation)));
|
|
2705
|
+
|
|
2706
|
+
if (!announced) {
|
|
2180
2707
|
return yield* LedgerError.make({
|
|
2181
2708
|
operation,
|
|
2182
2709
|
message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`,
|
|
2183
2710
|
});
|
|
2184
2711
|
}
|
|
2185
2712
|
if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
|
|
2186
|
-
return
|
|
2713
|
+
return NOT_WAITING;
|
|
2187
2714
|
}
|
|
2715
|
+
|
|
2188
2716
|
const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
|
|
2189
2717
|
parent.suspended_reason_json,
|
|
2190
2718
|
).pipe(
|
|
@@ -2197,20 +2725,28 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2197
2725
|
),
|
|
2198
2726
|
),
|
|
2199
2727
|
);
|
|
2728
|
+
|
|
2200
2729
|
if (reason._tag !== "WaitingForChild") {
|
|
2201
|
-
return
|
|
2730
|
+
return NOT_WAITING;
|
|
2202
2731
|
}
|
|
2203
2732
|
if (
|
|
2204
2733
|
!reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
|
|
2205
2734
|
) {
|
|
2206
|
-
return
|
|
2735
|
+
return NOT_WAITING;
|
|
2207
2736
|
}
|
|
2208
|
-
//
|
|
2209
|
-
//
|
|
2737
|
+
// Every listed child must be either finalized or canonically announced from the exact
|
|
2738
|
+
// terminalizing reservation. Replays re-run this coverage check idempotently.
|
|
2210
2739
|
for (const entry of reason.children) {
|
|
2211
2740
|
const listed = yield* readSubmission(operation, entry.childSubmissionId);
|
|
2212
|
-
|
|
2213
|
-
|
|
2741
|
+
const reservation = yield* readReservation(operation, entry.childSubmissionId);
|
|
2742
|
+
|
|
2743
|
+
const covered =
|
|
2744
|
+
Option.isSome(listed) &&
|
|
2745
|
+
(listed.value.state === "settled" ||
|
|
2746
|
+
(listed.value.state === "terminalizing" && Option.isSome(reservation)));
|
|
2747
|
+
|
|
2748
|
+
if (!covered) {
|
|
2749
|
+
return STILL_WAITING;
|
|
2214
2750
|
}
|
|
2215
2751
|
}
|
|
2216
2752
|
yield* sql`
|
|
@@ -2221,10 +2757,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2221
2757
|
suspended_at = NULL
|
|
2222
2758
|
WHERE submission_id = ${validated.parentSubmissionId}
|
|
2223
2759
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2224
|
-
|
|
2760
|
+
|
|
2761
|
+
return WOKEN;
|
|
2225
2762
|
}),
|
|
2226
2763
|
);
|
|
2764
|
+
|
|
2227
2765
|
yield* hitFailpoint("ledger:child-settled:after", operation);
|
|
2766
|
+
|
|
2228
2767
|
return outcome;
|
|
2229
2768
|
});
|
|
2230
2769
|
|
|
@@ -2232,25 +2771,36 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2232
2771
|
"SqliteSubmissionLedger.reserveChildBudget",
|
|
2233
2772
|
)(function* (request: ChildBudgetReservationRequest) {
|
|
2234
2773
|
const operation = "ledger reserve child budget";
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2774
|
+
|
|
2775
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ChildBudgetReservationRequest))(
|
|
2776
|
+
request,
|
|
2777
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2778
|
+
|
|
2238
2779
|
const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
|
|
2239
2780
|
Effect.mapError(internalFailure(operation)),
|
|
2240
2781
|
);
|
|
2782
|
+
|
|
2241
2783
|
yield* hitFailpoint("ledger:child-reservation:before", operation);
|
|
2784
|
+
|
|
2242
2785
|
const reserved = yield* inWriteTransaction(
|
|
2243
2786
|
operation,
|
|
2244
2787
|
Effect.gen(function* () {
|
|
2245
2788
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
2789
|
+
|
|
2246
2790
|
if (Option.isSome(existing)) {
|
|
2791
|
+
const existingSnapshot = yield* childReservationSnapshotFromRow(
|
|
2792
|
+
operation,
|
|
2793
|
+
existing.value,
|
|
2794
|
+
);
|
|
2795
|
+
|
|
2247
2796
|
// Identical replays short-circuit before the fence, mirroring reserveSettlement: a
|
|
2248
2797
|
// replay creates nothing, so a recovering caller resumes rather than duplicates.
|
|
2249
2798
|
const identical =
|
|
2250
2799
|
existing.value.parent_submission_id === validated.parentSubmissionId &&
|
|
2251
2800
|
existing.value.parent_tool_call_id === validated.parentToolCallId &&
|
|
2252
2801
|
existing.value.allocation_digest === validated.allocationDigest &&
|
|
2253
|
-
|
|
2802
|
+
equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
|
|
2803
|
+
|
|
2254
2804
|
if (!identical) {
|
|
2255
2805
|
return yield* ChildReservationConflict.make({
|
|
2256
2806
|
reservationId: validated.reservationId,
|
|
@@ -2259,16 +2809,19 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2259
2809
|
"A reservation with this identity exists with a different parent Tool Call or allocation.",
|
|
2260
2810
|
});
|
|
2261
2811
|
}
|
|
2812
|
+
|
|
2262
2813
|
return ReservedChildBudget.make({
|
|
2263
|
-
reservation:
|
|
2814
|
+
reservation: existingSnapshot,
|
|
2264
2815
|
replayed: true,
|
|
2265
2816
|
});
|
|
2266
2817
|
}
|
|
2818
|
+
|
|
2267
2819
|
const collision = yield* readChildReservationForCall(
|
|
2268
2820
|
operation,
|
|
2269
2821
|
validated.parentSubmissionId,
|
|
2270
2822
|
validated.parentToolCallId,
|
|
2271
2823
|
);
|
|
2824
|
+
|
|
2272
2825
|
if (Option.isSome(collision)) {
|
|
2273
2826
|
return yield* ChildReservationConflict.make({
|
|
2274
2827
|
reservationId: validated.reservationId,
|
|
@@ -2277,10 +2830,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2277
2830
|
});
|
|
2278
2831
|
}
|
|
2279
2832
|
const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
|
|
2833
|
+
|
|
2280
2834
|
// Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
|
|
2281
2835
|
// parent Attempt can never create new reservation state.
|
|
2282
2836
|
yield* requireOwnership(operation, parent, validated.ownershipToken);
|
|
2283
2837
|
const now = yield* currentInstant;
|
|
2838
|
+
|
|
2284
2839
|
yield* sql`
|
|
2285
2840
|
INSERT INTO effect_agent_child_reservations (
|
|
2286
2841
|
reservation_id,
|
|
@@ -2301,6 +2856,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2301
2856
|
)
|
|
2302
2857
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2303
2858
|
const inserted = yield* readChildReservation(operation, validated.reservationId);
|
|
2859
|
+
|
|
2304
2860
|
if (Option.isNone(inserted)) {
|
|
2305
2861
|
return yield* corruptionFailure(
|
|
2306
2862
|
operation,
|
|
@@ -2309,13 +2865,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2309
2865
|
"An inserted child reservation row is missing inside its own transaction.",
|
|
2310
2866
|
);
|
|
2311
2867
|
}
|
|
2868
|
+
|
|
2312
2869
|
return ReservedChildBudget.make({
|
|
2313
2870
|
reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
|
|
2314
2871
|
replayed: false,
|
|
2315
2872
|
});
|
|
2316
2873
|
}),
|
|
2317
2874
|
);
|
|
2875
|
+
|
|
2318
2876
|
yield* hitFailpoint("ledger:child-reservation:after", operation);
|
|
2877
|
+
|
|
2319
2878
|
return reserved;
|
|
2320
2879
|
});
|
|
2321
2880
|
|
|
@@ -2324,14 +2883,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2324
2883
|
request: AttachChildToReservationRequest,
|
|
2325
2884
|
) {
|
|
2326
2885
|
const operation = "ledger attach child to reservation";
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2886
|
+
|
|
2887
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AttachChildToReservationRequest))(
|
|
2888
|
+
request,
|
|
2889
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2890
|
+
|
|
2330
2891
|
yield* hitFailpoint("ledger:child-attach:before", operation);
|
|
2892
|
+
|
|
2331
2893
|
const attached = yield* inWriteTransaction(
|
|
2332
2894
|
operation,
|
|
2333
2895
|
Effect.gen(function* () {
|
|
2334
2896
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
2897
|
+
|
|
2335
2898
|
if (Option.isNone(existing)) {
|
|
2336
2899
|
return yield* LedgerError.make({
|
|
2337
2900
|
operation,
|
|
@@ -2343,6 +2906,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2343
2906
|
if (existing.value.child_submission_id === validated.childSubmissionId) {
|
|
2344
2907
|
return yield* childReservationSnapshotFromRow(operation, existing.value);
|
|
2345
2908
|
}
|
|
2909
|
+
|
|
2346
2910
|
return yield* ChildReservationConflict.make({
|
|
2347
2911
|
reservationId: validated.reservationId,
|
|
2348
2912
|
status: existing.value.status,
|
|
@@ -2350,6 +2914,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2350
2914
|
});
|
|
2351
2915
|
}
|
|
2352
2916
|
const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
|
|
2917
|
+
|
|
2353
2918
|
yield* requireOwnership(operation, parent, validated.ownershipToken);
|
|
2354
2919
|
if (existing.value.status !== "reserved") {
|
|
2355
2920
|
return yield* ChildReservationConflict.make({
|
|
@@ -2361,6 +2926,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2361
2926
|
// Single-store latitude: the admitted child must exist here, so a dangling
|
|
2362
2927
|
// attachment can never enter the recovery view.
|
|
2363
2928
|
const child = yield* readSubmission(operation, validated.childSubmissionId);
|
|
2929
|
+
|
|
2364
2930
|
if (Option.isNone(child)) {
|
|
2365
2931
|
return yield* LedgerError.make({
|
|
2366
2932
|
operation,
|
|
@@ -2373,6 +2939,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2373
2939
|
WHERE reservation_id = ${validated.reservationId}
|
|
2374
2940
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2375
2941
|
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
2942
|
+
|
|
2376
2943
|
if (Option.isNone(updated)) {
|
|
2377
2944
|
return yield* corruptionFailure(
|
|
2378
2945
|
operation,
|
|
@@ -2381,10 +2948,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2381
2948
|
"An updated child reservation row is missing inside its own transaction.",
|
|
2382
2949
|
);
|
|
2383
2950
|
}
|
|
2951
|
+
|
|
2384
2952
|
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
2385
2953
|
}),
|
|
2386
2954
|
);
|
|
2955
|
+
|
|
2387
2956
|
yield* hitFailpoint("ledger:child-attach:after", operation);
|
|
2957
|
+
|
|
2388
2958
|
return attached;
|
|
2389
2959
|
});
|
|
2390
2960
|
|
|
@@ -2392,17 +2962,22 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2392
2962
|
"SqliteSubmissionLedger.beginChildBudgetRelease",
|
|
2393
2963
|
)(function* (request: BeginChildBudgetReleaseRequest) {
|
|
2394
2964
|
const operation = "ledger begin child budget release";
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2965
|
+
|
|
2966
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(BeginChildBudgetReleaseRequest))(
|
|
2967
|
+
request,
|
|
2968
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2969
|
+
|
|
2398
2970
|
const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
|
|
2399
2971
|
Effect.mapError(internalFailure(operation)),
|
|
2400
2972
|
);
|
|
2973
|
+
|
|
2401
2974
|
yield* hitFailpoint("ledger:child-release-pending:before", operation);
|
|
2975
|
+
|
|
2402
2976
|
const frozen = yield* inWriteTransaction(
|
|
2403
2977
|
operation,
|
|
2404
2978
|
Effect.gen(function* () {
|
|
2405
2979
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
2980
|
+
|
|
2406
2981
|
if (Option.isNone(existing)) {
|
|
2407
2982
|
return yield* LedgerError.make({
|
|
2408
2983
|
operation,
|
|
@@ -2410,11 +2985,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2410
2985
|
});
|
|
2411
2986
|
}
|
|
2412
2987
|
if (existing.value.status !== "reserved") {
|
|
2988
|
+
const existingSnapshot = yield* childReservationSnapshotFromRow(
|
|
2989
|
+
operation,
|
|
2990
|
+
existing.value,
|
|
2991
|
+
);
|
|
2992
|
+
|
|
2413
2993
|
// The accounting decision was already frozen exactly once; an identical replay is a
|
|
2414
2994
|
// no-op and a divergent decision conflicts (spec §12 join step 6).
|
|
2415
|
-
if (
|
|
2416
|
-
|
|
2995
|
+
if (
|
|
2996
|
+
existingSnapshot.accounting !== undefined &&
|
|
2997
|
+
equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
|
|
2998
|
+
) {
|
|
2999
|
+
return existingSnapshot;
|
|
2417
3000
|
}
|
|
3001
|
+
|
|
2418
3002
|
return yield* ChildReservationConflict.make({
|
|
2419
3003
|
reservationId: validated.reservationId,
|
|
2420
3004
|
status: existing.value.status,
|
|
@@ -2422,6 +3006,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2422
3006
|
});
|
|
2423
3007
|
}
|
|
2424
3008
|
const now = yield* currentInstant;
|
|
3009
|
+
|
|
2425
3010
|
yield* sql`
|
|
2426
3011
|
UPDATE effect_agent_child_reservations
|
|
2427
3012
|
SET
|
|
@@ -2431,6 +3016,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2431
3016
|
WHERE reservation_id = ${validated.reservationId}
|
|
2432
3017
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2433
3018
|
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
3019
|
+
|
|
2434
3020
|
if (Option.isNone(updated)) {
|
|
2435
3021
|
return yield* corruptionFailure(
|
|
2436
3022
|
operation,
|
|
@@ -2439,10 +3025,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2439
3025
|
"An updated child reservation row is missing inside its own transaction.",
|
|
2440
3026
|
);
|
|
2441
3027
|
}
|
|
3028
|
+
|
|
2442
3029
|
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
2443
3030
|
}),
|
|
2444
3031
|
);
|
|
3032
|
+
|
|
2445
3033
|
yield* hitFailpoint("ledger:child-release-pending:after", operation);
|
|
3034
|
+
|
|
2446
3035
|
return frozen;
|
|
2447
3036
|
});
|
|
2448
3037
|
|
|
@@ -2450,14 +3039,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2450
3039
|
"SqliteSubmissionLedger.releaseChildBudget",
|
|
2451
3040
|
)(function* (request: ReleaseChildBudgetRequest) {
|
|
2452
3041
|
const operation = "ledger release child budget";
|
|
2453
|
-
|
|
3042
|
+
|
|
3043
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseChildBudgetRequest))(
|
|
2454
3044
|
request,
|
|
2455
3045
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3046
|
+
|
|
2456
3047
|
yield* hitFailpoint("ledger:child-release:before", operation);
|
|
3048
|
+
|
|
2457
3049
|
const released = yield* inWriteTransaction(
|
|
2458
3050
|
operation,
|
|
2459
3051
|
Effect.gen(function* () {
|
|
2460
3052
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
3053
|
+
|
|
2461
3054
|
if (Option.isNone(existing)) {
|
|
2462
3055
|
return yield* LedgerError.make({
|
|
2463
3056
|
operation,
|
|
@@ -2477,12 +3070,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2477
3070
|
});
|
|
2478
3071
|
}
|
|
2479
3072
|
const now = yield* currentInstant;
|
|
3073
|
+
|
|
2480
3074
|
yield* sql`
|
|
2481
3075
|
UPDATE effect_agent_child_reservations
|
|
2482
3076
|
SET status = 'released', released_at = ${now.iso}
|
|
2483
3077
|
WHERE reservation_id = ${validated.reservationId}
|
|
2484
3078
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2485
3079
|
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
3080
|
+
|
|
2486
3081
|
if (Option.isNone(updated)) {
|
|
2487
3082
|
return yield* corruptionFailure(
|
|
2488
3083
|
operation,
|
|
@@ -2491,15 +3086,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2491
3086
|
"An updated child reservation row is missing inside its own transaction.",
|
|
2492
3087
|
);
|
|
2493
3088
|
}
|
|
3089
|
+
|
|
2494
3090
|
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
2495
3091
|
}),
|
|
2496
3092
|
);
|
|
3093
|
+
|
|
2497
3094
|
yield* hitFailpoint("ledger:child-release:after", operation);
|
|
3095
|
+
|
|
2498
3096
|
return released;
|
|
2499
3097
|
});
|
|
2500
3098
|
|
|
2501
3099
|
interface ScanCursor {
|
|
2502
|
-
readonly
|
|
3100
|
+
readonly threadId: string;
|
|
2503
3101
|
readonly queueSequence: number;
|
|
2504
3102
|
}
|
|
2505
3103
|
|
|
@@ -2510,65 +3108,140 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2510
3108
|
LedgerError
|
|
2511
3109
|
> {
|
|
2512
3110
|
const operation = "ledger scan nonterminal";
|
|
3111
|
+
|
|
2513
3112
|
const rows = yield* (
|
|
2514
3113
|
cursor === undefined
|
|
2515
3114
|
? sql<Record<string, unknown>>`
|
|
2516
3115
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2517
3116
|
FROM effect_agent_submissions
|
|
2518
3117
|
WHERE state <> 'settled'
|
|
2519
|
-
ORDER BY
|
|
3118
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
2520
3119
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
2521
3120
|
`
|
|
2522
3121
|
: sql<Record<string, unknown>>`
|
|
2523
3122
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2524
3123
|
FROM effect_agent_submissions
|
|
2525
3124
|
WHERE state <> 'settled'
|
|
2526
|
-
AND (
|
|
2527
|
-
|
|
2528
|
-
OR (
|
|
2529
|
-
conversation_id = ${cursor.conversationId}
|
|
2530
|
-
AND queue_sequence > ${cursor.queueSequence}
|
|
2531
|
-
)
|
|
2532
|
-
)
|
|
2533
|
-
ORDER BY conversation_id ASC, queue_sequence ASC
|
|
3125
|
+
AND (thread_id, queue_sequence) > (${cursor.threadId}, ${cursor.queueSequence})
|
|
3126
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
2534
3127
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
2535
3128
|
`
|
|
2536
3129
|
).pipe(Effect.mapError(sqlFailure(operation)));
|
|
3130
|
+
|
|
2537
3131
|
const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
|
|
3132
|
+
|
|
2538
3133
|
const snapshots = yield* Effect.forEach(decoded, (row) =>
|
|
2539
3134
|
decodeSubmissionSnapshot(operation, row),
|
|
2540
3135
|
);
|
|
3136
|
+
|
|
2541
3137
|
const last = decoded[decoded.length - 1];
|
|
3138
|
+
|
|
2542
3139
|
const next: Option.Option<ScanCursor | undefined> =
|
|
2543
3140
|
last === undefined || decoded.length < SCAN_PAGE_SIZE
|
|
2544
3141
|
? Option.none()
|
|
2545
3142
|
: Option.some({
|
|
2546
|
-
|
|
3143
|
+
threadId: last.thread_id,
|
|
2547
3144
|
queueSequence: last.queue_sequence,
|
|
2548
3145
|
});
|
|
3146
|
+
|
|
2549
3147
|
return [snapshots, next] as const;
|
|
2550
3148
|
});
|
|
2551
3149
|
|
|
2552
|
-
const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
3150
|
+
const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate<
|
|
3151
|
+
ScanCursor | undefined,
|
|
3152
|
+
SubmissionSnapshot,
|
|
3153
|
+
LedgerError
|
|
3154
|
+
>(undefined, scanPage);
|
|
3155
|
+
|
|
3156
|
+
const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
|
|
3157
|
+
"SqliteSubmissionLedger.readAbortIntentForSubmission",
|
|
3158
|
+
)(function* (request) {
|
|
3159
|
+
const operation = "ledger read abort intent";
|
|
3160
|
+
|
|
3161
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
|
|
3162
|
+
Effect.mapError(internalFailure(operation)),
|
|
3163
|
+
);
|
|
3164
|
+
|
|
3165
|
+
const recordId = submissionAbortRecordId(validated.submissionId);
|
|
3166
|
+
|
|
3167
|
+
const rows = yield* sql<Record<string, unknown>>`
|
|
3168
|
+
SELECT
|
|
3169
|
+
submission.submission_id,
|
|
3170
|
+
abort.submission_id AS abort_submission_id,
|
|
3171
|
+
abort.author,
|
|
3172
|
+
abort.reason,
|
|
3173
|
+
abort.requested_at,
|
|
3174
|
+
canonical.record_id AS canonical_record_id
|
|
3175
|
+
FROM effect_agent_submissions AS submission
|
|
3176
|
+
LEFT JOIN effect_agent_abort_intents AS abort
|
|
3177
|
+
ON abort.submission_id = submission.submission_id
|
|
3178
|
+
LEFT JOIN effect_agent_canonical_records AS canonical
|
|
3179
|
+
ON canonical.thread_id = submission.thread_id
|
|
3180
|
+
AND canonical.record_id = ${recordId}
|
|
3181
|
+
WHERE submission.submission_id = ${validated.submissionId}
|
|
3182
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
3183
|
+
|
|
3184
|
+
const decoded = yield* decodeRows(
|
|
3185
|
+
Schema.Array(AbortIntentLookupRow),
|
|
3186
|
+
"effect_agent_abort_intents",
|
|
3187
|
+
validated.submissionId,
|
|
3188
|
+
rows,
|
|
3189
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3190
|
+
|
|
3191
|
+
if (decoded.length === 0) {
|
|
3192
|
+
return yield* LedgerError.make({
|
|
3193
|
+
operation,
|
|
3194
|
+
message: `Unknown submission ${validated.submissionId}.`,
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
if (decoded.length !== 1) {
|
|
3198
|
+
return yield* corruptionFailure(
|
|
3199
|
+
operation,
|
|
3200
|
+
"effect_agent_abort_intents",
|
|
3201
|
+
validated.submissionId,
|
|
3202
|
+
"An abort intent lookup returned more than one row.",
|
|
3203
|
+
);
|
|
3204
|
+
}
|
|
3205
|
+
const row = decoded[0];
|
|
3206
|
+
|
|
3207
|
+
if (row.abort_submission_id === null) return undefined;
|
|
3208
|
+
|
|
3209
|
+
return yield* decodeAbortIntent({
|
|
3210
|
+
submissionId: row.abort_submission_id,
|
|
3211
|
+
author: row.author,
|
|
3212
|
+
reason: row.reason,
|
|
3213
|
+
requestedAt: row.requested_at,
|
|
3214
|
+
...(row.canonical_record_id === null ? {} : { canonicalRecordId: row.canonical_record_id }),
|
|
3215
|
+
}).pipe(
|
|
3216
|
+
Effect.mapError((error) =>
|
|
3217
|
+
corruptionFailure(
|
|
3218
|
+
operation,
|
|
3219
|
+
"effect_agent_abort_intents",
|
|
3220
|
+
validated.submissionId,
|
|
3221
|
+
error.message,
|
|
3222
|
+
),
|
|
3223
|
+
),
|
|
3224
|
+
);
|
|
3225
|
+
});
|
|
2556
3226
|
|
|
2557
3227
|
const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
|
|
2558
3228
|
"SqliteSubmissionLedger.loadRecoverySnapshot",
|
|
2559
3229
|
)(function* (request: RecoverySnapshotRequest) {
|
|
2560
3230
|
const operation = "ledger load recovery snapshot";
|
|
2561
|
-
|
|
3231
|
+
|
|
3232
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
|
|
2562
3233
|
request,
|
|
2563
3234
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2564
|
-
|
|
2565
|
-
|
|
3235
|
+
|
|
3236
|
+
return yield* journal
|
|
3237
|
+
.withReadTransaction(operation)(
|
|
2566
3238
|
Effect.gen(function* () {
|
|
2567
3239
|
const submissionRow = yield* requireSubmission(operation, validated.submissionId);
|
|
2568
3240
|
const submission = yield* decodeSubmissionSnapshot(operation, submissionRow);
|
|
2569
3241
|
|
|
2570
3242
|
let ownership: OwnershipSnapshot | undefined;
|
|
2571
3243
|
const ownershipRow = yield* readOwnership(operation, validated.submissionId);
|
|
3244
|
+
|
|
2572
3245
|
if (Option.isSome(ownershipRow)) {
|
|
2573
3246
|
ownership = yield* decodeOwnershipSnapshot({
|
|
2574
3247
|
attemptId: ownershipRow.value.attempt_id,
|
|
@@ -2579,6 +3252,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2579
3252
|
}
|
|
2580
3253
|
|
|
2581
3254
|
let inputApplied: InputAppliedMarker | undefined;
|
|
3255
|
+
|
|
2582
3256
|
if (
|
|
2583
3257
|
submissionRow.input_applied_record_id !== null &&
|
|
2584
3258
|
submissionRow.input_applied_sequence !== null
|
|
@@ -2591,6 +3265,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2591
3265
|
|
|
2592
3266
|
let reservation: SettlementReservationSnapshot | undefined;
|
|
2593
3267
|
const reservationRow = yield* readReservation(operation, validated.submissionId);
|
|
3268
|
+
|
|
2594
3269
|
if (Option.isSome(reservationRow)) {
|
|
2595
3270
|
const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(
|
|
2596
3271
|
Effect.mapError((error) =>
|
|
@@ -2602,9 +3277,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2602
3277
|
),
|
|
2603
3278
|
),
|
|
2604
3279
|
);
|
|
2605
|
-
|
|
3280
|
+
|
|
3281
|
+
const settlementId = yield* Schema.decodeEffect(
|
|
2606
3282
|
SettlementReservationSnapshot.fields.settlementId,
|
|
2607
3283
|
)(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
|
|
3284
|
+
|
|
2608
3285
|
reservation = SettlementReservationSnapshot.make({
|
|
2609
3286
|
settlementId,
|
|
2610
3287
|
outcome: reservationRow.value.outcome,
|
|
@@ -2616,6 +3293,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2616
3293
|
|
|
2617
3294
|
let abortIntent: AbortIntent | undefined;
|
|
2618
3295
|
const abortRow = yield* readAbortIntent(operation, validated.submissionId);
|
|
3296
|
+
|
|
2619
3297
|
if (Option.isSome(abortRow)) {
|
|
2620
3298
|
abortIntent = yield* abortIntentFromRow(
|
|
2621
3299
|
operation,
|
|
@@ -2633,11 +3311,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2633
3311
|
WHERE joined_host_submission_id = ${validated.submissionId}
|
|
2634
3312
|
ORDER BY queue_sequence ASC
|
|
2635
3313
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
3314
|
+
|
|
2636
3315
|
const joinSubmissions = yield* decodeSubmissionRows(
|
|
2637
3316
|
operation,
|
|
2638
3317
|
validated.submissionId,
|
|
2639
3318
|
joinRows,
|
|
2640
3319
|
);
|
|
3320
|
+
|
|
2641
3321
|
const joins = yield* Effect.forEach(joinSubmissions, (row) =>
|
|
2642
3322
|
decodeJoinSnapshot({
|
|
2643
3323
|
submissionId: row.submission_id,
|
|
@@ -2647,6 +3327,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2647
3327
|
);
|
|
2648
3328
|
|
|
2649
3329
|
let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
|
|
3330
|
+
|
|
2650
3331
|
if (submissionRow.joined_host_submission_id !== null) {
|
|
2651
3332
|
hostSubmissionId = yield* decodeSubmissionId(
|
|
2652
3333
|
submissionRow.joined_host_submission_id,
|
|
@@ -2654,6 +3335,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2654
3335
|
}
|
|
2655
3336
|
|
|
2656
3337
|
let suspension: SuspensionSnapshot | undefined;
|
|
3338
|
+
|
|
2657
3339
|
if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
|
|
2658
3340
|
const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(
|
|
2659
3341
|
Effect.mapError((error) =>
|
|
@@ -2665,6 +3347,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2665
3347
|
),
|
|
2666
3348
|
),
|
|
2667
3349
|
);
|
|
3350
|
+
|
|
2668
3351
|
suspension = yield* decodeSuspensionSnapshot({
|
|
2669
3352
|
reason,
|
|
2670
3353
|
suspendedAt: submissionRow.suspended_at,
|
|
@@ -2681,11 +3364,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2681
3364
|
}
|
|
2682
3365
|
|
|
2683
3366
|
const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
3367
|
+
|
|
2684
3368
|
const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
|
|
2685
3369
|
approvalIntentFromRow(operation, row),
|
|
2686
3370
|
);
|
|
2687
3371
|
|
|
2688
3372
|
const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
3373
|
+
|
|
2689
3374
|
const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
|
|
2690
3375
|
unknownResolutionIntentFromRow(operation, row),
|
|
2691
3376
|
);
|
|
@@ -2699,18 +3384,23 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2699
3384
|
WHERE parent_submission_id = ${validated.submissionId}
|
|
2700
3385
|
ORDER BY parent_tool_call_id ASC
|
|
2701
3386
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
3387
|
+
|
|
2702
3388
|
const decodedChildReservations = yield* decodeChildReservationRows(
|
|
2703
3389
|
operation,
|
|
2704
3390
|
validated.submissionId,
|
|
2705
3391
|
childReservationRows,
|
|
2706
3392
|
);
|
|
3393
|
+
|
|
2707
3394
|
const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
|
|
2708
3395
|
childReservationSnapshotFromRow(operation, row),
|
|
2709
3396
|
);
|
|
3397
|
+
|
|
2710
3398
|
const childAttachments: Array<ChildAttachmentSnapshot> = [];
|
|
3399
|
+
|
|
2711
3400
|
for (const row of decodedChildReservations) {
|
|
2712
3401
|
if (row.child_submission_id === null) continue;
|
|
2713
3402
|
const child = yield* readSubmission(operation, row.child_submission_id);
|
|
3403
|
+
|
|
2714
3404
|
if (Option.isNone(child)) continue;
|
|
2715
3405
|
childAttachments.push(
|
|
2716
3406
|
yield* decodeChildAttachmentSnapshot({
|
|
@@ -2725,6 +3415,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2725
3415
|
}
|
|
2726
3416
|
|
|
2727
3417
|
let parentLinkage: ParentLinkage | undefined;
|
|
3418
|
+
|
|
2728
3419
|
if (
|
|
2729
3420
|
submissionRow.parent_submission_id !== null &&
|
|
2730
3421
|
submissionRow.parent_tool_call_id !== null
|
|
@@ -2752,7 +3443,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2752
3443
|
});
|
|
2753
3444
|
}),
|
|
2754
3445
|
)
|
|
2755
|
-
.pipe(
|
|
3446
|
+
.pipe(
|
|
3447
|
+
Effect.catchTag("SqliteStorageError", (error) =>
|
|
3448
|
+
Effect.fail(internalFailure(operation)(error)),
|
|
3449
|
+
),
|
|
3450
|
+
);
|
|
2756
3451
|
});
|
|
2757
3452
|
|
|
2758
3453
|
return Context.make(
|
|
@@ -2784,6 +3479,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2784
3479
|
releaseChildBudget,
|
|
2785
3480
|
scanNonterminal,
|
|
2786
3481
|
loadRecoverySnapshot,
|
|
3482
|
+
readAbortIntent: readAbortIntentForSubmission,
|
|
2787
3483
|
}),
|
|
2788
3484
|
);
|
|
2789
3485
|
});
|
|
@@ -2801,18 +3497,22 @@ export const submissionLedgerLayer: Layer.Layer<
|
|
|
2801
3497
|
|
|
2802
3498
|
/**
|
|
2803
3499
|
* A composition-root convenience Layer for the durable Submission Ledger. Point it at the
|
|
2804
|
-
* same database file as the
|
|
3500
|
+
* same database file as the ThreadStore so claims fence the same producer epochs.
|
|
2805
3501
|
*/
|
|
2806
3502
|
export const ledgerLayer = (
|
|
2807
3503
|
options: SqliteStorageOptions,
|
|
2808
3504
|
): Layer.Layer<SubmissionLedger, SqliteStorageInitializationError> =>
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
3505
|
+
Layer.unwrap(
|
|
3506
|
+
Effect.map(SqliteStorageConfig, (config) =>
|
|
3507
|
+
submissionLedgerLayer.pipe(
|
|
3508
|
+
Layer.provide(
|
|
3509
|
+
Layer.mergeAll(
|
|
3510
|
+
Layer.succeed(SqliteStorageConfig)(config),
|
|
3511
|
+
storageFailpointLayer(options),
|
|
3512
|
+
SqliteClient.layer({ filename: options.filename }),
|
|
3513
|
+
NodeCrypto.layer,
|
|
3514
|
+
),
|
|
3515
|
+
),
|
|
2816
3516
|
),
|
|
2817
3517
|
),
|
|
2818
|
-
);
|
|
3518
|
+
).pipe(Layer.provide(storageConfigLayer(options)));
|