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