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