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