@effect-agent/storage-sqlite 0.1.0-beta.11 → 0.1.0-beta.111
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 +14 -0
- package/dist/SqliteStorageFailpointTesting.mjs +19 -0
- package/dist/SqliteStorageFailpointTesting.mjs.map +1 -0
- package/dist/SqliteStorageVersion-BO6U_VHq.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 +1803 -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 +464 -0
- package/dist/SqliteThreadStore.mjs.map +1 -0
- package/dist/index.d.mts +11 -193
- package/dist/index.mjs +11 -3082
- package/dist/migrations-BZ89iaTY.mjs +350 -0
- package/dist/migrations-BZ89iaTY.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/sqlite-journal-BCyBc87Y.mjs +1044 -0
- package/dist/sqlite-journal-BCyBc87Y.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} +958 -233
- package/src/SqliteSubscriptionStore.ts +55 -0
- package/src/SqliteThreadStore.ts +1004 -0
- package/src/index.ts +10 -6
- package/src/internal/message-delivery-schema.ts +24 -0
- package/src/internal/migrations.ts +356 -0
- package/src/internal/recovery-checkpoint-schema.ts +17 -0
- package/src/internal/sqlite-journal.ts +1684 -0
- package/dist/index.mjs.map +0 -1
- package/src/migrations.ts +0 -275
- package/src/sqlite-conversation-store.ts +0 -841
- package/src/sqlite-journal.ts +0 -1086
|
@@ -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,20 @@ 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);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
513
|
+
const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
|
|
514
|
+
|
|
515
|
+
if (
|
|
516
|
+
Option.isNone(ownership) ||
|
|
517
|
+
ownership.value.ownership_token !== ownershipToken ||
|
|
518
|
+
ownership.value.producer_epoch !== actualEpoch
|
|
519
|
+
) {
|
|
520
|
+
const submissionId = yield* Schema.decodeEffect(SubmissionSnapshot.fields.submissionId)(
|
|
521
|
+
submission.submission_id,
|
|
522
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
523
|
+
|
|
468
524
|
return yield* OwnershipLost.make({ submissionId, actualEpoch });
|
|
469
525
|
}
|
|
526
|
+
|
|
470
527
|
return ownership.value;
|
|
471
528
|
});
|
|
472
529
|
|
|
@@ -485,6 +542,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
485
542
|
),
|
|
486
543
|
),
|
|
487
544
|
);
|
|
545
|
+
|
|
488
546
|
const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
|
|
489
547
|
Effect.mapError((error) =>
|
|
490
548
|
corruptionFailure(
|
|
@@ -495,6 +553,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
495
553
|
),
|
|
496
554
|
),
|
|
497
555
|
);
|
|
556
|
+
|
|
498
557
|
if ((row.parent_submission_id === null) !== (row.parent_tool_call_id === null)) {
|
|
499
558
|
return yield* corruptionFailure(
|
|
500
559
|
operation,
|
|
@@ -503,9 +562,10 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
503
562
|
"A parent linkage must record both the parent Submission and the parent Tool Call.",
|
|
504
563
|
);
|
|
505
564
|
}
|
|
565
|
+
|
|
506
566
|
return yield* decodeSubmissionSnapshotUnknown({
|
|
507
567
|
submissionId: row.submission_id,
|
|
508
|
-
|
|
568
|
+
threadId: row.thread_id,
|
|
509
569
|
queueSequence: row.queue_sequence,
|
|
510
570
|
principal: row.principal,
|
|
511
571
|
idempotencyKey: row.idempotency_key,
|
|
@@ -517,6 +577,28 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
517
577
|
receiptId: row.receipt_id,
|
|
518
578
|
state: row.state,
|
|
519
579
|
createdAt: row.created_at,
|
|
580
|
+
...(row.admission_group === null ? {} : { admissionGroup: row.admission_group }),
|
|
581
|
+
...(row.worker_admission_json === null
|
|
582
|
+
? {}
|
|
583
|
+
: {
|
|
584
|
+
workerAdmission: yield* parseStoredJsonText(row.worker_admission_json).pipe(
|
|
585
|
+
Effect.mapError(internalFailure(operation)),
|
|
586
|
+
),
|
|
587
|
+
}),
|
|
588
|
+
...(row.message_admission_json === null
|
|
589
|
+
? {}
|
|
590
|
+
: {
|
|
591
|
+
messageAdmission: yield* parseStoredJsonText(row.message_admission_json).pipe(
|
|
592
|
+
Effect.mapError(internalFailure(operation)),
|
|
593
|
+
),
|
|
594
|
+
}),
|
|
595
|
+
...(row.admission_fence_json === null
|
|
596
|
+
? {}
|
|
597
|
+
: {
|
|
598
|
+
admissionFence: yield* parseStoredJsonText(row.admission_fence_json).pipe(
|
|
599
|
+
Effect.mapError(internalFailure(operation)),
|
|
600
|
+
),
|
|
601
|
+
}),
|
|
520
602
|
...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
|
|
521
603
|
...(row.ready_at === null ? {} : { readyAt: row.ready_at }),
|
|
522
604
|
...(row.parent_submission_id === null || row.parent_tool_call_id === null
|
|
@@ -557,12 +639,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
557
639
|
FROM effect_agent_settlement_reservations
|
|
558
640
|
WHERE submission_id = ${submissionId}
|
|
559
641
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
642
|
+
|
|
560
643
|
const decoded = yield* decodeRows(
|
|
561
644
|
Schema.Array(ReservationRow),
|
|
562
645
|
"effect_agent_settlement_reservations",
|
|
563
646
|
submissionId,
|
|
564
647
|
rows,
|
|
565
648
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
649
|
+
|
|
566
650
|
if (decoded.length > 1) {
|
|
567
651
|
return yield* corruptionFailure(
|
|
568
652
|
operation,
|
|
@@ -571,6 +655,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
571
655
|
"A settlement reservation primary key returned more than one row.",
|
|
572
656
|
);
|
|
573
657
|
}
|
|
658
|
+
|
|
574
659
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
575
660
|
});
|
|
576
661
|
|
|
@@ -588,12 +673,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
588
673
|
FROM effect_agent_abort_intents
|
|
589
674
|
WHERE submission_id = ${submissionId}
|
|
590
675
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
676
|
+
|
|
591
677
|
const decoded = yield* decodeRows(
|
|
592
678
|
Schema.Array(AbortIntentRow),
|
|
593
679
|
"effect_agent_abort_intents",
|
|
594
680
|
submissionId,
|
|
595
681
|
rows,
|
|
596
682
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
683
|
+
|
|
597
684
|
if (decoded.length > 1) {
|
|
598
685
|
return yield* corruptionFailure(
|
|
599
686
|
operation,
|
|
@@ -602,6 +689,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
602
689
|
"An abort intent primary key returned more than one row.",
|
|
603
690
|
);
|
|
604
691
|
}
|
|
692
|
+
|
|
605
693
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
606
694
|
});
|
|
607
695
|
|
|
@@ -622,7 +710,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
622
710
|
FROM effect_agent_child_reservations
|
|
623
711
|
WHERE reservation_id = ${reservationId}
|
|
624
712
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
713
|
+
|
|
625
714
|
const decoded = yield* decodeChildReservationRows(operation, reservationId, rows);
|
|
715
|
+
|
|
626
716
|
if (decoded.length > 1) {
|
|
627
717
|
return yield* corruptionFailure(
|
|
628
718
|
operation,
|
|
@@ -631,6 +721,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
631
721
|
"A child reservation primary key returned more than one row.",
|
|
632
722
|
);
|
|
633
723
|
}
|
|
724
|
+
|
|
634
725
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
635
726
|
});
|
|
636
727
|
|
|
@@ -647,11 +738,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
647
738
|
WHERE parent_submission_id = ${parentSubmissionId}
|
|
648
739
|
AND parent_tool_call_id = ${parentToolCallId}
|
|
649
740
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
741
|
+
|
|
650
742
|
const decoded = yield* decodeChildReservationRows(
|
|
651
743
|
operation,
|
|
652
744
|
`${parentSubmissionId}/${parentToolCallId}`,
|
|
653
745
|
rows,
|
|
654
746
|
);
|
|
747
|
+
|
|
655
748
|
if (decoded.length > 1) {
|
|
656
749
|
return yield* corruptionFailure(
|
|
657
750
|
operation,
|
|
@@ -660,6 +753,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
660
753
|
"A parent Tool Call returned more than one child reservation.",
|
|
661
754
|
);
|
|
662
755
|
}
|
|
756
|
+
|
|
663
757
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
664
758
|
});
|
|
665
759
|
|
|
@@ -676,13 +770,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
676
770
|
row.reservation_id,
|
|
677
771
|
error.message,
|
|
678
772
|
);
|
|
773
|
+
|
|
679
774
|
const allocation = yield* parseStoredJsonText(row.allocation_json).pipe(
|
|
680
775
|
Effect.mapError(rowFailure),
|
|
681
776
|
);
|
|
777
|
+
|
|
682
778
|
const accounting =
|
|
683
779
|
row.accounting_json === null
|
|
684
780
|
? undefined
|
|
685
781
|
: yield* parseStoredJsonText(row.accounting_json).pipe(Effect.mapError(rowFailure));
|
|
782
|
+
|
|
686
783
|
return yield* decodeChildReservationSnapshotUnknown({
|
|
687
784
|
reservationId: row.reservation_id,
|
|
688
785
|
parentSubmissionId: row.parent_submission_id,
|
|
@@ -715,6 +812,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
715
812
|
WHERE submission_id = ${submissionId}
|
|
716
813
|
ORDER BY tool_call_id ASC
|
|
717
814
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
815
|
+
|
|
718
816
|
return yield* decodeRows(
|
|
719
817
|
Schema.Array(ApprovalDecisionRow),
|
|
720
818
|
"effect_agent_approval_decisions",
|
|
@@ -766,6 +864,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
766
864
|
WHERE submission_id = ${submissionId}
|
|
767
865
|
ORDER BY tool_call_id ASC
|
|
768
866
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
867
|
+
|
|
769
868
|
return yield* decodeRows(
|
|
770
869
|
Schema.Array(UnknownResolutionRow),
|
|
771
870
|
"effect_agent_unknown_resolutions",
|
|
@@ -791,6 +890,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
791
890
|
),
|
|
792
891
|
),
|
|
793
892
|
);
|
|
893
|
+
|
|
794
894
|
return yield* decodeUnknownResolutionIntent({
|
|
795
895
|
submissionId: row.submission_id,
|
|
796
896
|
toolCallId: row.tool_call_id,
|
|
@@ -817,6 +917,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
817
917
|
submission: SubmissionRow,
|
|
818
918
|
): Effect.fn.Return<ReadonlyArray<typeof ToolCallIdSchema.Type>, LedgerError> {
|
|
819
919
|
if (submission.unknown_tool_call_ids_json === null) return [];
|
|
920
|
+
|
|
820
921
|
return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(
|
|
821
922
|
Effect.mapError((error) =>
|
|
822
923
|
corruptionFailure(
|
|
@@ -838,22 +939,25 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
838
939
|
const canonicalAbortRecordId = Effect.fn("SqliteSubmissionLedger.canonicalAbortRecordId")(
|
|
839
940
|
function* (
|
|
840
941
|
operation: string,
|
|
841
|
-
|
|
942
|
+
threadId: string,
|
|
842
943
|
submissionId: SubmissionId,
|
|
843
944
|
): Effect.fn.Return<string | undefined, LedgerError> {
|
|
844
945
|
const recordId = submissionAbortRecordId(submissionId);
|
|
946
|
+
|
|
845
947
|
const rows = yield* sql<Record<string, unknown>>`
|
|
846
948
|
SELECT record_id
|
|
847
949
|
FROM effect_agent_canonical_records
|
|
848
|
-
WHERE
|
|
950
|
+
WHERE thread_id = ${threadId}
|
|
849
951
|
AND record_id = ${recordId}
|
|
850
952
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
953
|
+
|
|
851
954
|
const decoded = yield* decodeRows(
|
|
852
955
|
Schema.Array(CanonicalRecordIdRow),
|
|
853
956
|
"effect_agent_canonical_records",
|
|
854
|
-
`${
|
|
957
|
+
`${threadId}/${recordId}`,
|
|
855
958
|
rows,
|
|
856
959
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
960
|
+
|
|
857
961
|
return decoded.length === 0 ? undefined : recordId;
|
|
858
962
|
},
|
|
859
963
|
);
|
|
@@ -866,9 +970,10 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
866
970
|
): Effect.fn.Return<AbortIntent, LedgerError> {
|
|
867
971
|
const canonicalRecordId = yield* canonicalAbortRecordId(
|
|
868
972
|
operation,
|
|
869
|
-
submission.
|
|
973
|
+
submission.thread_id,
|
|
870
974
|
submissionId,
|
|
871
975
|
);
|
|
976
|
+
|
|
872
977
|
return yield* decodeAbortIntent({
|
|
873
978
|
submissionId: row.submission_id,
|
|
874
979
|
author: row.author,
|
|
@@ -892,30 +997,73 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
892
997
|
const admit: SubmissionLedger["Service"]["admit"] = Effect.fn("SqliteSubmissionLedger.admit")(
|
|
893
998
|
function* (request: AdmissionRequest) {
|
|
894
999
|
const operation = "ledger admit";
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1000
|
+
|
|
1001
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AdmissionRequest))(request).pipe(
|
|
1002
|
+
Effect.mapError(internalFailure(operation)),
|
|
1003
|
+
);
|
|
1004
|
+
|
|
898
1005
|
const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(
|
|
899
1006
|
Effect.mapError(internalFailure(operation)),
|
|
900
1007
|
);
|
|
1008
|
+
|
|
1009
|
+
const workerAdmissionJson =
|
|
1010
|
+
validated.workerAdmission === undefined
|
|
1011
|
+
? null
|
|
1012
|
+
: yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1013
|
+
validated.workerAdmission,
|
|
1014
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1015
|
+
|
|
1016
|
+
if (
|
|
1017
|
+
workerAdmissionJson !== null &&
|
|
1018
|
+
new TextEncoder().encode(workerAdmissionJson).byteLength > 16 * 1024 * 1024
|
|
1019
|
+
) {
|
|
1020
|
+
return yield* LedgerError.make({
|
|
1021
|
+
operation,
|
|
1022
|
+
message: "Worker admission metadata exceeds the stored value bound",
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const messageAdmissionJson =
|
|
1027
|
+
validated.messageAdmission === undefined
|
|
1028
|
+
? null
|
|
1029
|
+
: yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
|
|
1030
|
+
validated.messageAdmission,
|
|
1031
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1032
|
+
|
|
1033
|
+
if (
|
|
1034
|
+
messageAdmissionJson !== null &&
|
|
1035
|
+
new TextEncoder().encode(messageAdmissionJson).byteLength > 16 * 1024 * 1024
|
|
1036
|
+
) {
|
|
1037
|
+
return yield* LedgerError.make({
|
|
1038
|
+
operation,
|
|
1039
|
+
message: "Message admission metadata exceeds the stored value bound",
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
901
1043
|
const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
|
|
902
1044
|
Effect.mapError(internalFailure(operation)),
|
|
903
1045
|
);
|
|
1046
|
+
|
|
904
1047
|
const mintedSubmissionId = yield* mintIdentifier("submission", operation);
|
|
905
1048
|
const mintedReceiptId = yield* mintIdentifier("receipt", operation);
|
|
1049
|
+
|
|
906
1050
|
yield* hitFailpoint("ledger:admit:before", operation);
|
|
1051
|
+
|
|
907
1052
|
const result = yield* inWriteTransaction(
|
|
908
1053
|
operation,
|
|
909
1054
|
Effect.gen(function* () {
|
|
910
|
-
const keyRowKey = `${validated.
|
|
1055
|
+
const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
|
|
1056
|
+
|
|
911
1057
|
const existingRows = yield* sql<Record<string, unknown>>`
|
|
912
1058
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
913
1059
|
FROM effect_agent_submissions
|
|
914
|
-
WHERE
|
|
1060
|
+
WHERE thread_id = ${validated.threadId}
|
|
915
1061
|
AND principal = ${validated.principal}
|
|
916
1062
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
917
1063
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1064
|
+
|
|
918
1065
|
const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
|
|
1066
|
+
|
|
919
1067
|
if (existing.length > 1) {
|
|
920
1068
|
return yield* corruptionFailure(
|
|
921
1069
|
operation,
|
|
@@ -933,15 +1081,61 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
933
1081
|
existing[0].parent_tool_call_id === null
|
|
934
1082
|
: existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId &&
|
|
935
1083
|
existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
|
|
1084
|
+
|
|
936
1085
|
if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
|
|
937
1086
|
return yield* AdmissionConflict.make({
|
|
938
|
-
|
|
1087
|
+
threadId: validated.threadId,
|
|
939
1088
|
principal: validated.principal,
|
|
940
1089
|
idempotencyKey: validated.idempotencyKey,
|
|
941
1090
|
existingInputDigest: existing[0].input_digest,
|
|
942
1091
|
attemptedInputDigest: validated.inputDigest,
|
|
943
1092
|
});
|
|
944
1093
|
}
|
|
1094
|
+
|
|
1095
|
+
const retainedWorkerAdmission =
|
|
1096
|
+
existing[0].worker_admission_json === null
|
|
1097
|
+
? undefined
|
|
1098
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1099
|
+
existing[0].worker_admission_json,
|
|
1100
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1101
|
+
|
|
1102
|
+
const retainedInputMessage =
|
|
1103
|
+
existing[0].message_admission_json === null
|
|
1104
|
+
? undefined
|
|
1105
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(InputMessage))(
|
|
1106
|
+
existing[0].message_admission_json,
|
|
1107
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1108
|
+
|
|
1109
|
+
const retainedFence =
|
|
1110
|
+
existing[0].admission_fence_json === null
|
|
1111
|
+
? undefined
|
|
1112
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(AdmissionFence))(
|
|
1113
|
+
existing[0].admission_fence_json,
|
|
1114
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1115
|
+
|
|
1116
|
+
if (
|
|
1117
|
+
(existing[0].admission_group ?? undefined) !== validated.admissionGroup ||
|
|
1118
|
+
!Schema.toEquivalence(Schema.optional(WorkerAdmission))(
|
|
1119
|
+
retainedWorkerAdmission,
|
|
1120
|
+
validated.workerAdmission,
|
|
1121
|
+
) ||
|
|
1122
|
+
!Schema.toEquivalence(Schema.optional(InputMessage))(
|
|
1123
|
+
retainedInputMessage,
|
|
1124
|
+
validated.messageAdmission,
|
|
1125
|
+
) ||
|
|
1126
|
+
!Schema.toEquivalence(Schema.optional(AdmissionFence))(
|
|
1127
|
+
retainedFence,
|
|
1128
|
+
validated.admissionFence,
|
|
1129
|
+
)
|
|
1130
|
+
)
|
|
1131
|
+
return yield* AdmissionConflict.make({
|
|
1132
|
+
threadId: validated.threadId,
|
|
1133
|
+
principal: validated.principal,
|
|
1134
|
+
idempotencyKey: validated.idempotencyKey,
|
|
1135
|
+
existingInputDigest: existing[0].input_digest,
|
|
1136
|
+
attemptedInputDigest: validated.inputDigest,
|
|
1137
|
+
});
|
|
1138
|
+
|
|
945
1139
|
return yield* decodeAdmissionResult({
|
|
946
1140
|
submissionId: existing[0].submission_id,
|
|
947
1141
|
receiptId: existing[0].receipt_id,
|
|
@@ -951,26 +1145,78 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
951
1145
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
952
1146
|
}
|
|
953
1147
|
|
|
1148
|
+
// The first accepted input fixes ordinary/worker lane identity atomically with admission.
|
|
1149
|
+
// Canonical origin materialization can lag admission; a log scan cannot fence that race.
|
|
1150
|
+
const firstRows = yield* sql<Record<string, unknown>>`
|
|
1151
|
+
SELECT worker_admission_json FROM effect_agent_submissions
|
|
1152
|
+
WHERE thread_id=${validated.threadId} ORDER BY queue_sequence LIMIT 1
|
|
1153
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1154
|
+
|
|
1155
|
+
const first = yield* Schema.decodeUnknownEffect(
|
|
1156
|
+
Schema.Array(
|
|
1157
|
+
Schema.Struct({
|
|
1158
|
+
worker_admission_json: Schema.NullOr(BoundedStoredText),
|
|
1159
|
+
}),
|
|
1160
|
+
),
|
|
1161
|
+
)(firstRows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1162
|
+
|
|
1163
|
+
if (first[0] !== undefined) {
|
|
1164
|
+
const previous =
|
|
1165
|
+
first[0].worker_admission_json === null
|
|
1166
|
+
? undefined
|
|
1167
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1168
|
+
first[0].worker_admission_json,
|
|
1169
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1170
|
+
|
|
1171
|
+
if (
|
|
1172
|
+
!Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
|
|
1173
|
+
previous?.origin,
|
|
1174
|
+
validated.workerAdmission?.origin,
|
|
1175
|
+
)
|
|
1176
|
+
)
|
|
1177
|
+
return yield* AdmissionPolicyError.make({
|
|
1178
|
+
reason: "refused",
|
|
1179
|
+
code: "worker-origin-conflict",
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
yield* admissionFence.check(validated);
|
|
1184
|
+
if (validated.admissionGroup !== undefined) {
|
|
1185
|
+
const occupied = yield* sql<Record<string, unknown>>`
|
|
1186
|
+
SELECT submission_id FROM effect_agent_submissions
|
|
1187
|
+
WHERE thread_id=${validated.threadId} AND admission_group=${validated.admissionGroup} AND state<>'settled' LIMIT 1
|
|
1188
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1189
|
+
|
|
1190
|
+
if (occupied.length > 0)
|
|
1191
|
+
return yield* AdmissionPolicyError.make({
|
|
1192
|
+
reason: "occupied",
|
|
1193
|
+
code: "admission-group",
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
|
|
954
1197
|
const maxRows = yield* sql<Record<string, unknown>>`
|
|
955
1198
|
SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
|
|
956
1199
|
FROM effect_agent_submissions
|
|
957
|
-
WHERE
|
|
1200
|
+
WHERE thread_id = ${validated.threadId}
|
|
958
1201
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1202
|
+
|
|
959
1203
|
const decodedMax = yield* decodeRows(
|
|
960
1204
|
Schema.Array(MaxQueueSequenceRow),
|
|
961
1205
|
"effect_agent_submissions",
|
|
962
|
-
validated.
|
|
1206
|
+
validated.threadId,
|
|
963
1207
|
maxRows,
|
|
964
1208
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1209
|
+
|
|
965
1210
|
const queueSequence = yield* decodeQueueSequence(
|
|
966
1211
|
(decodedMax[0]?.max_queue_sequence ?? 0) + 1,
|
|
967
1212
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1213
|
+
|
|
968
1214
|
const now = yield* currentInstant;
|
|
969
1215
|
|
|
970
1216
|
yield* sql`
|
|
971
1217
|
INSERT INTO effect_agent_submissions (
|
|
972
1218
|
submission_id,
|
|
973
|
-
|
|
1219
|
+
thread_id,
|
|
974
1220
|
queue_sequence,
|
|
975
1221
|
principal,
|
|
976
1222
|
idempotency_key,
|
|
@@ -983,10 +1229,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
983
1229
|
state,
|
|
984
1230
|
created_at,
|
|
985
1231
|
parent_submission_id,
|
|
986
|
-
parent_tool_call_id
|
|
1232
|
+
parent_tool_call_id,
|
|
1233
|
+
admission_group,
|
|
1234
|
+
admission_fence_json,
|
|
1235
|
+
worker_admission_json,
|
|
1236
|
+
message_admission_json
|
|
987
1237
|
) VALUES (
|
|
988
1238
|
${mintedSubmissionId},
|
|
989
|
-
${validated.
|
|
1239
|
+
${validated.threadId},
|
|
990
1240
|
${queueSequence},
|
|
991
1241
|
${validated.principal},
|
|
992
1242
|
${validated.idempotencyKey},
|
|
@@ -999,7 +1249,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
999
1249
|
'admitted',
|
|
1000
1250
|
${now.iso},
|
|
1001
1251
|
${validated.parentLinkage?.parentSubmissionId ?? null},
|
|
1002
|
-
${validated.parentLinkage?.parentToolCallId ?? null}
|
|
1252
|
+
${validated.parentLinkage?.parentToolCallId ?? null},
|
|
1253
|
+
${validated.admissionGroup ?? null},
|
|
1254
|
+
${validated.admissionFence === undefined ? null : JSON.stringify(validated.admissionFence)},
|
|
1255
|
+
${workerAdmissionJson},
|
|
1256
|
+
${messageAdmissionJson}
|
|
1003
1257
|
)
|
|
1004
1258
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1005
1259
|
|
|
@@ -1012,7 +1266,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1012
1266
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1013
1267
|
}),
|
|
1014
1268
|
);
|
|
1269
|
+
|
|
1015
1270
|
yield* hitFailpoint("ledger:admit:after", operation);
|
|
1271
|
+
|
|
1016
1272
|
return result;
|
|
1017
1273
|
},
|
|
1018
1274
|
);
|
|
@@ -1021,16 +1277,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1021
1277
|
"SqliteSubmissionLedger.markReady",
|
|
1022
1278
|
)(function* (request: MarkReadyRequest) {
|
|
1023
1279
|
const operation = "ledger mark ready";
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1280
|
+
|
|
1281
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkReadyRequest))(request).pipe(
|
|
1282
|
+
Effect.mapError(internalFailure(operation)),
|
|
1283
|
+
);
|
|
1284
|
+
|
|
1027
1285
|
yield* hitFailpoint("ledger:mark-ready:before", operation);
|
|
1028
1286
|
yield* inWriteTransaction(
|
|
1029
1287
|
operation,
|
|
1030
1288
|
Effect.gen(function* () {
|
|
1031
1289
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1290
|
+
|
|
1032
1291
|
if (submission.state !== "admitted") return;
|
|
1033
1292
|
const now = yield* currentInstant;
|
|
1293
|
+
|
|
1034
1294
|
yield* sql`
|
|
1035
1295
|
UPDATE effect_agent_submissions
|
|
1036
1296
|
SET state = 'ready', ready_at = ${now.iso}
|
|
@@ -1044,35 +1304,43 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1044
1304
|
const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fn("SqliteSubmissionLedger.lookup")(
|
|
1045
1305
|
function* (request: SubmissionLookup) {
|
|
1046
1306
|
const operation = "ledger lookup";
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1307
|
+
|
|
1308
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookup))(request).pipe(
|
|
1309
|
+
Effect.mapError(internalFailure(operation)),
|
|
1310
|
+
);
|
|
1311
|
+
|
|
1050
1312
|
if (validated._tag === "SubmissionLookupById") {
|
|
1051
1313
|
const row = yield* readSubmission(operation, validated.submissionId);
|
|
1314
|
+
|
|
1052
1315
|
if (Option.isNone(row)) return Option.none();
|
|
1316
|
+
|
|
1053
1317
|
return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
|
|
1054
1318
|
}
|
|
1319
|
+
|
|
1055
1320
|
const rows = yield* sql<Record<string, unknown>>`
|
|
1056
1321
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1057
1322
|
FROM effect_agent_submissions
|
|
1058
|
-
WHERE
|
|
1323
|
+
WHERE thread_id = ${validated.threadId}
|
|
1059
1324
|
AND principal = ${validated.principal}
|
|
1060
1325
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
1061
1326
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1327
|
+
|
|
1062
1328
|
const decoded = yield* decodeSubmissionRows(
|
|
1063
1329
|
operation,
|
|
1064
|
-
`${validated.
|
|
1330
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1065
1331
|
rows,
|
|
1066
1332
|
);
|
|
1333
|
+
|
|
1067
1334
|
if (decoded.length > 1) {
|
|
1068
1335
|
return yield* corruptionFailure(
|
|
1069
1336
|
operation,
|
|
1070
1337
|
"effect_agent_submissions",
|
|
1071
|
-
`${validated.
|
|
1338
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1072
1339
|
"An admission idempotency key returned more than one row.",
|
|
1073
1340
|
);
|
|
1074
1341
|
}
|
|
1075
1342
|
if (decoded.length === 0) return Option.none();
|
|
1343
|
+
|
|
1076
1344
|
return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
|
|
1077
1345
|
},
|
|
1078
1346
|
);
|
|
@@ -1084,30 +1352,35 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1084
1352
|
"SqliteSubmissionLedger.resolveAdmission",
|
|
1085
1353
|
)(function* (request: SubmissionLookupByKey) {
|
|
1086
1354
|
const operation = "ledger resolve admission";
|
|
1087
|
-
|
|
1355
|
+
|
|
1356
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookupByKey))(
|
|
1088
1357
|
request,
|
|
1089
1358
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1359
|
+
|
|
1090
1360
|
const rows = yield* sql<Record<string, unknown>>`
|
|
1091
1361
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1092
1362
|
FROM effect_agent_submissions
|
|
1093
|
-
WHERE
|
|
1363
|
+
WHERE thread_id = ${validated.threadId}
|
|
1094
1364
|
AND principal = ${validated.principal}
|
|
1095
1365
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
1096
1366
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1367
|
+
|
|
1097
1368
|
const decoded = yield* decodeSubmissionRows(
|
|
1098
1369
|
operation,
|
|
1099
|
-
`${validated.
|
|
1370
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1100
1371
|
rows,
|
|
1101
1372
|
);
|
|
1373
|
+
|
|
1102
1374
|
if (decoded.length > 1) {
|
|
1103
1375
|
return yield* corruptionFailure(
|
|
1104
1376
|
operation,
|
|
1105
1377
|
"effect_agent_submissions",
|
|
1106
|
-
`${validated.
|
|
1378
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1107
1379
|
"An admission idempotency key returned more than one row.",
|
|
1108
1380
|
);
|
|
1109
1381
|
}
|
|
1110
1382
|
if (decoded.length === 0) return AdmissionNotAdmitted.make();
|
|
1383
|
+
|
|
1111
1384
|
return AdmissionAdmitted.make({
|
|
1112
1385
|
submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
|
|
1113
1386
|
});
|
|
@@ -1116,70 +1389,103 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1116
1389
|
const claim: SubmissionLedger["Service"]["claim"] = Effect.fn("SqliteSubmissionLedger.claim")(
|
|
1117
1390
|
function* (request: ClaimRequest) {
|
|
1118
1391
|
const operation = "ledger claim";
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1392
|
+
|
|
1393
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
|
|
1394
|
+
Effect.mapError(internalFailure(operation)),
|
|
1395
|
+
);
|
|
1396
|
+
|
|
1122
1397
|
const attemptId = yield* mintIdentifier("attempt", operation);
|
|
1123
1398
|
const ownershipToken = yield* mintIdentifier("owner", operation);
|
|
1399
|
+
|
|
1124
1400
|
yield* hitFailpoint("ledger:claim:before", operation);
|
|
1401
|
+
|
|
1125
1402
|
const claimed = yield* inWriteTransaction(
|
|
1126
1403
|
operation,
|
|
1127
1404
|
Effect.gen(function* () {
|
|
1405
|
+
const now = yield* currentInstant;
|
|
1406
|
+
|
|
1407
|
+
// Ownership belongs to the whole Thread, including unknown work skipped below.
|
|
1408
|
+
// Stored lease instants are normalized UTC strings, so the latest expiry covers
|
|
1409
|
+
// every live lease. This check and the epoch grant share one write transaction.
|
|
1410
|
+
const ownershipRows = yield* sql<Record<string, unknown>>`
|
|
1411
|
+
SELECT ownership.*
|
|
1412
|
+
FROM effect_agent_submission_ownership AS ownership
|
|
1413
|
+
JOIN effect_agent_submissions AS submission
|
|
1414
|
+
ON submission.submission_id = ownership.submission_id
|
|
1415
|
+
WHERE submission.thread_id = ${validated.threadId}
|
|
1416
|
+
ORDER BY ownership.lease_expires_at DESC
|
|
1417
|
+
LIMIT 1
|
|
1418
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1419
|
+
|
|
1420
|
+
const ownership = yield* decodeRows(
|
|
1421
|
+
Schema.Array(OwnershipRow),
|
|
1422
|
+
"effect_agent_submission_ownership",
|
|
1423
|
+
validated.threadId,
|
|
1424
|
+
ownershipRows,
|
|
1425
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1426
|
+
|
|
1427
|
+
if (ownership.length > 0) {
|
|
1428
|
+
const expiresAt = yield* timestampMillis(
|
|
1429
|
+
operation,
|
|
1430
|
+
ownership[0].submission_id,
|
|
1431
|
+
)(ownership[0].lease_expires_at);
|
|
1432
|
+
|
|
1433
|
+
if (expiresAt > now.millis) return Option.none<Claim>();
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1128
1436
|
const headRows = yield* sql<Record<string, unknown>>`
|
|
1129
1437
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1130
1438
|
FROM effect_agent_submissions
|
|
1131
|
-
WHERE
|
|
1439
|
+
WHERE thread_id = ${validated.threadId}
|
|
1132
1440
|
AND state <> 'settled'
|
|
1441
|
+
AND (
|
|
1442
|
+
state <> 'unknown' OR EXISTS (
|
|
1443
|
+
SELECT 1 FROM effect_agent_abort_intents
|
|
1444
|
+
WHERE submission_id = effect_agent_submissions.submission_id
|
|
1445
|
+
)
|
|
1446
|
+
)
|
|
1133
1447
|
ORDER BY queue_sequence ASC
|
|
1134
1448
|
LIMIT 1
|
|
1135
1449
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1136
|
-
|
|
1450
|
+
|
|
1451
|
+
const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
|
|
1452
|
+
|
|
1137
1453
|
if (heads.length === 0) return Option.none<Claim>();
|
|
1138
1454
|
const head = heads[0];
|
|
1139
1455
|
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
// skipped past the blocked head (DUR-004).
|
|
1456
|
+
// Approval/delegation suspension and joined work retain their queue barrier.
|
|
1457
|
+
// Unknown work with an abort intent is selected for cleanup without Tool replay.
|
|
1143
1458
|
if (
|
|
1144
1459
|
head.state === "joining" ||
|
|
1145
1460
|
head.state === "joined" ||
|
|
1146
1461
|
head.state === "suspended" ||
|
|
1147
|
-
head.state === "unknown"
|
|
1462
|
+
(head.state === "unknown" &&
|
|
1463
|
+
Option.isNone(yield* readAbortIntent(operation, head.submission_id)))
|
|
1148
1464
|
) {
|
|
1149
1465
|
return Option.none<Claim>();
|
|
1150
1466
|
}
|
|
1151
1467
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
if (Option.isSome(ownership)) {
|
|
1155
|
-
const expiresAt = yield* timestampMillis(
|
|
1156
|
-
operation,
|
|
1157
|
-
head.submission_id,
|
|
1158
|
-
)(ownership.value.lease_expires_at);
|
|
1159
|
-
// A live lease blocks every new claim; expiry alone only revokes the liveness
|
|
1160
|
-
// assumption — correctness stays with producer-epoch fencing (D5).
|
|
1161
|
-
if (expiresAt > now.millis) return Option.none<Claim>();
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// Bump the Conversation's producer epoch atomically with the claim so every stale
|
|
1165
|
-
// Attempt is fenced out of canonical appends (DUR-006). A Conversation that was
|
|
1468
|
+
// Bump the Thread's producer epoch atomically with the claim so every stale
|
|
1469
|
+
// Attempt is fenced out of canonical appends (DUR-006). A Thread that was
|
|
1166
1470
|
// never materialized (crash between admission and materialization) is created here
|
|
1167
1471
|
// so recovery can claim first and re-materialize idempotently at this epoch.
|
|
1168
|
-
const
|
|
1169
|
-
.
|
|
1472
|
+
const threads = yield* journal
|
|
1473
|
+
.getThread(head.thread_id)
|
|
1170
1474
|
.pipe(Effect.mapError(internalFailure(operation)));
|
|
1475
|
+
|
|
1171
1476
|
let producerEpoch: number;
|
|
1172
|
-
|
|
1477
|
+
|
|
1478
|
+
if (threads.length === 0) {
|
|
1173
1479
|
producerEpoch = 1;
|
|
1174
1480
|
yield* sql`
|
|
1175
|
-
INSERT INTO
|
|
1176
|
-
|
|
1481
|
+
INSERT INTO effect_agent_threads (
|
|
1482
|
+
thread_id,
|
|
1177
1483
|
created_at,
|
|
1178
1484
|
tail_sequence,
|
|
1179
1485
|
tail_digest,
|
|
1180
1486
|
producer_epoch
|
|
1181
1487
|
) VALUES (
|
|
1182
|
-
${head.
|
|
1488
|
+
${head.thread_id},
|
|
1183
1489
|
${now.iso},
|
|
1184
1490
|
0,
|
|
1185
1491
|
${EMPTY_TAIL_DIGEST},
|
|
@@ -1187,15 +1493,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1187
1493
|
)
|
|
1188
1494
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1189
1495
|
} else {
|
|
1190
|
-
producerEpoch =
|
|
1496
|
+
producerEpoch = threads[0].producer_epoch + 1;
|
|
1191
1497
|
yield* sql`
|
|
1192
|
-
UPDATE
|
|
1498
|
+
UPDATE effect_agent_threads
|
|
1193
1499
|
SET producer_epoch = ${producerEpoch}
|
|
1194
|
-
WHERE
|
|
1500
|
+
WHERE thread_id = ${head.thread_id}
|
|
1195
1501
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1196
1502
|
}
|
|
1197
1503
|
|
|
1198
1504
|
const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
|
|
1505
|
+
|
|
1199
1506
|
yield* sql`
|
|
1200
1507
|
INSERT INTO effect_agent_submission_ownership (
|
|
1201
1508
|
submission_id,
|
|
@@ -1224,14 +1531,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1224
1531
|
INSERT INTO effect_agent_attempts (
|
|
1225
1532
|
attempt_id,
|
|
1226
1533
|
submission_id,
|
|
1227
|
-
|
|
1534
|
+
thread_id,
|
|
1228
1535
|
owner_producer_id,
|
|
1229
1536
|
producer_epoch,
|
|
1230
1537
|
claimed_at
|
|
1231
1538
|
) VALUES (
|
|
1232
1539
|
${attemptId},
|
|
1233
1540
|
${head.submission_id},
|
|
1234
|
-
${head.
|
|
1541
|
+
${head.thread_id},
|
|
1235
1542
|
${validated.producerId},
|
|
1236
1543
|
${producerEpoch},
|
|
1237
1544
|
${now.iso}
|
|
@@ -1256,6 +1563,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1256
1563
|
),
|
|
1257
1564
|
),
|
|
1258
1565
|
);
|
|
1566
|
+
|
|
1259
1567
|
return Option.some(
|
|
1260
1568
|
yield* decodeClaim({
|
|
1261
1569
|
submissionId: head.submission_id,
|
|
@@ -1268,7 +1576,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1268
1576
|
);
|
|
1269
1577
|
}),
|
|
1270
1578
|
);
|
|
1579
|
+
|
|
1271
1580
|
yield* hitFailpoint("ledger:claim:after", operation);
|
|
1581
|
+
|
|
1272
1582
|
return claimed;
|
|
1273
1583
|
},
|
|
1274
1584
|
);
|
|
@@ -1277,29 +1587,37 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1277
1587
|
"SqliteSubmissionLedger.renewOwnership",
|
|
1278
1588
|
)(function* (request: RenewOwnershipRequest) {
|
|
1279
1589
|
const operation = "ledger renew ownership";
|
|
1280
|
-
|
|
1590
|
+
|
|
1591
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(RenewOwnershipRequest))(
|
|
1281
1592
|
request,
|
|
1282
1593
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1594
|
+
|
|
1283
1595
|
yield* hitFailpoint("ledger:renew:before", operation);
|
|
1596
|
+
|
|
1284
1597
|
const renewal = yield* inWriteTransaction(
|
|
1285
1598
|
operation,
|
|
1286
1599
|
Effect.gen(function* () {
|
|
1287
1600
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1601
|
+
|
|
1288
1602
|
yield* requireOwnership(operation, submission, validated.ownershipToken);
|
|
1289
1603
|
const now = yield* currentInstant;
|
|
1290
1604
|
const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
|
|
1605
|
+
|
|
1291
1606
|
yield* sql`
|
|
1292
1607
|
UPDATE effect_agent_submission_ownership
|
|
1293
1608
|
SET lease_expires_at = ${leaseExpiresAt}
|
|
1294
1609
|
WHERE submission_id = ${validated.submissionId}
|
|
1295
1610
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1611
|
+
|
|
1296
1612
|
return yield* decodeOwnershipRenewal({
|
|
1297
1613
|
ownershipToken: validated.ownershipToken,
|
|
1298
1614
|
leaseExpiresAt,
|
|
1299
1615
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1300
1616
|
}),
|
|
1301
1617
|
);
|
|
1618
|
+
|
|
1302
1619
|
yield* hitFailpoint("ledger:renew:after", operation);
|
|
1620
|
+
|
|
1303
1621
|
return renewal;
|
|
1304
1622
|
});
|
|
1305
1623
|
|
|
@@ -1307,14 +1625,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1307
1625
|
"SqliteSubmissionLedger.releaseOwnership",
|
|
1308
1626
|
)(function* (request: ReleaseOwnershipRequest) {
|
|
1309
1627
|
const operation = "ledger release ownership";
|
|
1310
|
-
|
|
1628
|
+
|
|
1629
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseOwnershipRequest))(
|
|
1311
1630
|
request,
|
|
1312
1631
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1632
|
+
|
|
1313
1633
|
yield* hitFailpoint("ledger:release:before", operation);
|
|
1314
1634
|
yield* inWriteTransaction(
|
|
1315
1635
|
operation,
|
|
1316
1636
|
Effect.gen(function* () {
|
|
1317
1637
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1638
|
+
|
|
1318
1639
|
yield* requireOwnership(operation, submission, validated.ownershipToken);
|
|
1319
1640
|
yield* sql`
|
|
1320
1641
|
DELETE FROM effect_agent_submission_ownership
|
|
@@ -1336,14 +1657,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1336
1657
|
"SqliteSubmissionLedger.markInputApplied",
|
|
1337
1658
|
)(function* (request: MarkInputAppliedRequest) {
|
|
1338
1659
|
const operation = "ledger mark input applied";
|
|
1339
|
-
|
|
1660
|
+
|
|
1661
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
|
|
1340
1662
|
request,
|
|
1341
1663
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1664
|
+
|
|
1342
1665
|
yield* hitFailpoint("ledger:mark-input-applied:before", operation);
|
|
1343
1666
|
yield* inWriteTransaction(
|
|
1344
1667
|
operation,
|
|
1345
1668
|
Effect.gen(function* () {
|
|
1346
1669
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1670
|
+
|
|
1347
1671
|
yield* requireOwnership(operation, submission, validated.ownershipToken);
|
|
1348
1672
|
if (submission.input_applied_record_id !== null) {
|
|
1349
1673
|
if (
|
|
@@ -1352,6 +1676,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1352
1676
|
) {
|
|
1353
1677
|
return;
|
|
1354
1678
|
}
|
|
1679
|
+
|
|
1355
1680
|
return yield* corruptionFailure(
|
|
1356
1681
|
operation,
|
|
1357
1682
|
"effect_agent_submissions",
|
|
@@ -1379,29 +1704,36 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1379
1704
|
"SqliteSubmissionLedger.reserveSettlement",
|
|
1380
1705
|
)(function* (request: SettlementReservation) {
|
|
1381
1706
|
const operation = "ledger reserve settlement";
|
|
1382
|
-
|
|
1707
|
+
|
|
1708
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SettlementReservation))(
|
|
1383
1709
|
request,
|
|
1384
1710
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1711
|
+
|
|
1385
1712
|
const recordJson = yield* encodeRecordEnvelopeText(validated.record).pipe(
|
|
1386
1713
|
Effect.mapError(internalFailure(operation)),
|
|
1387
1714
|
);
|
|
1715
|
+
|
|
1388
1716
|
yield* hitFailpoint("ledger:reserve-settlement:before", operation);
|
|
1717
|
+
|
|
1389
1718
|
const reserved = yield* inWriteTransaction(
|
|
1390
1719
|
operation,
|
|
1391
1720
|
Effect.gen(function* () {
|
|
1392
1721
|
const existing = yield* readReservation(operation, validated.submissionId);
|
|
1722
|
+
|
|
1393
1723
|
if (Option.isSome(existing)) {
|
|
1394
1724
|
const identical =
|
|
1395
1725
|
existing.value.settlement_id === validated.settlementId &&
|
|
1396
1726
|
existing.value.outcome === validated.outcome &&
|
|
1397
1727
|
existing.value.record_digest === validated.recordDigest &&
|
|
1398
1728
|
existing.value.record_json === recordJson;
|
|
1729
|
+
|
|
1399
1730
|
if (!identical) {
|
|
1400
1731
|
return yield* SettlementConflict.make({
|
|
1401
1732
|
submissionId: validated.submissionId,
|
|
1402
1733
|
existingOutcome: existing.value.outcome,
|
|
1403
1734
|
});
|
|
1404
1735
|
}
|
|
1736
|
+
|
|
1405
1737
|
const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(
|
|
1406
1738
|
Effect.mapError((error) =>
|
|
1407
1739
|
corruptionFailure(
|
|
@@ -1412,6 +1744,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1412
1744
|
),
|
|
1413
1745
|
),
|
|
1414
1746
|
);
|
|
1747
|
+
|
|
1415
1748
|
return ReservedSettlement.make({
|
|
1416
1749
|
submissionId: validated.submissionId,
|
|
1417
1750
|
settlementId: validated.settlementId,
|
|
@@ -1423,6 +1756,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1423
1756
|
}
|
|
1424
1757
|
|
|
1425
1758
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1759
|
+
|
|
1426
1760
|
if (submission.state === "settled") {
|
|
1427
1761
|
if (submission.settled_outcome === null) {
|
|
1428
1762
|
return yield* corruptionFailure(
|
|
@@ -1432,6 +1766,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1432
1766
|
"A settled Submission carries no terminal outcome.",
|
|
1433
1767
|
);
|
|
1434
1768
|
}
|
|
1769
|
+
|
|
1435
1770
|
return yield* SettlementConflict.make({
|
|
1436
1771
|
submissionId: validated.submissionId,
|
|
1437
1772
|
existingOutcome: submission.settled_outcome,
|
|
@@ -1446,13 +1781,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1446
1781
|
// ABORTED settlement (`terminalizing` is the same pass's crash replay). Every other
|
|
1447
1782
|
// reservation stays fenced by the target lane's live ownership.
|
|
1448
1783
|
let queuedAbortSettlement = false;
|
|
1784
|
+
|
|
1449
1785
|
if (
|
|
1450
1786
|
validated.outcome === "aborted" &&
|
|
1451
1787
|
(submission.state === "ready" || submission.state === "terminalizing")
|
|
1452
1788
|
) {
|
|
1453
1789
|
const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
|
|
1790
|
+
|
|
1454
1791
|
if (Option.isSome(abortIntent)) {
|
|
1455
1792
|
const ownership = yield* readOwnership(operation, validated.submissionId);
|
|
1793
|
+
|
|
1456
1794
|
queuedAbortSettlement = Option.isNone(ownership);
|
|
1457
1795
|
}
|
|
1458
1796
|
}
|
|
@@ -1461,6 +1799,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1461
1799
|
}
|
|
1462
1800
|
}
|
|
1463
1801
|
const now = yield* currentInstant;
|
|
1802
|
+
|
|
1464
1803
|
yield* sql`
|
|
1465
1804
|
INSERT INTO effect_agent_settlement_reservations (
|
|
1466
1805
|
submission_id,
|
|
@@ -1485,6 +1824,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1485
1824
|
SET state = 'terminalizing'
|
|
1486
1825
|
WHERE submission_id = ${validated.submissionId}
|
|
1487
1826
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1827
|
+
|
|
1488
1828
|
return ReservedSettlement.make({
|
|
1489
1829
|
submissionId: validated.submissionId,
|
|
1490
1830
|
settlementId: validated.settlementId,
|
|
@@ -1495,56 +1835,161 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1495
1835
|
});
|
|
1496
1836
|
}),
|
|
1497
1837
|
);
|
|
1838
|
+
|
|
1498
1839
|
yield* hitFailpoint("ledger:reserve-settlement:after", operation);
|
|
1840
|
+
|
|
1499
1841
|
return reserved;
|
|
1500
1842
|
});
|
|
1501
1843
|
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
|
|
1844
|
+
const validateFinalization = Effect.fn("SqliteSubmissionLedger.validateFinalization")(function* (
|
|
1845
|
+
validated: SettlementFinalization,
|
|
1846
|
+
reservation: Option.Option<ReservationRow>,
|
|
1847
|
+
) {
|
|
1505
1848
|
const operation = "ledger finalize settlement";
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1849
|
+
|
|
1850
|
+
if (Option.isNone(reservation)) {
|
|
1851
|
+
return yield* LedgerError.make({
|
|
1852
|
+
operation,
|
|
1853
|
+
message: `No settlement reservation exists for submission ${validated.submissionId}.`,
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
if (reservation.value.settlement_id !== validated.settlementId) {
|
|
1857
|
+
return yield* SettlementConflict.make({
|
|
1858
|
+
submissionId: validated.submissionId,
|
|
1859
|
+
existingOutcome: reservation.value.outcome,
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
const reservationRecord = yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(
|
|
1864
|
+
Effect.mapError((error) =>
|
|
1865
|
+
corruptionFailure(
|
|
1866
|
+
operation,
|
|
1867
|
+
"effect_agent_settlement_reservations",
|
|
1868
|
+
validated.submissionId,
|
|
1869
|
+
error.message,
|
|
1870
|
+
),
|
|
1871
|
+
),
|
|
1872
|
+
);
|
|
1873
|
+
|
|
1874
|
+
const settlementFailure = settlementFailureFromRecord(reservationRecord);
|
|
1875
|
+
|
|
1876
|
+
if ((reservation.value.outcome === "failed") !== (settlementFailure !== undefined)) {
|
|
1877
|
+
return yield* corruptionFailure(
|
|
1878
|
+
operation,
|
|
1879
|
+
"effect_agent_settlement_reservations",
|
|
1880
|
+
validated.submissionId,
|
|
1881
|
+
"The reserved outcome and canonical failure diagnostic disagree.",
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
return { reservation: reservation.value, settlementFailure };
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
const replayFinalization = Effect.fn("SqliteSubmissionLedger.replayFinalization")(function* (
|
|
1889
|
+
validated: SettlementFinalization,
|
|
1890
|
+
submission: SubmissionRow,
|
|
1891
|
+
{ reservation, settlementFailure }: Effect.Success<ReturnType<typeof validateFinalization>>,
|
|
1892
|
+
) {
|
|
1893
|
+
const operation = "ledger finalize settlement";
|
|
1894
|
+
|
|
1895
|
+
if (reservation.finalized_at === null) {
|
|
1896
|
+
return yield* corruptionFailure(
|
|
1897
|
+
operation,
|
|
1898
|
+
"effect_agent_settlement_reservations",
|
|
1899
|
+
validated.submissionId,
|
|
1900
|
+
"A settled Submission's reservation carries no finalization timestamp.",
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
return yield* decodeSettlement({
|
|
1905
|
+
submissionId: validated.submissionId,
|
|
1906
|
+
settlementId: validated.settlementId,
|
|
1907
|
+
receiptId: submission.receipt_id,
|
|
1908
|
+
outcome: reservation.outcome,
|
|
1909
|
+
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
1910
|
+
settledAt: reservation.finalized_at,
|
|
1911
|
+
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
const finalizeSettlement: SubmissionLedger["Service"]["finalizeSettlement"] = Effect.fn(
|
|
1915
|
+
"SqliteSubmissionLedger.finalizeSettlement",
|
|
1916
|
+
)(function* (request: SettlementFinalization) {
|
|
1917
|
+
const operation = "ledger finalize settlement";
|
|
1918
|
+
|
|
1919
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SettlementFinalization))(
|
|
1920
|
+
request,
|
|
1921
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1922
|
+
|
|
1923
|
+
yield* hitFailpoint("ledger:finalize-settlement:before", operation);
|
|
1924
|
+
|
|
1925
|
+
// A single statement captures the settled row and its immutable reservation together.
|
|
1926
|
+
// A miss does not authorize finalization: the write path re-reads under its transaction.
|
|
1927
|
+
const replayRows = yield* sql<Record<string, unknown>>`
|
|
1928
|
+
SELECT submission.*, reservation.settlement_id, reservation.outcome,
|
|
1929
|
+
reservation.record_id, reservation.record_json, reservation.record_digest,
|
|
1930
|
+
reservation.reserved_at, reservation.finalized_at
|
|
1931
|
+
FROM effect_agent_submissions AS submission
|
|
1932
|
+
INNER JOIN effect_agent_settlement_reservations AS reservation
|
|
1933
|
+
ON reservation.submission_id = submission.submission_id
|
|
1934
|
+
WHERE submission.submission_id = ${validated.submissionId} AND submission.state = 'settled'
|
|
1935
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1936
|
+
|
|
1937
|
+
if (replayRows.length > 1) {
|
|
1938
|
+
return yield* corruptionFailure(
|
|
1939
|
+
operation,
|
|
1940
|
+
"effect_agent_submissions",
|
|
1941
|
+
validated.submissionId,
|
|
1942
|
+
"A submission primary key returned more than one row.",
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
const replayRow = replayRows[0];
|
|
1946
|
+
|
|
1947
|
+
if (replayRow !== undefined) {
|
|
1948
|
+
const reservations = yield* decodeRows(
|
|
1949
|
+
Schema.Array(ReservationRow),
|
|
1950
|
+
"effect_agent_settlement_reservations",
|
|
1951
|
+
validated.submissionId,
|
|
1952
|
+
replayRows,
|
|
1953
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1954
|
+
|
|
1955
|
+
const state = yield* validateFinalization(validated, Option.fromUndefinedOr(reservations[0]));
|
|
1956
|
+
|
|
1957
|
+
const submission = yield* Schema.decodeUnknownEffect(SubmissionRow)(replayRow).pipe(
|
|
1958
|
+
Effect.mapError((error) =>
|
|
1959
|
+
corruptionFailure(
|
|
1960
|
+
operation,
|
|
1961
|
+
"effect_agent_submissions",
|
|
1962
|
+
validated.submissionId,
|
|
1963
|
+
error.message,
|
|
1964
|
+
),
|
|
1965
|
+
),
|
|
1966
|
+
);
|
|
1967
|
+
|
|
1968
|
+
const settlement = yield* replayFinalization(validated, submission, state);
|
|
1969
|
+
|
|
1970
|
+
yield* hitFailpoint("ledger:finalize-settlement:after", operation);
|
|
1971
|
+
|
|
1972
|
+
return settlement;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
const settlement = yield* inWriteTransaction(
|
|
1976
|
+
operation,
|
|
1977
|
+
Effect.gen(function* () {
|
|
1978
|
+
const state = yield* validateFinalization(
|
|
1979
|
+
validated,
|
|
1980
|
+
yield* readReservation(operation, validated.submissionId),
|
|
1981
|
+
);
|
|
1982
|
+
|
|
1983
|
+
const { reservation, settlementFailure } = state;
|
|
1984
|
+
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1985
|
+
|
|
1986
|
+
if (submission.state === "settled")
|
|
1987
|
+
return yield* replayFinalization(validated, submission, state);
|
|
1544
1988
|
const now = yield* currentInstant;
|
|
1989
|
+
|
|
1545
1990
|
yield* sql`
|
|
1546
1991
|
UPDATE effect_agent_submissions
|
|
1547
|
-
SET state = 'settled', settled_outcome = ${reservation.
|
|
1992
|
+
SET state = 'settled', settled_outcome = ${reservation.outcome}
|
|
1548
1993
|
WHERE submission_id = ${validated.submissionId}
|
|
1549
1994
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1550
1995
|
yield* sql`
|
|
@@ -1556,16 +2001,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1556
2001
|
DELETE FROM effect_agent_submission_ownership
|
|
1557
2002
|
WHERE submission_id = ${validated.submissionId}
|
|
1558
2003
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2004
|
+
|
|
1559
2005
|
return yield* decodeSettlement({
|
|
1560
2006
|
submissionId: validated.submissionId,
|
|
1561
2007
|
settlementId: validated.settlementId,
|
|
1562
2008
|
receiptId: submission.receipt_id,
|
|
1563
|
-
outcome: reservation.
|
|
2009
|
+
outcome: reservation.outcome,
|
|
2010
|
+
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
1564
2011
|
settledAt: now.iso,
|
|
1565
2012
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1566
2013
|
}),
|
|
1567
2014
|
);
|
|
2015
|
+
|
|
1568
2016
|
yield* hitFailpoint("ledger:finalize-settlement:after", operation);
|
|
2017
|
+
|
|
1569
2018
|
return settlement;
|
|
1570
2019
|
});
|
|
1571
2020
|
|
|
@@ -1573,14 +2022,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1573
2022
|
"SqliteSubmissionLedger.requestAbort",
|
|
1574
2023
|
)(function* (request: AbortCommand) {
|
|
1575
2024
|
const operation = "ledger request abort";
|
|
1576
|
-
|
|
2025
|
+
|
|
2026
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AbortCommand))(request).pipe(
|
|
1577
2027
|
Effect.mapError(internalFailure(operation)),
|
|
1578
2028
|
);
|
|
2029
|
+
|
|
1579
2030
|
yield* hitFailpoint("ledger:request-abort:before", operation);
|
|
2031
|
+
|
|
1580
2032
|
const intent = yield* inWriteTransaction(
|
|
1581
2033
|
operation,
|
|
1582
2034
|
Effect.gen(function* () {
|
|
1583
2035
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2036
|
+
|
|
1584
2037
|
if (submission.state === "settled") {
|
|
1585
2038
|
if (submission.settled_outcome === null) {
|
|
1586
2039
|
return yield* corruptionFailure(
|
|
@@ -1590,6 +2043,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1590
2043
|
"A settled Submission carries no terminal outcome.",
|
|
1591
2044
|
);
|
|
1592
2045
|
}
|
|
2046
|
+
|
|
1593
2047
|
return yield* SettlementConflict.make({
|
|
1594
2048
|
submissionId: validated.submissionId,
|
|
1595
2049
|
existingOutcome: submission.settled_outcome,
|
|
@@ -1607,15 +2061,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1607
2061
|
"A joined Submission carries no host linkage.",
|
|
1608
2062
|
);
|
|
1609
2063
|
}
|
|
2064
|
+
|
|
1610
2065
|
const hostSubmissionId = yield* decodeSubmissionId(
|
|
1611
2066
|
submission.joined_host_submission_id,
|
|
1612
2067
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2068
|
+
|
|
1613
2069
|
return yield* JoinedToHost.make({
|
|
1614
2070
|
submissionId: validated.submissionId,
|
|
1615
2071
|
hostSubmissionId,
|
|
1616
2072
|
});
|
|
1617
2073
|
}
|
|
1618
2074
|
const existing = yield* readAbortIntent(operation, validated.submissionId);
|
|
2075
|
+
|
|
1619
2076
|
if (Option.isSome(existing)) {
|
|
1620
2077
|
return yield* abortIntentFromRow(
|
|
1621
2078
|
operation,
|
|
@@ -1625,6 +2082,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1625
2082
|
);
|
|
1626
2083
|
}
|
|
1627
2084
|
const now = yield* currentInstant;
|
|
2085
|
+
|
|
1628
2086
|
yield* sql`
|
|
1629
2087
|
INSERT INTO effect_agent_abort_intents (
|
|
1630
2088
|
submission_id,
|
|
@@ -1638,11 +2096,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1638
2096
|
${now.iso}
|
|
1639
2097
|
)
|
|
1640
2098
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2099
|
+
|
|
1641
2100
|
const canonicalRecordId = yield* canonicalAbortRecordId(
|
|
1642
2101
|
operation,
|
|
1643
|
-
submission.
|
|
2102
|
+
submission.thread_id,
|
|
1644
2103
|
validated.submissionId,
|
|
1645
2104
|
);
|
|
2105
|
+
|
|
1646
2106
|
return yield* decodeAbortIntent({
|
|
1647
2107
|
submissionId: validated.submissionId,
|
|
1648
2108
|
author: validated.author,
|
|
@@ -1652,7 +2112,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1652
2112
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1653
2113
|
}),
|
|
1654
2114
|
);
|
|
2115
|
+
|
|
1655
2116
|
yield* hitFailpoint("ledger:request-abort:after", operation);
|
|
2117
|
+
|
|
1656
2118
|
return intent;
|
|
1657
2119
|
});
|
|
1658
2120
|
|
|
@@ -1660,31 +2122,38 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1660
2122
|
"SqliteSubmissionLedger.claimJoining",
|
|
1661
2123
|
)(function* (request: ClaimJoiningRequest) {
|
|
1662
2124
|
const operation = "ledger claim joining";
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
2125
|
+
|
|
2126
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(
|
|
2127
|
+
Effect.mapError(internalFailure(operation)),
|
|
2128
|
+
);
|
|
2129
|
+
|
|
1666
2130
|
yield* hitFailpoint("ledger:claim-joining:before", operation);
|
|
2131
|
+
|
|
1667
2132
|
const claims = yield* inWriteTransaction(
|
|
1668
2133
|
operation,
|
|
1669
2134
|
Effect.gen(function* () {
|
|
1670
2135
|
const host = yield* requireSubmission(operation, validated.hostSubmissionId);
|
|
1671
|
-
|
|
2136
|
+
|
|
2137
|
+
if (host.thread_id !== validated.threadId) {
|
|
1672
2138
|
return yield* LedgerError.make({
|
|
1673
2139
|
operation,
|
|
1674
|
-
message: `Host submission ${validated.hostSubmissionId} does not belong to
|
|
2140
|
+
message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
|
|
1675
2141
|
});
|
|
1676
2142
|
}
|
|
1677
2143
|
// The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
|
|
1678
2144
|
yield* requireOwnership(operation, host, validated.ownershipToken);
|
|
2145
|
+
|
|
1679
2146
|
const laterRows = yield* sql<Record<string, unknown>>`
|
|
1680
2147
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1681
2148
|
FROM effect_agent_submissions
|
|
1682
|
-
WHERE
|
|
2149
|
+
WHERE thread_id = ${validated.threadId}
|
|
1683
2150
|
AND queue_sequence > ${host.queue_sequence}
|
|
1684
2151
|
ORDER BY queue_sequence ASC
|
|
1685
2152
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1686
|
-
|
|
2153
|
+
|
|
2154
|
+
const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
|
|
1687
2155
|
const claimed: Array<JoiningClaim> = [];
|
|
2156
|
+
|
|
1688
2157
|
for (const row of later) {
|
|
1689
2158
|
if (claimed.length >= validated.maxCount) break;
|
|
1690
2159
|
// Rows already claimed by THIS host extend its contiguous prefix and are skipped;
|
|
@@ -1707,6 +2176,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1707
2176
|
SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
|
|
1708
2177
|
WHERE submission_id = ${row.submission_id}
|
|
1709
2178
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2179
|
+
|
|
1710
2180
|
const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
|
|
1711
2181
|
Effect.mapError((error) =>
|
|
1712
2182
|
corruptionFailure(
|
|
@@ -1717,6 +2187,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1717
2187
|
),
|
|
1718
2188
|
),
|
|
1719
2189
|
);
|
|
2190
|
+
|
|
1720
2191
|
claimed.push(
|
|
1721
2192
|
yield* decodeJoiningClaim({
|
|
1722
2193
|
submissionId: row.submission_id,
|
|
@@ -1725,10 +2196,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1725
2196
|
}).pipe(Effect.mapError(internalFailure(operation))),
|
|
1726
2197
|
);
|
|
1727
2198
|
}
|
|
1728
|
-
|
|
2199
|
+
|
|
2200
|
+
return claimed;
|
|
1729
2201
|
}),
|
|
1730
2202
|
);
|
|
2203
|
+
|
|
1731
2204
|
yield* hitFailpoint("ledger:claim-joining:after", operation);
|
|
2205
|
+
|
|
1732
2206
|
return claims;
|
|
1733
2207
|
});
|
|
1734
2208
|
|
|
@@ -1736,14 +2210,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1736
2210
|
"SqliteSubmissionLedger.markJoined",
|
|
1737
2211
|
)(function* (request: MarkJoinedRequest) {
|
|
1738
2212
|
const operation = "ledger mark joined";
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
2213
|
+
|
|
2214
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkJoinedRequest))(request).pipe(
|
|
2215
|
+
Effect.mapError(internalFailure(operation)),
|
|
2216
|
+
);
|
|
2217
|
+
|
|
1742
2218
|
yield* hitFailpoint("ledger:mark-joined:before", operation);
|
|
1743
2219
|
yield* inWriteTransaction(
|
|
1744
2220
|
operation,
|
|
1745
2221
|
Effect.gen(function* () {
|
|
1746
2222
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2223
|
+
|
|
1747
2224
|
if (submission.joined_host_submission_id === null) {
|
|
1748
2225
|
return yield* LedgerError.make({
|
|
1749
2226
|
operation,
|
|
@@ -1751,6 +2228,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1751
2228
|
});
|
|
1752
2229
|
}
|
|
1753
2230
|
const host = yield* requireSubmission(operation, submission.joined_host_submission_id);
|
|
2231
|
+
|
|
1754
2232
|
// The lane is host-owned: the presented token must own the HOST's ownership period,
|
|
1755
2233
|
// which also lets a later host Attempt repair a lost marker from history (DUR-016).
|
|
1756
2234
|
yield* requireOwnership(operation, host, validated.ownershipToken);
|
|
@@ -1761,6 +2239,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1761
2239
|
) {
|
|
1762
2240
|
return;
|
|
1763
2241
|
}
|
|
2242
|
+
|
|
1764
2243
|
return yield* corruptionFailure(
|
|
1765
2244
|
operation,
|
|
1766
2245
|
"effect_agent_submissions",
|
|
@@ -1791,14 +2270,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1791
2270
|
"SqliteSubmissionLedger.revertJoining",
|
|
1792
2271
|
)(function* (request: RevertJoiningRequest) {
|
|
1793
2272
|
const operation = "ledger revert joining";
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2273
|
+
|
|
2274
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(RevertJoiningRequest))(request).pipe(
|
|
2275
|
+
Effect.mapError(internalFailure(operation)),
|
|
2276
|
+
);
|
|
2277
|
+
|
|
1797
2278
|
yield* hitFailpoint("ledger:revert-joining:before", operation);
|
|
1798
2279
|
yield* inWriteTransaction(
|
|
1799
2280
|
operation,
|
|
1800
2281
|
Effect.gen(function* () {
|
|
1801
2282
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2283
|
+
|
|
1802
2284
|
// Idempotent and recovery-only: only a still-`joining` Submission reverts; an
|
|
1803
2285
|
// already-joined (or already-reverted) Submission is a no-op (DUR-016).
|
|
1804
2286
|
if (submission.state !== "joining") return;
|
|
@@ -1816,17 +2298,22 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1816
2298
|
"SqliteSubmissionLedger.suspend",
|
|
1817
2299
|
)(function* (request: SuspendRequest) {
|
|
1818
2300
|
const operation = "ledger suspend";
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2301
|
+
|
|
2302
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SuspendRequest))(request).pipe(
|
|
2303
|
+
Effect.mapError(internalFailure(operation)),
|
|
2304
|
+
);
|
|
2305
|
+
|
|
1822
2306
|
const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
|
|
1823
2307
|
Effect.mapError(internalFailure(operation)),
|
|
1824
2308
|
);
|
|
2309
|
+
|
|
1825
2310
|
yield* hitFailpoint("ledger:suspend:before", operation);
|
|
2311
|
+
|
|
1826
2312
|
const outcome = yield* inWriteTransaction(
|
|
1827
2313
|
operation,
|
|
1828
2314
|
Effect.gen(function* () {
|
|
1829
2315
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2316
|
+
|
|
1830
2317
|
if (submission.state === "settled") {
|
|
1831
2318
|
if (submission.settled_outcome === null) {
|
|
1832
2319
|
return yield* corruptionFailure(
|
|
@@ -1836,6 +2323,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1836
2323
|
"A settled Submission carries no terminal outcome.",
|
|
1837
2324
|
);
|
|
1838
2325
|
}
|
|
2326
|
+
|
|
1839
2327
|
return yield* SettlementConflict.make({
|
|
1840
2328
|
submissionId: validated.submissionId,
|
|
1841
2329
|
existingOutcome: submission.settled_outcome,
|
|
@@ -1844,6 +2332,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1844
2332
|
// An exact terminal outcome is already reserved (DUR-011); suspension would
|
|
1845
2333
|
// contradict it, so the reservation wins.
|
|
1846
2334
|
const reservation = yield* readReservation(operation, validated.submissionId);
|
|
2335
|
+
|
|
1847
2336
|
if (Option.isSome(reservation)) {
|
|
1848
2337
|
return yield* SettlementConflict.make({
|
|
1849
2338
|
submissionId: validated.submissionId,
|
|
@@ -1857,23 +2346,27 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1857
2346
|
if (validated.reason._tag === "ApprovalPending") {
|
|
1858
2347
|
const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
1859
2348
|
const decided = new Set(decisions.map((row) => row.tool_call_id));
|
|
2349
|
+
|
|
1860
2350
|
if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
|
|
1861
|
-
return
|
|
2351
|
+
return RESUME_IMMEDIATELY;
|
|
1862
2352
|
}
|
|
1863
2353
|
} else {
|
|
1864
2354
|
let allSettled = true;
|
|
2355
|
+
|
|
1865
2356
|
for (const child of validated.reason.children) {
|
|
1866
2357
|
const childRow = yield* readSubmission(operation, child.childSubmissionId);
|
|
2358
|
+
|
|
1867
2359
|
if (Option.isNone(childRow) || childRow.value.state !== "settled") {
|
|
1868
2360
|
allSettled = false;
|
|
1869
2361
|
break;
|
|
1870
2362
|
}
|
|
1871
2363
|
}
|
|
1872
2364
|
if (allSettled) {
|
|
1873
|
-
return
|
|
2365
|
+
return RESUME_IMMEDIATELY;
|
|
1874
2366
|
}
|
|
1875
2367
|
}
|
|
1876
2368
|
const now = yield* currentInstant;
|
|
2369
|
+
|
|
1877
2370
|
yield* sql`
|
|
1878
2371
|
UPDATE effect_agent_submissions
|
|
1879
2372
|
SET
|
|
@@ -1888,10 +2381,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1888
2381
|
DELETE FROM effect_agent_submission_ownership
|
|
1889
2382
|
WHERE submission_id = ${validated.submissionId}
|
|
1890
2383
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1891
|
-
|
|
2384
|
+
|
|
2385
|
+
return SUSPENDED;
|
|
1892
2386
|
}),
|
|
1893
2387
|
);
|
|
2388
|
+
|
|
1894
2389
|
yield* hitFailpoint("ledger:suspend:after", operation);
|
|
2390
|
+
|
|
1895
2391
|
return outcome;
|
|
1896
2392
|
});
|
|
1897
2393
|
|
|
@@ -1904,6 +2400,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1904
2400
|
const wakeSuspendedIfCovered = Effect.fn("SqliteSubmissionLedger.wakeSuspendedIfCovered")(
|
|
1905
2401
|
function* (operation: string, submission: SubmissionRow): Effect.fn.Return<void, LedgerError> {
|
|
1906
2402
|
if (submission.state !== "suspended" || submission.suspended_reason_json === null) return;
|
|
2403
|
+
|
|
1907
2404
|
const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
|
|
1908
2405
|
submission.suspended_reason_json,
|
|
1909
2406
|
).pipe(
|
|
@@ -1916,9 +2413,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1916
2413
|
),
|
|
1917
2414
|
),
|
|
1918
2415
|
);
|
|
2416
|
+
|
|
1919
2417
|
if (reason._tag !== "ApprovalPending") return;
|
|
1920
2418
|
const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
|
|
1921
2419
|
const decided = new Set(decisions.map((row) => row.tool_call_id));
|
|
2420
|
+
|
|
1922
2421
|
if (!reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) return;
|
|
1923
2422
|
yield* sql`
|
|
1924
2423
|
UPDATE effect_agent_submissions
|
|
@@ -1935,14 +2434,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1935
2434
|
"SqliteSubmissionLedger.recordApprovalDecision",
|
|
1936
2435
|
)(function* (command: ApprovalDecisionCommand) {
|
|
1937
2436
|
const operation = "ledger record approval decision";
|
|
1938
|
-
|
|
2437
|
+
|
|
2438
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ApprovalDecisionCommand))(
|
|
1939
2439
|
command,
|
|
1940
2440
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2441
|
+
|
|
1941
2442
|
yield* hitFailpoint("ledger:approval-decision:before", operation);
|
|
2443
|
+
|
|
1942
2444
|
const intent = yield* inWriteTransaction(
|
|
1943
2445
|
operation,
|
|
1944
2446
|
Effect.gen(function* () {
|
|
1945
2447
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2448
|
+
|
|
1946
2449
|
if (submission.state === "settled") {
|
|
1947
2450
|
if (submission.settled_outcome === null) {
|
|
1948
2451
|
return yield* corruptionFailure(
|
|
@@ -1952,6 +2455,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1952
2455
|
"A settled Submission carries no terminal outcome.",
|
|
1953
2456
|
);
|
|
1954
2457
|
}
|
|
2458
|
+
|
|
1955
2459
|
return yield* SettlementConflict.make({
|
|
1956
2460
|
submissionId: validated.submissionId,
|
|
1957
2461
|
existingOutcome: submission.settled_outcome,
|
|
@@ -1959,6 +2463,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1959
2463
|
}
|
|
1960
2464
|
const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
1961
2465
|
const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
|
|
2466
|
+
|
|
1962
2467
|
if (existing !== undefined) {
|
|
1963
2468
|
// Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
|
|
1964
2469
|
// the recorded intent unchanged; a divergent re-decision conflicts.
|
|
@@ -1969,9 +2474,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1969
2474
|
existingDecision: existing.decision,
|
|
1970
2475
|
});
|
|
1971
2476
|
}
|
|
2477
|
+
|
|
1972
2478
|
return yield* approvalIntentFromRow(operation, existing);
|
|
1973
2479
|
}
|
|
1974
2480
|
const now = yield* currentInstant;
|
|
2481
|
+
|
|
1975
2482
|
yield* sql`
|
|
1976
2483
|
INSERT INTO effect_agent_approval_decisions (
|
|
1977
2484
|
submission_id,
|
|
@@ -1990,6 +2497,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1990
2497
|
)
|
|
1991
2498
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1992
2499
|
yield* wakeSuspendedIfCovered(operation, submission);
|
|
2500
|
+
|
|
1993
2501
|
return yield* decodeApprovalDecisionIntent({
|
|
1994
2502
|
submissionId: validated.submissionId,
|
|
1995
2503
|
toolCallId: validated.toolCallId,
|
|
@@ -2000,7 +2508,9 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2000
2508
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
2001
2509
|
}),
|
|
2002
2510
|
);
|
|
2511
|
+
|
|
2003
2512
|
yield* hitFailpoint("ledger:approval-decision:after", operation);
|
|
2513
|
+
|
|
2004
2514
|
return intent;
|
|
2005
2515
|
});
|
|
2006
2516
|
|
|
@@ -2008,14 +2518,17 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2008
2518
|
"SqliteSubmissionLedger.markUnknown",
|
|
2009
2519
|
)(function* (request: MarkUnknownRequest) {
|
|
2010
2520
|
const operation = "ledger mark unknown";
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2521
|
+
|
|
2522
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkUnknownRequest))(request).pipe(
|
|
2523
|
+
Effect.mapError(internalFailure(operation)),
|
|
2524
|
+
);
|
|
2525
|
+
|
|
2014
2526
|
yield* hitFailpoint("ledger:mark-unknown:before", operation);
|
|
2015
2527
|
yield* inWriteTransaction(
|
|
2016
2528
|
operation,
|
|
2017
2529
|
Effect.gen(function* () {
|
|
2018
2530
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2531
|
+
|
|
2019
2532
|
if (submission.state === "settled") {
|
|
2020
2533
|
if (submission.settled_outcome === null) {
|
|
2021
2534
|
return yield* corruptionFailure(
|
|
@@ -2025,6 +2538,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2025
2538
|
"A settled Submission carries no terminal outcome.",
|
|
2026
2539
|
);
|
|
2027
2540
|
}
|
|
2541
|
+
|
|
2028
2542
|
return yield* SettlementConflict.make({
|
|
2029
2543
|
submissionId: validated.submissionId,
|
|
2030
2544
|
existingOutcome: submission.settled_outcome,
|
|
@@ -2033,6 +2547,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2033
2547
|
// A reserved exact outcome wins over a late Unknown marking (DUR-011); the recovery
|
|
2034
2548
|
// classifier orders reservation ahead of MarkUnknown for the same reason.
|
|
2035
2549
|
const reservation = yield* readReservation(operation, validated.submissionId);
|
|
2550
|
+
|
|
2036
2551
|
if (Option.isSome(reservation)) {
|
|
2037
2552
|
return yield* SettlementConflict.make({
|
|
2038
2553
|
submissionId: validated.submissionId,
|
|
@@ -2043,13 +2558,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2043
2558
|
// set while the first recorded reason is kept.
|
|
2044
2559
|
const existingIds = yield* storedUnknownToolCallIds(operation, submission);
|
|
2045
2560
|
const known = new Set(existingIds);
|
|
2561
|
+
|
|
2046
2562
|
const merged = [
|
|
2047
2563
|
...existingIds,
|
|
2048
2564
|
...validated.toolCallIds.filter((toolCallId) => !known.has(toolCallId)),
|
|
2049
2565
|
];
|
|
2566
|
+
|
|
2050
2567
|
const idsJson = yield* encodeToolCallIdsText(merged).pipe(
|
|
2051
2568
|
Effect.mapError(internalFailure(operation)),
|
|
2052
2569
|
);
|
|
2570
|
+
|
|
2053
2571
|
yield* sql`
|
|
2054
2572
|
UPDATE effect_agent_submissions
|
|
2055
2573
|
SET
|
|
@@ -2067,17 +2585,22 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2067
2585
|
"SqliteSubmissionLedger.recordUnknownResolution",
|
|
2068
2586
|
)(function* (command: UnknownResolutionCommand) {
|
|
2069
2587
|
const operation = "ledger record unknown resolution";
|
|
2070
|
-
|
|
2588
|
+
|
|
2589
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(UnknownResolutionCommand))(
|
|
2071
2590
|
command,
|
|
2072
2591
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2592
|
+
|
|
2073
2593
|
const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
|
|
2074
2594
|
Effect.mapError(internalFailure(operation)),
|
|
2075
2595
|
);
|
|
2596
|
+
|
|
2076
2597
|
yield* hitFailpoint("ledger:unknown-resolution:before", operation);
|
|
2598
|
+
|
|
2077
2599
|
const intent = yield* inWriteTransaction(
|
|
2078
2600
|
operation,
|
|
2079
2601
|
Effect.gen(function* () {
|
|
2080
2602
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2603
|
+
|
|
2081
2604
|
if (submission.state === "settled") {
|
|
2082
2605
|
if (submission.settled_outcome === null) {
|
|
2083
2606
|
return yield* corruptionFailure(
|
|
@@ -2087,6 +2610,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2087
2610
|
"A settled Submission carries no terminal outcome.",
|
|
2088
2611
|
);
|
|
2089
2612
|
}
|
|
2613
|
+
|
|
2090
2614
|
return yield* SettlementConflict.make({
|
|
2091
2615
|
submissionId: validated.submissionId,
|
|
2092
2616
|
existingOutcome: submission.settled_outcome,
|
|
@@ -2094,19 +2618,30 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2094
2618
|
}
|
|
2095
2619
|
const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
2096
2620
|
const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
|
|
2097
|
-
|
|
2621
|
+
|
|
2622
|
+
const existingIntent =
|
|
2623
|
+
existing === undefined
|
|
2624
|
+
? undefined
|
|
2625
|
+
: yield* unknownResolutionIntentFromRow(operation, existing);
|
|
2626
|
+
|
|
2627
|
+
if (
|
|
2628
|
+
existingIntent !== undefined &&
|
|
2629
|
+
!equivalentUnknownResolution(existingIntent.resolution, validated.resolution)
|
|
2630
|
+
) {
|
|
2098
2631
|
return yield* UnknownResolutionConflict.make({
|
|
2099
2632
|
submissionId: validated.submissionId,
|
|
2100
2633
|
toolCallId: validated.toolCallId,
|
|
2101
2634
|
});
|
|
2102
2635
|
}
|
|
2103
2636
|
let resolved: UnknownResolutionIntent;
|
|
2104
|
-
|
|
2637
|
+
|
|
2638
|
+
if (existingIntent !== undefined) {
|
|
2105
2639
|
// Idempotent replay of the recorded intent (author/reason may differ; the stored
|
|
2106
2640
|
// audit fields win, exactly like requestAbort).
|
|
2107
|
-
resolved =
|
|
2641
|
+
resolved = existingIntent;
|
|
2108
2642
|
} else {
|
|
2109
2643
|
const now = yield* currentInstant;
|
|
2644
|
+
|
|
2110
2645
|
yield* sql`
|
|
2111
2646
|
INSERT INTO effect_agent_unknown_resolutions (
|
|
2112
2647
|
submission_id,
|
|
@@ -2124,9 +2659,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2124
2659
|
${now.iso}
|
|
2125
2660
|
)
|
|
2126
2661
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2662
|
+
|
|
2127
2663
|
const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
|
|
2128
2664
|
Effect.mapError(internalFailure(operation)),
|
|
2129
2665
|
);
|
|
2666
|
+
|
|
2130
2667
|
resolved = yield* decodeUnknownResolutionIntent({
|
|
2131
2668
|
submissionId: validated.submissionId,
|
|
2132
2669
|
toolCallId: validated.toolCallId,
|
|
@@ -2143,6 +2680,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2143
2680
|
const markedIds = yield* storedUnknownToolCallIds(operation, submission);
|
|
2144
2681
|
const covering = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
2145
2682
|
const coveredIds = new Set(covering.map((row) => row.tool_call_id));
|
|
2683
|
+
|
|
2146
2684
|
if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) {
|
|
2147
2685
|
yield* sql`
|
|
2148
2686
|
UPDATE effect_agent_submissions
|
|
@@ -2154,10 +2692,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2154
2692
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2155
2693
|
}
|
|
2156
2694
|
}
|
|
2695
|
+
|
|
2157
2696
|
return resolved;
|
|
2158
2697
|
}),
|
|
2159
2698
|
);
|
|
2699
|
+
|
|
2160
2700
|
yield* hitFailpoint("ledger:unknown-resolution:after", operation);
|
|
2701
|
+
|
|
2161
2702
|
return intent;
|
|
2162
2703
|
});
|
|
2163
2704
|
|
|
@@ -2165,26 +2706,38 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2165
2706
|
"SqliteSubmissionLedger.recordChildSettled",
|
|
2166
2707
|
)(function* (request: ChildSettledNotification) {
|
|
2167
2708
|
const operation = "ledger record child settled";
|
|
2168
|
-
|
|
2709
|
+
|
|
2710
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ChildSettledNotification))(
|
|
2169
2711
|
request,
|
|
2170
2712
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2713
|
+
|
|
2171
2714
|
yield* hitFailpoint("ledger:child-settled:before", operation);
|
|
2715
|
+
|
|
2172
2716
|
const outcome = yield* inWriteTransaction(
|
|
2173
2717
|
operation,
|
|
2174
2718
|
Effect.gen(function* () {
|
|
2175
2719
|
const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
|
|
2176
|
-
// The
|
|
2177
|
-
//
|
|
2720
|
+
// The caller may notify after the canonical Settlement append but before ledger
|
|
2721
|
+
// finalization. An exact reservation plus `terminalizing` is the narrow durable prefix
|
|
2722
|
+
// that makes that ordering admissible; earlier states remain a caller error.
|
|
2178
2723
|
const child = yield* readSubmission(operation, validated.childSubmissionId);
|
|
2179
|
-
|
|
2724
|
+
const childReservation = yield* readReservation(operation, validated.childSubmissionId);
|
|
2725
|
+
|
|
2726
|
+
const announced =
|
|
2727
|
+
Option.isSome(child) &&
|
|
2728
|
+
(child.value.state === "settled" ||
|
|
2729
|
+
(child.value.state === "terminalizing" && Option.isSome(childReservation)));
|
|
2730
|
+
|
|
2731
|
+
if (!announced) {
|
|
2180
2732
|
return yield* LedgerError.make({
|
|
2181
2733
|
operation,
|
|
2182
2734
|
message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`,
|
|
2183
2735
|
});
|
|
2184
2736
|
}
|
|
2185
2737
|
if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
|
|
2186
|
-
return
|
|
2738
|
+
return NOT_WAITING;
|
|
2187
2739
|
}
|
|
2740
|
+
|
|
2188
2741
|
const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
|
|
2189
2742
|
parent.suspended_reason_json,
|
|
2190
2743
|
).pipe(
|
|
@@ -2197,20 +2750,28 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2197
2750
|
),
|
|
2198
2751
|
),
|
|
2199
2752
|
);
|
|
2753
|
+
|
|
2200
2754
|
if (reason._tag !== "WaitingForChild") {
|
|
2201
|
-
return
|
|
2755
|
+
return NOT_WAITING;
|
|
2202
2756
|
}
|
|
2203
2757
|
if (
|
|
2204
2758
|
!reason.children.some((entry) => entry.childSubmissionId === validated.childSubmissionId)
|
|
2205
2759
|
) {
|
|
2206
|
-
return
|
|
2760
|
+
return NOT_WAITING;
|
|
2207
2761
|
}
|
|
2208
|
-
//
|
|
2209
|
-
//
|
|
2762
|
+
// Every listed child must be either finalized or canonically announced from the exact
|
|
2763
|
+
// terminalizing reservation. Replays re-run this coverage check idempotently.
|
|
2210
2764
|
for (const entry of reason.children) {
|
|
2211
2765
|
const listed = yield* readSubmission(operation, entry.childSubmissionId);
|
|
2212
|
-
|
|
2213
|
-
|
|
2766
|
+
const reservation = yield* readReservation(operation, entry.childSubmissionId);
|
|
2767
|
+
|
|
2768
|
+
const covered =
|
|
2769
|
+
Option.isSome(listed) &&
|
|
2770
|
+
(listed.value.state === "settled" ||
|
|
2771
|
+
(listed.value.state === "terminalizing" && Option.isSome(reservation)));
|
|
2772
|
+
|
|
2773
|
+
if (!covered) {
|
|
2774
|
+
return STILL_WAITING;
|
|
2214
2775
|
}
|
|
2215
2776
|
}
|
|
2216
2777
|
yield* sql`
|
|
@@ -2221,10 +2782,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2221
2782
|
suspended_at = NULL
|
|
2222
2783
|
WHERE submission_id = ${validated.parentSubmissionId}
|
|
2223
2784
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2224
|
-
|
|
2785
|
+
|
|
2786
|
+
return WOKEN;
|
|
2225
2787
|
}),
|
|
2226
2788
|
);
|
|
2789
|
+
|
|
2227
2790
|
yield* hitFailpoint("ledger:child-settled:after", operation);
|
|
2791
|
+
|
|
2228
2792
|
return outcome;
|
|
2229
2793
|
});
|
|
2230
2794
|
|
|
@@ -2232,25 +2796,36 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2232
2796
|
"SqliteSubmissionLedger.reserveChildBudget",
|
|
2233
2797
|
)(function* (request: ChildBudgetReservationRequest) {
|
|
2234
2798
|
const operation = "ledger reserve child budget";
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2799
|
+
|
|
2800
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ChildBudgetReservationRequest))(
|
|
2801
|
+
request,
|
|
2802
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2803
|
+
|
|
2238
2804
|
const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
|
|
2239
2805
|
Effect.mapError(internalFailure(operation)),
|
|
2240
2806
|
);
|
|
2807
|
+
|
|
2241
2808
|
yield* hitFailpoint("ledger:child-reservation:before", operation);
|
|
2809
|
+
|
|
2242
2810
|
const reserved = yield* inWriteTransaction(
|
|
2243
2811
|
operation,
|
|
2244
2812
|
Effect.gen(function* () {
|
|
2245
2813
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
2814
|
+
|
|
2246
2815
|
if (Option.isSome(existing)) {
|
|
2816
|
+
const existingSnapshot = yield* childReservationSnapshotFromRow(
|
|
2817
|
+
operation,
|
|
2818
|
+
existing.value,
|
|
2819
|
+
);
|
|
2820
|
+
|
|
2247
2821
|
// Identical replays short-circuit before the fence, mirroring reserveSettlement: a
|
|
2248
2822
|
// replay creates nothing, so a recovering caller resumes rather than duplicates.
|
|
2249
2823
|
const identical =
|
|
2250
2824
|
existing.value.parent_submission_id === validated.parentSubmissionId &&
|
|
2251
2825
|
existing.value.parent_tool_call_id === validated.parentToolCallId &&
|
|
2252
2826
|
existing.value.allocation_digest === validated.allocationDigest &&
|
|
2253
|
-
|
|
2827
|
+
equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
|
|
2828
|
+
|
|
2254
2829
|
if (!identical) {
|
|
2255
2830
|
return yield* ChildReservationConflict.make({
|
|
2256
2831
|
reservationId: validated.reservationId,
|
|
@@ -2259,16 +2834,19 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2259
2834
|
"A reservation with this identity exists with a different parent Tool Call or allocation.",
|
|
2260
2835
|
});
|
|
2261
2836
|
}
|
|
2837
|
+
|
|
2262
2838
|
return ReservedChildBudget.make({
|
|
2263
|
-
reservation:
|
|
2839
|
+
reservation: existingSnapshot,
|
|
2264
2840
|
replayed: true,
|
|
2265
2841
|
});
|
|
2266
2842
|
}
|
|
2843
|
+
|
|
2267
2844
|
const collision = yield* readChildReservationForCall(
|
|
2268
2845
|
operation,
|
|
2269
2846
|
validated.parentSubmissionId,
|
|
2270
2847
|
validated.parentToolCallId,
|
|
2271
2848
|
);
|
|
2849
|
+
|
|
2272
2850
|
if (Option.isSome(collision)) {
|
|
2273
2851
|
return yield* ChildReservationConflict.make({
|
|
2274
2852
|
reservationId: validated.reservationId,
|
|
@@ -2277,10 +2855,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2277
2855
|
});
|
|
2278
2856
|
}
|
|
2279
2857
|
const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
|
|
2858
|
+
|
|
2280
2859
|
// Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
|
|
2281
2860
|
// parent Attempt can never create new reservation state.
|
|
2282
2861
|
yield* requireOwnership(operation, parent, validated.ownershipToken);
|
|
2283
2862
|
const now = yield* currentInstant;
|
|
2863
|
+
|
|
2284
2864
|
yield* sql`
|
|
2285
2865
|
INSERT INTO effect_agent_child_reservations (
|
|
2286
2866
|
reservation_id,
|
|
@@ -2301,6 +2881,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2301
2881
|
)
|
|
2302
2882
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2303
2883
|
const inserted = yield* readChildReservation(operation, validated.reservationId);
|
|
2884
|
+
|
|
2304
2885
|
if (Option.isNone(inserted)) {
|
|
2305
2886
|
return yield* corruptionFailure(
|
|
2306
2887
|
operation,
|
|
@@ -2309,13 +2890,16 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2309
2890
|
"An inserted child reservation row is missing inside its own transaction.",
|
|
2310
2891
|
);
|
|
2311
2892
|
}
|
|
2893
|
+
|
|
2312
2894
|
return ReservedChildBudget.make({
|
|
2313
2895
|
reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
|
|
2314
2896
|
replayed: false,
|
|
2315
2897
|
});
|
|
2316
2898
|
}),
|
|
2317
2899
|
);
|
|
2900
|
+
|
|
2318
2901
|
yield* hitFailpoint("ledger:child-reservation:after", operation);
|
|
2902
|
+
|
|
2319
2903
|
return reserved;
|
|
2320
2904
|
});
|
|
2321
2905
|
|
|
@@ -2324,14 +2908,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2324
2908
|
request: AttachChildToReservationRequest,
|
|
2325
2909
|
) {
|
|
2326
2910
|
const operation = "ledger attach child to reservation";
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2911
|
+
|
|
2912
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AttachChildToReservationRequest))(
|
|
2913
|
+
request,
|
|
2914
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2915
|
+
|
|
2330
2916
|
yield* hitFailpoint("ledger:child-attach:before", operation);
|
|
2917
|
+
|
|
2331
2918
|
const attached = yield* inWriteTransaction(
|
|
2332
2919
|
operation,
|
|
2333
2920
|
Effect.gen(function* () {
|
|
2334
2921
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
2922
|
+
|
|
2335
2923
|
if (Option.isNone(existing)) {
|
|
2336
2924
|
return yield* LedgerError.make({
|
|
2337
2925
|
operation,
|
|
@@ -2343,6 +2931,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2343
2931
|
if (existing.value.child_submission_id === validated.childSubmissionId) {
|
|
2344
2932
|
return yield* childReservationSnapshotFromRow(operation, existing.value);
|
|
2345
2933
|
}
|
|
2934
|
+
|
|
2346
2935
|
return yield* ChildReservationConflict.make({
|
|
2347
2936
|
reservationId: validated.reservationId,
|
|
2348
2937
|
status: existing.value.status,
|
|
@@ -2350,6 +2939,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2350
2939
|
});
|
|
2351
2940
|
}
|
|
2352
2941
|
const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
|
|
2942
|
+
|
|
2353
2943
|
yield* requireOwnership(operation, parent, validated.ownershipToken);
|
|
2354
2944
|
if (existing.value.status !== "reserved") {
|
|
2355
2945
|
return yield* ChildReservationConflict.make({
|
|
@@ -2361,6 +2951,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2361
2951
|
// Single-store latitude: the admitted child must exist here, so a dangling
|
|
2362
2952
|
// attachment can never enter the recovery view.
|
|
2363
2953
|
const child = yield* readSubmission(operation, validated.childSubmissionId);
|
|
2954
|
+
|
|
2364
2955
|
if (Option.isNone(child)) {
|
|
2365
2956
|
return yield* LedgerError.make({
|
|
2366
2957
|
operation,
|
|
@@ -2373,6 +2964,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2373
2964
|
WHERE reservation_id = ${validated.reservationId}
|
|
2374
2965
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2375
2966
|
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
2967
|
+
|
|
2376
2968
|
if (Option.isNone(updated)) {
|
|
2377
2969
|
return yield* corruptionFailure(
|
|
2378
2970
|
operation,
|
|
@@ -2381,10 +2973,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2381
2973
|
"An updated child reservation row is missing inside its own transaction.",
|
|
2382
2974
|
);
|
|
2383
2975
|
}
|
|
2976
|
+
|
|
2384
2977
|
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
2385
2978
|
}),
|
|
2386
2979
|
);
|
|
2980
|
+
|
|
2387
2981
|
yield* hitFailpoint("ledger:child-attach:after", operation);
|
|
2982
|
+
|
|
2388
2983
|
return attached;
|
|
2389
2984
|
});
|
|
2390
2985
|
|
|
@@ -2392,17 +2987,22 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2392
2987
|
"SqliteSubmissionLedger.beginChildBudgetRelease",
|
|
2393
2988
|
)(function* (request: BeginChildBudgetReleaseRequest) {
|
|
2394
2989
|
const operation = "ledger begin child budget release";
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2990
|
+
|
|
2991
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(BeginChildBudgetReleaseRequest))(
|
|
2992
|
+
request,
|
|
2993
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2994
|
+
|
|
2398
2995
|
const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
|
|
2399
2996
|
Effect.mapError(internalFailure(operation)),
|
|
2400
2997
|
);
|
|
2998
|
+
|
|
2401
2999
|
yield* hitFailpoint("ledger:child-release-pending:before", operation);
|
|
3000
|
+
|
|
2402
3001
|
const frozen = yield* inWriteTransaction(
|
|
2403
3002
|
operation,
|
|
2404
3003
|
Effect.gen(function* () {
|
|
2405
3004
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
3005
|
+
|
|
2406
3006
|
if (Option.isNone(existing)) {
|
|
2407
3007
|
return yield* LedgerError.make({
|
|
2408
3008
|
operation,
|
|
@@ -2410,11 +3010,20 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2410
3010
|
});
|
|
2411
3011
|
}
|
|
2412
3012
|
if (existing.value.status !== "reserved") {
|
|
3013
|
+
const existingSnapshot = yield* childReservationSnapshotFromRow(
|
|
3014
|
+
operation,
|
|
3015
|
+
existing.value,
|
|
3016
|
+
);
|
|
3017
|
+
|
|
2413
3018
|
// The accounting decision was already frozen exactly once; an identical replay is a
|
|
2414
3019
|
// no-op and a divergent decision conflicts (spec §12 join step 6).
|
|
2415
|
-
if (
|
|
2416
|
-
|
|
3020
|
+
if (
|
|
3021
|
+
existingSnapshot.accounting !== undefined &&
|
|
3022
|
+
equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
|
|
3023
|
+
) {
|
|
3024
|
+
return existingSnapshot;
|
|
2417
3025
|
}
|
|
3026
|
+
|
|
2418
3027
|
return yield* ChildReservationConflict.make({
|
|
2419
3028
|
reservationId: validated.reservationId,
|
|
2420
3029
|
status: existing.value.status,
|
|
@@ -2422,6 +3031,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2422
3031
|
});
|
|
2423
3032
|
}
|
|
2424
3033
|
const now = yield* currentInstant;
|
|
3034
|
+
|
|
2425
3035
|
yield* sql`
|
|
2426
3036
|
UPDATE effect_agent_child_reservations
|
|
2427
3037
|
SET
|
|
@@ -2431,6 +3041,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2431
3041
|
WHERE reservation_id = ${validated.reservationId}
|
|
2432
3042
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2433
3043
|
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
3044
|
+
|
|
2434
3045
|
if (Option.isNone(updated)) {
|
|
2435
3046
|
return yield* corruptionFailure(
|
|
2436
3047
|
operation,
|
|
@@ -2439,10 +3050,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2439
3050
|
"An updated child reservation row is missing inside its own transaction.",
|
|
2440
3051
|
);
|
|
2441
3052
|
}
|
|
3053
|
+
|
|
2442
3054
|
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
2443
3055
|
}),
|
|
2444
3056
|
);
|
|
3057
|
+
|
|
2445
3058
|
yield* hitFailpoint("ledger:child-release-pending:after", operation);
|
|
3059
|
+
|
|
2446
3060
|
return frozen;
|
|
2447
3061
|
});
|
|
2448
3062
|
|
|
@@ -2450,14 +3064,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2450
3064
|
"SqliteSubmissionLedger.releaseChildBudget",
|
|
2451
3065
|
)(function* (request: ReleaseChildBudgetRequest) {
|
|
2452
3066
|
const operation = "ledger release child budget";
|
|
2453
|
-
|
|
3067
|
+
|
|
3068
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseChildBudgetRequest))(
|
|
2454
3069
|
request,
|
|
2455
3070
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3071
|
+
|
|
2456
3072
|
yield* hitFailpoint("ledger:child-release:before", operation);
|
|
3073
|
+
|
|
2457
3074
|
const released = yield* inWriteTransaction(
|
|
2458
3075
|
operation,
|
|
2459
3076
|
Effect.gen(function* () {
|
|
2460
3077
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
3078
|
+
|
|
2461
3079
|
if (Option.isNone(existing)) {
|
|
2462
3080
|
return yield* LedgerError.make({
|
|
2463
3081
|
operation,
|
|
@@ -2477,12 +3095,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2477
3095
|
});
|
|
2478
3096
|
}
|
|
2479
3097
|
const now = yield* currentInstant;
|
|
3098
|
+
|
|
2480
3099
|
yield* sql`
|
|
2481
3100
|
UPDATE effect_agent_child_reservations
|
|
2482
3101
|
SET status = 'released', released_at = ${now.iso}
|
|
2483
3102
|
WHERE reservation_id = ${validated.reservationId}
|
|
2484
3103
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2485
3104
|
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
3105
|
+
|
|
2486
3106
|
if (Option.isNone(updated)) {
|
|
2487
3107
|
return yield* corruptionFailure(
|
|
2488
3108
|
operation,
|
|
@@ -2491,15 +3111,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2491
3111
|
"An updated child reservation row is missing inside its own transaction.",
|
|
2492
3112
|
);
|
|
2493
3113
|
}
|
|
3114
|
+
|
|
2494
3115
|
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
2495
3116
|
}),
|
|
2496
3117
|
);
|
|
3118
|
+
|
|
2497
3119
|
yield* hitFailpoint("ledger:child-release:after", operation);
|
|
3120
|
+
|
|
2498
3121
|
return released;
|
|
2499
3122
|
});
|
|
2500
3123
|
|
|
2501
3124
|
interface ScanCursor {
|
|
2502
|
-
readonly
|
|
3125
|
+
readonly threadId: string;
|
|
2503
3126
|
readonly queueSequence: number;
|
|
2504
3127
|
}
|
|
2505
3128
|
|
|
@@ -2510,65 +3133,140 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2510
3133
|
LedgerError
|
|
2511
3134
|
> {
|
|
2512
3135
|
const operation = "ledger scan nonterminal";
|
|
3136
|
+
|
|
2513
3137
|
const rows = yield* (
|
|
2514
3138
|
cursor === undefined
|
|
2515
3139
|
? sql<Record<string, unknown>>`
|
|
2516
3140
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2517
3141
|
FROM effect_agent_submissions
|
|
2518
3142
|
WHERE state <> 'settled'
|
|
2519
|
-
ORDER BY
|
|
3143
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
2520
3144
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
2521
3145
|
`
|
|
2522
3146
|
: sql<Record<string, unknown>>`
|
|
2523
3147
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2524
3148
|
FROM effect_agent_submissions
|
|
2525
3149
|
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
|
|
3150
|
+
AND (thread_id, queue_sequence) > (${cursor.threadId}, ${cursor.queueSequence})
|
|
3151
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
2534
3152
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
2535
3153
|
`
|
|
2536
3154
|
).pipe(Effect.mapError(sqlFailure(operation)));
|
|
3155
|
+
|
|
2537
3156
|
const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
|
|
3157
|
+
|
|
2538
3158
|
const snapshots = yield* Effect.forEach(decoded, (row) =>
|
|
2539
3159
|
decodeSubmissionSnapshot(operation, row),
|
|
2540
3160
|
);
|
|
3161
|
+
|
|
2541
3162
|
const last = decoded[decoded.length - 1];
|
|
3163
|
+
|
|
2542
3164
|
const next: Option.Option<ScanCursor | undefined> =
|
|
2543
3165
|
last === undefined || decoded.length < SCAN_PAGE_SIZE
|
|
2544
3166
|
? Option.none()
|
|
2545
3167
|
: Option.some({
|
|
2546
|
-
|
|
3168
|
+
threadId: last.thread_id,
|
|
2547
3169
|
queueSequence: last.queue_sequence,
|
|
2548
3170
|
});
|
|
3171
|
+
|
|
2549
3172
|
return [snapshots, next] as const;
|
|
2550
3173
|
});
|
|
2551
3174
|
|
|
2552
|
-
const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
3175
|
+
const scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError> = Stream.paginate<
|
|
3176
|
+
ScanCursor | undefined,
|
|
3177
|
+
SubmissionSnapshot,
|
|
3178
|
+
LedgerError
|
|
3179
|
+
>(undefined, scanPage);
|
|
3180
|
+
|
|
3181
|
+
const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] = Effect.fn(
|
|
3182
|
+
"SqliteSubmissionLedger.readAbortIntentForSubmission",
|
|
3183
|
+
)(function* (request) {
|
|
3184
|
+
const operation = "ledger read abort intent";
|
|
3185
|
+
|
|
3186
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
|
|
3187
|
+
Effect.mapError(internalFailure(operation)),
|
|
3188
|
+
);
|
|
3189
|
+
|
|
3190
|
+
const recordId = submissionAbortRecordId(validated.submissionId);
|
|
3191
|
+
|
|
3192
|
+
const rows = yield* sql<Record<string, unknown>>`
|
|
3193
|
+
SELECT
|
|
3194
|
+
submission.submission_id,
|
|
3195
|
+
abort.submission_id AS abort_submission_id,
|
|
3196
|
+
abort.author,
|
|
3197
|
+
abort.reason,
|
|
3198
|
+
abort.requested_at,
|
|
3199
|
+
canonical.record_id AS canonical_record_id
|
|
3200
|
+
FROM effect_agent_submissions AS submission
|
|
3201
|
+
LEFT JOIN effect_agent_abort_intents AS abort
|
|
3202
|
+
ON abort.submission_id = submission.submission_id
|
|
3203
|
+
LEFT JOIN effect_agent_canonical_records AS canonical
|
|
3204
|
+
ON canonical.thread_id = submission.thread_id
|
|
3205
|
+
AND canonical.record_id = ${recordId}
|
|
3206
|
+
WHERE submission.submission_id = ${validated.submissionId}
|
|
3207
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
3208
|
+
|
|
3209
|
+
const decoded = yield* decodeRows(
|
|
3210
|
+
Schema.Array(AbortIntentLookupRow),
|
|
3211
|
+
"effect_agent_abort_intents",
|
|
3212
|
+
validated.submissionId,
|
|
3213
|
+
rows,
|
|
3214
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3215
|
+
|
|
3216
|
+
if (decoded.length === 0) {
|
|
3217
|
+
return yield* LedgerError.make({
|
|
3218
|
+
operation,
|
|
3219
|
+
message: `Unknown submission ${validated.submissionId}.`,
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
if (decoded.length !== 1) {
|
|
3223
|
+
return yield* corruptionFailure(
|
|
3224
|
+
operation,
|
|
3225
|
+
"effect_agent_abort_intents",
|
|
3226
|
+
validated.submissionId,
|
|
3227
|
+
"An abort intent lookup returned more than one row.",
|
|
3228
|
+
);
|
|
3229
|
+
}
|
|
3230
|
+
const row = decoded[0];
|
|
3231
|
+
|
|
3232
|
+
if (row.abort_submission_id === null) return undefined;
|
|
3233
|
+
|
|
3234
|
+
return yield* decodeAbortIntent({
|
|
3235
|
+
submissionId: row.abort_submission_id,
|
|
3236
|
+
author: row.author,
|
|
3237
|
+
reason: row.reason,
|
|
3238
|
+
requestedAt: row.requested_at,
|
|
3239
|
+
...(row.canonical_record_id === null ? {} : { canonicalRecordId: row.canonical_record_id }),
|
|
3240
|
+
}).pipe(
|
|
3241
|
+
Effect.mapError((error) =>
|
|
3242
|
+
corruptionFailure(
|
|
3243
|
+
operation,
|
|
3244
|
+
"effect_agent_abort_intents",
|
|
3245
|
+
validated.submissionId,
|
|
3246
|
+
error.message,
|
|
3247
|
+
),
|
|
3248
|
+
),
|
|
3249
|
+
);
|
|
3250
|
+
});
|
|
2556
3251
|
|
|
2557
3252
|
const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] = Effect.fn(
|
|
2558
3253
|
"SqliteSubmissionLedger.loadRecoverySnapshot",
|
|
2559
3254
|
)(function* (request: RecoverySnapshotRequest) {
|
|
2560
3255
|
const operation = "ledger load recovery snapshot";
|
|
2561
|
-
|
|
3256
|
+
|
|
3257
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
|
|
2562
3258
|
request,
|
|
2563
3259
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2564
|
-
|
|
2565
|
-
|
|
3260
|
+
|
|
3261
|
+
return yield* journal
|
|
3262
|
+
.withReadTransaction(operation)(
|
|
2566
3263
|
Effect.gen(function* () {
|
|
2567
3264
|
const submissionRow = yield* requireSubmission(operation, validated.submissionId);
|
|
2568
3265
|
const submission = yield* decodeSubmissionSnapshot(operation, submissionRow);
|
|
2569
3266
|
|
|
2570
3267
|
let ownership: OwnershipSnapshot | undefined;
|
|
2571
3268
|
const ownershipRow = yield* readOwnership(operation, validated.submissionId);
|
|
3269
|
+
|
|
2572
3270
|
if (Option.isSome(ownershipRow)) {
|
|
2573
3271
|
ownership = yield* decodeOwnershipSnapshot({
|
|
2574
3272
|
attemptId: ownershipRow.value.attempt_id,
|
|
@@ -2579,6 +3277,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2579
3277
|
}
|
|
2580
3278
|
|
|
2581
3279
|
let inputApplied: InputAppliedMarker | undefined;
|
|
3280
|
+
|
|
2582
3281
|
if (
|
|
2583
3282
|
submissionRow.input_applied_record_id !== null &&
|
|
2584
3283
|
submissionRow.input_applied_sequence !== null
|
|
@@ -2591,6 +3290,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2591
3290
|
|
|
2592
3291
|
let reservation: SettlementReservationSnapshot | undefined;
|
|
2593
3292
|
const reservationRow = yield* readReservation(operation, validated.submissionId);
|
|
3293
|
+
|
|
2594
3294
|
if (Option.isSome(reservationRow)) {
|
|
2595
3295
|
const record = yield* decodeRecordEnvelopeText(reservationRow.value.record_json).pipe(
|
|
2596
3296
|
Effect.mapError((error) =>
|
|
@@ -2602,9 +3302,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2602
3302
|
),
|
|
2603
3303
|
),
|
|
2604
3304
|
);
|
|
2605
|
-
|
|
3305
|
+
|
|
3306
|
+
const settlementId = yield* Schema.decodeEffect(
|
|
2606
3307
|
SettlementReservationSnapshot.fields.settlementId,
|
|
2607
3308
|
)(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
|
|
3309
|
+
|
|
2608
3310
|
reservation = SettlementReservationSnapshot.make({
|
|
2609
3311
|
settlementId,
|
|
2610
3312
|
outcome: reservationRow.value.outcome,
|
|
@@ -2616,6 +3318,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2616
3318
|
|
|
2617
3319
|
let abortIntent: AbortIntent | undefined;
|
|
2618
3320
|
const abortRow = yield* readAbortIntent(operation, validated.submissionId);
|
|
3321
|
+
|
|
2619
3322
|
if (Option.isSome(abortRow)) {
|
|
2620
3323
|
abortIntent = yield* abortIntentFromRow(
|
|
2621
3324
|
operation,
|
|
@@ -2633,11 +3336,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2633
3336
|
WHERE joined_host_submission_id = ${validated.submissionId}
|
|
2634
3337
|
ORDER BY queue_sequence ASC
|
|
2635
3338
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
3339
|
+
|
|
2636
3340
|
const joinSubmissions = yield* decodeSubmissionRows(
|
|
2637
3341
|
operation,
|
|
2638
3342
|
validated.submissionId,
|
|
2639
3343
|
joinRows,
|
|
2640
3344
|
);
|
|
3345
|
+
|
|
2641
3346
|
const joins = yield* Effect.forEach(joinSubmissions, (row) =>
|
|
2642
3347
|
decodeJoinSnapshot({
|
|
2643
3348
|
submissionId: row.submission_id,
|
|
@@ -2647,6 +3352,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2647
3352
|
);
|
|
2648
3353
|
|
|
2649
3354
|
let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
|
|
3355
|
+
|
|
2650
3356
|
if (submissionRow.joined_host_submission_id !== null) {
|
|
2651
3357
|
hostSubmissionId = yield* decodeSubmissionId(
|
|
2652
3358
|
submissionRow.joined_host_submission_id,
|
|
@@ -2654,6 +3360,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2654
3360
|
}
|
|
2655
3361
|
|
|
2656
3362
|
let suspension: SuspensionSnapshot | undefined;
|
|
3363
|
+
|
|
2657
3364
|
if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
|
|
2658
3365
|
const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(
|
|
2659
3366
|
Effect.mapError((error) =>
|
|
@@ -2665,6 +3372,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2665
3372
|
),
|
|
2666
3373
|
),
|
|
2667
3374
|
);
|
|
3375
|
+
|
|
2668
3376
|
suspension = yield* decodeSuspensionSnapshot({
|
|
2669
3377
|
reason,
|
|
2670
3378
|
suspendedAt: submissionRow.suspended_at,
|
|
@@ -2681,11 +3389,13 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2681
3389
|
}
|
|
2682
3390
|
|
|
2683
3391
|
const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
3392
|
+
|
|
2684
3393
|
const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
|
|
2685
3394
|
approvalIntentFromRow(operation, row),
|
|
2686
3395
|
);
|
|
2687
3396
|
|
|
2688
3397
|
const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
3398
|
+
|
|
2689
3399
|
const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
|
|
2690
3400
|
unknownResolutionIntentFromRow(operation, row),
|
|
2691
3401
|
);
|
|
@@ -2699,18 +3409,23 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2699
3409
|
WHERE parent_submission_id = ${validated.submissionId}
|
|
2700
3410
|
ORDER BY parent_tool_call_id ASC
|
|
2701
3411
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
3412
|
+
|
|
2702
3413
|
const decodedChildReservations = yield* decodeChildReservationRows(
|
|
2703
3414
|
operation,
|
|
2704
3415
|
validated.submissionId,
|
|
2705
3416
|
childReservationRows,
|
|
2706
3417
|
);
|
|
3418
|
+
|
|
2707
3419
|
const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
|
|
2708
3420
|
childReservationSnapshotFromRow(operation, row),
|
|
2709
3421
|
);
|
|
3422
|
+
|
|
2710
3423
|
const childAttachments: Array<ChildAttachmentSnapshot> = [];
|
|
3424
|
+
|
|
2711
3425
|
for (const row of decodedChildReservations) {
|
|
2712
3426
|
if (row.child_submission_id === null) continue;
|
|
2713
3427
|
const child = yield* readSubmission(operation, row.child_submission_id);
|
|
3428
|
+
|
|
2714
3429
|
if (Option.isNone(child)) continue;
|
|
2715
3430
|
childAttachments.push(
|
|
2716
3431
|
yield* decodeChildAttachmentSnapshot({
|
|
@@ -2725,6 +3440,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2725
3440
|
}
|
|
2726
3441
|
|
|
2727
3442
|
let parentLinkage: ParentLinkage | undefined;
|
|
3443
|
+
|
|
2728
3444
|
if (
|
|
2729
3445
|
submissionRow.parent_submission_id !== null &&
|
|
2730
3446
|
submissionRow.parent_tool_call_id !== null
|
|
@@ -2752,7 +3468,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2752
3468
|
});
|
|
2753
3469
|
}),
|
|
2754
3470
|
)
|
|
2755
|
-
.pipe(
|
|
3471
|
+
.pipe(
|
|
3472
|
+
Effect.catchTag("SqliteStorageError", (error) =>
|
|
3473
|
+
Effect.fail(internalFailure(operation)(error)),
|
|
3474
|
+
),
|
|
3475
|
+
);
|
|
2756
3476
|
});
|
|
2757
3477
|
|
|
2758
3478
|
return Context.make(
|
|
@@ -2784,6 +3504,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2784
3504
|
releaseChildBudget,
|
|
2785
3505
|
scanNonterminal,
|
|
2786
3506
|
loadRecoverySnapshot,
|
|
3507
|
+
readAbortIntent: readAbortIntentForSubmission,
|
|
2787
3508
|
}),
|
|
2788
3509
|
);
|
|
2789
3510
|
});
|
|
@@ -2801,18 +3522,22 @@ export const submissionLedgerLayer: Layer.Layer<
|
|
|
2801
3522
|
|
|
2802
3523
|
/**
|
|
2803
3524
|
* A composition-root convenience Layer for the durable Submission Ledger. Point it at the
|
|
2804
|
-
* same database file as the
|
|
3525
|
+
* same database file as the ThreadStore so claims fence the same producer epochs.
|
|
2805
3526
|
*/
|
|
2806
3527
|
export const ledgerLayer = (
|
|
2807
3528
|
options: SqliteStorageOptions,
|
|
2808
3529
|
): Layer.Layer<SubmissionLedger, SqliteStorageInitializationError> =>
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
3530
|
+
Layer.unwrap(
|
|
3531
|
+
Effect.map(SqliteStorageConfig, (config) =>
|
|
3532
|
+
submissionLedgerLayer.pipe(
|
|
3533
|
+
Layer.provide(
|
|
3534
|
+
Layer.mergeAll(
|
|
3535
|
+
Layer.succeed(SqliteStorageConfig)(config),
|
|
3536
|
+
storageFailpointLayer(options),
|
|
3537
|
+
SqliteClient.layer({ filename: options.filename }),
|
|
3538
|
+
NodeCrypto.layer,
|
|
3539
|
+
),
|
|
3540
|
+
),
|
|
2816
3541
|
),
|
|
2817
3542
|
),
|
|
2818
|
-
);
|
|
3543
|
+
).pipe(Layer.provide(storageConfigLayer(options)));
|