@yielded/agent-storage-cloudflare 0.1.0-beta.166 → 0.1.0-beta.168
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.mjs +1 -1
- package/dist/DoMessageDeliveryStore.d.mts +1 -1
- package/dist/DoMessageDeliveryStore.mjs +3 -2
- package/dist/DoMessageDeliveryStore.mjs.map +1 -1
- package/dist/DoScheduleStore.mjs +16 -37
- package/dist/DoScheduleStore.mjs.map +1 -1
- package/dist/DoStorageConfig.d.mts +2 -10
- package/dist/DoStorageConfig.mjs +2 -11
- package/dist/DoStorageConfig.mjs.map +1 -1
- package/dist/{DoStorageError-CGuAfSwF.d.mts → DoStorageError-CxjMnzUA.d.mts} +7 -18
- package/dist/DoStorageError.d.mts +2 -2
- package/dist/DoStorageError.mjs +14 -30
- package/dist/DoStorageError.mjs.map +1 -1
- package/dist/DoStorageFailpoint.d.mts +1 -1
- package/dist/DoStorageFailpointTesting.d.mts +1 -1
- package/dist/{DoStorageVersion-iNSyi3cJ.d.mts → DoStorageVersion-DTFoLglB.d.mts} +3 -4
- package/dist/DoStorageVersion.d.mts +1 -1
- package/dist/DoStorageVersion.mjs +2 -2
- package/dist/DoStorageVersion.mjs.map +1 -1
- package/dist/DoSubmissionLedger.d.mts +7 -5
- package/dist/DoSubmissionLedger.mjs +378 -528
- package/dist/DoSubmissionLedger.mjs.map +1 -1
- package/dist/DoSubscriptionStore.mjs +5 -24
- package/dist/DoSubscriptionStore.mjs.map +1 -1
- package/dist/{DoThreadStore-D_83OjCH.d.mts → DoThreadStore-Bd4fTns3.d.mts} +17 -18
- package/dist/DoThreadStore-CwBvTLoq.mjs +468 -0
- package/dist/DoThreadStore-CwBvTLoq.mjs.map +1 -0
- package/dist/DoThreadStore.d.mts +2 -2
- package/dist/DoThreadStore.mjs +4 -529
- package/dist/MemoryProtocol.mjs +3 -3
- package/dist/MemoryProtocol.mjs.map +1 -1
- package/dist/PortProtocol.d.mts +11727 -3410
- package/dist/PortProtocol.mjs +103 -15
- package/dist/PortProtocol.mjs.map +1 -1
- package/dist/PortRouting.d.mts +14 -9
- package/dist/PortRouting.mjs +155 -29
- package/dist/PortRouting.mjs.map +1 -1
- package/dist/do-journal-Cawnsetj.mjs +761 -0
- package/dist/do-journal-Cawnsetj.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +1 -1
- package/dist/migrations-D_EP3KYf.mjs +148 -0
- package/dist/migrations-D_EP3KYf.mjs.map +1 -0
- package/dist/{owned-state-DHm2aRXx.mjs → owned-state-BHzUi0eb.mjs} +50 -19
- package/dist/owned-state-BHzUi0eb.mjs.map +1 -0
- package/package.json +1 -1
- package/src/DoMessageDeliveryStore.ts +2 -1
- package/src/DoScheduleStore.ts +19 -59
- package/src/DoStorageConfig.ts +2 -11
- package/src/DoStorageError.ts +3 -26
- package/src/DoStorageVersion.ts +1 -1
- package/src/DoSubmissionLedger.ts +1806 -2030
- package/src/DoSubscriptionStore.ts +5 -47
- package/src/DoThreadStore.ts +276 -622
- package/src/MemoryProtocol.ts +3 -3
- package/src/PortProtocol.ts +198 -12
- package/src/PortRouting.ts +527 -108
- package/src/internal/canonical-append.ts +15 -0
- package/src/internal/do-journal.ts +548 -1440
- package/src/internal/migrations.ts +184 -287
- package/src/internal/owned-state.ts +81 -30
- package/src/internal/prompt-checkpoint.ts +158 -0
- package/src/internal/storage-span.ts +2 -5
- package/dist/DoThreadStore.mjs.map +0 -1
- package/dist/do-journal-DUhjZkOf.mjs +0 -1352
- package/dist/do-journal-DUhjZkOf.mjs.map +0 -1
- package/dist/migrations-DtKz_Gxr.mjs +0 -306
- package/dist/migrations-DtKz_Gxr.mjs.map +0 -1
- package/dist/owned-state-DHm2aRXx.mjs.map +0 -1
- package/src/internal/message-delivery-schema.ts +0 -24
- package/src/internal/recovery-checkpoint-schema.ts +0 -17
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { BrowserCrypto } from "@effect/platform-browser";
|
|
2
2
|
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
3
|
+
import {
|
|
4
|
+
decodeAbortFact,
|
|
5
|
+
decodeAdmissionFact,
|
|
6
|
+
decodeApprovalFact,
|
|
7
|
+
decodeResolutionFact,
|
|
8
|
+
} from "@yielded/agent-storage-sql/sql-admission-facts";
|
|
9
|
+
import {
|
|
10
|
+
definitionJson,
|
|
11
|
+
makeCanonicalStorage,
|
|
12
|
+
recordBatchId,
|
|
13
|
+
runKey,
|
|
14
|
+
} from "@yielded/agent-storage-sql/sql-canonical-storage";
|
|
3
15
|
import {
|
|
4
16
|
SqlStorageProgress,
|
|
5
17
|
type SqlStorageProgressKind,
|
|
6
18
|
} from "@yielded/agent-storage-sql/sql-storage-progress";
|
|
7
|
-
import {
|
|
19
|
+
import { canonicalRecordJson } from "@yielded/agent-storage-sql/sql-thread-archive-range";
|
|
8
20
|
import type { LifecyclePublicationFact } from "@yielded/agent/lifecycle-publication";
|
|
9
21
|
import { InputMessage } from "@yielded/agent/messaging";
|
|
10
22
|
import {
|
|
@@ -14,10 +26,18 @@ import {
|
|
|
14
26
|
Digest,
|
|
15
27
|
PersistedJson,
|
|
16
28
|
WorkerAdmission,
|
|
17
|
-
ProducerEpoch,
|
|
18
29
|
RecordEnvelope,
|
|
19
30
|
SettlementOutcome,
|
|
20
31
|
} from "@yielded/agent/records";
|
|
32
|
+
import { runIdForSubmission } from "@yielded/agent/run-journal";
|
|
33
|
+
import { StorageOwner } from "@yielded/agent/run-storage";
|
|
34
|
+
import {
|
|
35
|
+
SettlementPublicationResult,
|
|
36
|
+
SettlementPublisher,
|
|
37
|
+
validatePublication,
|
|
38
|
+
validateCanonicalSettlement,
|
|
39
|
+
validateJoinedSettlement,
|
|
40
|
+
} from "@yielded/agent/settlement-publisher";
|
|
21
41
|
import {
|
|
22
42
|
AbortCommand,
|
|
23
43
|
WorkerStopCommand,
|
|
@@ -37,6 +57,7 @@ import {
|
|
|
37
57
|
ApprovalConflict,
|
|
38
58
|
ApprovalDecisionCommand,
|
|
39
59
|
ApprovalDecisionIntent,
|
|
60
|
+
approvalDecisionsCover,
|
|
40
61
|
AttachChildToReservationRequest,
|
|
41
62
|
BeginChildBudgetReleaseRequest,
|
|
42
63
|
ChildAttachmentSnapshot,
|
|
@@ -59,29 +80,24 @@ import {
|
|
|
59
80
|
MarkReadyRequest,
|
|
60
81
|
MarkUnknownRequest,
|
|
61
82
|
OwnershipLost,
|
|
62
|
-
OwnershipRenewal,
|
|
63
|
-
OwnershipSnapshot,
|
|
64
83
|
ParentLinkage,
|
|
65
84
|
QueueSequence,
|
|
66
85
|
RecoverySnapshot,
|
|
67
86
|
RecoverySnapshotRequest,
|
|
68
87
|
ReleaseChildBudgetRequest,
|
|
69
|
-
ReleaseOwnershipRequest,
|
|
70
|
-
RenewOwnershipRequest,
|
|
71
88
|
ReservedChildBudget,
|
|
72
|
-
ReservedSettlement,
|
|
73
89
|
RevertJoiningRequest,
|
|
74
90
|
Settlement,
|
|
75
91
|
SettlementConflict,
|
|
76
92
|
SettlementFinalization,
|
|
77
|
-
SettlementReservation,
|
|
78
93
|
SubmissionLedger,
|
|
79
94
|
SubmissionLookup,
|
|
80
95
|
SubmissionLookupByKey,
|
|
81
96
|
SubmissionSnapshot,
|
|
97
|
+
FundingOwnerRequest,
|
|
98
|
+
FundingOwner,
|
|
82
99
|
SubmissionWorkItem,
|
|
83
100
|
SubmissionState,
|
|
84
|
-
SettlementReservationSnapshot,
|
|
85
101
|
settlementFailureFromRecord,
|
|
86
102
|
SuspendRequest,
|
|
87
103
|
SuspensionReason,
|
|
@@ -90,11 +106,22 @@ import {
|
|
|
90
106
|
UnknownResolutionCommand,
|
|
91
107
|
UnknownResolutionConflict,
|
|
92
108
|
UnknownResolutionIntent,
|
|
109
|
+
UnknownResolutionKind,
|
|
110
|
+
unknownResolutionKind,
|
|
111
|
+
unknownResolutionMatchesResult,
|
|
93
112
|
submissionAbortRecordId,
|
|
113
|
+
submissionSettlementRecordId,
|
|
114
|
+
submissionSettlementBatchId,
|
|
94
115
|
type ChildSettledOutcome,
|
|
95
116
|
type SuspensionOutcome,
|
|
96
117
|
} from "@yielded/agent/submission-ledger";
|
|
97
|
-
import {
|
|
118
|
+
import {
|
|
119
|
+
makeTransferFactCheck,
|
|
120
|
+
AppendConflict,
|
|
121
|
+
ThreadNotMaterialized,
|
|
122
|
+
ThreadStoreDiagnostic,
|
|
123
|
+
ThreadStoreError,
|
|
124
|
+
} from "@yielded/agent/thread-store";
|
|
98
125
|
import { AssignmentTerminal } from "@yielded/agent/worker";
|
|
99
126
|
import {
|
|
100
127
|
Cause,
|
|
@@ -125,9 +152,9 @@ import {
|
|
|
125
152
|
type DoStorageInitializationError,
|
|
126
153
|
type DoStorageOptions,
|
|
127
154
|
} from "./DoThreadStore.ts";
|
|
155
|
+
import { prepareCanonicalAppend } from "./internal/canonical-append.ts";
|
|
128
156
|
import { decodeRows, initializeDoJournal } from "./internal/do-journal.ts";
|
|
129
157
|
import { ownedRows } from "./internal/owned-state.ts";
|
|
130
|
-
import { withStorageSpan } from "./internal/storage-span.ts";
|
|
131
158
|
|
|
132
159
|
type SubmissionId = SubmissionSnapshot["submissionId"];
|
|
133
160
|
|
|
@@ -140,7 +167,7 @@ const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
|
|
|
140
167
|
const BoundedTimestamp = Schema.NonEmptyString.check(Schema.isMaxLength(128));
|
|
141
168
|
|
|
142
169
|
const SCAN_PAGE_SIZE = 256;
|
|
143
|
-
const
|
|
170
|
+
const admissionFitsTransfer = makeTransferFactCheck(AdmissionRequest);
|
|
144
171
|
const RESUME_IMMEDIATELY: SuspensionOutcome = "resume-immediately";
|
|
145
172
|
const SUSPENDED: SuspensionOutcome = "suspended";
|
|
146
173
|
const NOT_WAITING: ChildSettledOutcome = "not-waiting";
|
|
@@ -162,6 +189,8 @@ class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
|
|
|
162
189
|
receipt_id: BoundedIdentifier,
|
|
163
190
|
state: SubmissionState,
|
|
164
191
|
settled_outcome: Schema.NullOr(SettlementOutcome),
|
|
192
|
+
settled_record_id: Schema.NullOr(BoundedIdentifier),
|
|
193
|
+
finalized_at: Schema.NullOr(BoundedTimestamp),
|
|
165
194
|
created_at: BoundedTimestamp,
|
|
166
195
|
ready_at: Schema.NullOr(BoundedTimestamp),
|
|
167
196
|
input_applied_record_id: Schema.NullOr(BoundedIdentifier),
|
|
@@ -226,32 +255,13 @@ class ApprovalDecisionRow extends Schema.Class<ApprovalDecisionRow>("ApprovalDec
|
|
|
226
255
|
class UnknownResolutionRow extends Schema.Class<UnknownResolutionRow>("UnknownResolutionRow")({
|
|
227
256
|
submission_id: BoundedIdentifier,
|
|
228
257
|
tool_call_id: BoundedIdentifier,
|
|
258
|
+
resolution_kind: UnknownResolutionKind,
|
|
229
259
|
author: BoundedIdentifier,
|
|
230
260
|
reason: BoundedStoredText,
|
|
231
261
|
resolution_json: BoundedStoredText,
|
|
232
262
|
resolved_at: BoundedTimestamp,
|
|
233
263
|
}) {}
|
|
234
264
|
|
|
235
|
-
class OwnershipRow extends Schema.Class<OwnershipRow>("OwnershipRow")({
|
|
236
|
-
submission_id: BoundedIdentifier,
|
|
237
|
-
attempt_id: BoundedIdentifier,
|
|
238
|
-
ownership_token: BoundedIdentifier,
|
|
239
|
-
producer_epoch: ProducerEpoch,
|
|
240
|
-
owner_producer_id: BoundedIdentifier,
|
|
241
|
-
lease_expires_at: BoundedTimestamp,
|
|
242
|
-
}) {}
|
|
243
|
-
|
|
244
|
-
class ReservationRow extends Schema.Class<ReservationRow>("ReservationRow")({
|
|
245
|
-
submission_id: BoundedIdentifier,
|
|
246
|
-
settlement_id: BoundedIdentifier,
|
|
247
|
-
outcome: SettlementOutcome,
|
|
248
|
-
record_id: BoundedIdentifier,
|
|
249
|
-
record_json: BoundedStoredText,
|
|
250
|
-
record_digest: Digest,
|
|
251
|
-
reserved_at: BoundedTimestamp,
|
|
252
|
-
finalized_at: Schema.NullOr(BoundedTimestamp),
|
|
253
|
-
}) {}
|
|
254
|
-
|
|
255
265
|
class AbortIntentRow extends Schema.Class<AbortIntentRow>("AbortIntentRow")({
|
|
256
266
|
submission_id: BoundedIdentifier,
|
|
257
267
|
author: BoundedIdentifier,
|
|
@@ -276,13 +286,14 @@ const SUBMISSION_COLUMNS = `
|
|
|
276
286
|
principal,
|
|
277
287
|
idempotency_key,
|
|
278
288
|
agent_id,
|
|
279
|
-
agent_digests_json,
|
|
280
289
|
deployment_id,
|
|
281
290
|
input_json,
|
|
282
291
|
input_digest,
|
|
283
292
|
receipt_id,
|
|
284
293
|
state,
|
|
285
294
|
settled_outcome,
|
|
295
|
+
settled_record_id,
|
|
296
|
+
finalized_at,
|
|
286
297
|
created_at,
|
|
287
298
|
ready_at,
|
|
288
299
|
input_applied_record_id,
|
|
@@ -320,7 +331,6 @@ const ToolCallIdList = Schema.Array(ToolCallIdSchema);
|
|
|
320
331
|
|
|
321
332
|
const encodePersistedJsonText = Schema.encodeEffect(Schema.fromJsonString(PersistedJson));
|
|
322
333
|
const encodeDefinitionDigestsText = Schema.encodeEffect(Schema.fromJsonString(DefinitionDigests));
|
|
323
|
-
const encodeRecordEnvelopeText = Schema.encodeEffect(Schema.fromJsonString(RecordEnvelope));
|
|
324
334
|
const decodeRecordEnvelopeText = Schema.decodeEffect(Schema.fromJsonString(RecordEnvelope));
|
|
325
335
|
const encodeSuspensionReasonText = Schema.encodeEffect(Schema.fromJsonString(SuspensionReason));
|
|
326
336
|
const encodeUnknownResolutionText = Schema.encodeEffect(Schema.fromJsonString(UnknownResolution));
|
|
@@ -329,12 +339,15 @@ const decodeToolCallIdsText = Schema.decodeEffect(Schema.fromJsonString(ToolCall
|
|
|
329
339
|
const parseStoredJsonText = Schema.decodeEffect(Schema.fromJsonString(Schema.Json));
|
|
330
340
|
const decodeAdmissionResult = Schema.decodeUnknownEffect(AdmissionResult);
|
|
331
341
|
const decodeClaim = Schema.decodeUnknownEffect(Claim);
|
|
332
|
-
const decodeOwnershipRenewal = Schema.decodeUnknownEffect(OwnershipRenewal);
|
|
333
342
|
const decodeSettlement = Schema.decodeUnknownEffect(Settlement);
|
|
334
343
|
const decodeAbortIntent = Schema.decodeUnknownEffect(AbortIntent);
|
|
335
|
-
const decodeOwnershipSnapshot = Schema.decodeUnknownEffect(OwnershipSnapshot);
|
|
336
344
|
const decodeInputAppliedMarker = Schema.decodeUnknownEffect(InputAppliedMarker);
|
|
337
|
-
|
|
345
|
+
|
|
346
|
+
const decodeSubmissionSnapshotType = (input: typeof SubmissionSnapshot.Type) =>
|
|
347
|
+
Schema.decodeEffect(Schema.toType(Schema.Struct(SubmissionSnapshot.fields)))(input).pipe(
|
|
348
|
+
Effect.map((fields) => SubmissionSnapshot.make(fields)),
|
|
349
|
+
);
|
|
350
|
+
|
|
338
351
|
const decodeSubmissionId = Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId);
|
|
339
352
|
const decodeQueueSequence = Schema.decodeUnknownEffect(QueueSequence);
|
|
340
353
|
const decodeUtcInstant = Schema.decodeUnknownEffect(Schema.DateTimeUtcFromString);
|
|
@@ -385,20 +398,7 @@ const submissionsRows = ownedRows(
|
|
|
385
398
|
(row) => row.submission_id,
|
|
386
399
|
"submission_id",
|
|
387
400
|
[["thread_id", "principal", "idempotency_key"]],
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
const ownershipRows = ownedRows(
|
|
391
|
-
OwnershipRow,
|
|
392
|
-
"effect_agent_submission_ownership",
|
|
393
|
-
(row) => row.submission_id,
|
|
394
|
-
"submission_id",
|
|
395
|
-
);
|
|
396
|
-
|
|
397
|
-
const reservationsRows = ownedRows(
|
|
398
|
-
ReservationRow,
|
|
399
|
-
"effect_agent_settlement_reservations",
|
|
400
|
-
(row) => row.submission_id,
|
|
401
|
-
"submission_id",
|
|
401
|
+
(sql) => sql`*, ${definitionJson(sql)} AS agent_digests_json`,
|
|
402
402
|
);
|
|
403
403
|
|
|
404
404
|
const abortsRows = ownedRows(
|
|
@@ -413,7 +413,7 @@ const approvalsRows = ownedRows(ApprovalDecisionRow, "effect_agent_approval_deci
|
|
|
413
413
|
);
|
|
414
414
|
|
|
415
415
|
const resolutionsRows = ownedRows(UnknownResolutionRow, "effect_agent_unknown_resolutions", (row) =>
|
|
416
|
-
JSON.stringify([row.submission_id, row.tool_call_id]),
|
|
416
|
+
JSON.stringify([row.submission_id, row.tool_call_id, row.resolution_kind]),
|
|
417
417
|
);
|
|
418
418
|
|
|
419
419
|
const childSettlementsRows = ownedRows(
|
|
@@ -442,7 +442,7 @@ const submissionWorkRows = ownedRows(
|
|
|
442
442
|
(row) => row.submission_id,
|
|
443
443
|
);
|
|
444
444
|
|
|
445
|
-
const makeServices = Effect.
|
|
445
|
+
const makeServices = Effect.fnUntraced(function* () {
|
|
446
446
|
const progress = yield* SqlStorageProgress;
|
|
447
447
|
|
|
448
448
|
const recordProgress = (kind: SqlStorageProgressKind, operation: string) =>
|
|
@@ -458,10 +458,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
458
458
|
const failpoint = yield* DoStorageFailpoint;
|
|
459
459
|
const sql = yield* SqlClientService.SqlClient;
|
|
460
460
|
const crypto = yield* Crypto.Crypto;
|
|
461
|
+
const physical = yield* makeCanonicalStorage();
|
|
462
|
+
const submissionColumns = sql`${sql.literal(SUBMISSION_COLUMNS)}, ${definitionJson(sql)} AS agent_digests_json`;
|
|
461
463
|
const admissionFence = yield* SubmissionAdmissionFence;
|
|
462
464
|
const journal = yield* initializeDoJournal(sql, failpoint.hit, config.maxStoredValueBytes);
|
|
463
465
|
const lifecycle = journal.lifecycle;
|
|
464
466
|
const state = journal.state;
|
|
467
|
+
const owner = state.owner;
|
|
465
468
|
const submissionViews = submissionsRows(state, sql);
|
|
466
469
|
const workRows = submissionWorkRows(state, sql);
|
|
467
470
|
|
|
@@ -476,8 +479,6 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
476
479
|
.write(effect)
|
|
477
480
|
.pipe(Effect.tap((changed) => workRows.write(Effect.succeed(changed)))),
|
|
478
481
|
},
|
|
479
|
-
ownership: ownershipRows(state, sql),
|
|
480
|
-
reservations: reservationsRows(state, sql),
|
|
481
482
|
aborts: abortsRows(state, sql),
|
|
482
483
|
approvals: approvalsRows(state, sql),
|
|
483
484
|
resolutions: resolutionsRows(state, sql),
|
|
@@ -492,46 +493,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
492
493
|
operation: string,
|
|
493
494
|
) => effect.pipe(Effect.mapError(internalFailure(operation)));
|
|
494
495
|
|
|
495
|
-
// Small lanes share control metadata; large retained histories keep indexed SQL reads.
|
|
496
|
-
const laneWork = Effect.fnUntraced(function* (threadId: string, operation: string) {
|
|
497
|
-
const work = yield* cached(
|
|
498
|
-
workRows.matching(
|
|
499
|
-
JSON.stringify(["lane", threadId]),
|
|
500
|
-
(row) => row.thread_id === threadId,
|
|
501
|
-
sql`SELECT submission_id, thread_id, queue_sequence, principal, idempotency_key,
|
|
502
|
-
deployment_id, receipt_id, state FROM effect_agent_submissions WHERE thread_id = ${threadId} LIMIT 129`,
|
|
503
|
-
128,
|
|
504
|
-
),
|
|
505
|
-
operation,
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
return work.length > 128
|
|
509
|
-
? undefined
|
|
510
|
-
: [...work].sort((a, b) => a.queue_sequence - b.queue_sequence);
|
|
511
|
-
});
|
|
512
|
-
|
|
513
496
|
const byKey = Effect.fnUntraced(function* (
|
|
514
497
|
request: Pick<SubmissionLookupByKey, "threadId" | "principal" | "idempotencyKey">,
|
|
515
498
|
operation: string,
|
|
516
499
|
) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
["idempotency_key", request.idempotencyKey],
|
|
525
|
-
]),
|
|
526
|
-
operation,
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
const matches = lane.filter(
|
|
530
|
-
(row) =>
|
|
531
|
-
row.principal === request.principal && row.idempotency_key === request.idempotencyKey,
|
|
500
|
+
return yield* cached(
|
|
501
|
+
rows.submissions.byFields([
|
|
502
|
+
["thread_id", request.threadId],
|
|
503
|
+
["principal", request.principal],
|
|
504
|
+
["idempotency_key", request.idempotencyKey],
|
|
505
|
+
]),
|
|
506
|
+
operation,
|
|
532
507
|
);
|
|
533
|
-
|
|
534
|
-
return yield* Effect.forEach(matches, (row) => requireSubmission(operation, row.submission_id));
|
|
535
508
|
}, state.read);
|
|
536
509
|
|
|
537
510
|
const retainLifecycle = (submission: SubmissionRow, fact: LifecyclePublicationFact) =>
|
|
@@ -624,27 +597,19 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
624
597
|
);
|
|
625
598
|
|
|
626
599
|
const mintUuid = (operation: string): Effect.Effect<string, LedgerError> =>
|
|
627
|
-
crypto.randomUUIDv7.pipe(Effect.mapError((error) => internalFailure(operation)(error)));
|
|
600
|
+
crypto.randomUUIDv7().pipe(Effect.mapError((error) => internalFailure(operation)(error)));
|
|
628
601
|
|
|
629
602
|
const currentInstant = Effect.map(Clock.currentTimeMillis, (millis) => ({
|
|
630
603
|
millis,
|
|
631
604
|
iso: new Date(millis).toISOString(),
|
|
632
605
|
}));
|
|
633
606
|
|
|
634
|
-
const timestampMillis = (operation: string, rowKey: string) => (timestamp: string) =>
|
|
635
|
-
decodeUtcInstant(timestamp).pipe(
|
|
636
|
-
Effect.map(DateTime.toEpochMillis),
|
|
637
|
-
Effect.mapError((error) =>
|
|
638
|
-
corruptionFailure(operation, "effect_agent_submission_ownership", rowKey, error.message),
|
|
639
|
-
),
|
|
640
|
-
);
|
|
641
|
-
|
|
642
607
|
const decodeSubmissionRows = (operation: string, rowKey: string, rows: unknown) =>
|
|
643
608
|
decodeRows(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(
|
|
644
609
|
Effect.mapError(internalFailure(operation)),
|
|
645
610
|
);
|
|
646
611
|
|
|
647
|
-
const readSubmission = Effect.
|
|
612
|
+
const readSubmission = Effect.fnUntraced(function* (
|
|
648
613
|
operation: string,
|
|
649
614
|
submissionId: string,
|
|
650
615
|
): Effect.fn.Return<Option.Option<SubmissionRow>, LedgerError> {
|
|
@@ -653,7 +618,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
653
618
|
return Option.fromUndefinedOr(found[0]);
|
|
654
619
|
});
|
|
655
620
|
|
|
656
|
-
const requireSubmission = Effect.
|
|
621
|
+
const requireSubmission = Effect.fnUntraced(function* (
|
|
657
622
|
operation: string,
|
|
658
623
|
submissionId: string,
|
|
659
624
|
): Effect.fn.Return<SubmissionRow, LedgerError> {
|
|
@@ -669,167 +634,122 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
669
634
|
return submission.value;
|
|
670
635
|
});
|
|
671
636
|
|
|
672
|
-
const
|
|
637
|
+
const requireOwner = Effect.fnUntraced(function* (
|
|
673
638
|
operation: string,
|
|
674
|
-
|
|
675
|
-
): Effect.fn.Return<
|
|
676
|
-
|
|
639
|
+
submission: SubmissionRow,
|
|
640
|
+
): Effect.fn.Return<void, OwnershipLost | LedgerError> {
|
|
641
|
+
if (owner.hasOwner(submission.thread_id, submission.submission_id)) return;
|
|
677
642
|
|
|
678
|
-
|
|
643
|
+
const submissionId = yield* decodeSubmissionId(submission.submission_id).pipe(
|
|
644
|
+
Effect.mapError(internalFailure(operation)),
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
return yield* OwnershipLost.make({ submissionId });
|
|
679
648
|
});
|
|
680
649
|
|
|
681
|
-
const
|
|
682
|
-
operation: string,
|
|
683
|
-
threadId: string,
|
|
684
|
-
): Effect.fn.Return<ProducerEpoch, LedgerError> {
|
|
685
|
-
const threads = yield* journal
|
|
686
|
-
.getThread(threadId)
|
|
687
|
-
.pipe(Effect.mapError(internalFailure(operation)));
|
|
650
|
+
const submissions = new WeakMap<SubmissionRow, SubmissionSnapshot>();
|
|
688
651
|
|
|
689
|
-
|
|
690
|
-
|
|
652
|
+
const decodeSubmissionSnapshot = Effect.fnUntraced(function* (
|
|
653
|
+
operation: string,
|
|
654
|
+
row: SubmissionRow,
|
|
655
|
+
): Effect.fn.Return<SubmissionSnapshot, LedgerError> {
|
|
656
|
+
const existing = submissions.get(row);
|
|
657
|
+
|
|
658
|
+
if (existing !== undefined) return existing;
|
|
659
|
+
|
|
660
|
+
const decodeFailure = (error: Schema.SchemaError) =>
|
|
661
|
+
internalFailure(operation)(
|
|
662
|
+
DoStorageCorruptionError.make({
|
|
663
|
+
table: "effect_agent_submissions",
|
|
664
|
+
rowKey: row.submission_id,
|
|
665
|
+
message: "Stored submission does not satisfy the ledger schema",
|
|
666
|
+
diagnostic: ThreadStoreDiagnostic.make({
|
|
667
|
+
causeTag: error._tag,
|
|
668
|
+
operation,
|
|
669
|
+
decoder: "SubmissionSnapshot",
|
|
670
|
+
issueTag: error.issue._tag,
|
|
671
|
+
}),
|
|
672
|
+
}),
|
|
673
|
+
);
|
|
691
674
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
submission: SubmissionRow,
|
|
701
|
-
ownershipToken: string,
|
|
702
|
-
): Effect.fn.Return<OwnershipRow, OwnershipLost | LedgerError> {
|
|
703
|
-
const ownership = yield* readOwnership(operation, submission.submission_id);
|
|
704
|
-
const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
|
|
675
|
+
if ((row.parent_submission_id === null) !== (row.parent_tool_call_id === null)) {
|
|
676
|
+
return yield* corruptionFailure(
|
|
677
|
+
operation,
|
|
678
|
+
"effect_agent_submissions",
|
|
679
|
+
row.submission_id,
|
|
680
|
+
"A parent linkage must record both the parent Submission and the parent Tool Call.",
|
|
681
|
+
);
|
|
682
|
+
}
|
|
705
683
|
|
|
706
|
-
|
|
707
|
-
Option.isNone(ownership) ||
|
|
708
|
-
ownership.value.ownership_token !== ownershipToken ||
|
|
709
|
-
ownership.value.producer_epoch !== actualEpoch
|
|
710
|
-
) {
|
|
711
|
-
const submissionId = yield* Schema.decodeEffect(SubmissionSnapshot.fields.submissionId)(
|
|
712
|
-
submission.submission_id,
|
|
713
|
-
).pipe(Effect.mapError(internalFailure(operation)));
|
|
684
|
+
const admission = yield* decodeAdmissionFact(row).pipe(Effect.mapError(decodeFailure));
|
|
714
685
|
|
|
715
|
-
|
|
716
|
-
|
|
686
|
+
const readyAt =
|
|
687
|
+
row.ready_at === null
|
|
688
|
+
? undefined
|
|
689
|
+
: yield* decodeUtcInstant(row.ready_at).pipe(Effect.mapError(decodeFailure));
|
|
717
690
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
(
|
|
723
|
-
)
|
|
724
|
-
);
|
|
691
|
+
const snapshot = yield* decodeSubmissionSnapshotType({
|
|
692
|
+
...admission,
|
|
693
|
+
state: row.state,
|
|
694
|
+
...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
|
|
695
|
+
...(readyAt === undefined ? {} : { readyAt }),
|
|
696
|
+
}).pipe(Effect.mapError(decodeFailure));
|
|
725
697
|
|
|
726
|
-
|
|
698
|
+
submissions.set(row, snapshot);
|
|
727
699
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
operation: string,
|
|
731
|
-
row: SubmissionRow,
|
|
732
|
-
): Effect.fn.Return<SubmissionSnapshot, LedgerError> {
|
|
733
|
-
const existing = submissions.get(row);
|
|
734
|
-
|
|
735
|
-
if (existing !== undefined) return existing;
|
|
736
|
-
|
|
737
|
-
const decodeFailure = (error: Schema.SchemaError) =>
|
|
738
|
-
internalFailure(operation)(
|
|
739
|
-
DoStorageCorruptionError.make({
|
|
740
|
-
table: "effect_agent_submissions",
|
|
741
|
-
rowKey: row.submission_id,
|
|
742
|
-
message: "Stored submission does not satisfy the ledger schema",
|
|
743
|
-
diagnostic: ThreadStoreDiagnostic.make({
|
|
744
|
-
causeTag: error._tag,
|
|
745
|
-
operation,
|
|
746
|
-
decoder: "SubmissionSnapshot",
|
|
747
|
-
issueTag: error.issue._tag,
|
|
748
|
-
}),
|
|
749
|
-
}),
|
|
750
|
-
);
|
|
700
|
+
return snapshot;
|
|
701
|
+
});
|
|
751
702
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
703
|
+
const readCanonicalSettlement = Effect.fnUntraced(function* (
|
|
704
|
+
operation: string,
|
|
705
|
+
submission: SubmissionRow,
|
|
706
|
+
) {
|
|
707
|
+
const expected = yield* decodeSubmissionSnapshot(operation, submission);
|
|
708
|
+
const recordId = submissionSettlementRecordId(expected.submissionId);
|
|
755
709
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
710
|
+
const found =
|
|
711
|
+
yield* sql`SELECT ${recordBatchId(sql)} AS batch_id, record_id, ${canonicalRecordJson(sql)} AS record_json
|
|
712
|
+
FROM effect_agent_canonical_records WHERE thread_id = ${submission.thread_id}
|
|
713
|
+
AND record_id = ${JSON.stringify(recordId)}`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
759
714
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
)
|
|
767
|
-
|
|
715
|
+
const decoded = yield* decodeRows(
|
|
716
|
+
Schema.Array(
|
|
717
|
+
Schema.Struct({
|
|
718
|
+
batch_id: Schema.fromJsonString(BoundedIdentifier),
|
|
719
|
+
record_id: Schema.fromJsonString(BoundedIdentifier),
|
|
720
|
+
record_json: BoundedStoredText,
|
|
721
|
+
}),
|
|
722
|
+
),
|
|
723
|
+
"effect_agent_canonical_records",
|
|
724
|
+
recordId,
|
|
725
|
+
found,
|
|
726
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
768
727
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
threadId: row.thread_id,
|
|
772
|
-
queueSequence: row.queue_sequence,
|
|
773
|
-
principal: row.principal,
|
|
774
|
-
idempotencyKey: row.idempotency_key,
|
|
775
|
-
agentId: row.agent_id,
|
|
776
|
-
agentDigests,
|
|
777
|
-
deploymentId: row.deployment_id,
|
|
778
|
-
inputPayload,
|
|
779
|
-
inputDigest: row.input_digest,
|
|
780
|
-
receiptId: row.receipt_id,
|
|
781
|
-
state: row.state,
|
|
782
|
-
createdAt: row.created_at,
|
|
783
|
-
...(row.admission_group === null ? {} : { admissionGroup: row.admission_group }),
|
|
784
|
-
...(row.worker_admission_json === null
|
|
785
|
-
? {}
|
|
786
|
-
: {
|
|
787
|
-
workerAdmission: yield* parseStoredJsonText(row.worker_admission_json).pipe(
|
|
788
|
-
Effect.mapError(decodeFailure),
|
|
789
|
-
),
|
|
790
|
-
}),
|
|
791
|
-
...(row.message_admission_json === null
|
|
792
|
-
? {}
|
|
793
|
-
: {
|
|
794
|
-
messageAdmission: yield* parseStoredJsonText(row.message_admission_json).pipe(
|
|
795
|
-
Effect.mapError(decodeFailure),
|
|
796
|
-
),
|
|
797
|
-
}),
|
|
798
|
-
...(row.admission_fence_json === null
|
|
799
|
-
? {}
|
|
800
|
-
: {
|
|
801
|
-
admissionFence: yield* parseStoredJsonText(row.admission_fence_json).pipe(
|
|
802
|
-
Effect.mapError(decodeFailure),
|
|
803
|
-
),
|
|
804
|
-
}),
|
|
805
|
-
...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
|
|
806
|
-
...(row.ready_at === null ? {} : { readyAt: row.ready_at }),
|
|
807
|
-
...(row.parent_submission_id === null || row.parent_tool_call_id === null
|
|
808
|
-
? {}
|
|
809
|
-
: {
|
|
810
|
-
parentLinkage: {
|
|
811
|
-
parentSubmissionId: row.parent_submission_id,
|
|
812
|
-
parentToolCallId: row.parent_tool_call_id,
|
|
813
|
-
},
|
|
814
|
-
}),
|
|
815
|
-
}).pipe(Effect.mapError(decodeFailure));
|
|
728
|
+
if (decoded.length === 0) return undefined;
|
|
729
|
+
const row = decoded[0];
|
|
816
730
|
|
|
817
|
-
|
|
731
|
+
if (
|
|
732
|
+
decoded.length !== 1 ||
|
|
733
|
+
row.batch_id !== submissionSettlementBatchId(expected.submissionId) ||
|
|
734
|
+
row.record_id !== recordId
|
|
735
|
+
)
|
|
736
|
+
return yield* corruptionFailure(
|
|
737
|
+
operation,
|
|
738
|
+
"effect_agent_canonical_records",
|
|
739
|
+
recordId,
|
|
740
|
+
"Canonical settlement identity is inconsistent.",
|
|
741
|
+
);
|
|
818
742
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
743
|
+
const record = yield* decodeRecordEnvelopeText(row.record_json).pipe(
|
|
744
|
+
Effect.mapError(internalFailure(operation)),
|
|
745
|
+
);
|
|
822
746
|
|
|
823
|
-
|
|
824
|
-
operation: string,
|
|
825
|
-
submissionId: string,
|
|
826
|
-
): Effect.fn.Return<Option.Option<ReservationRow>, LedgerError> {
|
|
827
|
-
const found = yield* cached(rows.reservations.by("submission_id", submissionId), operation);
|
|
747
|
+
const settlement = yield* validateCanonicalSettlement(record, expected);
|
|
828
748
|
|
|
829
|
-
return
|
|
749
|
+
return { record, settlement };
|
|
830
750
|
});
|
|
831
751
|
|
|
832
|
-
const readAbortIntent = Effect.
|
|
752
|
+
const readAbortIntent = Effect.fnUntraced(function* (
|
|
833
753
|
operation: string,
|
|
834
754
|
submissionId: string,
|
|
835
755
|
): Effect.fn.Return<Option.Option<AbortIntentRow>, LedgerError> {
|
|
@@ -843,25 +763,23 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
843
763
|
* Submission (the DC realization of the port's "cross-store adapters record a durable
|
|
844
764
|
* notification marker" contract).
|
|
845
765
|
*/
|
|
846
|
-
const readChildSettlementMarkers = Effect.
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
);
|
|
766
|
+
const readChildSettlementMarkers = Effect.fnUntraced(function* (
|
|
767
|
+
operation: string,
|
|
768
|
+
parentSubmissionId: string,
|
|
769
|
+
): Effect.fn.Return<ReadonlyArray<ChildSettlementMarkerRow>, LedgerError> {
|
|
770
|
+
const found = yield* cached(
|
|
771
|
+
rows.childSettlements.by("parent_submission_id", parentSubmissionId),
|
|
772
|
+
operation,
|
|
773
|
+
);
|
|
855
774
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
);
|
|
775
|
+
return found.toSorted((a, b) =>
|
|
776
|
+
a.child_submission_id < b.child_submission_id
|
|
777
|
+
? -1
|
|
778
|
+
: a.child_submission_id > b.child_submission_id
|
|
779
|
+
? 1
|
|
780
|
+
: 0,
|
|
781
|
+
);
|
|
782
|
+
});
|
|
865
783
|
|
|
866
784
|
/**
|
|
867
785
|
* Whether one listed child is provably settled from THIS store: either its own row lives
|
|
@@ -869,7 +787,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
869
787
|
* cross-store notification marker was recorded for it (the child's row lives in another
|
|
870
788
|
* Durable Object and its owner reported the settlement through `recordChildSettled`).
|
|
871
789
|
*/
|
|
872
|
-
const childProvablySettled = Effect.
|
|
790
|
+
const childProvablySettled = Effect.fnUntraced(function* (
|
|
873
791
|
operation: string,
|
|
874
792
|
markerChildren: ReadonlySet<string>,
|
|
875
793
|
childSubmissionId: string,
|
|
@@ -877,7 +795,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
877
795
|
if (markerChildren.has(childSubmissionId)) return true;
|
|
878
796
|
const childRow = yield* readSubmission(operation, childSubmissionId);
|
|
879
797
|
|
|
880
|
-
return
|
|
798
|
+
return (
|
|
799
|
+
Option.isSome(childRow) &&
|
|
800
|
+
(yield* readCanonicalSettlement(operation, childRow.value)) !== undefined
|
|
801
|
+
);
|
|
881
802
|
});
|
|
882
803
|
|
|
883
804
|
const decodeChildReservationRows = (operation: string, rowKey: string, rows: unknown) =>
|
|
@@ -888,7 +809,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
888
809
|
rows,
|
|
889
810
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
890
811
|
|
|
891
|
-
const readChildReservation = Effect.
|
|
812
|
+
const readChildReservation = Effect.fnUntraced(function* (
|
|
892
813
|
operation: string,
|
|
893
814
|
reservationId: string,
|
|
894
815
|
): Effect.fn.Return<Option.Option<ChildReservationRow>, LedgerError> {
|
|
@@ -912,41 +833,37 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
912
833
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
913
834
|
});
|
|
914
835
|
|
|
915
|
-
const readChildReservationForCall = Effect.
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
const rows = yield* sql<Record<string, unknown>>`
|
|
836
|
+
const readChildReservationForCall = Effect.fnUntraced(function* (
|
|
837
|
+
operation: string,
|
|
838
|
+
parentSubmissionId: string,
|
|
839
|
+
parentToolCallId: string,
|
|
840
|
+
): Effect.fn.Return<Option.Option<ChildReservationRow>, LedgerError> {
|
|
841
|
+
const rows = yield* sql<Record<string, unknown>>`
|
|
922
842
|
SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
|
|
923
843
|
FROM effect_agent_child_reservations
|
|
924
844
|
WHERE parent_submission_id = ${parentSubmissionId}
|
|
925
845
|
AND parent_tool_call_id = ${parentToolCallId}
|
|
926
846
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
927
847
|
|
|
928
|
-
|
|
848
|
+
const decoded = yield* decodeChildReservationRows(
|
|
849
|
+
operation,
|
|
850
|
+
`${parentSubmissionId}/${parentToolCallId}`,
|
|
851
|
+
rows,
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
if (decoded.length > 1) {
|
|
855
|
+
return yield* corruptionFailure(
|
|
929
856
|
operation,
|
|
857
|
+
"effect_agent_child_reservations",
|
|
930
858
|
`${parentSubmissionId}/${parentToolCallId}`,
|
|
931
|
-
|
|
859
|
+
"A parent Tool Call returned more than one child reservation.",
|
|
932
860
|
);
|
|
861
|
+
}
|
|
933
862
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
operation,
|
|
937
|
-
"effect_agent_child_reservations",
|
|
938
|
-
`${parentSubmissionId}/${parentToolCallId}`,
|
|
939
|
-
"A parent Tool Call returned more than one child reservation.",
|
|
940
|
-
);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
944
|
-
},
|
|
945
|
-
);
|
|
863
|
+
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
864
|
+
});
|
|
946
865
|
|
|
947
|
-
const childReservationSnapshotFromRow = Effect.
|
|
948
|
-
"DoSubmissionLedger.childReservationSnapshotFromRow",
|
|
949
|
-
)(function* (
|
|
866
|
+
const childReservationSnapshotFromRow = Effect.fnUntraced(function* (
|
|
950
867
|
operation: string,
|
|
951
868
|
row: ChildReservationRow,
|
|
952
869
|
): Effect.fn.Return<ChildBudgetReservationSnapshot, LedgerError> {
|
|
@@ -982,7 +899,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
982
899
|
}).pipe(Effect.mapError(rowFailure));
|
|
983
900
|
});
|
|
984
901
|
|
|
985
|
-
const readApprovalDecisions = Effect.
|
|
902
|
+
const readApprovalDecisions = Effect.fnUntraced(function* (
|
|
986
903
|
operation: string,
|
|
987
904
|
submissionId: string,
|
|
988
905
|
): Effect.fn.Return<ReadonlyArray<ApprovalDecisionRow>, LedgerError> {
|
|
@@ -993,18 +910,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
993
910
|
);
|
|
994
911
|
});
|
|
995
912
|
|
|
996
|
-
const approvalIntentFromRow =
|
|
913
|
+
const approvalIntentFromRow = (
|
|
997
914
|
operation: string,
|
|
998
915
|
row: ApprovalDecisionRow,
|
|
999
|
-
): Effect.
|
|
1000
|
-
|
|
1001
|
-
submissionId: row.submission_id,
|
|
1002
|
-
toolCallId: row.tool_call_id,
|
|
1003
|
-
decision: row.decision,
|
|
1004
|
-
resolver: row.resolver,
|
|
1005
|
-
reason: row.reason,
|
|
1006
|
-
decidedAt: row.decided_at,
|
|
1007
|
-
}).pipe(
|
|
916
|
+
): Effect.Effect<ApprovalDecisionIntent, LedgerError> =>
|
|
917
|
+
decodeApprovalFact(row).pipe(
|
|
1008
918
|
Effect.mapError((error) =>
|
|
1009
919
|
corruptionFailure(
|
|
1010
920
|
operation,
|
|
@@ -1014,9 +924,8 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1014
924
|
),
|
|
1015
925
|
),
|
|
1016
926
|
);
|
|
1017
|
-
});
|
|
1018
927
|
|
|
1019
|
-
const readUnknownResolutions = Effect.
|
|
928
|
+
const readUnknownResolutions = Effect.fnUntraced(function* (
|
|
1020
929
|
operation: string,
|
|
1021
930
|
submissionId: string,
|
|
1022
931
|
): Effect.fn.Return<ReadonlyArray<UnknownResolutionRow>, LedgerError> {
|
|
@@ -1027,13 +936,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1027
936
|
);
|
|
1028
937
|
});
|
|
1029
938
|
|
|
1030
|
-
const unknownResolutionIntentFromRow = Effect.
|
|
1031
|
-
"DoSubmissionLedger.unknownResolutionIntentFromRow",
|
|
1032
|
-
)(function* (
|
|
939
|
+
const unknownResolutionIntentFromRow = Effect.fnUntraced(function* (
|
|
1033
940
|
operation: string,
|
|
1034
941
|
row: UnknownResolutionRow,
|
|
1035
942
|
): Effect.fn.Return<UnknownResolutionIntent, LedgerError> {
|
|
1036
|
-
|
|
943
|
+
return yield* decodeResolutionFact(row).pipe(
|
|
1037
944
|
Effect.mapError((error) =>
|
|
1038
945
|
corruptionFailure(
|
|
1039
946
|
operation,
|
|
@@ -1043,57 +950,37 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1043
950
|
),
|
|
1044
951
|
),
|
|
1045
952
|
);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
/** The Submission's marked-unknown open Tool Call identities, empty when never marked. */
|
|
956
|
+
const storedUnknownToolCallIds = Effect.fnUntraced(function* (
|
|
957
|
+
operation: string,
|
|
958
|
+
submission: SubmissionRow,
|
|
959
|
+
): Effect.fn.Return<ReadonlyArray<typeof ToolCallIdSchema.Type>, LedgerError> {
|
|
960
|
+
if (submission.unknown_tool_call_ids_json === null) return [];
|
|
1046
961
|
|
|
1047
|
-
return yield*
|
|
1048
|
-
submissionId: row.submission_id,
|
|
1049
|
-
toolCallId: row.tool_call_id,
|
|
1050
|
-
author: row.author,
|
|
1051
|
-
reason: row.reason,
|
|
1052
|
-
resolution,
|
|
1053
|
-
resolvedAt: row.resolved_at,
|
|
1054
|
-
}).pipe(
|
|
962
|
+
return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(
|
|
1055
963
|
Effect.mapError((error) =>
|
|
1056
964
|
corruptionFailure(
|
|
1057
965
|
operation,
|
|
1058
|
-
"
|
|
1059
|
-
|
|
966
|
+
"effect_agent_submissions",
|
|
967
|
+
submission.submission_id,
|
|
1060
968
|
error.message,
|
|
1061
969
|
),
|
|
1062
970
|
),
|
|
1063
971
|
);
|
|
1064
972
|
});
|
|
1065
973
|
|
|
1066
|
-
/** The Submission's marked-unknown open Tool Call identities, empty when never marked. */
|
|
1067
|
-
const storedUnknownToolCallIds = Effect.fn("DoSubmissionLedger.storedUnknownToolCallIds")(
|
|
1068
|
-
function* (
|
|
1069
|
-
operation: string,
|
|
1070
|
-
submission: SubmissionRow,
|
|
1071
|
-
): Effect.fn.Return<ReadonlyArray<typeof ToolCallIdSchema.Type>, LedgerError> {
|
|
1072
|
-
if (submission.unknown_tool_call_ids_json === null) return [];
|
|
1073
|
-
|
|
1074
|
-
return yield* decodeToolCallIdsText(submission.unknown_tool_call_ids_json).pipe(
|
|
1075
|
-
Effect.mapError((error) =>
|
|
1076
|
-
corruptionFailure(
|
|
1077
|
-
operation,
|
|
1078
|
-
"effect_agent_submissions",
|
|
1079
|
-
submission.submission_id,
|
|
1080
|
-
error.message,
|
|
1081
|
-
),
|
|
1082
|
-
),
|
|
1083
|
-
);
|
|
1084
|
-
},
|
|
1085
|
-
);
|
|
1086
|
-
|
|
1087
974
|
/**
|
|
1088
975
|
* Canonical history is the abort authority (DUR-015): the intent's canonicalRecordId is
|
|
1089
976
|
* derived from the shared canonical-records table using the deterministic abort record
|
|
1090
977
|
* identity, never from a cached ledger marker.
|
|
1091
978
|
*/
|
|
1092
|
-
const canonicalAbortRecordId = Effect.
|
|
979
|
+
const canonicalAbortRecordId = Effect.fnUntraced(function* (
|
|
1093
980
|
operation: string,
|
|
1094
981
|
threadId: string,
|
|
1095
982
|
submissionId: SubmissionId,
|
|
1096
|
-
): Effect.fn.Return<
|
|
983
|
+
): Effect.fn.Return<AbortIntent["canonicalRecordId"], LedgerError> {
|
|
1097
984
|
const recordId = submissionAbortRecordId(submissionId);
|
|
1098
985
|
|
|
1099
986
|
const present = yield* journal
|
|
@@ -1103,7 +990,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1103
990
|
return present ? recordId : undefined;
|
|
1104
991
|
});
|
|
1105
992
|
|
|
1106
|
-
const abortIntentFromRow = Effect.
|
|
993
|
+
const abortIntentFromRow = Effect.fnUntraced(function* (
|
|
1107
994
|
operation: string,
|
|
1108
995
|
submission: SubmissionRow,
|
|
1109
996
|
submissionId: SubmissionId,
|
|
@@ -1115,13 +1002,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1115
1002
|
submissionId,
|
|
1116
1003
|
);
|
|
1117
1004
|
|
|
1118
|
-
|
|
1119
|
-
submissionId: row.submission_id,
|
|
1120
|
-
author: row.author,
|
|
1121
|
-
reason: row.reason,
|
|
1122
|
-
requestedAt: row.requested_at,
|
|
1123
|
-
...(canonicalRecordId === undefined ? {} : { canonicalRecordId }),
|
|
1124
|
-
}).pipe(
|
|
1005
|
+
const fact = yield* decodeAbortFact(row).pipe(
|
|
1125
1006
|
Effect.mapError((error) =>
|
|
1126
1007
|
corruptionFailure(
|
|
1127
1008
|
operation,
|
|
@@ -1131,6 +1012,11 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1131
1012
|
),
|
|
1132
1013
|
),
|
|
1133
1014
|
);
|
|
1015
|
+
|
|
1016
|
+
return AbortIntent.make({
|
|
1017
|
+
...fact,
|
|
1018
|
+
...(canonicalRecordId === undefined ? {} : { canonicalRecordId }),
|
|
1019
|
+
});
|
|
1134
1020
|
});
|
|
1135
1021
|
|
|
1136
1022
|
// Durable Object storage is the single serialized owner: writes confirm through output
|
|
@@ -1140,257 +1026,254 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1140
1026
|
LedgerCapabilities.make({ durability: "durable-cloudflare" }),
|
|
1141
1027
|
);
|
|
1142
1028
|
|
|
1143
|
-
const admit: SubmissionLedger["Service"]["admit"] = Effect.
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(AdmissionRequest))(request).pipe(
|
|
1148
|
-
Effect.mapError(internalFailure(operation)),
|
|
1149
|
-
);
|
|
1150
|
-
|
|
1151
|
-
const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(
|
|
1152
|
-
Effect.mapError(internalFailure(operation)),
|
|
1153
|
-
);
|
|
1029
|
+
const admit: SubmissionLedger["Service"]["admit"] = Effect.fnUntraced(function* (
|
|
1030
|
+
request: AdmissionRequest,
|
|
1031
|
+
) {
|
|
1032
|
+
const operation = "ledger admit";
|
|
1154
1033
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
.checkValueBound(operation, inputJson)
|
|
1159
|
-
.pipe(Effect.mapError(internalFailure(operation)));
|
|
1034
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AdmissionRequest))(request).pipe(
|
|
1035
|
+
Effect.mapError(internalFailure(operation)),
|
|
1036
|
+
);
|
|
1160
1037
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
: yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1165
|
-
validated.workerAdmission,
|
|
1166
|
-
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1038
|
+
const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(
|
|
1039
|
+
Effect.mapError(internalFailure(operation)),
|
|
1040
|
+
);
|
|
1167
1041
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
)
|
|
1172
|
-
|
|
1173
|
-
operation,
|
|
1174
|
-
message: "Worker admission metadata exceeds the stored value bound",
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1042
|
+
// The platform's ~2 MB per-value bound, refused typed BEFORE any durable mutation
|
|
1043
|
+
// (resource-limits gate; oversized payloads are the designed R2 overflow path).
|
|
1044
|
+
yield* journal
|
|
1045
|
+
.checkValueBound(operation, inputJson)
|
|
1046
|
+
.pipe(Effect.mapError(internalFailure(operation)));
|
|
1177
1047
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1048
|
+
const workerAdmissionJson =
|
|
1049
|
+
validated.workerAdmission === undefined
|
|
1050
|
+
? null
|
|
1051
|
+
: yield* Schema.encodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1052
|
+
validated.workerAdmission,
|
|
1053
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1184
1054
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1055
|
+
if (
|
|
1056
|
+
workerAdmissionJson !== null &&
|
|
1057
|
+
new TextEncoder().encode(workerAdmissionJson).byteLength > config.maxStoredValueBytes
|
|
1058
|
+
) {
|
|
1059
|
+
return yield* LedgerError.make({
|
|
1060
|
+
operation,
|
|
1061
|
+
message: "Worker admission metadata exceeds the stored value bound",
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1194
1064
|
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1065
|
+
const messageAdmissionJson =
|
|
1066
|
+
validated.messageAdmission === undefined
|
|
1067
|
+
? null
|
|
1068
|
+
: yield* Schema.encodeEffect(Schema.fromJsonString(InputMessage))(
|
|
1069
|
+
validated.messageAdmission,
|
|
1070
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1198
1071
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1072
|
+
if (
|
|
1073
|
+
messageAdmissionJson !== null &&
|
|
1074
|
+
new TextEncoder().encode(messageAdmissionJson).byteLength > config.maxStoredValueBytes
|
|
1075
|
+
) {
|
|
1076
|
+
return yield* LedgerError.make({
|
|
1077
|
+
operation,
|
|
1078
|
+
message: "Message admission metadata exceeds the stored value bound",
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1204
1081
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
message:
|
|
1209
|
-
`A routable Submission identity of ${mintedSubmissionId.length} characters exceeds ` +
|
|
1210
|
-
`the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Thread identity.`,
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
|
|
1082
|
+
const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(
|
|
1083
|
+
Effect.mapError(internalFailure(operation)),
|
|
1084
|
+
);
|
|
1214
1085
|
|
|
1215
|
-
|
|
1086
|
+
// Routable Submission identity (D-P6-5): `{uuidv7}:{threadId}`. The cross-DO
|
|
1087
|
+
// routing layer parses ITS OWN minted format (split at the first ":") to address
|
|
1088
|
+
// submissionId-only operations to the owning Thread Object; the id stays opaque
|
|
1089
|
+
// to every other component, exactly like DN's `submission-{uuid}` prefix.
|
|
1090
|
+
const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.threadId}`;
|
|
1216
1091
|
|
|
1217
|
-
|
|
1092
|
+
if (mintedSubmissionId.length > MAX_IDENTIFIER_LENGTH) {
|
|
1093
|
+
return yield* LedgerError.make({
|
|
1218
1094
|
operation,
|
|
1219
|
-
|
|
1220
|
-
|
|
1095
|
+
message:
|
|
1096
|
+
`A routable Submission identity of ${mintedSubmissionId.length} characters exceeds ` +
|
|
1097
|
+
`the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Thread identity.`,
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
|
|
1221
1101
|
|
|
1222
|
-
|
|
1102
|
+
yield* hitFailpoint("ledger:admit:before", operation);
|
|
1223
1103
|
|
|
1224
|
-
|
|
1104
|
+
const result = yield* inWriteTransaction(
|
|
1105
|
+
operation,
|
|
1106
|
+
Effect.gen(function* () {
|
|
1107
|
+
const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
|
|
1225
1108
|
|
|
1226
|
-
|
|
1227
|
-
return yield* corruptionFailure(
|
|
1228
|
-
operation,
|
|
1229
|
-
"effect_agent_submissions",
|
|
1230
|
-
keyRowKey,
|
|
1231
|
-
"An admission idempotency key returned more than one row.",
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
if (existing.length === 1) {
|
|
1235
|
-
// A replay must repeat the exact canonical input AND the exact parent linkage (or
|
|
1236
|
-
// its absence): linkage is immutable child lineage (spec §12 step 5, SUB-016).
|
|
1237
|
-
const sameLinkage =
|
|
1238
|
-
validated.parentLinkage === undefined
|
|
1239
|
-
? existing[0].parent_submission_id === null &&
|
|
1240
|
-
existing[0].parent_tool_call_id === null
|
|
1241
|
-
: existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId &&
|
|
1242
|
-
existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
|
|
1243
|
-
|
|
1244
|
-
if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
|
|
1245
|
-
return yield* AdmissionConflict.make({
|
|
1246
|
-
threadId: validated.threadId,
|
|
1247
|
-
principal: validated.principal,
|
|
1248
|
-
idempotencyKey: validated.idempotencyKey,
|
|
1249
|
-
existingInputDigest: existing[0].input_digest,
|
|
1250
|
-
attemptedInputDigest: validated.inputDigest,
|
|
1251
|
-
});
|
|
1252
|
-
}
|
|
1109
|
+
const existingRows = yield* byKey(validated, operation);
|
|
1253
1110
|
|
|
1254
|
-
|
|
1255
|
-
existing[0].worker_admission_json === null
|
|
1256
|
-
? undefined
|
|
1257
|
-
: yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1258
|
-
existing[0].worker_admission_json,
|
|
1259
|
-
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1111
|
+
const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
|
|
1260
1112
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1113
|
+
if (existing.length > 1) {
|
|
1114
|
+
return yield* corruptionFailure(
|
|
1115
|
+
operation,
|
|
1116
|
+
"effect_agent_submissions",
|
|
1117
|
+
keyRowKey,
|
|
1118
|
+
"An admission idempotency key returned more than one row.",
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
if (existing.length === 1) {
|
|
1122
|
+
// A replay must repeat the exact canonical input AND the exact parent linkage (or
|
|
1123
|
+
// its absence): linkage is immutable child lineage (spec §12 step 5, SUB-016).
|
|
1124
|
+
const sameLinkage =
|
|
1125
|
+
validated.parentLinkage === undefined
|
|
1126
|
+
? existing[0].parent_submission_id === null &&
|
|
1127
|
+
existing[0].parent_tool_call_id === null
|
|
1128
|
+
: existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId &&
|
|
1129
|
+
existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
|
|
1130
|
+
|
|
1131
|
+
if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) {
|
|
1132
|
+
return yield* AdmissionConflict.make({
|
|
1133
|
+
threadId: validated.threadId,
|
|
1134
|
+
principal: validated.principal,
|
|
1135
|
+
idempotencyKey: validated.idempotencyKey,
|
|
1136
|
+
existingInputDigest: existing[0].input_digest,
|
|
1137
|
+
attemptedInputDigest: validated.inputDigest,
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1267
1140
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1141
|
+
const retainedWorkerAdmission =
|
|
1142
|
+
existing[0].worker_admission_json === null
|
|
1143
|
+
? undefined
|
|
1144
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1145
|
+
existing[0].worker_admission_json,
|
|
1146
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1147
|
+
|
|
1148
|
+
const retainedInputMessage =
|
|
1149
|
+
existing[0].message_admission_json === null
|
|
1150
|
+
? undefined
|
|
1151
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(InputMessage))(
|
|
1152
|
+
existing[0].message_admission_json,
|
|
1153
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1154
|
+
|
|
1155
|
+
const retainedFence =
|
|
1156
|
+
existing[0].admission_fence_json === null
|
|
1157
|
+
? undefined
|
|
1158
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(AdmissionFence))(
|
|
1159
|
+
existing[0].admission_fence_json,
|
|
1160
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1274
1161
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
)
|
|
1162
|
+
if (
|
|
1163
|
+
(existing[0].admission_group ?? undefined) !== validated.admissionGroup ||
|
|
1164
|
+
!Schema.toEquivalence(Schema.optional(WorkerAdmission))(
|
|
1165
|
+
retainedWorkerAdmission,
|
|
1166
|
+
validated.workerAdmission,
|
|
1167
|
+
) ||
|
|
1168
|
+
!Schema.toEquivalence(Schema.optional(InputMessage))(
|
|
1169
|
+
retainedInputMessage,
|
|
1170
|
+
validated.messageAdmission,
|
|
1171
|
+
) ||
|
|
1172
|
+
!Schema.toEquivalence(Schema.optional(AdmissionFence))(
|
|
1173
|
+
retainedFence,
|
|
1174
|
+
validated.admissionFence,
|
|
1289
1175
|
)
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1176
|
+
)
|
|
1177
|
+
return yield* AdmissionConflict.make({
|
|
1178
|
+
threadId: validated.threadId,
|
|
1179
|
+
principal: validated.principal,
|
|
1180
|
+
idempotencyKey: validated.idempotencyKey,
|
|
1181
|
+
existingInputDigest: existing[0].input_digest,
|
|
1182
|
+
attemptedInputDigest: validated.inputDigest,
|
|
1183
|
+
});
|
|
1297
1184
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1185
|
+
return yield* decodeAdmissionResult({
|
|
1186
|
+
submissionId: existing[0].submission_id,
|
|
1187
|
+
receiptId: existing[0].receipt_id,
|
|
1188
|
+
queueSequence: existing[0].queue_sequence,
|
|
1189
|
+
state: existing[0].state,
|
|
1190
|
+
replayed: true,
|
|
1191
|
+
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1192
|
+
}
|
|
1306
1193
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1194
|
+
if (!admissionFitsTransfer(validated.threadId, validated))
|
|
1195
|
+
return yield* LedgerError.make({
|
|
1309
1196
|
operation,
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
if (stopped.length > 0)
|
|
1313
|
-
return yield* AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" });
|
|
1197
|
+
message: "Admission exceeds the complete transfer page byte bound",
|
|
1198
|
+
});
|
|
1314
1199
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1200
|
+
const stopped = yield* cached(
|
|
1201
|
+
rows.workerStops.by("thread_id", validated.threadId),
|
|
1202
|
+
operation,
|
|
1203
|
+
);
|
|
1318
1204
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1205
|
+
if (stopped.length > 0)
|
|
1206
|
+
return yield* AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" });
|
|
1207
|
+
|
|
1208
|
+
// The first accepted input fixes ordinary/worker lane identity atomically with admission.
|
|
1209
|
+
// Canonical origin materialization can lag admission; a log scan cannot fence that race.
|
|
1210
|
+
const firstRows = yield* sql<Record<string, unknown>>`
|
|
1322
1211
|
SELECT worker_admission_json FROM effect_agent_submissions
|
|
1323
1212
|
WHERE thread_id=${validated.threadId} ORDER BY queue_sequence LIMIT 1
|
|
1324
|
-
`.pipe(Effect.mapError(sqlFailure(operation)))
|
|
1325
|
-
: lane[0] === undefined
|
|
1326
|
-
? []
|
|
1327
|
-
: [yield* requireSubmission(operation, lane[0].submission_id)];
|
|
1328
|
-
|
|
1329
|
-
const first = yield* Schema.decodeUnknownEffect(
|
|
1330
|
-
Schema.Array(
|
|
1331
|
-
Schema.Struct({
|
|
1332
|
-
worker_admission_json: Schema.NullOr(BoundedStoredText),
|
|
1333
|
-
}),
|
|
1334
|
-
),
|
|
1335
|
-
)(firstRows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1213
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1336
1214
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1215
|
+
const first = yield* Schema.decodeUnknownEffect(
|
|
1216
|
+
Schema.Array(
|
|
1217
|
+
Schema.Struct({
|
|
1218
|
+
worker_admission_json: Schema.NullOr(BoundedStoredText),
|
|
1219
|
+
}),
|
|
1220
|
+
),
|
|
1221
|
+
)(firstRows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1344
1222
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
)
|
|
1223
|
+
if (first[0] !== undefined) {
|
|
1224
|
+
const previous =
|
|
1225
|
+
first[0].worker_admission_json === null
|
|
1226
|
+
? undefined
|
|
1227
|
+
: yield* Schema.decodeEffect(Schema.fromJsonString(WorkerAdmission))(
|
|
1228
|
+
first[0].worker_admission_json,
|
|
1229
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1230
|
+
|
|
1231
|
+
if (
|
|
1232
|
+
!Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
|
|
1233
|
+
previous?.origin,
|
|
1234
|
+
validated.workerAdmission?.origin,
|
|
1350
1235
|
)
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1236
|
+
)
|
|
1237
|
+
return yield* AdmissionPolicyError.make({
|
|
1238
|
+
reason: "refused",
|
|
1239
|
+
code: "worker-origin-conflict",
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1356
1242
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1243
|
+
yield* admissionFence.check(validated);
|
|
1244
|
+
if (validated.admissionGroup !== undefined) {
|
|
1245
|
+
const occupied = yield* sql<Record<string, unknown>>`
|
|
1360
1246
|
SELECT submission_id FROM effect_agent_submissions
|
|
1361
1247
|
WHERE thread_id=${validated.threadId} AND admission_group=${validated.admissionGroup} AND state<>'settled' LIMIT 1
|
|
1362
1248
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1363
1249
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1250
|
+
if (occupied.length > 0)
|
|
1251
|
+
return yield* AdmissionPolicyError.make({
|
|
1252
|
+
reason: "occupied",
|
|
1253
|
+
code: "admission-group",
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1370
1256
|
|
|
1371
|
-
|
|
1372
|
-
lane === undefined
|
|
1373
|
-
? yield* sql<Record<string, unknown>>`
|
|
1257
|
+
const maxRows = yield* sql<Record<string, unknown>>`
|
|
1374
1258
|
SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
|
|
1375
1259
|
FROM effect_agent_submissions
|
|
1376
1260
|
WHERE thread_id = ${validated.threadId}
|
|
1377
|
-
`.pipe(Effect.mapError(sqlFailure(operation)))
|
|
1378
|
-
: [{ max_queue_sequence: lane.at(-1)?.queue_sequence ?? 0 }];
|
|
1261
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1379
1262
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1263
|
+
const decodedMax = yield* decodeRows(
|
|
1264
|
+
Schema.Array(MaxQueueSequenceRow),
|
|
1265
|
+
"effect_agent_submissions",
|
|
1266
|
+
validated.threadId,
|
|
1267
|
+
maxRows,
|
|
1268
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1386
1269
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1270
|
+
const queueSequence = yield* decodeQueueSequence(
|
|
1271
|
+
(decodedMax[0]?.max_queue_sequence ?? 0) + 1,
|
|
1272
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1390
1273
|
|
|
1391
|
-
|
|
1274
|
+
const now = yield* currentInstant;
|
|
1392
1275
|
|
|
1393
|
-
|
|
1276
|
+
yield* sql`
|
|
1394
1277
|
INSERT INTO effect_agent_submissions (
|
|
1395
1278
|
submission_id,
|
|
1396
1279
|
thread_id,
|
|
@@ -1398,7 +1281,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1398
1281
|
principal,
|
|
1399
1282
|
idempotency_key,
|
|
1400
1283
|
agent_id,
|
|
1401
|
-
|
|
1284
|
+
definition_key,
|
|
1402
1285
|
deployment_id,
|
|
1403
1286
|
input_json,
|
|
1404
1287
|
input_digest,
|
|
@@ -1418,7 +1301,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1418
1301
|
${validated.principal},
|
|
1419
1302
|
${validated.idempotencyKey},
|
|
1420
1303
|
${validated.agentId},
|
|
1421
|
-
${agentDigestsJson},
|
|
1304
|
+
${yield* physical.definition(agentDigestsJson).pipe(Effect.mapError(internalFailure(operation)))},
|
|
1422
1305
|
${validated.deploymentId},
|
|
1423
1306
|
${inputJson},
|
|
1424
1307
|
${validated.inputDigest},
|
|
@@ -1432,40 +1315,51 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1432
1315
|
${workerAdmissionJson},
|
|
1433
1316
|
${messageAdmissionJson}
|
|
1434
1317
|
)
|
|
1435
|
-
RETURNING
|
|
1318
|
+
RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
|
|
1319
|
+
rows.submissions.write,
|
|
1320
|
+
Effect.mapError(internalFailure(operation)),
|
|
1321
|
+
);
|
|
1436
1322
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
rows.reservations.seed("submission_id", mintedSubmissionId, []);
|
|
1442
|
-
rows.submissions.seed("joined_host_submission_id", mintedSubmissionId, []);
|
|
1443
|
-
rows.approvals.seed("submission_id", mintedSubmissionId, []);
|
|
1444
|
-
rows.resolutions.seed("submission_id", mintedSubmissionId, []);
|
|
1445
|
-
rows.childReservations.seed("parent_submission_id", mintedSubmissionId, []);
|
|
1446
|
-
rows.childSettlements.seed("parent_submission_id", mintedSubmissionId, []);
|
|
1323
|
+
if (queueSequence === 1) {
|
|
1324
|
+
const threads = yield* journal
|
|
1325
|
+
.getThread(validated.threadId)
|
|
1326
|
+
.pipe(Effect.mapError(internalFailure(operation)));
|
|
1447
1327
|
|
|
1448
|
-
|
|
1328
|
+
if (threads.length === 0)
|
|
1329
|
+
yield* journal.work
|
|
1330
|
+
.initialize(validated.threadId)
|
|
1331
|
+
.pipe(Effect.mapError(internalFailure(operation)));
|
|
1332
|
+
}
|
|
1449
1333
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
);
|
|
1334
|
+
// The INSERT (never replay or hydration) proves these new submission-owned sets
|
|
1335
|
+
// empty. Gated writes update them; rollback/invalidation discards that proof.
|
|
1336
|
+
rows.aborts.seed("submission_id", mintedSubmissionId, []);
|
|
1337
|
+
rows.submissions.seed("joined_host_submission_id", mintedSubmissionId, []);
|
|
1338
|
+
rows.approvals.seed("submission_id", mintedSubmissionId, []);
|
|
1339
|
+
rows.resolutions.seed("submission_id", mintedSubmissionId, []);
|
|
1340
|
+
rows.childReservations.seed("parent_submission_id", mintedSubmissionId, []);
|
|
1341
|
+
rows.childSettlements.seed("parent_submission_id", mintedSubmissionId, []);
|
|
1459
1342
|
|
|
1460
|
-
|
|
1343
|
+
yield* recordProgress("submission", operation);
|
|
1461
1344
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1345
|
+
return yield* decodeAdmissionResult({
|
|
1346
|
+
submissionId: mintedSubmissionId,
|
|
1347
|
+
receiptId: mintedReceiptId,
|
|
1348
|
+
queueSequence,
|
|
1349
|
+
state: "admitted",
|
|
1350
|
+
replayed: false,
|
|
1351
|
+
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1352
|
+
}),
|
|
1353
|
+
);
|
|
1465
1354
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1355
|
+
yield* hitFailpoint("ledger:admit:after", operation);
|
|
1356
|
+
|
|
1357
|
+
return result;
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
const markReady: SubmissionLedger["Service"]["markReady"] = Effect.fnUntraced(function* (
|
|
1361
|
+
request: MarkReadyRequest,
|
|
1362
|
+
) {
|
|
1469
1363
|
const operation = "ledger mark ready";
|
|
1470
1364
|
|
|
1471
1365
|
const validated = yield* Schema.decodeEffect(Schema.toType(MarkReadyRequest))(request).pipe(
|
|
@@ -1485,7 +1379,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1485
1379
|
UPDATE effect_agent_submissions
|
|
1486
1380
|
SET state = 'ready', ready_at = ${now.iso}
|
|
1487
1381
|
WHERE submission_id = ${validated.submissionId}
|
|
1488
|
-
RETURNING
|
|
1382
|
+
RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
|
|
1383
|
+
rows.submissions.write,
|
|
1384
|
+
Effect.mapError(internalFailure(operation)),
|
|
1385
|
+
);
|
|
1489
1386
|
yield* recordProgress("submission", operation);
|
|
1490
1387
|
yield* retainLifecycle(submission, {
|
|
1491
1388
|
_tag: "SubmissionReady",
|
|
@@ -1496,56 +1393,82 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1496
1393
|
yield* hitFailpoint("ledger:mark-ready:after", operation);
|
|
1497
1394
|
});
|
|
1498
1395
|
|
|
1499
|
-
const
|
|
1500
|
-
function* (
|
|
1501
|
-
const operation = "
|
|
1396
|
+
const resolveFundingOwner: NonNullable<SubmissionLedger["Service"]["resolveFundingOwner"]> =
|
|
1397
|
+
Effect.fnUntraced(function* (input) {
|
|
1398
|
+
const operation = "resolve funding owner";
|
|
1502
1399
|
|
|
1503
|
-
const
|
|
1400
|
+
const request = yield* Schema.decodeEffect(FundingOwnerRequest)(input).pipe(
|
|
1504
1401
|
Effect.mapError(internalFailure(operation)),
|
|
1505
1402
|
);
|
|
1506
1403
|
|
|
1507
|
-
|
|
1508
|
-
const row = yield* readSubmission(operation, validated.submissionId);
|
|
1509
|
-
|
|
1510
|
-
if (Option.isNone(row)) return Option.none();
|
|
1511
|
-
|
|
1512
|
-
return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
|
|
1513
|
-
}
|
|
1404
|
+
const selected = yield* requireSubmission(operation, request.submissionId);
|
|
1514
1405
|
|
|
1515
|
-
|
|
1406
|
+
if (selected.thread_id !== request.threadId)
|
|
1407
|
+
return yield* LedgerError.make({
|
|
1408
|
+
operation,
|
|
1409
|
+
message: "Selected funding input belongs to another Thread.",
|
|
1410
|
+
});
|
|
1411
|
+
if (selected.state === "joining")
|
|
1412
|
+
return yield* LedgerError.make({
|
|
1413
|
+
operation,
|
|
1414
|
+
message: "Selected funding input is still joining; retry after durable binding.",
|
|
1415
|
+
});
|
|
1416
|
+
const hostId = selected.joined_host_submission_id;
|
|
1516
1417
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1418
|
+
if (hostId === null && selected.state === "joined")
|
|
1419
|
+
return yield* corruptionFailure(
|
|
1420
|
+
operation,
|
|
1421
|
+
"effect_agent_submissions",
|
|
1422
|
+
selected.submission_id,
|
|
1423
|
+
"Joined input has no stable host identity.",
|
|
1424
|
+
);
|
|
1425
|
+
if (hostId !== null && selected.state !== "joined" && selected.state !== "settled")
|
|
1426
|
+
return yield* corruptionFailure(
|
|
1427
|
+
operation,
|
|
1428
|
+
"effect_agent_submissions",
|
|
1429
|
+
selected.submission_id,
|
|
1430
|
+
"Host linkage is inconsistent with selected input state.",
|
|
1431
|
+
);
|
|
1432
|
+
const owner = hostId === null ? selected : yield* requireSubmission(operation, hostId);
|
|
1522
1433
|
|
|
1523
|
-
if (
|
|
1434
|
+
if (
|
|
1435
|
+
owner.thread_id !== request.threadId ||
|
|
1436
|
+
(hostId !== null &&
|
|
1437
|
+
(owner.submission_id === selected.submission_id ||
|
|
1438
|
+
owner.joined_host_submission_id !== null ||
|
|
1439
|
+
owner.state === "joining" ||
|
|
1440
|
+
owner.state === "joined" ||
|
|
1441
|
+
owner.queue_sequence >= selected.queue_sequence))
|
|
1442
|
+
)
|
|
1524
1443
|
return yield* corruptionFailure(
|
|
1525
1444
|
operation,
|
|
1526
1445
|
"effect_agent_submissions",
|
|
1527
|
-
|
|
1528
|
-
"
|
|
1446
|
+
selected.submission_id,
|
|
1447
|
+
"Funding host is not a stable earlier input in this Thread.",
|
|
1529
1448
|
);
|
|
1530
|
-
}
|
|
1531
|
-
if (decoded.length === 0) return Option.none();
|
|
1532
1449
|
|
|
1533
|
-
return
|
|
1534
|
-
|
|
1535
|
-
|
|
1450
|
+
return yield* Schema.decodeEffect(Schema.toType(FundingOwner))({
|
|
1451
|
+
selected: yield* decodeSubmissionSnapshot(operation, selected),
|
|
1452
|
+
owner: yield* decodeSubmissionSnapshot(operation, owner),
|
|
1453
|
+
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1454
|
+
}, state.read);
|
|
1536
1455
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
const resolveAdmission: SubmissionLedger["Service"]["resolveAdmission"] = Effect.fn(
|
|
1542
|
-
"DoSubmissionLedger.resolveAdmission",
|
|
1543
|
-
)(function* (request: SubmissionLookupByKey) {
|
|
1544
|
-
const operation = "ledger resolve admission";
|
|
1456
|
+
const lookup: SubmissionLedger["Service"]["lookup"] = Effect.fnUntraced(function* (
|
|
1457
|
+
request: SubmissionLookup,
|
|
1458
|
+
) {
|
|
1459
|
+
const operation = "ledger lookup";
|
|
1545
1460
|
|
|
1546
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(
|
|
1547
|
-
|
|
1548
|
-
)
|
|
1461
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookup))(request).pipe(
|
|
1462
|
+
Effect.mapError(internalFailure(operation)),
|
|
1463
|
+
);
|
|
1464
|
+
|
|
1465
|
+
if (validated._tag === "SubmissionLookupById") {
|
|
1466
|
+
const row = yield* readSubmission(operation, validated.submissionId);
|
|
1467
|
+
|
|
1468
|
+
if (Option.isNone(row)) return Option.none();
|
|
1469
|
+
|
|
1470
|
+
return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
|
|
1471
|
+
}
|
|
1549
1472
|
|
|
1550
1473
|
const foundRows = yield* byKey(validated, operation);
|
|
1551
1474
|
|
|
@@ -1563,76 +1486,66 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1563
1486
|
"An admission idempotency key returned more than one row.",
|
|
1564
1487
|
);
|
|
1565
1488
|
}
|
|
1566
|
-
if (decoded.length === 0) return
|
|
1489
|
+
if (decoded.length === 0) return Option.none();
|
|
1567
1490
|
|
|
1568
|
-
return
|
|
1569
|
-
submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
|
|
1570
|
-
});
|
|
1491
|
+
return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
|
|
1571
1492
|
});
|
|
1572
1493
|
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1494
|
+
// This LOCAL facet is the authoritative owner of every Thread stored in this Durable
|
|
1495
|
+
// Object, so the key-scoped read IS the admission truth and the tri-state degenerates to
|
|
1496
|
+
// NotAdmitted or Admitted (SUB-031). `AdmissionIndeterminate` becomes real one layer out:
|
|
1497
|
+
// the WP2 routed decorator answers it when the OWNING Durable Object is unreachable.
|
|
1498
|
+
const resolveAdmission: SubmissionLedger["Service"]["resolveAdmission"] = Effect.fnUntraced(
|
|
1499
|
+
function* (request: SubmissionLookupByKey) {
|
|
1500
|
+
const operation = "ledger resolve admission";
|
|
1501
|
+
|
|
1502
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookupByKey))(
|
|
1503
|
+
request,
|
|
1504
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1576
1505
|
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1506
|
+
const foundRows = yield* byKey(validated, operation);
|
|
1507
|
+
|
|
1508
|
+
const decoded = yield* decodeSubmissionRows(
|
|
1509
|
+
operation,
|
|
1510
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1511
|
+
foundRows,
|
|
1579
1512
|
);
|
|
1580
1513
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1514
|
+
if (decoded.length > 1) {
|
|
1515
|
+
return yield* corruptionFailure(
|
|
1516
|
+
operation,
|
|
1517
|
+
"effect_agent_submissions",
|
|
1518
|
+
`${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
|
|
1519
|
+
"An admission idempotency key returned more than one row.",
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
if (decoded.length === 0) return AdmissionNotAdmitted.make();
|
|
1523
|
+
|
|
1524
|
+
return AdmissionAdmitted.make({
|
|
1525
|
+
submission: yield* decodeSubmissionSnapshot(operation, decoded[0]),
|
|
1526
|
+
});
|
|
1527
|
+
},
|
|
1528
|
+
);
|
|
1583
1529
|
|
|
1584
|
-
|
|
1530
|
+
const selectHead: SubmissionLedger["Service"]["selectHead"] = Effect.fn(
|
|
1531
|
+
"DoSubmissionLedger.selectHead",
|
|
1532
|
+
)(function* (request: ClaimRequest) {
|
|
1533
|
+
const operation = "ledger select head";
|
|
1585
1534
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
const now = yield* currentInstant;
|
|
1535
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
|
|
1536
|
+
Effect.mapError(internalFailure(operation)),
|
|
1537
|
+
);
|
|
1590
1538
|
|
|
1591
|
-
|
|
1592
|
-
// Stored lease instants are normalized UTC strings, so the latest expiry covers
|
|
1593
|
-
// every live lease. This check and the epoch grant share one write transaction.
|
|
1594
|
-
const lane = yield* laneWork(validated.threadId, operation);
|
|
1595
|
-
|
|
1596
|
-
const ownershipRows =
|
|
1597
|
-
lane === undefined
|
|
1598
|
-
? yield* sql<Record<string, unknown>>`
|
|
1599
|
-
SELECT ownership.*
|
|
1600
|
-
FROM effect_agent_submission_ownership AS ownership
|
|
1601
|
-
JOIN effect_agent_submissions AS submission
|
|
1602
|
-
ON submission.submission_id = ownership.submission_id
|
|
1603
|
-
WHERE submission.thread_id = ${validated.threadId}
|
|
1604
|
-
ORDER BY ownership.lease_expires_at DESC
|
|
1605
|
-
LIMIT 1
|
|
1606
|
-
`.pipe(Effect.mapError(sqlFailure(operation)))
|
|
1607
|
-
: (yield* Effect.forEach(
|
|
1608
|
-
lane.filter((item) => item.state !== "settled"),
|
|
1609
|
-
(item) =>
|
|
1610
|
-
cached(rows.ownership.by("submission_id", item.submission_id), operation),
|
|
1611
|
-
))
|
|
1612
|
-
.flat()
|
|
1613
|
-
.sort((a, b) => b.lease_expires_at.localeCompare(a.lease_expires_at))
|
|
1614
|
-
.slice(0, 1);
|
|
1615
|
-
|
|
1616
|
-
const ownership = yield* decodeRows(
|
|
1617
|
-
Schema.Array(OwnershipRow),
|
|
1618
|
-
"effect_agent_submission_ownership",
|
|
1619
|
-
validated.threadId,
|
|
1620
|
-
ownershipRows,
|
|
1621
|
-
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1539
|
+
const attemptId = `attempt-${yield* mintUuid(operation)}`;
|
|
1622
1540
|
|
|
1623
|
-
|
|
1624
|
-
const expiresAt = yield* timestampMillis(
|
|
1625
|
-
operation,
|
|
1626
|
-
ownership[0].submission_id,
|
|
1627
|
-
)(ownership[0].lease_expires_at);
|
|
1541
|
+
yield* hitFailpoint("ledger:claim:before", operation);
|
|
1628
1542
|
|
|
1629
|
-
|
|
1630
|
-
|
|
1543
|
+
const claimed = yield* owner.withMutation(
|
|
1544
|
+
Effect.gen(function* () {
|
|
1545
|
+
if (owner.hasOwner(validated.threadId)) return Option.none<Claim>();
|
|
1631
1546
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
? yield* sql<Record<string, unknown>>`
|
|
1635
|
-
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1547
|
+
const headRows = yield* sql<Record<string, unknown>>`
|
|
1548
|
+
SELECT ${submissionColumns}
|
|
1636
1549
|
FROM effect_agent_submissions
|
|
1637
1550
|
WHERE thread_id = ${validated.threadId}
|
|
1638
1551
|
AND state <> 'settled'
|
|
@@ -1644,533 +1557,330 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1644
1557
|
)
|
|
1645
1558
|
ORDER BY queue_sequence ASC
|
|
1646
1559
|
LIMIT ${validated.handoff === undefined ? 1 : validated.handoff.deferredSubmissionIds.length + 1}
|
|
1647
|
-
`.pipe(Effect.mapError(sqlFailure(operation)))
|
|
1648
|
-
: yield* Effect.gen(function* () {
|
|
1649
|
-
const heads: Array<SubmissionRow> = [];
|
|
1650
|
-
|
|
1651
|
-
for (const item of lane) {
|
|
1652
|
-
if (item.state === "settled") continue;
|
|
1653
|
-
if (
|
|
1654
|
-
item.state === "unknown" &&
|
|
1655
|
-
Option.isNone(yield* readAbortIntent(operation, item.submission_id))
|
|
1656
|
-
)
|
|
1657
|
-
continue;
|
|
1658
|
-
heads.push(yield* requireSubmission(operation, item.submission_id));
|
|
1659
|
-
if (
|
|
1660
|
-
heads.length >=
|
|
1661
|
-
(validated.handoff === undefined
|
|
1662
|
-
? 1
|
|
1663
|
-
: validated.handoff.deferredSubmissionIds.length + 1)
|
|
1664
|
-
)
|
|
1665
|
-
break;
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
return heads;
|
|
1669
|
-
});
|
|
1560
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1670
1561
|
|
|
1671
|
-
|
|
1562
|
+
const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
|
|
1672
1563
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1564
|
+
if (heads.length === 0) return Option.none<Claim>();
|
|
1565
|
+
let head = heads[0];
|
|
1675
1566
|
|
|
1676
|
-
|
|
1677
|
-
|
|
1567
|
+
if (validated.handoff !== undefined) {
|
|
1568
|
+
const handoff = validated.handoff;
|
|
1678
1569
|
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
}
|
|
1684
|
-
if (
|
|
1685
|
-
!handoff.deferredSubmissionIds.some((id) => id === candidate.submission_id) ||
|
|
1686
|
-
candidate.state !== "input-applied" ||
|
|
1687
|
-
Option.isSome(yield* readAbortIntent(operation, candidate.submission_id))
|
|
1688
|
-
)
|
|
1689
|
-
return Option.none<Claim>();
|
|
1570
|
+
for (const candidate of heads) {
|
|
1571
|
+
if (candidate.submission_id === handoff.submissionId) {
|
|
1572
|
+
head = candidate;
|
|
1573
|
+
break;
|
|
1690
1574
|
}
|
|
1691
1575
|
if (
|
|
1692
|
-
|
|
1693
|
-
|
|
1576
|
+
!handoff.deferredSubmissionIds.some((id) => id === candidate.submission_id) ||
|
|
1577
|
+
candidate.state !== "input-applied" ||
|
|
1578
|
+
Option.isSome(yield* readAbortIntent(operation, candidate.submission_id))
|
|
1694
1579
|
)
|
|
1695
1580
|
return Option.none<Claim>();
|
|
1696
1581
|
}
|
|
1697
|
-
|
|
1698
|
-
// Approval/delegation suspension and joined work retain their queue barrier.
|
|
1699
|
-
// Unknown work with an abort intent is selected for cleanup without Tool replay.
|
|
1700
|
-
if (
|
|
1701
|
-
head.state === "joining" ||
|
|
1702
|
-
head.state === "joined" ||
|
|
1703
|
-
head.state === "suspended" ||
|
|
1704
|
-
(head.state === "unknown" &&
|
|
1705
|
-
Option.isNone(yield* readAbortIntent(operation, head.submission_id)))
|
|
1706
|
-
) {
|
|
1707
|
-
return Option.none<Claim>();
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
// Bump the Thread's producer epoch atomically with the claim so every stale
|
|
1711
|
-
// Attempt is fenced out of canonical appends (DUR-006). A Thread that was
|
|
1712
|
-
// never materialized (eviction between admission and materialization) is created
|
|
1713
|
-
// here so recovery can claim first and re-materialize idempotently at this epoch.
|
|
1714
|
-
const threads = yield* journal
|
|
1715
|
-
.getThread(head.thread_id)
|
|
1716
|
-
.pipe(Effect.mapError(internalFailure(operation)));
|
|
1717
|
-
|
|
1718
1582
|
if (
|
|
1719
|
-
|
|
1720
|
-
|
|
1583
|
+
head.submission_id !== handoff.submissionId ||
|
|
1584
|
+
(head.state !== "ready" && head.state !== "input-applied")
|
|
1721
1585
|
)
|
|
1722
1586
|
return Option.none<Claim>();
|
|
1587
|
+
}
|
|
1723
1588
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
) VALUES (
|
|
1736
|
-
${head.thread_id},
|
|
1737
|
-
${now.iso},
|
|
1738
|
-
0,
|
|
1739
|
-
${EMPTY_TAIL_DIGEST},
|
|
1740
|
-
${producerEpoch}
|
|
1741
|
-
)
|
|
1742
|
-
RETURNING *`.pipe(rows.threads.write, Effect.mapError(internalFailure(operation)));
|
|
1743
|
-
} else {
|
|
1744
|
-
producerEpoch = threads[0].producer_epoch + 1;
|
|
1745
|
-
yield* sql`
|
|
1746
|
-
UPDATE effect_agent_threads
|
|
1747
|
-
SET producer_epoch = ${producerEpoch}
|
|
1748
|
-
WHERE thread_id = ${head.thread_id}
|
|
1749
|
-
RETURNING *`.pipe(rows.threads.write, Effect.mapError(internalFailure(operation)));
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
|
|
1753
|
-
|
|
1754
|
-
yield* sql`
|
|
1755
|
-
INSERT INTO effect_agent_submission_ownership (
|
|
1756
|
-
submission_id,
|
|
1757
|
-
attempt_id,
|
|
1758
|
-
ownership_token,
|
|
1759
|
-
producer_epoch,
|
|
1760
|
-
owner_producer_id,
|
|
1761
|
-
lease_expires_at
|
|
1762
|
-
) VALUES (
|
|
1763
|
-
${head.submission_id},
|
|
1764
|
-
${attemptId},
|
|
1765
|
-
${ownershipToken},
|
|
1766
|
-
${producerEpoch},
|
|
1767
|
-
${validated.producerId},
|
|
1768
|
-
${leaseExpiresAt}
|
|
1769
|
-
)
|
|
1770
|
-
ON CONFLICT (submission_id) DO UPDATE SET
|
|
1771
|
-
attempt_id = excluded.attempt_id,
|
|
1772
|
-
ownership_token = excluded.ownership_token,
|
|
1773
|
-
producer_epoch = excluded.producer_epoch,
|
|
1774
|
-
owner_producer_id = excluded.owner_producer_id,
|
|
1775
|
-
lease_expires_at = excluded.lease_expires_at
|
|
1776
|
-
RETURNING *`.pipe(rows.ownership.write, Effect.mapError(internalFailure(operation)));
|
|
1777
|
-
|
|
1778
|
-
yield* sql`
|
|
1779
|
-
INSERT INTO effect_agent_attempts (
|
|
1780
|
-
attempt_id,
|
|
1781
|
-
submission_id,
|
|
1782
|
-
thread_id,
|
|
1783
|
-
owner_producer_id,
|
|
1784
|
-
producer_epoch,
|
|
1785
|
-
claimed_at
|
|
1786
|
-
) VALUES (
|
|
1787
|
-
${attemptId},
|
|
1788
|
-
${head.submission_id},
|
|
1789
|
-
${head.thread_id},
|
|
1790
|
-
${validated.producerId},
|
|
1791
|
-
${producerEpoch},
|
|
1792
|
-
${now.iso}
|
|
1793
|
-
)
|
|
1794
|
-
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1795
|
-
|
|
1796
|
-
if (head.state === "ready") {
|
|
1797
|
-
yield* sql`
|
|
1798
|
-
UPDATE effect_agent_submissions
|
|
1799
|
-
SET state = 'running'
|
|
1800
|
-
WHERE submission_id = ${head.submission_id}
|
|
1801
|
-
RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
|
|
1802
|
-
}
|
|
1589
|
+
// Approval/delegation suspension and joined work retain their queue barrier.
|
|
1590
|
+
// Unknown work with an abort intent is selected for cleanup without Tool replay.
|
|
1591
|
+
if (
|
|
1592
|
+
head.state === "joining" ||
|
|
1593
|
+
head.state === "joined" ||
|
|
1594
|
+
head.state === "suspended" ||
|
|
1595
|
+
(head.state === "unknown" &&
|
|
1596
|
+
Option.isNone(yield* readAbortIntent(operation, head.submission_id)))
|
|
1597
|
+
) {
|
|
1598
|
+
return Option.none<Claim>();
|
|
1599
|
+
}
|
|
1803
1600
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
),
|
|
1601
|
+
const inputPayload = yield* parseStoredJsonText(head.input_json).pipe(
|
|
1602
|
+
Effect.mapError((error) =>
|
|
1603
|
+
corruptionFailure(
|
|
1604
|
+
operation,
|
|
1605
|
+
"effect_agent_submissions",
|
|
1606
|
+
head.submission_id,
|
|
1607
|
+
error.message,
|
|
1812
1608
|
),
|
|
1813
|
-
)
|
|
1814
|
-
|
|
1815
|
-
return Option.some(
|
|
1816
|
-
yield* decodeClaim({
|
|
1817
|
-
submissionId: head.submission_id,
|
|
1818
|
-
attemptId,
|
|
1819
|
-
ownershipToken,
|
|
1820
|
-
producerEpoch,
|
|
1821
|
-
leaseExpiresAt,
|
|
1822
|
-
inputPayload,
|
|
1823
|
-
}).pipe(Effect.mapError(internalFailure(operation))),
|
|
1824
|
-
);
|
|
1825
|
-
}),
|
|
1826
|
-
);
|
|
1827
|
-
|
|
1828
|
-
yield* hitFailpoint("ledger:claim:after", operation);
|
|
1829
|
-
|
|
1830
|
-
return claimed;
|
|
1831
|
-
},
|
|
1832
|
-
);
|
|
1833
|
-
|
|
1834
|
-
const renewOwnership: SubmissionLedger["Service"]["renewOwnership"] = Effect.fn(
|
|
1835
|
-
"DoSubmissionLedger.renewOwnership",
|
|
1836
|
-
)(function* (request: RenewOwnershipRequest) {
|
|
1837
|
-
const operation = "ledger renew ownership";
|
|
1838
|
-
|
|
1839
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(RenewOwnershipRequest))(
|
|
1840
|
-
request,
|
|
1841
|
-
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1842
|
-
|
|
1843
|
-
yield* hitFailpoint("ledger:renew:before", operation);
|
|
1844
|
-
|
|
1845
|
-
const renewal = yield* inWriteTransaction(
|
|
1846
|
-
operation,
|
|
1847
|
-
Effect.gen(function* () {
|
|
1848
|
-
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1849
|
-
|
|
1850
|
-
yield* requireOwnership(operation, submission, validated.ownershipToken);
|
|
1851
|
-
const now = yield* currentInstant;
|
|
1852
|
-
const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
|
|
1853
|
-
|
|
1854
|
-
yield* sql`
|
|
1855
|
-
UPDATE effect_agent_submission_ownership
|
|
1856
|
-
SET lease_expires_at = ${leaseExpiresAt}
|
|
1857
|
-
WHERE submission_id = ${validated.submissionId}
|
|
1858
|
-
RETURNING *`.pipe(rows.ownership.write, Effect.mapError(internalFailure(operation)));
|
|
1609
|
+
),
|
|
1610
|
+
);
|
|
1859
1611
|
|
|
1860
|
-
return
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1612
|
+
return Option.some(
|
|
1613
|
+
yield* decodeClaim({
|
|
1614
|
+
submissionId: head.submission_id,
|
|
1615
|
+
attemptId,
|
|
1616
|
+
inputPayload,
|
|
1617
|
+
}).pipe(Effect.mapError(internalFailure(operation))),
|
|
1618
|
+
);
|
|
1864
1619
|
}),
|
|
1865
1620
|
);
|
|
1866
1621
|
|
|
1867
|
-
yield* hitFailpoint("ledger:
|
|
1622
|
+
yield* hitFailpoint("ledger:claim:after", operation);
|
|
1868
1623
|
|
|
1869
|
-
return
|
|
1624
|
+
return claimed;
|
|
1870
1625
|
});
|
|
1871
1626
|
|
|
1872
|
-
const
|
|
1873
|
-
function* (request:
|
|
1874
|
-
const operation = "ledger
|
|
1627
|
+
const markInputApplied: SubmissionLedger["Service"]["markInputApplied"] = Effect.fnUntraced(
|
|
1628
|
+
function* (request: MarkInputAppliedRequest) {
|
|
1629
|
+
const operation = "ledger mark input applied";
|
|
1875
1630
|
|
|
1876
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(
|
|
1631
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
|
|
1877
1632
|
request,
|
|
1878
1633
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1879
1634
|
|
|
1880
|
-
yield* hitFailpoint("ledger:
|
|
1881
|
-
// Settlement and suspension already remove ownership. Cleanup can observe that without
|
|
1882
|
-
// opening a transaction; an owned lane must still pass the atomic check below.
|
|
1883
|
-
const current = yield* requireSubmission(operation, validated.submissionId);
|
|
1884
|
-
|
|
1885
|
-
yield* requireOwnership(operation, current, validated.ownershipToken);
|
|
1635
|
+
yield* hitFailpoint("ledger:mark-input-applied:before", operation);
|
|
1886
1636
|
yield* inWriteTransaction(
|
|
1887
1637
|
operation,
|
|
1888
1638
|
Effect.gen(function* () {
|
|
1889
1639
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1890
1640
|
|
|
1891
|
-
yield*
|
|
1641
|
+
yield* requireOwner(operation, submission);
|
|
1642
|
+
if (submission.input_applied_record_id !== null) {
|
|
1643
|
+
if (
|
|
1644
|
+
submission.input_applied_record_id === validated.recordId &&
|
|
1645
|
+
submission.input_applied_sequence === validated.sequence
|
|
1646
|
+
) {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
return yield* corruptionFailure(
|
|
1651
|
+
operation,
|
|
1652
|
+
"effect_agent_submissions",
|
|
1653
|
+
validated.submissionId,
|
|
1654
|
+
"A different canonical input marker is already recorded for this Submission.",
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1892
1657
|
yield* sql`
|
|
1893
|
-
|
|
1658
|
+
UPDATE effect_agent_submissions
|
|
1659
|
+
SET
|
|
1660
|
+
input_applied_record_id = ${validated.recordId},
|
|
1661
|
+
input_applied_sequence = ${validated.sequence},
|
|
1662
|
+
state = CASE
|
|
1663
|
+
WHEN state IN ('admitted', 'ready') THEN 'input-applied'
|
|
1664
|
+
ELSE state
|
|
1665
|
+
END
|
|
1894
1666
|
WHERE submission_id = ${validated.submissionId}
|
|
1895
|
-
RETURNING
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
SET state = 'ready'
|
|
1900
|
-
WHERE submission_id = ${validated.submissionId}
|
|
1901
|
-
RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
|
|
1902
|
-
}
|
|
1667
|
+
RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
|
|
1668
|
+
rows.submissions.write,
|
|
1669
|
+
Effect.mapError(internalFailure(operation)),
|
|
1670
|
+
);
|
|
1903
1671
|
}),
|
|
1904
|
-
(error) => error._tag === "OwnershipLost",
|
|
1905
1672
|
);
|
|
1906
|
-
yield* hitFailpoint("ledger:
|
|
1673
|
+
yield* hitFailpoint("ledger:mark-input-applied:after", operation);
|
|
1907
1674
|
},
|
|
1908
|
-
withStorageSpan(
|
|
1909
|
-
"DoSubmissionLedger.releaseOwnership",
|
|
1910
|
-
(error) => error._tag === "OwnershipLost",
|
|
1911
|
-
),
|
|
1912
1675
|
);
|
|
1913
1676
|
|
|
1914
|
-
const
|
|
1915
|
-
"DoSubmissionLedger.
|
|
1916
|
-
)(function* (
|
|
1917
|
-
const operation = "
|
|
1677
|
+
const publish: SettlementPublisher["Service"]["publish"] = Effect.fn(
|
|
1678
|
+
"DoSubmissionLedger.publishSettlement",
|
|
1679
|
+
)(function* (input) {
|
|
1680
|
+
const operation = "publish settlement";
|
|
1681
|
+
const { request, record, settlement } = yield* validatePublication(input);
|
|
1918
1682
|
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
)
|
|
1683
|
+
const prepared = yield* prepareCanonicalAppend(request.append).pipe(
|
|
1684
|
+
Effect.provideService(Crypto.Crypto, crypto),
|
|
1685
|
+
);
|
|
1922
1686
|
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
operation,
|
|
1926
|
-
Effect.gen(function* () {
|
|
1927
|
-
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1928
|
-
|
|
1929
|
-
yield* requireOwnership(operation, submission, validated.ownershipToken);
|
|
1930
|
-
if (submission.input_applied_record_id !== null) {
|
|
1931
|
-
if (
|
|
1932
|
-
submission.input_applied_record_id === validated.recordId &&
|
|
1933
|
-
submission.input_applied_sequence === validated.sequence
|
|
1934
|
-
) {
|
|
1935
|
-
return;
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
return yield* corruptionFailure(
|
|
1939
|
-
operation,
|
|
1940
|
-
"effect_agent_submissions",
|
|
1941
|
-
validated.submissionId,
|
|
1942
|
-
"A different canonical input marker is already recorded for this Submission.",
|
|
1943
|
-
);
|
|
1944
|
-
}
|
|
1945
|
-
yield* sql`
|
|
1946
|
-
UPDATE effect_agent_submissions
|
|
1947
|
-
SET
|
|
1948
|
-
input_applied_record_id = ${validated.recordId},
|
|
1949
|
-
input_applied_sequence = ${validated.sequence},
|
|
1950
|
-
state = CASE
|
|
1951
|
-
WHEN state IN ('admitted', 'ready', 'running') THEN 'input-applied'
|
|
1952
|
-
ELSE state
|
|
1953
|
-
END
|
|
1954
|
-
WHERE submission_id = ${validated.submissionId}
|
|
1955
|
-
RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
|
|
1956
|
-
}),
|
|
1957
|
-
);
|
|
1958
|
-
yield* hitFailpoint("ledger:mark-input-applied:after", operation);
|
|
1959
|
-
});
|
|
1960
|
-
|
|
1961
|
-
const reserveSettlement: SubmissionLedger["Service"]["reserveSettlement"] = Effect.fn(
|
|
1962
|
-
"DoSubmissionLedger.reserveSettlement",
|
|
1963
|
-
)(function* (request: SettlementReservation) {
|
|
1964
|
-
const operation = "ledger reserve settlement";
|
|
1965
|
-
|
|
1966
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(SettlementReservation))(
|
|
1967
|
-
request,
|
|
1968
|
-
).pipe(Effect.mapError(internalFailure(operation)));
|
|
1687
|
+
const mapAppendError = (error: { readonly _tag: string; readonly message: string }) =>
|
|
1688
|
+
ThreadStoreError.make({ operation, message: error.message, cause: error });
|
|
1969
1689
|
|
|
1970
|
-
|
|
1971
|
-
Effect.mapError(internalFailure(operation)),
|
|
1972
|
-
);
|
|
1973
|
-
|
|
1974
|
-
// The reserved record is appended canonically later; refuse over-bound payloads typed
|
|
1975
|
-
// before the reservation row exists.
|
|
1976
|
-
yield* journal
|
|
1977
|
-
.checkValueBound(operation, recordJson)
|
|
1978
|
-
.pipe(Effect.mapError(internalFailure(operation)));
|
|
1979
|
-
yield* hitFailpoint("ledger:reserve-settlement:before", operation);
|
|
1980
|
-
|
|
1981
|
-
const reserved = yield* inWriteTransaction(
|
|
1982
|
-
operation,
|
|
1983
|
-
Effect.gen(function* () {
|
|
1984
|
-
const existing = yield* readReservation(operation, validated.submissionId);
|
|
1985
|
-
|
|
1986
|
-
if (Option.isSome(existing)) {
|
|
1987
|
-
const identical =
|
|
1988
|
-
existing.value.settlement_id === validated.settlementId &&
|
|
1989
|
-
existing.value.outcome === validated.outcome &&
|
|
1990
|
-
existing.value.record_digest === validated.recordDigest &&
|
|
1991
|
-
existing.value.record_json === recordJson;
|
|
1992
|
-
|
|
1993
|
-
if (!identical) {
|
|
1994
|
-
return yield* SettlementConflict.make({
|
|
1995
|
-
submissionId: validated.submissionId,
|
|
1996
|
-
existingOutcome: existing.value.outcome,
|
|
1997
|
-
});
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
const record = yield* decodeRecordEnvelopeText(existing.value.record_json).pipe(
|
|
2001
|
-
Effect.mapError((error) =>
|
|
2002
|
-
corruptionFailure(
|
|
2003
|
-
operation,
|
|
2004
|
-
"effect_agent_settlement_reservations",
|
|
2005
|
-
validated.submissionId,
|
|
2006
|
-
error.message,
|
|
2007
|
-
),
|
|
2008
|
-
),
|
|
2009
|
-
);
|
|
2010
|
-
|
|
2011
|
-
return ReservedSettlement.make({
|
|
2012
|
-
submissionId: validated.submissionId,
|
|
2013
|
-
settlementId: validated.settlementId,
|
|
2014
|
-
outcome: validated.outcome,
|
|
2015
|
-
record,
|
|
2016
|
-
recordDigest: validated.recordDigest,
|
|
2017
|
-
replayed: true,
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1690
|
+
yield* journal.prepareAppend(prepared.raw).pipe(Effect.mapError(mapAppendError));
|
|
2022
1691
|
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
1692
|
+
return yield* owner.withProgress(
|
|
1693
|
+
request.append.threadId,
|
|
1694
|
+
owner.withMutation(
|
|
1695
|
+
Effect.gen(function* () {
|
|
1696
|
+
yield* hitFailpoint("append:before", operation);
|
|
1697
|
+
|
|
1698
|
+
const result = yield* journal
|
|
1699
|
+
.withWriteTransaction(operation)(
|
|
1700
|
+
Effect.gen(function* () {
|
|
1701
|
+
const submission = yield* requireSubmission(operation, request.submissionId);
|
|
1702
|
+
|
|
1703
|
+
if (submission.thread_id !== prepared.request.threadId)
|
|
1704
|
+
return yield* LedgerError.make({
|
|
1705
|
+
operation,
|
|
1706
|
+
message: "Settlement publication belongs to another Thread.",
|
|
1707
|
+
});
|
|
1708
|
+
const snapshot = yield* decodeSubmissionSnapshot(operation, submission);
|
|
1709
|
+
|
|
1710
|
+
yield* validateCanonicalSettlement(record, snapshot);
|
|
1711
|
+
const existing = yield* readCanonicalSettlement(operation, submission);
|
|
1712
|
+
const thread = (yield* journal.getThread(prepared.request.threadId))[0];
|
|
1713
|
+
|
|
1714
|
+
if (thread === undefined)
|
|
1715
|
+
return yield* ThreadNotMaterialized.make({ threadId: prepared.request.threadId });
|
|
1716
|
+
|
|
1717
|
+
// Replay still requires authority: finalization releases Owned tokens, while
|
|
1718
|
+
// retained host linkage or abort intent can authorize tokenless settled replay.
|
|
1719
|
+
switch (request.authority._tag) {
|
|
1720
|
+
case "Owned":
|
|
1721
|
+
yield* requireOwner(operation, submission);
|
|
1722
|
+
yield* validateCanonicalSettlement(record, {
|
|
1723
|
+
submissionId: request.submissionId,
|
|
1724
|
+
receiptId: snapshot.receiptId,
|
|
1725
|
+
...(settlement.outcome === "aborted" && settlement.runId === undefined
|
|
1726
|
+
? {}
|
|
1727
|
+
: { runId: runIdForSubmission(request.submissionId) }),
|
|
1728
|
+
});
|
|
1729
|
+
break;
|
|
1730
|
+
case "Joined": {
|
|
1731
|
+
if (
|
|
1732
|
+
(submission.state !== "joined" &&
|
|
1733
|
+
!(submission.state === "settled" && existing !== undefined)) ||
|
|
1734
|
+
submission.joined_host_submission_id !== request.authority.hostSubmissionId
|
|
1735
|
+
)
|
|
1736
|
+
return yield* LedgerError.make({
|
|
1737
|
+
operation,
|
|
1738
|
+
message: "Submission is not joined to the named host.",
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
const host = yield* requireSubmission(
|
|
1742
|
+
operation,
|
|
1743
|
+
request.authority.hostSubmissionId,
|
|
1744
|
+
);
|
|
1745
|
+
|
|
1746
|
+
if (host.thread_id !== submission.thread_id)
|
|
1747
|
+
return yield* LedgerError.make({
|
|
1748
|
+
operation,
|
|
1749
|
+
message: "Joined host belongs to another Thread.",
|
|
1750
|
+
});
|
|
1751
|
+
const canonicalHost = yield* readCanonicalSettlement(operation, host);
|
|
1752
|
+
|
|
1753
|
+
if (canonicalHost === undefined)
|
|
1754
|
+
return yield* LedgerError.make({
|
|
1755
|
+
operation,
|
|
1756
|
+
message: "Joined host has no canonical settlement.",
|
|
1757
|
+
});
|
|
1758
|
+
yield* validateJoinedSettlement(settlement, canonicalHost.settlement);
|
|
1759
|
+
break;
|
|
1760
|
+
}
|
|
1761
|
+
case "QueuedAbort":
|
|
1762
|
+
if (
|
|
1763
|
+
(submission.state !== "ready" &&
|
|
1764
|
+
!(submission.state === "settled" && existing !== undefined)) ||
|
|
1765
|
+
settlement.outcome !== "aborted" ||
|
|
1766
|
+
Option.isNone(yield* readAbortIntent(operation, request.submissionId)) ||
|
|
1767
|
+
owner.hasOwner(submission.thread_id, request.submissionId)
|
|
1768
|
+
)
|
|
1769
|
+
return yield* LedgerError.make({
|
|
1770
|
+
operation,
|
|
1771
|
+
message: "Queued abort is no longer authorized.",
|
|
1772
|
+
});
|
|
1773
|
+
yield* validateCanonicalSettlement(record, {
|
|
1774
|
+
submissionId: request.submissionId,
|
|
1775
|
+
receiptId: snapshot.receiptId,
|
|
1776
|
+
runId: undefined,
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
if (existing !== undefined) {
|
|
1780
|
+
yield* journal.checkCommitIntent(prepared.raw);
|
|
1781
|
+
|
|
1782
|
+
return SettlementPublicationResult.make({
|
|
1783
|
+
record: existing.record,
|
|
1784
|
+
tailSequence: thread.tail_sequence,
|
|
1785
|
+
replayed: true,
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
if (submission.state === "settled")
|
|
1789
|
+
return yield* LedgerError.make({
|
|
1790
|
+
operation,
|
|
1791
|
+
message: "Finalized Submission has no canonical settlement.",
|
|
1792
|
+
});
|
|
1793
|
+
const appended = yield* journal.appendPrepared(prepared.raw);
|
|
1794
|
+
|
|
1795
|
+
return SettlementPublicationResult.make({
|
|
1796
|
+
record,
|
|
1797
|
+
tailSequence: appended.lastSequence,
|
|
1798
|
+
replayed: appended.replayed,
|
|
1799
|
+
});
|
|
1800
|
+
}),
|
|
1801
|
+
)
|
|
1802
|
+
.pipe(
|
|
1803
|
+
Effect.mapError((error) => {
|
|
1804
|
+
if (error._tag === "DoAppendConflict")
|
|
1805
|
+
return AppendConflict.make({
|
|
1806
|
+
threadId: prepared.request.threadId,
|
|
1807
|
+
batchId: prepared.request.batch.batchId,
|
|
1808
|
+
reason: error.reason,
|
|
1809
|
+
...(error.actualTailSequence !== undefined
|
|
1810
|
+
? {
|
|
1811
|
+
actualTailSequence: error.actualTailSequence,
|
|
1812
|
+
}
|
|
1813
|
+
: {}),
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
return isDoStorageError(error) ||
|
|
1817
|
+
error._tag === "DoStorageCorruptionError" ||
|
|
1818
|
+
error._tag === "DoStorageFailpointError" ||
|
|
1819
|
+
error._tag === "DoValueBoundExceeded"
|
|
1820
|
+
? mapAppendError(error)
|
|
1821
|
+
: error;
|
|
1822
|
+
}),
|
|
2030
1823
|
);
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
return yield* SettlementConflict.make({
|
|
2034
|
-
submissionId: validated.submissionId,
|
|
2035
|
-
existingOutcome: submission.settled_outcome,
|
|
2036
|
-
});
|
|
2037
|
-
}
|
|
2038
|
-
// A `joined` Submission settles WITH its host (plan §2.5) and its lane is never
|
|
2039
|
-
// worker-claimable, so no ownership token can exist for it: the recorded host linkage
|
|
2040
|
-
// authorizes the reservation and the presented token is not consulted.
|
|
2041
|
-
if (!(submission.state === "joined" && submission.joined_host_submission_id !== null)) {
|
|
2042
|
-
// P7 §7(c): an aborted, never-claimed, still-queued Submission likewise has no live
|
|
2043
|
-
// ownership to fence against — its durable abort intent authorizes exactly its
|
|
2044
|
-
// ABORTED settlement (`terminalizing` is the same pass's crash replay). Every other
|
|
2045
|
-
// reservation stays fenced by the target lane's live ownership.
|
|
2046
|
-
let queuedAbortSettlement = false;
|
|
2047
|
-
|
|
2048
|
-
if (
|
|
2049
|
-
validated.outcome === "aborted" &&
|
|
2050
|
-
(submission.state === "ready" || submission.state === "terminalizing")
|
|
2051
|
-
) {
|
|
2052
|
-
const abortIntent = yield* readAbortIntent(operation, validated.submissionId);
|
|
2053
|
-
|
|
2054
|
-
if (Option.isSome(abortIntent)) {
|
|
2055
|
-
const ownership = yield* readOwnership(operation, validated.submissionId);
|
|
2056
|
-
|
|
2057
|
-
queuedAbortSettlement = Option.isNone(ownership);
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
if (!queuedAbortSettlement) {
|
|
2061
|
-
yield* requireOwnership(operation, submission, validated.ownershipToken);
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
const now = yield* currentInstant;
|
|
2065
1824
|
|
|
2066
|
-
|
|
2067
|
-
INSERT INTO effect_agent_settlement_reservations (
|
|
2068
|
-
submission_id,
|
|
2069
|
-
settlement_id,
|
|
2070
|
-
outcome,
|
|
2071
|
-
record_id,
|
|
2072
|
-
record_json,
|
|
2073
|
-
record_digest,
|
|
2074
|
-
reserved_at
|
|
2075
|
-
) VALUES (
|
|
2076
|
-
${validated.submissionId},
|
|
2077
|
-
${validated.settlementId},
|
|
2078
|
-
${validated.outcome},
|
|
2079
|
-
${validated.record.recordId},
|
|
2080
|
-
${recordJson},
|
|
2081
|
-
${validated.recordDigest},
|
|
2082
|
-
${now.iso}
|
|
2083
|
-
)
|
|
2084
|
-
RETURNING *`.pipe(rows.reservations.write, Effect.mapError(internalFailure(operation)));
|
|
2085
|
-
yield* sql`
|
|
2086
|
-
UPDATE effect_agent_submissions
|
|
2087
|
-
SET state = 'terminalizing'
|
|
2088
|
-
WHERE submission_id = ${validated.submissionId}
|
|
2089
|
-
RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
|
|
1825
|
+
yield* hitFailpoint("append:after", operation);
|
|
2090
1826
|
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
outcome: validated.outcome,
|
|
2095
|
-
record: validated.record,
|
|
2096
|
-
recordDigest: validated.recordDigest,
|
|
2097
|
-
replayed: false,
|
|
2098
|
-
});
|
|
2099
|
-
}),
|
|
1827
|
+
return result;
|
|
1828
|
+
}),
|
|
1829
|
+
),
|
|
2100
1830
|
);
|
|
2101
|
-
|
|
2102
|
-
yield* hitFailpoint("ledger:reserve-settlement:after", operation);
|
|
2103
|
-
|
|
2104
|
-
return reserved;
|
|
2105
1831
|
});
|
|
2106
1832
|
|
|
2107
|
-
const validateFinalization = Effect.
|
|
1833
|
+
const validateFinalization = Effect.fnUntraced(function* (
|
|
2108
1834
|
validated: SettlementFinalization,
|
|
2109
|
-
|
|
1835
|
+
submission: SubmissionRow,
|
|
2110
1836
|
) {
|
|
2111
1837
|
const operation = "ledger finalize settlement";
|
|
1838
|
+
const canonical = yield* readCanonicalSettlement(operation, submission);
|
|
2112
1839
|
|
|
2113
|
-
if (
|
|
1840
|
+
if (canonical === undefined)
|
|
2114
1841
|
return yield* LedgerError.make({
|
|
2115
1842
|
operation,
|
|
2116
|
-
message: `No settlement
|
|
1843
|
+
message: `No canonical settlement exists for submission ${validated.submissionId}.`,
|
|
2117
1844
|
});
|
|
2118
|
-
|
|
2119
|
-
if (reservation.value.settlement_id !== validated.settlementId) {
|
|
1845
|
+
if (canonical.settlement.settlementId !== validated.settlementId)
|
|
2120
1846
|
return yield* SettlementConflict.make({
|
|
2121
1847
|
submissionId: validated.submissionId,
|
|
2122
|
-
existingOutcome:
|
|
1848
|
+
existingOutcome: canonical.settlement.outcome,
|
|
2123
1849
|
});
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
const reservationRecord = yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(
|
|
2127
|
-
Effect.mapError((error) =>
|
|
2128
|
-
corruptionFailure(
|
|
2129
|
-
operation,
|
|
2130
|
-
"effect_agent_settlement_reservations",
|
|
2131
|
-
validated.submissionId,
|
|
2132
|
-
error.message,
|
|
2133
|
-
),
|
|
2134
|
-
),
|
|
2135
|
-
);
|
|
2136
1850
|
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
if ((reservation.value.outcome === "failed") !== (settlementFailure !== undefined)) {
|
|
2140
|
-
return yield* corruptionFailure(
|
|
2141
|
-
operation,
|
|
2142
|
-
"effect_agent_settlement_reservations",
|
|
2143
|
-
validated.submissionId,
|
|
2144
|
-
"The reserved outcome and canonical failure diagnostic disagree.",
|
|
2145
|
-
);
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
return { reservation: reservation.value, reservationRecord, settlementFailure };
|
|
1851
|
+
return { ...canonical, settlementFailure: settlementFailureFromRecord(canonical.record) };
|
|
2149
1852
|
});
|
|
2150
1853
|
|
|
2151
|
-
const replayFinalization = Effect.
|
|
1854
|
+
const replayFinalization = Effect.fnUntraced(function* (
|
|
2152
1855
|
validated: SettlementFinalization,
|
|
2153
1856
|
submission: SubmissionRow,
|
|
2154
|
-
{
|
|
1857
|
+
{
|
|
1858
|
+
record,
|
|
1859
|
+
settlement,
|
|
1860
|
+
settlementFailure,
|
|
1861
|
+
}: Effect.Success<ReturnType<typeof validateFinalization>>,
|
|
2155
1862
|
) {
|
|
2156
1863
|
const operation = "ledger finalize settlement";
|
|
2157
1864
|
|
|
2158
|
-
if (
|
|
1865
|
+
if (
|
|
1866
|
+
submission.finalized_at === null ||
|
|
1867
|
+
submission.settled_record_id !== record.recordId ||
|
|
1868
|
+
submission.settled_outcome !== settlement.outcome
|
|
1869
|
+
)
|
|
2159
1870
|
return yield* corruptionFailure(
|
|
2160
1871
|
operation,
|
|
2161
|
-
"
|
|
1872
|
+
"effect_agent_submissions",
|
|
2162
1873
|
validated.submissionId,
|
|
2163
|
-
"
|
|
1874
|
+
"Settled projection disagrees with its canonical settlement.",
|
|
2164
1875
|
);
|
|
2165
|
-
}
|
|
2166
1876
|
|
|
2167
1877
|
return yield* decodeSettlement({
|
|
2168
1878
|
submissionId: validated.submissionId,
|
|
2169
1879
|
settlementId: validated.settlementId,
|
|
2170
1880
|
receiptId: submission.receipt_id,
|
|
2171
|
-
outcome:
|
|
1881
|
+
outcome: settlement.outcome,
|
|
2172
1882
|
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
2173
|
-
settledAt:
|
|
1883
|
+
settledAt: submission.finalized_at,
|
|
2174
1884
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
2175
1885
|
});
|
|
2176
1886
|
|
|
@@ -2192,34 +1902,39 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2192
1902
|
|
|
2193
1903
|
if (Option.isNone(submission) || submission.value.state !== "settled") return Option.none();
|
|
2194
1904
|
|
|
2195
|
-
const finalization = yield* validateFinalization(
|
|
2196
|
-
validated,
|
|
2197
|
-
yield* readReservation(operation, validated.submissionId),
|
|
2198
|
-
);
|
|
1905
|
+
const finalization = yield* validateFinalization(validated, submission.value);
|
|
2199
1906
|
|
|
2200
|
-
return Option.some(
|
|
1907
|
+
return Option.some({
|
|
1908
|
+
settlement: yield* replayFinalization(validated, submission.value, finalization),
|
|
1909
|
+
threadId: submission.value.thread_id,
|
|
1910
|
+
});
|
|
2201
1911
|
}),
|
|
2202
1912
|
);
|
|
2203
1913
|
|
|
2204
1914
|
if (Option.isSome(replay)) {
|
|
1915
|
+
owner.closeSubmission(replay.value.threadId, validated.submissionId);
|
|
2205
1916
|
yield* hitFailpoint("ledger:finalize-settlement:after", operation);
|
|
2206
1917
|
|
|
2207
|
-
return replay.value;
|
|
1918
|
+
return replay.value.settlement;
|
|
2208
1919
|
}
|
|
2209
1920
|
|
|
2210
1921
|
const settlement = yield* inWriteTransaction(
|
|
2211
1922
|
operation,
|
|
2212
1923
|
Effect.gen(function* () {
|
|
2213
|
-
const state = yield* validateFinalization(
|
|
2214
|
-
validated,
|
|
2215
|
-
yield* readReservation(operation, validated.submissionId),
|
|
2216
|
-
);
|
|
2217
|
-
|
|
2218
|
-
const { reservation, reservationRecord, settlementFailure } = state;
|
|
2219
1924
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
1925
|
+
const state = yield* validateFinalization(validated, submission);
|
|
1926
|
+
|
|
1927
|
+
const {
|
|
1928
|
+
settlement: canonicalSettlement,
|
|
1929
|
+
record: canonicalRecord,
|
|
1930
|
+
settlementFailure,
|
|
1931
|
+
} = state;
|
|
2220
1932
|
|
|
2221
1933
|
if (submission.state === "settled")
|
|
2222
|
-
return
|
|
1934
|
+
return {
|
|
1935
|
+
settlement: yield* replayFinalization(validated, submission, state),
|
|
1936
|
+
threadId: submission.thread_id,
|
|
1937
|
+
};
|
|
2223
1938
|
const now = yield* currentInstant;
|
|
2224
1939
|
|
|
2225
1940
|
const terminal =
|
|
@@ -2227,7 +1942,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2227
1942
|
? undefined
|
|
2228
1943
|
: workerTerminalFromRecord(
|
|
2229
1944
|
yield* decodeSubmissionSnapshot(operation, submission),
|
|
2230
|
-
|
|
1945
|
+
canonicalRecord,
|
|
2231
1946
|
);
|
|
2232
1947
|
|
|
2233
1948
|
let sealedTerminal: typeof terminal;
|
|
@@ -2269,55 +1984,51 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2269
1984
|
|
|
2270
1985
|
yield* sql`
|
|
2271
1986
|
UPDATE effect_agent_submissions
|
|
2272
|
-
SET state = 'settled', settled_outcome = ${
|
|
2273
|
-
|
|
2274
|
-
RETURNING *`.pipe(rows.submissions.write, Effect.mapError(internalFailure(operation)));
|
|
2275
|
-
yield* sql`
|
|
2276
|
-
UPDATE effect_agent_settlement_reservations
|
|
2277
|
-
SET finalized_at = ${now.iso}
|
|
1987
|
+
SET state = 'settled', settled_outcome = ${canonicalSettlement.outcome},
|
|
1988
|
+
settled_record_id = ${canonicalRecord.recordId}, finalized_at = ${now.iso}
|
|
2278
1989
|
WHERE submission_id = ${validated.submissionId}
|
|
2279
|
-
RETURNING
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
RETURNING *`.pipe(rows.ownership.remove, Effect.mapError(internalFailure(operation)));
|
|
1990
|
+
RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
|
|
1991
|
+
rows.submissions.write,
|
|
1992
|
+
Effect.mapError(internalFailure(operation)),
|
|
1993
|
+
);
|
|
2284
1994
|
|
|
2285
1995
|
yield* recordProgress("submission", operation);
|
|
2286
1996
|
|
|
2287
1997
|
if (sealedTerminal !== undefined)
|
|
2288
1998
|
yield* retainWorkerSeal(submission.thread_id, sealedTerminal);
|
|
2289
1999
|
|
|
2290
|
-
return
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2000
|
+
return {
|
|
2001
|
+
threadId: submission.thread_id,
|
|
2002
|
+
settlement: yield* decodeSettlement({
|
|
2003
|
+
submissionId: validated.submissionId,
|
|
2004
|
+
settlementId: validated.settlementId,
|
|
2005
|
+
receiptId: submission.receipt_id,
|
|
2006
|
+
outcome: canonicalSettlement.outcome,
|
|
2007
|
+
...(settlementFailure === undefined ? {} : { failure: settlementFailure }),
|
|
2008
|
+
settledAt: now.iso,
|
|
2009
|
+
}).pipe(Effect.mapError(internalFailure(operation))),
|
|
2010
|
+
};
|
|
2298
2011
|
}),
|
|
2299
2012
|
);
|
|
2300
2013
|
|
|
2014
|
+
owner.closeSubmission(settlement.threadId, validated.submissionId);
|
|
2301
2015
|
yield* hitFailpoint("ledger:finalize-settlement:after", operation);
|
|
2302
2016
|
|
|
2303
|
-
return settlement;
|
|
2017
|
+
return settlement.settlement;
|
|
2304
2018
|
});
|
|
2305
2019
|
|
|
2306
|
-
const inspectWorker = Effect.
|
|
2307
|
-
threadId: SubmissionSnapshot["threadId"],
|
|
2308
|
-
) {
|
|
2020
|
+
const inspectWorker = Effect.fnUntraced(function* (threadId: SubmissionSnapshot["threadId"]) {
|
|
2309
2021
|
const operation = "inspect worker";
|
|
2310
2022
|
|
|
2311
2023
|
return yield* state
|
|
2312
2024
|
.transaction(
|
|
2313
2025
|
Effect.gen(function* () {
|
|
2314
2026
|
const read = Effect.fnUntraced(function* (active: boolean) {
|
|
2315
|
-
const rows =
|
|
2316
|
-
yield* sql`SELECT ${sql.literal(SUBMISSION_COLUMNS)} FROM effect_agent_submissions
|
|
2027
|
+
const rows = yield* sql`SELECT ${submissionColumns} FROM effect_agent_submissions
|
|
2317
2028
|
WHERE thread_id = ${threadId} ${active ? sql`AND state <> 'settled'` : sql``}
|
|
2318
2029
|
ORDER BY queue_sequence ${active ? sql`ASC` : sql`DESC`} LIMIT 1`.pipe(
|
|
2319
|
-
|
|
2320
|
-
|
|
2030
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2031
|
+
);
|
|
2321
2032
|
|
|
2322
2033
|
const decoded = yield* decodeSubmissionRows(operation, threadId, rows);
|
|
2323
2034
|
|
|
@@ -2347,9 +2058,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2347
2058
|
);
|
|
2348
2059
|
});
|
|
2349
2060
|
|
|
2350
|
-
const stopWorker = Effect.
|
|
2351
|
-
request: WorkerStopCommand,
|
|
2352
|
-
) {
|
|
2061
|
+
const stopWorker = Effect.fnUntraced(function* (request: WorkerStopCommand) {
|
|
2353
2062
|
const operation = "ledger stop worker";
|
|
2354
2063
|
|
|
2355
2064
|
const validated = yield* Schema.decodeEffect(WorkerStopCommand)(request).pipe(
|
|
@@ -2375,23 +2084,17 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2375
2084
|
Effect.mapError(internalFailure(operation)),
|
|
2376
2085
|
);
|
|
2377
2086
|
|
|
2378
|
-
const owners = yield* sql`SELECT o.submission_id FROM effect_agent_submission_ownership o
|
|
2379
|
-
JOIN effect_agent_submissions s ON s.submission_id = o.submission_id
|
|
2380
|
-
WHERE s.thread_id = ${validated.threadId} AND s.state <> 'settled'`.pipe(
|
|
2381
|
-
Effect.mapError(sqlFailure(operation)),
|
|
2382
|
-
);
|
|
2383
|
-
|
|
2384
2087
|
if (sealed.length > 0 || aborted.length > 0) yield* recordProgress("control", operation);
|
|
2385
2088
|
if (sealed.length > 0) yield* retainWorkerSeal(validated.threadId, null);
|
|
2386
2089
|
|
|
2387
|
-
return
|
|
2090
|
+
return owner.hasOwner(validated.threadId) ? 1 : 0;
|
|
2388
2091
|
}),
|
|
2389
2092
|
);
|
|
2390
2093
|
});
|
|
2391
2094
|
|
|
2392
|
-
const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.
|
|
2393
|
-
|
|
2394
|
-
)
|
|
2095
|
+
const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fnUntraced(function* (
|
|
2096
|
+
request: AbortCommand,
|
|
2097
|
+
) {
|
|
2395
2098
|
const operation = "ledger request abort";
|
|
2396
2099
|
|
|
2397
2100
|
const validated = yield* Schema.decodeEffect(Schema.toType(AbortCommand))(request).pipe(
|
|
@@ -2495,9 +2198,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2495
2198
|
return intent;
|
|
2496
2199
|
});
|
|
2497
2200
|
|
|
2498
|
-
const claimJoining: SubmissionLedger["Service"]["claimJoining"] = Effect.
|
|
2499
|
-
|
|
2500
|
-
)
|
|
2201
|
+
const claimJoining: SubmissionLedger["Service"]["claimJoining"] = Effect.fnUntraced(function* (
|
|
2202
|
+
request: ClaimJoiningRequest,
|
|
2203
|
+
) {
|
|
2501
2204
|
const operation = "ledger claim joining";
|
|
2502
2205
|
|
|
2503
2206
|
const validated = yield* Schema.decodeEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(
|
|
@@ -2517,8 +2220,8 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2517
2220
|
message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`,
|
|
2518
2221
|
});
|
|
2519
2222
|
}
|
|
2520
|
-
// The host Attempt
|
|
2521
|
-
yield*
|
|
2223
|
+
// The host Attempt retains its local ownership while the ready prefix joins it.
|
|
2224
|
+
yield* requireOwner(operation, host);
|
|
2522
2225
|
|
|
2523
2226
|
const stopped = yield* cached(
|
|
2524
2227
|
rows.workerStops.by("thread_id", validated.threadId),
|
|
@@ -2527,21 +2230,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2527
2230
|
|
|
2528
2231
|
if (stopped.length > 0) return [];
|
|
2529
2232
|
|
|
2530
|
-
const
|
|
2531
|
-
|
|
2532
|
-
const laterRows =
|
|
2533
|
-
lane === undefined
|
|
2534
|
-
? yield* sql<Record<string, unknown>>`
|
|
2535
|
-
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2233
|
+
const laterRows = yield* sql<Record<string, unknown>>`
|
|
2234
|
+
SELECT ${submissionColumns}
|
|
2536
2235
|
FROM effect_agent_submissions
|
|
2537
2236
|
WHERE thread_id = ${validated.threadId}
|
|
2538
2237
|
AND queue_sequence > ${host.queue_sequence}
|
|
2539
2238
|
ORDER BY queue_sequence ASC
|
|
2540
|
-
`.pipe(Effect.mapError(sqlFailure(operation)))
|
|
2541
|
-
: yield* Effect.forEach(
|
|
2542
|
-
lane.filter((item) => item.queue_sequence > host.queue_sequence),
|
|
2543
|
-
(item) => requireSubmission(operation, item.submission_id),
|
|
2544
|
-
);
|
|
2239
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2545
2240
|
|
|
2546
2241
|
const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
|
|
2547
2242
|
const claimed: Array<JoiningClaim> = [];
|
|
@@ -2563,11 +2258,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2563
2258
|
// Any other non-ready row — an admitted-not-ready gap in particular — breaks the
|
|
2564
2259
|
// contiguous ready prefix (plan §2.5); later ready work stays queued (DUR-004).
|
|
2565
2260
|
if (row.state !== "ready") break;
|
|
2261
|
+
const canonical = yield* readCanonicalSettlement(operation, row);
|
|
2262
|
+
|
|
2263
|
+
if (canonical?.settlement.outcome === "aborted") continue;
|
|
2264
|
+
if (canonical !== undefined) break;
|
|
2566
2265
|
yield* sql`
|
|
2567
2266
|
UPDATE effect_agent_submissions
|
|
2568
2267
|
SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
|
|
2569
2268
|
WHERE submission_id = ${row.submission_id}
|
|
2570
|
-
RETURNING
|
|
2269
|
+
RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
|
|
2270
|
+
rows.submissions.write,
|
|
2271
|
+
Effect.mapError(internalFailure(operation)),
|
|
2272
|
+
);
|
|
2571
2273
|
|
|
2572
2274
|
const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
|
|
2573
2275
|
Effect.mapError((error) =>
|
|
@@ -2598,9 +2300,9 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2598
2300
|
return claims;
|
|
2599
2301
|
});
|
|
2600
2302
|
|
|
2601
|
-
const markJoined: SubmissionLedger["Service"]["markJoined"] = Effect.
|
|
2602
|
-
|
|
2603
|
-
)
|
|
2303
|
+
const markJoined: SubmissionLedger["Service"]["markJoined"] = Effect.fnUntraced(function* (
|
|
2304
|
+
request: MarkJoinedRequest,
|
|
2305
|
+
) {
|
|
2604
2306
|
const operation = "ledger mark joined";
|
|
2605
2307
|
|
|
2606
2308
|
const validated = yield* Schema.decodeEffect(Schema.toType(MarkJoinedRequest))(request).pipe(
|
|
@@ -2623,7 +2325,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2623
2325
|
|
|
2624
2326
|
// The lane is host-owned: the presented token must own the HOST's ownership period,
|
|
2625
2327
|
// which also lets a later host Attempt repair a lost marker from history (DUR-016).
|
|
2626
|
-
yield*
|
|
2328
|
+
yield* requireOwner(operation, host);
|
|
2627
2329
|
if (submission.input_applied_record_id !== null) {
|
|
2628
2330
|
if (
|
|
2629
2331
|
submission.input_applied_record_id === validated.recordId &&
|
|
@@ -2652,15 +2354,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2652
2354
|
input_applied_sequence = ${validated.sequence},
|
|
2653
2355
|
state = 'joined'
|
|
2654
2356
|
WHERE submission_id = ${validated.submissionId}
|
|
2655
|
-
RETURNING
|
|
2357
|
+
RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
|
|
2358
|
+
rows.submissions.write,
|
|
2359
|
+
Effect.mapError(internalFailure(operation)),
|
|
2360
|
+
);
|
|
2656
2361
|
}),
|
|
2657
2362
|
);
|
|
2658
2363
|
yield* hitFailpoint("ledger:mark-joined:after", operation);
|
|
2659
2364
|
});
|
|
2660
2365
|
|
|
2661
|
-
const revertJoining: SubmissionLedger["Service"]["revertJoining"] = Effect.
|
|
2662
|
-
|
|
2663
|
-
)
|
|
2366
|
+
const revertJoining: SubmissionLedger["Service"]["revertJoining"] = Effect.fnUntraced(function* (
|
|
2367
|
+
request: RevertJoiningRequest,
|
|
2368
|
+
) {
|
|
2664
2369
|
const operation = "ledger revert joining";
|
|
2665
2370
|
|
|
2666
2371
|
const validated = yield* Schema.decodeEffect(Schema.toType(RevertJoiningRequest))(request).pipe(
|
|
@@ -2676,135 +2381,169 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2676
2381
|
// Idempotent and recovery-only: only a still-`joining` Submission reverts; an
|
|
2677
2382
|
// already-joined (or already-reverted) Submission is a no-op (DUR-016).
|
|
2678
2383
|
if (submission.state !== "joining") return;
|
|
2384
|
+
const guard = validated.guard;
|
|
2385
|
+
|
|
2386
|
+
if (guard !== undefined) {
|
|
2387
|
+
if (submission.joined_host_submission_id !== guard.hostSubmissionId) return;
|
|
2388
|
+
const host = yield* requireSubmission(operation, guard.hostSubmissionId);
|
|
2389
|
+
|
|
2390
|
+
if (host.thread_id !== submission.thread_id)
|
|
2391
|
+
return yield* corruptionFailure(
|
|
2392
|
+
operation,
|
|
2393
|
+
"effect_agent_submissions",
|
|
2394
|
+
validated.submissionId,
|
|
2395
|
+
"The joined host belongs to another Thread.",
|
|
2396
|
+
);
|
|
2397
|
+
if (host.state !== "settled") {
|
|
2398
|
+
yield* requireOwner(operation, host).pipe(
|
|
2399
|
+
Effect.mapError((cause) =>
|
|
2400
|
+
cause._tag === "OwnershipLost"
|
|
2401
|
+
? LedgerError.make({
|
|
2402
|
+
operation,
|
|
2403
|
+
message: "Joining recovery no longer owns the linked host.",
|
|
2404
|
+
cause,
|
|
2405
|
+
})
|
|
2406
|
+
: cause,
|
|
2407
|
+
),
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2679
2411
|
yield* sql`
|
|
2680
2412
|
UPDATE effect_agent_submissions
|
|
2681
2413
|
SET state = 'ready', joined_host_submission_id = NULL
|
|
2682
2414
|
WHERE submission_id = ${validated.submissionId}
|
|
2683
|
-
RETURNING
|
|
2415
|
+
RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
|
|
2416
|
+
rows.submissions.write,
|
|
2417
|
+
Effect.mapError(internalFailure(operation)),
|
|
2418
|
+
);
|
|
2684
2419
|
}),
|
|
2685
2420
|
);
|
|
2686
2421
|
yield* hitFailpoint("ledger:revert-joining:after", operation);
|
|
2687
2422
|
});
|
|
2688
2423
|
|
|
2689
|
-
const suspend: SubmissionLedger["Service"]["suspend"] = Effect.
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(SuspendRequest))(request).pipe(
|
|
2694
|
-
Effect.mapError(internalFailure(operation)),
|
|
2695
|
-
);
|
|
2424
|
+
const suspend: SubmissionLedger["Service"]["suspend"] = Effect.fnUntraced(function* (
|
|
2425
|
+
request: SuspendRequest,
|
|
2426
|
+
) {
|
|
2427
|
+
const operation = "ledger suspend";
|
|
2696
2428
|
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2429
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SuspendRequest))(request).pipe(
|
|
2430
|
+
Effect.mapError(internalFailure(operation)),
|
|
2431
|
+
);
|
|
2700
2432
|
|
|
2701
|
-
|
|
2433
|
+
const reasonJson = yield* encodeSuspensionReasonText(validated.reason).pipe(
|
|
2434
|
+
Effect.mapError(internalFailure(operation)),
|
|
2435
|
+
);
|
|
2702
2436
|
|
|
2703
|
-
|
|
2704
|
-
operation,
|
|
2705
|
-
Effect.gen(function* () {
|
|
2706
|
-
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2437
|
+
yield* hitFailpoint("ledger:suspend:before", operation);
|
|
2707
2438
|
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
"effect_agent_submissions",
|
|
2713
|
-
validated.submissionId,
|
|
2714
|
-
"A settled Submission carries no terminal outcome.",
|
|
2715
|
-
);
|
|
2716
|
-
}
|
|
2439
|
+
const outcome = yield* inWriteTransaction(
|
|
2440
|
+
operation,
|
|
2441
|
+
Effect.gen(function* () {
|
|
2442
|
+
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2717
2443
|
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2444
|
+
if (submission.state === "settled") {
|
|
2445
|
+
if (submission.settled_outcome === null) {
|
|
2446
|
+
return yield* corruptionFailure(
|
|
2447
|
+
operation,
|
|
2448
|
+
"effect_agent_submissions",
|
|
2449
|
+
validated.submissionId,
|
|
2450
|
+
"A settled Submission carries no terminal outcome.",
|
|
2451
|
+
);
|
|
2722
2452
|
}
|
|
2723
|
-
// An exact terminal outcome is already reserved (DUR-011); suspension would
|
|
2724
|
-
// contradict it, so the reservation wins.
|
|
2725
|
-
const reservation = yield* readReservation(operation, validated.submissionId);
|
|
2726
2453
|
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2454
|
+
return yield* SettlementConflict.make({
|
|
2455
|
+
submissionId: validated.submissionId,
|
|
2456
|
+
existingOutcome: submission.settled_outcome,
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
// Canonical terminal intent wins over a later suspension.
|
|
2460
|
+
const canonical = yield* readCanonicalSettlement(operation, submission);
|
|
2461
|
+
|
|
2462
|
+
if (canonical !== undefined) {
|
|
2463
|
+
return yield* SettlementConflict.make({
|
|
2464
|
+
submissionId: validated.submissionId,
|
|
2465
|
+
existingOutcome: canonical.settlement.outcome,
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
yield* requireOwner(operation, submission);
|
|
2469
|
+
// A covering event that raced ahead of the suspend transaction resumes the caller
|
|
2470
|
+
// immediately WITHOUT releasing the lane (plan §2.6, §12). For WaitingForChild the
|
|
2471
|
+
// covering evidence is EITHER a locally settled child row OR a durable cross-store
|
|
2472
|
+
// notification marker: parent and child Threads live in different Durable
|
|
2473
|
+
// Objects, and the port contract requires that a child settlement reported (via
|
|
2474
|
+
// `recordChildSettled` → marker) before this suspend commits is observed here.
|
|
2475
|
+
if (validated.reason._tag === "ApprovalPending") {
|
|
2476
|
+
const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
2477
|
+
const decided = new Map(decisions.map((row) => [row.tool_call_id, row.decision]));
|
|
2478
|
+
|
|
2479
|
+
if (
|
|
2480
|
+
approvalDecisionsCover(validated.reason.toolCallIds, (toolCallId) =>
|
|
2481
|
+
decided.get(toolCallId),
|
|
2482
|
+
)
|
|
2483
|
+
) {
|
|
2484
|
+
return { outcome: RESUME_IMMEDIATELY, threadId: submission.thread_id };
|
|
2732
2485
|
}
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
// notification marker: parent and child Threads live in different Durable
|
|
2738
|
-
// Objects, and the port contract requires that a child settlement reported (via
|
|
2739
|
-
// `recordChildSettled` → marker) before this suspend commits is observed here.
|
|
2740
|
-
if (validated.reason._tag === "ApprovalPending") {
|
|
2741
|
-
const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
2742
|
-
const decided = new Set(decisions.map((row) => row.tool_call_id));
|
|
2743
|
-
|
|
2744
|
-
if (validated.reason.toolCallIds.every((toolCallId) => decided.has(toolCallId))) {
|
|
2745
|
-
return RESUME_IMMEDIATELY;
|
|
2746
|
-
}
|
|
2747
|
-
} else {
|
|
2748
|
-
const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
|
|
2749
|
-
const markerChildren = new Set(markers.map((row) => row.child_submission_id));
|
|
2750
|
-
let allSettled = true;
|
|
2486
|
+
} else {
|
|
2487
|
+
const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
|
|
2488
|
+
const markerChildren = new Set(markers.map((row) => row.child_submission_id));
|
|
2489
|
+
let allSettled = true;
|
|
2751
2490
|
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2491
|
+
for (const child of validated.reason.children) {
|
|
2492
|
+
const settled = yield* childProvablySettled(
|
|
2493
|
+
operation,
|
|
2494
|
+
markerChildren,
|
|
2495
|
+
child.childSubmissionId,
|
|
2496
|
+
);
|
|
2758
2497
|
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
if (allSettled) {
|
|
2765
|
-
return RESUME_IMMEDIATELY;
|
|
2498
|
+
if (!settled) {
|
|
2499
|
+
allSettled = false;
|
|
2500
|
+
break;
|
|
2766
2501
|
}
|
|
2767
2502
|
}
|
|
2768
|
-
|
|
2503
|
+
if (allSettled) {
|
|
2504
|
+
return { outcome: RESUME_IMMEDIATELY, threadId: submission.thread_id };
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
const now = yield* currentInstant;
|
|
2769
2508
|
|
|
2770
|
-
|
|
2509
|
+
yield* sql`
|
|
2771
2510
|
UPDATE effect_agent_submissions
|
|
2772
2511
|
SET
|
|
2773
2512
|
state = 'suspended',
|
|
2774
2513
|
suspended_reason_json = ${reasonJson},
|
|
2775
2514
|
suspended_at = ${now.iso}
|
|
2776
2515
|
WHERE submission_id = ${validated.submissionId}
|
|
2777
|
-
RETURNING
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
RETURNING *`.pipe(rows.ownership.remove, Effect.mapError(internalFailure(operation)));
|
|
2516
|
+
RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
|
|
2517
|
+
rows.submissions.write,
|
|
2518
|
+
Effect.mapError(internalFailure(operation)),
|
|
2519
|
+
);
|
|
2520
|
+
// Suspension ends the ownership period WITHOUT settling: the accepted-work
|
|
2521
|
+
// obligation stays owed while the lane consumes no worker permit (plan §2.6).
|
|
2784
2522
|
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2523
|
+
yield* retainLifecycle(submission, {
|
|
2524
|
+
_tag: "SubmissionSuspended",
|
|
2525
|
+
submissionId: validated.submissionId,
|
|
2526
|
+
reason: validated.reason,
|
|
2527
|
+
});
|
|
2790
2528
|
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2529
|
+
return { outcome: SUSPENDED, threadId: submission.thread_id };
|
|
2530
|
+
}),
|
|
2531
|
+
);
|
|
2794
2532
|
|
|
2795
|
-
|
|
2533
|
+
if (outcome.outcome === SUSPENDED)
|
|
2534
|
+
owner.closeSubmission(outcome.threadId, validated.submissionId);
|
|
2535
|
+
yield* hitFailpoint("ledger:suspend:after", operation);
|
|
2796
2536
|
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
);
|
|
2537
|
+
return outcome.outcome;
|
|
2538
|
+
});
|
|
2800
2539
|
|
|
2801
2540
|
/**
|
|
2802
|
-
* Once
|
|
2541
|
+
* Once one pending call is denied or every pending call has a decision intent,
|
|
2803
2542
|
* the lane wakes: suspended → input-applied, suspension cleared (plan §2.6). A
|
|
2804
2543
|
* WaitingForChild suspension wakes only through recordChildSettled. Runs inside the caller's
|
|
2805
2544
|
* write transaction.
|
|
2806
2545
|
*/
|
|
2807
|
-
const wakeSuspendedIfCovered = Effect.
|
|
2546
|
+
const wakeSuspendedIfCovered = Effect.fnUntraced(function* (
|
|
2808
2547
|
operation: string,
|
|
2809
2548
|
submission: SubmissionRow,
|
|
2810
2549
|
): Effect.fn.Return<void, LedgerError> {
|
|
@@ -2825,9 +2564,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2825
2564
|
|
|
2826
2565
|
if (reason._tag !== "ApprovalPending") return;
|
|
2827
2566
|
const decisions = yield* readApprovalDecisions(operation, submission.submission_id);
|
|
2828
|
-
const decided = new
|
|
2567
|
+
const decided = new Map(decisions.map((row) => [row.tool_call_id, row.decision]));
|
|
2829
2568
|
|
|
2830
|
-
if (!reason.toolCallIds
|
|
2569
|
+
if (!approvalDecisionsCover(reason.toolCallIds, (toolCallId) => decided.get(toolCallId)))
|
|
2570
|
+
return;
|
|
2831
2571
|
yield* sql`
|
|
2832
2572
|
UPDATE effect_agent_submissions
|
|
2833
2573
|
SET
|
|
@@ -2835,7 +2575,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2835
2575
|
suspended_reason_json = NULL,
|
|
2836
2576
|
suspended_at = NULL
|
|
2837
2577
|
WHERE submission_id = ${submission.submission_id}
|
|
2838
|
-
RETURNING
|
|
2578
|
+
RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
|
|
2579
|
+
rows.submissions.write,
|
|
2580
|
+
Effect.mapError(internalFailure(operation)),
|
|
2581
|
+
);
|
|
2839
2582
|
yield* retainLifecycle(submission, {
|
|
2840
2583
|
_tag: "SubmissionResumed",
|
|
2841
2584
|
submissionId: yield* decodeSubmissionId(submission.submission_id).pipe(
|
|
@@ -2844,56 +2587,55 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2844
2587
|
});
|
|
2845
2588
|
});
|
|
2846
2589
|
|
|
2847
|
-
const recordApprovalDecision: SubmissionLedger["Service"]["recordApprovalDecision"] =
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
const operation = "ledger record approval decision";
|
|
2590
|
+
const recordApprovalDecision: SubmissionLedger["Service"]["recordApprovalDecision"] =
|
|
2591
|
+
Effect.fnUntraced(function* (command: ApprovalDecisionCommand) {
|
|
2592
|
+
const operation = "ledger record approval decision";
|
|
2851
2593
|
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2594
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ApprovalDecisionCommand))(
|
|
2595
|
+
command,
|
|
2596
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2855
2597
|
|
|
2856
|
-
|
|
2598
|
+
yield* hitFailpoint("ledger:approval-decision:before", operation);
|
|
2857
2599
|
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2600
|
+
const intent = yield* inWriteTransaction(
|
|
2601
|
+
operation,
|
|
2602
|
+
Effect.gen(function* () {
|
|
2603
|
+
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2862
2604
|
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2605
|
+
if (submission.state === "settled") {
|
|
2606
|
+
if (submission.settled_outcome === null) {
|
|
2607
|
+
return yield* corruptionFailure(
|
|
2608
|
+
operation,
|
|
2609
|
+
"effect_agent_submissions",
|
|
2610
|
+
validated.submissionId,
|
|
2611
|
+
"A settled Submission carries no terminal outcome.",
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2872
2614
|
|
|
2873
|
-
|
|
2874
|
-
submissionId: validated.submissionId,
|
|
2875
|
-
existingOutcome: submission.settled_outcome,
|
|
2876
|
-
});
|
|
2877
|
-
}
|
|
2878
|
-
const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
2879
|
-
const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
|
|
2880
|
-
|
|
2881
|
-
if (existing !== undefined) {
|
|
2882
|
-
// Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
|
|
2883
|
-
// the recorded intent unchanged; a divergent re-decision conflicts.
|
|
2884
|
-
if (existing.decision !== validated.decision) {
|
|
2885
|
-
return yield* ApprovalConflict.make({
|
|
2615
|
+
return yield* SettlementConflict.make({
|
|
2886
2616
|
submissionId: validated.submissionId,
|
|
2887
|
-
|
|
2888
|
-
existingDecision: existing.decision,
|
|
2617
|
+
existingOutcome: submission.settled_outcome,
|
|
2889
2618
|
});
|
|
2890
2619
|
}
|
|
2620
|
+
const decisions = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
2621
|
+
const existing = decisions.find((row) => row.tool_call_id === validated.toolCallId);
|
|
2622
|
+
|
|
2623
|
+
if (existing !== undefined) {
|
|
2624
|
+
// Idempotent per (submissionId, toolCallId): repeating the SAME decision replays
|
|
2625
|
+
// the recorded intent unchanged; a divergent re-decision conflicts.
|
|
2626
|
+
if (existing.decision !== validated.decision) {
|
|
2627
|
+
return yield* ApprovalConflict.make({
|
|
2628
|
+
submissionId: validated.submissionId,
|
|
2629
|
+
toolCallId: validated.toolCallId,
|
|
2630
|
+
existingDecision: existing.decision,
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2891
2633
|
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2634
|
+
return yield* approvalIntentFromRow(operation, existing);
|
|
2635
|
+
}
|
|
2636
|
+
const now = yield* currentInstant;
|
|
2895
2637
|
|
|
2896
|
-
|
|
2638
|
+
yield* sql`
|
|
2897
2639
|
INSERT INTO effect_agent_approval_decisions (
|
|
2898
2640
|
submission_id,
|
|
2899
2641
|
tool_call_id,
|
|
@@ -2910,28 +2652,28 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2910
2652
|
${now.iso}
|
|
2911
2653
|
)
|
|
2912
2654
|
RETURNING *`.pipe(rows.approvals.write, Effect.mapError(internalFailure(operation)));
|
|
2913
|
-
|
|
2914
|
-
|
|
2655
|
+
yield* recordProgress("control", operation);
|
|
2656
|
+
yield* wakeSuspendedIfCovered(operation, submission);
|
|
2915
2657
|
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2658
|
+
return yield* decodeApprovalDecisionIntent({
|
|
2659
|
+
submissionId: validated.submissionId,
|
|
2660
|
+
toolCallId: validated.toolCallId,
|
|
2661
|
+
decision: validated.decision,
|
|
2662
|
+
resolver: validated.resolver,
|
|
2663
|
+
reason: validated.reason,
|
|
2664
|
+
decidedAt: now.iso,
|
|
2665
|
+
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
2666
|
+
}),
|
|
2667
|
+
);
|
|
2926
2668
|
|
|
2927
|
-
|
|
2669
|
+
yield* hitFailpoint("ledger:approval-decision:after", operation);
|
|
2928
2670
|
|
|
2929
|
-
|
|
2930
|
-
|
|
2671
|
+
return intent;
|
|
2672
|
+
});
|
|
2931
2673
|
|
|
2932
|
-
const markUnknown: SubmissionLedger["Service"]["markUnknown"] = Effect.
|
|
2933
|
-
|
|
2934
|
-
)
|
|
2674
|
+
const markUnknown: SubmissionLedger["Service"]["markUnknown"] = Effect.fnUntraced(function* (
|
|
2675
|
+
request: MarkUnknownRequest,
|
|
2676
|
+
) {
|
|
2935
2677
|
const operation = "ledger mark unknown";
|
|
2936
2678
|
|
|
2937
2679
|
const validated = yield* Schema.decodeEffect(Schema.toType(MarkUnknownRequest))(request).pipe(
|
|
@@ -2959,14 +2701,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2959
2701
|
existingOutcome: submission.settled_outcome,
|
|
2960
2702
|
});
|
|
2961
2703
|
}
|
|
2962
|
-
//
|
|
2963
|
-
|
|
2964
|
-
const reservation = yield* readReservation(operation, validated.submissionId);
|
|
2704
|
+
// Canonical terminal intent wins over a late Unknown marking.
|
|
2705
|
+
const canonical = yield* readCanonicalSettlement(operation, submission);
|
|
2965
2706
|
|
|
2966
|
-
if (
|
|
2707
|
+
if (canonical !== undefined) {
|
|
2967
2708
|
return yield* SettlementConflict.make({
|
|
2968
2709
|
submissionId: validated.submissionId,
|
|
2969
|
-
existingOutcome:
|
|
2710
|
+
existingOutcome: canonical.settlement.outcome,
|
|
2970
2711
|
});
|
|
2971
2712
|
}
|
|
2972
2713
|
// Idempotent merge: repeating is a no-op; additional open calls extend the marked
|
|
@@ -2990,7 +2731,10 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2990
2731
|
unknown_reason = ${submission.unknown_reason ?? validated.reason},
|
|
2991
2732
|
unknown_tool_call_ids_json = ${idsJson}
|
|
2992
2733
|
WHERE submission_id = ${validated.submissionId}
|
|
2993
|
-
RETURNING
|
|
2734
|
+
RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
|
|
2735
|
+
rows.submissions.write,
|
|
2736
|
+
Effect.mapError(internalFailure(operation)),
|
|
2737
|
+
);
|
|
2994
2738
|
if (submission.state !== "unknown")
|
|
2995
2739
|
yield* retainLifecycle(submission, {
|
|
2996
2740
|
_tag: "SubmissionUnknown",
|
|
@@ -3001,71 +2745,136 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
3001
2745
|
yield* hitFailpoint("ledger:mark-unknown:after", operation);
|
|
3002
2746
|
});
|
|
3003
2747
|
|
|
3004
|
-
const recordUnknownResolution: SubmissionLedger["Service"]["recordUnknownResolution"] =
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
const operation = "ledger record unknown resolution";
|
|
2748
|
+
const recordUnknownResolution: SubmissionLedger["Service"]["recordUnknownResolution"] =
|
|
2749
|
+
Effect.fnUntraced(function* (command: UnknownResolutionCommand) {
|
|
2750
|
+
const operation = "ledger record unknown resolution";
|
|
3008
2751
|
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
2752
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(UnknownResolutionCommand))(
|
|
2753
|
+
command,
|
|
2754
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3012
2755
|
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
2756
|
+
const resolutionJson = yield* encodeUnknownResolutionText(validated.resolution).pipe(
|
|
2757
|
+
Effect.mapError(internalFailure(operation)),
|
|
2758
|
+
);
|
|
3016
2759
|
|
|
3017
|
-
|
|
2760
|
+
yield* hitFailpoint("ledger:unknown-resolution:before", operation);
|
|
3018
2761
|
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
2762
|
+
const intent = yield* inWriteTransaction(
|
|
2763
|
+
operation,
|
|
2764
|
+
Effect.gen(function* () {
|
|
2765
|
+
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
3023
2766
|
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
2767
|
+
if (submission.state === "settled") {
|
|
2768
|
+
if (submission.settled_outcome === null) {
|
|
2769
|
+
return yield* corruptionFailure(
|
|
2770
|
+
operation,
|
|
2771
|
+
"effect_agent_submissions",
|
|
2772
|
+
validated.submissionId,
|
|
2773
|
+
"A settled Submission carries no terminal outcome.",
|
|
2774
|
+
);
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
// Factual closure may outlive settlement; it never restores execution authority.
|
|
2778
|
+
if (
|
|
2779
|
+
validated.resolution._tag !== "CompletedWithResult" &&
|
|
2780
|
+
validated.resolution._tag !== "NeverHappened"
|
|
2781
|
+
)
|
|
2782
|
+
return yield* SettlementConflict.make({
|
|
2783
|
+
submissionId: validated.submissionId,
|
|
2784
|
+
existingOutcome: submission.settled_outcome,
|
|
2785
|
+
});
|
|
3032
2786
|
}
|
|
2787
|
+
const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
2788
|
+
const kind = unknownResolutionKind(validated.resolution);
|
|
3033
2789
|
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
});
|
|
3038
|
-
}
|
|
3039
|
-
const resolutions = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
3040
|
-
const existing = resolutions.find((row) => row.tool_call_id === validated.toolCallId);
|
|
2790
|
+
const existing = resolutions.find(
|
|
2791
|
+
(row) => row.tool_call_id === validated.toolCallId && row.resolution_kind === kind,
|
|
2792
|
+
);
|
|
3041
2793
|
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
2794
|
+
const existingIntent =
|
|
2795
|
+
existing === undefined
|
|
2796
|
+
? undefined
|
|
2797
|
+
: yield* unknownResolutionIntentFromRow(operation, existing);
|
|
3046
2798
|
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
2799
|
+
if (
|
|
2800
|
+
(existingIntent !== undefined &&
|
|
2801
|
+
!equivalentUnknownResolution(existingIntent.resolution, validated.resolution)) ||
|
|
2802
|
+
(existing === undefined &&
|
|
2803
|
+
kind === "execution" &&
|
|
2804
|
+
resolutions.some(
|
|
2805
|
+
(row) =>
|
|
2806
|
+
row.tool_call_id === validated.toolCallId && row.resolution_kind === "factual",
|
|
2807
|
+
))
|
|
2808
|
+
) {
|
|
2809
|
+
return yield* UnknownResolutionConflict.make({
|
|
2810
|
+
submissionId: validated.submissionId,
|
|
2811
|
+
toolCallId: validated.toolCallId,
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
3057
2814
|
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
// audit fields win, exactly like requestAbort).
|
|
3061
|
-
resolved = existingIntent;
|
|
3062
|
-
} else {
|
|
3063
|
-
const now = yield* currentInstant;
|
|
2815
|
+
if (existingIntent === undefined && kind === "factual") {
|
|
2816
|
+
const runId = runIdForSubmission(validated.submissionId);
|
|
3064
2817
|
|
|
3065
|
-
|
|
2818
|
+
const found = yield* sql`
|
|
2819
|
+
SELECT ${canonicalRecordJson(sql)} AS record_json FROM effect_agent_canonical_records
|
|
2820
|
+
WHERE thread_id = ${submission.thread_id}
|
|
2821
|
+
AND record_tag = 27
|
|
2822
|
+
AND run_id = ${runKey(sql, sql`${submission.thread_id}`, sql`${JSON.stringify(runId)}`)}
|
|
2823
|
+
AND tool_call_id = ${JSON.stringify(validated.toolCallId)}
|
|
2824
|
+
LIMIT 2
|
|
2825
|
+
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2826
|
+
|
|
2827
|
+
const outcomes = yield* decodeRows(
|
|
2828
|
+
Schema.Array(Schema.Struct({ record_json: BoundedStoredText })),
|
|
2829
|
+
"effect_agent_canonical_records",
|
|
2830
|
+
validated.toolCallId,
|
|
2831
|
+
found,
|
|
2832
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
2833
|
+
|
|
2834
|
+
const result =
|
|
2835
|
+
outcomes[0] === undefined
|
|
2836
|
+
? undefined
|
|
2837
|
+
: (yield* decodeRecordEnvelopeText(outcomes[0].record_json).pipe(
|
|
2838
|
+
Effect.mapError(internalFailure(operation)),
|
|
2839
|
+
)).payload;
|
|
2840
|
+
|
|
2841
|
+
if (
|
|
2842
|
+
outcomes.length > 1 ||
|
|
2843
|
+
(result !== undefined &&
|
|
2844
|
+
(result._tag !== "ToolCallSettled" ||
|
|
2845
|
+
result.runId !== runId ||
|
|
2846
|
+
result.toolCallId !== validated.toolCallId))
|
|
2847
|
+
)
|
|
2848
|
+
return yield* corruptionFailure(
|
|
2849
|
+
operation,
|
|
2850
|
+
"effect_agent_canonical_records",
|
|
2851
|
+
validated.toolCallId,
|
|
2852
|
+
"Canonical Tool result identity is inconsistent.",
|
|
2853
|
+
);
|
|
2854
|
+
|
|
2855
|
+
if (
|
|
2856
|
+
result?._tag === "ToolCallSettled" &&
|
|
2857
|
+
!unknownResolutionMatchesResult(validated.resolution, result)
|
|
2858
|
+
)
|
|
2859
|
+
return yield* UnknownResolutionConflict.make({
|
|
2860
|
+
submissionId: validated.submissionId,
|
|
2861
|
+
toolCallId: validated.toolCallId,
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
let resolved: UnknownResolutionIntent;
|
|
2865
|
+
|
|
2866
|
+
if (existingIntent !== undefined) {
|
|
2867
|
+
// Idempotent replay of the recorded intent (author/reason may differ; the stored
|
|
2868
|
+
// audit fields win, exactly like requestAbort).
|
|
2869
|
+
resolved = existingIntent;
|
|
2870
|
+
} else {
|
|
2871
|
+
const now = yield* currentInstant;
|
|
2872
|
+
|
|
2873
|
+
yield* sql`
|
|
3066
2874
|
INSERT INTO effect_agent_unknown_resolutions (
|
|
3067
2875
|
submission_id,
|
|
3068
2876
|
tool_call_id,
|
|
2877
|
+
resolution_kind,
|
|
3069
2878
|
author,
|
|
3070
2879
|
reason,
|
|
3071
2880
|
resolution_json,
|
|
@@ -3073,101 +2882,103 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
3073
2882
|
) VALUES (
|
|
3074
2883
|
${validated.submissionId},
|
|
3075
2884
|
${validated.toolCallId},
|
|
2885
|
+
${kind},
|
|
3076
2886
|
${validated.author},
|
|
3077
2887
|
${validated.reason},
|
|
3078
2888
|
${resolutionJson},
|
|
3079
2889
|
${now.iso}
|
|
3080
2890
|
)
|
|
3081
2891
|
RETURNING *`.pipe(rows.resolutions.write, Effect.mapError(internalFailure(operation)));
|
|
3082
|
-
|
|
2892
|
+
yield* recordProgress("control", operation);
|
|
3083
2893
|
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
2894
|
+
const resolution = yield* parseStoredJsonText(resolutionJson).pipe(
|
|
2895
|
+
Effect.mapError(internalFailure(operation)),
|
|
2896
|
+
);
|
|
3087
2897
|
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
2898
|
+
resolved = yield* decodeUnknownResolutionIntent({
|
|
2899
|
+
submissionId: validated.submissionId,
|
|
2900
|
+
toolCallId: validated.toolCallId,
|
|
2901
|
+
author: validated.author,
|
|
2902
|
+
reason: validated.reason,
|
|
2903
|
+
resolution,
|
|
2904
|
+
resolvedAt: now.iso,
|
|
2905
|
+
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
2906
|
+
}
|
|
2907
|
+
// The lane reopens only when EVERY marked open call has a durable resolution intent:
|
|
2908
|
+
// unknown → input-applied (DUR-017). Replays re-run the coverage check so a
|
|
2909
|
+
// recovering caller can wake the lane idempotently.
|
|
2910
|
+
if (submission.state === "unknown" && submission.unknown_tool_call_ids_json !== null) {
|
|
2911
|
+
const markedIds = yield* storedUnknownToolCallIds(operation, submission);
|
|
2912
|
+
const covering = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
2913
|
+
const coveredIds = new Set(covering.map((row) => row.tool_call_id));
|
|
2914
|
+
|
|
2915
|
+
if (markedIds.every((toolCallId) => coveredIds.has(toolCallId))) {
|
|
2916
|
+
yield* sql`
|
|
3107
2917
|
UPDATE effect_agent_submissions
|
|
3108
2918
|
SET
|
|
3109
2919
|
state = 'input-applied',
|
|
3110
2920
|
unknown_reason = NULL,
|
|
3111
2921
|
unknown_tool_call_ids_json = NULL
|
|
3112
2922
|
WHERE submission_id = ${validated.submissionId}
|
|
3113
|
-
RETURNING
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
2923
|
+
RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
|
|
2924
|
+
rows.submissions.write,
|
|
2925
|
+
Effect.mapError(internalFailure(operation)),
|
|
2926
|
+
);
|
|
2927
|
+
yield* retainLifecycle(submission, {
|
|
2928
|
+
_tag: "SubmissionResumed",
|
|
2929
|
+
submissionId: validated.submissionId,
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
3118
2932
|
}
|
|
3119
|
-
}
|
|
3120
2933
|
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
2934
|
+
return resolved;
|
|
2935
|
+
}),
|
|
2936
|
+
);
|
|
3124
2937
|
|
|
3125
|
-
|
|
2938
|
+
yield* hitFailpoint("ledger:unknown-resolution:after", operation);
|
|
3126
2939
|
|
|
3127
|
-
|
|
3128
|
-
|
|
2940
|
+
return intent;
|
|
2941
|
+
});
|
|
3129
2942
|
|
|
3130
|
-
const recordChildSettled: SubmissionLedger["Service"]["recordChildSettled"] = Effect.
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
const operation = "ledger record child settled";
|
|
2943
|
+
const recordChildSettled: SubmissionLedger["Service"]["recordChildSettled"] = Effect.fnUntraced(
|
|
2944
|
+
function* (request: ChildSettledNotification) {
|
|
2945
|
+
const operation = "ledger record child settled";
|
|
3134
2946
|
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
yield* hitFailpoint("ledger:child-settled:before", operation);
|
|
2947
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ChildSettledNotification))(
|
|
2948
|
+
request,
|
|
2949
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3140
2950
|
|
|
3141
|
-
|
|
3142
|
-
operation,
|
|
3143
|
-
Effect.gen(function* () {
|
|
3144
|
-
const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
|
|
3145
|
-
// The child's canonical Settlement is the authority for this wake. When the child's
|
|
3146
|
-
// row lives in THIS store (single-store latitude, and every conformance lane), either a
|
|
3147
|
-
// finalized row or an exact terminalizing reservation admits the notification: the
|
|
3148
|
-
// runtime calls only after the canonical append and before ledger finalization. When the
|
|
3149
|
-
// row does not live here — the normal cross-DO case — the routed notification from the
|
|
3150
|
-
// child's owning Durable Object is the settlement evidence this store records durably.
|
|
3151
|
-
const child = yield* readSubmission(operation, validated.childSubmissionId);
|
|
3152
|
-
const childReservation = yield* readReservation(operation, validated.childSubmissionId);
|
|
2951
|
+
yield* hitFailpoint("ledger:child-settled:before", operation);
|
|
3153
2952
|
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
2953
|
+
const outcome = yield* inWriteTransaction(
|
|
2954
|
+
operation,
|
|
2955
|
+
Effect.gen(function* () {
|
|
2956
|
+
const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
|
|
2957
|
+
// The child's canonical Settlement is the authority for this wake. When the child's
|
|
2958
|
+
// row lives in THIS store (single-store latitude, and every conformance lane), either a
|
|
2959
|
+
// canonical settlement admits the notification: the
|
|
2960
|
+
// runtime calls only after the canonical append and before ledger finalization. When the
|
|
2961
|
+
// row does not live here — the normal cross-DO case — the routed notification from the
|
|
2962
|
+
// child's owning Durable Object is the settlement evidence this store records durably.
|
|
2963
|
+
const child = yield* readSubmission(operation, validated.childSubmissionId);
|
|
2964
|
+
|
|
2965
|
+
const childCanonical = Option.isSome(child)
|
|
2966
|
+
? yield* readCanonicalSettlement(operation, child.value)
|
|
2967
|
+
: undefined;
|
|
2968
|
+
|
|
2969
|
+
if (Option.isSome(child) && childCanonical === undefined) {
|
|
2970
|
+
return yield* LedgerError.make({
|
|
2971
|
+
operation,
|
|
2972
|
+
message: `Child submission ${validated.childSubmissionId} has no recorded settlement.`,
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
// Record the durable notification marker FIRST and unconditionally (idempotent):
|
|
2976
|
+
// the port's cross-store race guarantee requires that a notification committed
|
|
2977
|
+
// before the parent's suspend transaction is observed by that suspend's covering
|
|
2978
|
+
// check, even when the parent is not (or not yet) suspended.
|
|
2979
|
+
const now = yield* currentInstant;
|
|
3169
2980
|
|
|
3170
|
-
|
|
2981
|
+
const added = yield* sql`
|
|
3171
2982
|
INSERT INTO effect_agent_child_settlements (
|
|
3172
2983
|
parent_submission_id,
|
|
3173
2984
|
child_submission_id,
|
|
@@ -3176,158 +2987,162 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
3176
2987
|
) VALUES (
|
|
3177
2988
|
${validated.parentSubmissionId},
|
|
3178
2989
|
${validated.childSubmissionId},
|
|
3179
|
-
${
|
|
3180
|
-
Option.isSome(child) && child.value.state === "settled"
|
|
3181
|
-
? child.value.settled_outcome
|
|
3182
|
-
: Option.isSome(childReservation)
|
|
3183
|
-
? childReservation.value.outcome
|
|
3184
|
-
: null
|
|
3185
|
-
},
|
|
2990
|
+
${childCanonical?.settlement.outcome ?? null},
|
|
3186
2991
|
${now.iso}
|
|
3187
2992
|
)
|
|
3188
2993
|
ON CONFLICT (parent_submission_id, child_submission_id) DO NOTHING
|
|
3189
2994
|
RETURNING *`.pipe(
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
2995
|
+
rows.childSettlements.write,
|
|
2996
|
+
Effect.mapError(internalFailure(operation)),
|
|
2997
|
+
);
|
|
3193
2998
|
|
|
3194
|
-
|
|
2999
|
+
if (added.length > 0) yield* recordProgress("control", operation);
|
|
3195
3000
|
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3001
|
+
if (parent.state !== "suspended" || parent.suspended_reason_json === null) {
|
|
3002
|
+
return NOT_WAITING;
|
|
3003
|
+
}
|
|
3199
3004
|
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3005
|
+
const reason = yield* Schema.decodeEffect(Schema.fromJsonString(SuspensionReason))(
|
|
3006
|
+
parent.suspended_reason_json,
|
|
3007
|
+
).pipe(
|
|
3008
|
+
Effect.mapError((error) =>
|
|
3009
|
+
corruptionFailure(
|
|
3010
|
+
operation,
|
|
3011
|
+
"effect_agent_submissions",
|
|
3012
|
+
parent.submission_id,
|
|
3013
|
+
error.message,
|
|
3014
|
+
),
|
|
3209
3015
|
),
|
|
3210
|
-
)
|
|
3211
|
-
);
|
|
3016
|
+
);
|
|
3212
3017
|
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
const settled = yield* childProvablySettled(
|
|
3018
|
+
if (reason._tag !== "WaitingForChild") {
|
|
3019
|
+
return NOT_WAITING;
|
|
3020
|
+
}
|
|
3021
|
+
if (
|
|
3022
|
+
!reason.children.some(
|
|
3023
|
+
(entry) => entry.childSubmissionId === validated.childSubmissionId,
|
|
3024
|
+
)
|
|
3025
|
+
) {
|
|
3026
|
+
return NOT_WAITING;
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// The parent wakes exactly when EVERY listed child is provably settled — from its
|
|
3030
|
+
// local row or a recorded marker (spec §12 step 10); replays re-run the coverage
|
|
3031
|
+
// check so a recovering caller wakes the lane idempotently.
|
|
3032
|
+
const markers = yield* readChildSettlementMarkers(
|
|
3229
3033
|
operation,
|
|
3230
|
-
|
|
3231
|
-
entry.childSubmissionId,
|
|
3034
|
+
validated.parentSubmissionId,
|
|
3232
3035
|
);
|
|
3233
3036
|
|
|
3234
|
-
|
|
3235
|
-
|
|
3037
|
+
const markerChildren = new Set(markers.map((row) => row.child_submission_id));
|
|
3038
|
+
|
|
3039
|
+
for (const entry of reason.children) {
|
|
3040
|
+
const settled = yield* childProvablySettled(
|
|
3041
|
+
operation,
|
|
3042
|
+
markerChildren,
|
|
3043
|
+
entry.childSubmissionId,
|
|
3044
|
+
);
|
|
3045
|
+
|
|
3046
|
+
if (!settled) {
|
|
3047
|
+
return STILL_WAITING;
|
|
3048
|
+
}
|
|
3236
3049
|
}
|
|
3237
|
-
|
|
3238
|
-
yield* sql`
|
|
3050
|
+
yield* sql`
|
|
3239
3051
|
UPDATE effect_agent_submissions
|
|
3240
3052
|
SET
|
|
3241
3053
|
state = 'input-applied',
|
|
3242
3054
|
suspended_reason_json = NULL,
|
|
3243
3055
|
suspended_at = NULL
|
|
3244
3056
|
WHERE submission_id = ${validated.parentSubmissionId}
|
|
3245
|
-
RETURNING
|
|
3057
|
+
RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
|
|
3058
|
+
rows.submissions.write,
|
|
3059
|
+
Effect.mapError(internalFailure(operation)),
|
|
3060
|
+
);
|
|
3246
3061
|
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3062
|
+
yield* retainLifecycle(parent, {
|
|
3063
|
+
_tag: "SubmissionResumed",
|
|
3064
|
+
submissionId: validated.parentSubmissionId,
|
|
3065
|
+
});
|
|
3251
3066
|
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3067
|
+
return WOKEN;
|
|
3068
|
+
}),
|
|
3069
|
+
);
|
|
3255
3070
|
|
|
3256
|
-
|
|
3071
|
+
yield* hitFailpoint("ledger:child-settled:after", operation);
|
|
3257
3072
|
|
|
3258
|
-
|
|
3259
|
-
|
|
3073
|
+
return outcome;
|
|
3074
|
+
},
|
|
3075
|
+
);
|
|
3260
3076
|
|
|
3261
|
-
const reserveChildBudget: SubmissionLedger["Service"]["reserveChildBudget"] = Effect.
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
const operation = "ledger reserve child budget";
|
|
3077
|
+
const reserveChildBudget: SubmissionLedger["Service"]["reserveChildBudget"] = Effect.fnUntraced(
|
|
3078
|
+
function* (request: ChildBudgetReservationRequest) {
|
|
3079
|
+
const operation = "ledger reserve child budget";
|
|
3265
3080
|
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3081
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ChildBudgetReservationRequest))(
|
|
3082
|
+
request,
|
|
3083
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3269
3084
|
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3085
|
+
const allocationJson = yield* encodePersistedJsonText(validated.allocation).pipe(
|
|
3086
|
+
Effect.mapError(internalFailure(operation)),
|
|
3087
|
+
);
|
|
3273
3088
|
|
|
3274
|
-
|
|
3089
|
+
yield* hitFailpoint("ledger:child-reservation:before", operation);
|
|
3275
3090
|
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3091
|
+
const reserved = yield* inWriteTransaction(
|
|
3092
|
+
operation,
|
|
3093
|
+
Effect.gen(function* () {
|
|
3094
|
+
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
3280
3095
|
|
|
3281
|
-
|
|
3282
|
-
|
|
3096
|
+
if (Option.isSome(existing)) {
|
|
3097
|
+
const existingSnapshot = yield* childReservationSnapshotFromRow(
|
|
3098
|
+
operation,
|
|
3099
|
+
existing.value,
|
|
3100
|
+
);
|
|
3101
|
+
|
|
3102
|
+
// Identical replays short-circuit before the fence, retaining its first committed allocation: a
|
|
3103
|
+
// replay creates nothing, so a recovering caller resumes rather than duplicates.
|
|
3104
|
+
const identical =
|
|
3105
|
+
existing.value.parent_submission_id === validated.parentSubmissionId &&
|
|
3106
|
+
existing.value.parent_tool_call_id === validated.parentToolCallId &&
|
|
3107
|
+
existing.value.allocation_digest === validated.allocationDigest &&
|
|
3108
|
+
equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
|
|
3109
|
+
|
|
3110
|
+
if (!identical) {
|
|
3111
|
+
return yield* ChildReservationConflict.make({
|
|
3112
|
+
reservationId: validated.reservationId,
|
|
3113
|
+
status: existing.value.status,
|
|
3114
|
+
message:
|
|
3115
|
+
"A reservation with this identity exists with a different parent Tool Call or allocation.",
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
return ReservedChildBudget.make({
|
|
3120
|
+
reservation: existingSnapshot,
|
|
3121
|
+
replayed: true,
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
const collision = yield* readChildReservationForCall(
|
|
3283
3126
|
operation,
|
|
3284
|
-
|
|
3127
|
+
validated.parentSubmissionId,
|
|
3128
|
+
validated.parentToolCallId,
|
|
3285
3129
|
);
|
|
3286
3130
|
|
|
3287
|
-
|
|
3288
|
-
// replay creates nothing, so a recovering caller resumes rather than duplicates.
|
|
3289
|
-
const identical =
|
|
3290
|
-
existing.value.parent_submission_id === validated.parentSubmissionId &&
|
|
3291
|
-
existing.value.parent_tool_call_id === validated.parentToolCallId &&
|
|
3292
|
-
existing.value.allocation_digest === validated.allocationDigest &&
|
|
3293
|
-
equivalentPersistedJson(existingSnapshot.allocation, validated.allocation);
|
|
3294
|
-
|
|
3295
|
-
if (!identical) {
|
|
3131
|
+
if (Option.isSome(collision)) {
|
|
3296
3132
|
return yield* ChildReservationConflict.make({
|
|
3297
3133
|
reservationId: validated.reservationId,
|
|
3298
|
-
status:
|
|
3299
|
-
message:
|
|
3300
|
-
"A reservation with this identity exists with a different parent Tool Call or allocation.",
|
|
3134
|
+
status: collision.value.status,
|
|
3135
|
+
message: `Parent Tool Call ${validated.parentToolCallId} already owns reservation ${collision.value.reservation_id}.`,
|
|
3301
3136
|
});
|
|
3302
3137
|
}
|
|
3138
|
+
const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
|
|
3303
3139
|
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
}
|
|
3309
|
-
|
|
3310
|
-
const collision = yield* readChildReservationForCall(
|
|
3311
|
-
operation,
|
|
3312
|
-
validated.parentSubmissionId,
|
|
3313
|
-
validated.parentToolCallId,
|
|
3314
|
-
);
|
|
3315
|
-
|
|
3316
|
-
if (Option.isSome(collision)) {
|
|
3317
|
-
return yield* ChildReservationConflict.make({
|
|
3318
|
-
reservationId: validated.reservationId,
|
|
3319
|
-
status: collision.value.status,
|
|
3320
|
-
message: `Parent Tool Call ${validated.parentToolCallId} already owns reservation ${collision.value.reservation_id}.`,
|
|
3321
|
-
});
|
|
3322
|
-
}
|
|
3323
|
-
const parent = yield* requireSubmission(operation, validated.parentSubmissionId);
|
|
3324
|
-
|
|
3325
|
-
// Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
|
|
3326
|
-
// parent Attempt can never create new reservation state.
|
|
3327
|
-
yield* requireOwnership(operation, parent, validated.ownershipToken);
|
|
3328
|
-
const now = yield* currentInstant;
|
|
3140
|
+
// Creation is fenced by the parent lane's live ownership (spec §12 step 2): a stale
|
|
3141
|
+
// parent Attempt can never create new reservation state.
|
|
3142
|
+
yield* requireOwner(operation, parent);
|
|
3143
|
+
const now = yield* currentInstant;
|
|
3329
3144
|
|
|
3330
|
-
|
|
3145
|
+
yield* sql`
|
|
3331
3146
|
INSERT INTO effect_agent_child_reservations (
|
|
3332
3147
|
reservation_id,
|
|
3333
3148
|
parent_submission_id,
|
|
@@ -3346,45 +3161,198 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
3346
3161
|
${now.iso}
|
|
3347
3162
|
)
|
|
3348
3163
|
RETURNING *`.pipe(
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
);
|
|
3352
|
-
const inserted = yield* readChildReservation(operation, validated.reservationId);
|
|
3353
|
-
|
|
3354
|
-
if (Option.isNone(inserted)) {
|
|
3355
|
-
return yield* corruptionFailure(
|
|
3356
|
-
operation,
|
|
3357
|
-
"effect_agent_child_reservations",
|
|
3358
|
-
validated.reservationId,
|
|
3359
|
-
"An inserted child reservation row is missing inside its own transaction.",
|
|
3164
|
+
rows.childReservations.write,
|
|
3165
|
+
Effect.mapError(internalFailure(operation)),
|
|
3360
3166
|
);
|
|
3361
|
-
|
|
3167
|
+
const inserted = yield* readChildReservation(operation, validated.reservationId);
|
|
3362
3168
|
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3169
|
+
if (Option.isNone(inserted)) {
|
|
3170
|
+
return yield* corruptionFailure(
|
|
3171
|
+
operation,
|
|
3172
|
+
"effect_agent_child_reservations",
|
|
3173
|
+
validated.reservationId,
|
|
3174
|
+
"An inserted child reservation row is missing inside its own transaction.",
|
|
3175
|
+
);
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
return ReservedChildBudget.make({
|
|
3179
|
+
reservation: yield* childReservationSnapshotFromRow(operation, inserted.value),
|
|
3180
|
+
replayed: false,
|
|
3181
|
+
});
|
|
3182
|
+
}),
|
|
3183
|
+
);
|
|
3369
3184
|
|
|
3370
|
-
|
|
3185
|
+
yield* hitFailpoint("ledger:child-reservation:after", operation);
|
|
3371
3186
|
|
|
3372
|
-
|
|
3373
|
-
|
|
3187
|
+
return reserved;
|
|
3188
|
+
},
|
|
3189
|
+
);
|
|
3374
3190
|
|
|
3375
3191
|
const attachChildToReservation: SubmissionLedger["Service"]["attachChildToReservation"] =
|
|
3376
|
-
Effect.
|
|
3377
|
-
request: AttachChildToReservationRequest,
|
|
3378
|
-
) {
|
|
3192
|
+
Effect.fnUntraced(function* (request: AttachChildToReservationRequest) {
|
|
3379
3193
|
const operation = "ledger attach child to reservation";
|
|
3380
3194
|
|
|
3381
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(AttachChildToReservationRequest))(
|
|
3195
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AttachChildToReservationRequest))(
|
|
3196
|
+
request,
|
|
3197
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3198
|
+
|
|
3199
|
+
yield* hitFailpoint("ledger:child-attach:before", operation);
|
|
3200
|
+
|
|
3201
|
+
const attached = yield* inWriteTransaction(
|
|
3202
|
+
operation,
|
|
3203
|
+
Effect.gen(function* () {
|
|
3204
|
+
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
3205
|
+
|
|
3206
|
+
if (Option.isNone(existing)) {
|
|
3207
|
+
return yield* LedgerError.make({
|
|
3208
|
+
operation,
|
|
3209
|
+
message: `Unknown child reservation ${validated.reservationId}.`,
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
if (existing.value.child_submission_id !== null) {
|
|
3213
|
+
// Idempotent replay of the recorded attachment (unfenced — it mutates nothing).
|
|
3214
|
+
if (existing.value.child_submission_id === validated.childSubmissionId) {
|
|
3215
|
+
return yield* childReservationSnapshotFromRow(operation, existing.value);
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
return yield* ChildReservationConflict.make({
|
|
3219
|
+
reservationId: validated.reservationId,
|
|
3220
|
+
status: existing.value.status,
|
|
3221
|
+
message: `Reservation ${validated.reservationId} already records child ${existing.value.child_submission_id}.`,
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
const parent = yield* requireSubmission(operation, existing.value.parent_submission_id);
|
|
3225
|
+
|
|
3226
|
+
yield* requireOwner(operation, parent);
|
|
3227
|
+
if (existing.value.status !== "reserved") {
|
|
3228
|
+
return yield* ChildReservationConflict.make({
|
|
3229
|
+
reservationId: validated.reservationId,
|
|
3230
|
+
status: existing.value.status,
|
|
3231
|
+
message: `Cannot attach a child to a ${existing.value.status} reservation.`,
|
|
3232
|
+
});
|
|
3233
|
+
}
|
|
3234
|
+
// Unlike the single-store Node adapter, the admitted child's row lives in ANOTHER
|
|
3235
|
+
// Durable Object, so no local existence check is possible here. The canonical
|
|
3236
|
+
// `SubagentStarted` record remains the attachment's repair authority (DUR-015),
|
|
3237
|
+
// and the coordinator only attaches after the child's admission committed.
|
|
3238
|
+
yield* sql`
|
|
3239
|
+
UPDATE effect_agent_child_reservations
|
|
3240
|
+
SET child_submission_id = ${validated.childSubmissionId}
|
|
3241
|
+
WHERE reservation_id = ${validated.reservationId}
|
|
3242
|
+
RETURNING *`.pipe(
|
|
3243
|
+
rows.childReservations.write,
|
|
3244
|
+
Effect.mapError(internalFailure(operation)),
|
|
3245
|
+
);
|
|
3246
|
+
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
3247
|
+
|
|
3248
|
+
if (Option.isNone(updated)) {
|
|
3249
|
+
return yield* corruptionFailure(
|
|
3250
|
+
operation,
|
|
3251
|
+
"effect_agent_child_reservations",
|
|
3252
|
+
validated.reservationId,
|
|
3253
|
+
"An updated child reservation row is missing inside its own transaction.",
|
|
3254
|
+
);
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
3258
|
+
}),
|
|
3259
|
+
);
|
|
3260
|
+
|
|
3261
|
+
yield* hitFailpoint("ledger:child-attach:after", operation);
|
|
3262
|
+
|
|
3263
|
+
return attached;
|
|
3264
|
+
});
|
|
3265
|
+
|
|
3266
|
+
const beginChildBudgetRelease: SubmissionLedger["Service"]["beginChildBudgetRelease"] =
|
|
3267
|
+
Effect.fnUntraced(function* (request: BeginChildBudgetReleaseRequest) {
|
|
3268
|
+
const operation = "ledger begin child budget release";
|
|
3269
|
+
|
|
3270
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(BeginChildBudgetReleaseRequest))(
|
|
3271
|
+
request,
|
|
3272
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3273
|
+
|
|
3274
|
+
const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
|
|
3275
|
+
Effect.mapError(internalFailure(operation)),
|
|
3276
|
+
);
|
|
3277
|
+
|
|
3278
|
+
yield* hitFailpoint("ledger:child-release-pending:before", operation);
|
|
3279
|
+
|
|
3280
|
+
const frozen = yield* inWriteTransaction(
|
|
3281
|
+
operation,
|
|
3282
|
+
Effect.gen(function* () {
|
|
3283
|
+
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
3284
|
+
|
|
3285
|
+
if (Option.isNone(existing)) {
|
|
3286
|
+
return yield* LedgerError.make({
|
|
3287
|
+
operation,
|
|
3288
|
+
message: `Unknown child reservation ${validated.reservationId}.`,
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
if (existing.value.status !== "reserved") {
|
|
3292
|
+
const existingSnapshot = yield* childReservationSnapshotFromRow(
|
|
3293
|
+
operation,
|
|
3294
|
+
existing.value,
|
|
3295
|
+
);
|
|
3296
|
+
|
|
3297
|
+
// The accounting decision was already frozen exactly once; an identical replay is a
|
|
3298
|
+
// no-op and a divergent decision conflicts (spec §12 join step 6).
|
|
3299
|
+
if (
|
|
3300
|
+
existingSnapshot.accounting !== undefined &&
|
|
3301
|
+
equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
|
|
3302
|
+
) {
|
|
3303
|
+
return existingSnapshot;
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
return yield* ChildReservationConflict.make({
|
|
3307
|
+
reservationId: validated.reservationId,
|
|
3308
|
+
status: existing.value.status,
|
|
3309
|
+
message: "A different accounting decision is already frozen for this reservation.",
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
const now = yield* currentInstant;
|
|
3313
|
+
|
|
3314
|
+
yield* sql`
|
|
3315
|
+
UPDATE effect_agent_child_reservations
|
|
3316
|
+
SET
|
|
3317
|
+
status = 'releasePending',
|
|
3318
|
+
accounting_json = ${accountingJson},
|
|
3319
|
+
release_began_at = ${now.iso}
|
|
3320
|
+
WHERE reservation_id = ${validated.reservationId}
|
|
3321
|
+
RETURNING *`.pipe(
|
|
3322
|
+
rows.childReservations.write,
|
|
3323
|
+
Effect.mapError(internalFailure(operation)),
|
|
3324
|
+
);
|
|
3325
|
+
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
3326
|
+
|
|
3327
|
+
if (Option.isNone(updated)) {
|
|
3328
|
+
return yield* corruptionFailure(
|
|
3329
|
+
operation,
|
|
3330
|
+
"effect_agent_child_reservations",
|
|
3331
|
+
validated.reservationId,
|
|
3332
|
+
"An updated child reservation row is missing inside its own transaction.",
|
|
3333
|
+
);
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
3337
|
+
}),
|
|
3338
|
+
);
|
|
3339
|
+
|
|
3340
|
+
yield* hitFailpoint("ledger:child-release-pending:after", operation);
|
|
3341
|
+
|
|
3342
|
+
return frozen;
|
|
3343
|
+
});
|
|
3344
|
+
|
|
3345
|
+
const releaseChildBudget: SubmissionLedger["Service"]["releaseChildBudget"] = Effect.fnUntraced(
|
|
3346
|
+
function* (request: ReleaseChildBudgetRequest) {
|
|
3347
|
+
const operation = "ledger release child budget";
|
|
3348
|
+
|
|
3349
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseChildBudgetRequest))(
|
|
3382
3350
|
request,
|
|
3383
3351
|
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3384
3352
|
|
|
3385
|
-
yield* hitFailpoint("ledger:child-
|
|
3353
|
+
yield* hitFailpoint("ledger:child-release:before", operation);
|
|
3386
3354
|
|
|
3387
|
-
const
|
|
3355
|
+
const released = yield* inWriteTransaction(
|
|
3388
3356
|
operation,
|
|
3389
3357
|
Effect.gen(function* () {
|
|
3390
3358
|
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
@@ -3395,37 +3363,25 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
3395
3363
|
message: `Unknown child reservation ${validated.reservationId}.`,
|
|
3396
3364
|
});
|
|
3397
3365
|
}
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
}
|
|
3403
|
-
|
|
3404
|
-
return yield* ChildReservationConflict.make({
|
|
3405
|
-
reservationId: validated.reservationId,
|
|
3406
|
-
status: existing.value.status,
|
|
3407
|
-
message: `Reservation ${validated.reservationId} already records child ${existing.value.child_submission_id}.`,
|
|
3408
|
-
});
|
|
3366
|
+
// Applied exactly once: replaying a released reservation returns the stored row
|
|
3367
|
+
// unchanged (spec §12: "never available twice").
|
|
3368
|
+
if (existing.value.status === "released") {
|
|
3369
|
+
return yield* childReservationSnapshotFromRow(operation, existing.value);
|
|
3409
3370
|
}
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
yield* requireOwnership(operation, parent, validated.ownershipToken);
|
|
3413
|
-
if (existing.value.status !== "reserved") {
|
|
3371
|
+
if (existing.value.status !== "releasePending") {
|
|
3414
3372
|
return yield* ChildReservationConflict.make({
|
|
3415
3373
|
reservationId: validated.reservationId,
|
|
3416
3374
|
status: existing.value.status,
|
|
3417
|
-
message:
|
|
3375
|
+
message: "Cannot release a reservation whose accounting decision is not frozen.",
|
|
3418
3376
|
});
|
|
3419
3377
|
}
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
// `SubagentStarted` record remains the attachment's repair authority (DUR-015),
|
|
3423
|
-
// and the coordinator only attaches after the child's admission committed.
|
|
3378
|
+
const now = yield* currentInstant;
|
|
3379
|
+
|
|
3424
3380
|
yield* sql`
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3381
|
+
UPDATE effect_agent_child_reservations
|
|
3382
|
+
SET status = 'released', released_at = ${now.iso}
|
|
3383
|
+
WHERE reservation_id = ${validated.reservationId}
|
|
3384
|
+
RETURNING *`.pipe(
|
|
3429
3385
|
rows.childReservations.write,
|
|
3430
3386
|
Effect.mapError(internalFailure(operation)),
|
|
3431
3387
|
);
|
|
@@ -3444,161 +3400,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
3444
3400
|
}),
|
|
3445
3401
|
);
|
|
3446
3402
|
|
|
3447
|
-
yield* hitFailpoint("ledger:child-
|
|
3448
|
-
|
|
3449
|
-
return attached;
|
|
3450
|
-
});
|
|
3451
|
-
|
|
3452
|
-
const beginChildBudgetRelease: SubmissionLedger["Service"]["beginChildBudgetRelease"] = Effect.fn(
|
|
3453
|
-
"DoSubmissionLedger.beginChildBudgetRelease",
|
|
3454
|
-
)(function* (request: BeginChildBudgetReleaseRequest) {
|
|
3455
|
-
const operation = "ledger begin child budget release";
|
|
3456
|
-
|
|
3457
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(BeginChildBudgetReleaseRequest))(
|
|
3458
|
-
request,
|
|
3459
|
-
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3460
|
-
|
|
3461
|
-
const accountingJson = yield* encodePersistedJsonText(validated.accounting).pipe(
|
|
3462
|
-
Effect.mapError(internalFailure(operation)),
|
|
3463
|
-
);
|
|
3464
|
-
|
|
3465
|
-
yield* hitFailpoint("ledger:child-release-pending:before", operation);
|
|
3466
|
-
|
|
3467
|
-
const frozen = yield* inWriteTransaction(
|
|
3468
|
-
operation,
|
|
3469
|
-
Effect.gen(function* () {
|
|
3470
|
-
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
3471
|
-
|
|
3472
|
-
if (Option.isNone(existing)) {
|
|
3473
|
-
return yield* LedgerError.make({
|
|
3474
|
-
operation,
|
|
3475
|
-
message: `Unknown child reservation ${validated.reservationId}.`,
|
|
3476
|
-
});
|
|
3477
|
-
}
|
|
3478
|
-
if (existing.value.status !== "reserved") {
|
|
3479
|
-
const existingSnapshot = yield* childReservationSnapshotFromRow(
|
|
3480
|
-
operation,
|
|
3481
|
-
existing.value,
|
|
3482
|
-
);
|
|
3483
|
-
|
|
3484
|
-
// The accounting decision was already frozen exactly once; an identical replay is a
|
|
3485
|
-
// no-op and a divergent decision conflicts (spec §12 join step 6).
|
|
3486
|
-
if (
|
|
3487
|
-
existingSnapshot.accounting !== undefined &&
|
|
3488
|
-
equivalentPersistedJson(existingSnapshot.accounting, validated.accounting)
|
|
3489
|
-
) {
|
|
3490
|
-
return existingSnapshot;
|
|
3491
|
-
}
|
|
3492
|
-
|
|
3493
|
-
return yield* ChildReservationConflict.make({
|
|
3494
|
-
reservationId: validated.reservationId,
|
|
3495
|
-
status: existing.value.status,
|
|
3496
|
-
message: "A different accounting decision is already frozen for this reservation.",
|
|
3497
|
-
});
|
|
3498
|
-
}
|
|
3499
|
-
const now = yield* currentInstant;
|
|
3500
|
-
|
|
3501
|
-
yield* sql`
|
|
3502
|
-
UPDATE effect_agent_child_reservations
|
|
3503
|
-
SET
|
|
3504
|
-
status = 'releasePending',
|
|
3505
|
-
accounting_json = ${accountingJson},
|
|
3506
|
-
release_began_at = ${now.iso}
|
|
3507
|
-
WHERE reservation_id = ${validated.reservationId}
|
|
3508
|
-
RETURNING *`.pipe(
|
|
3509
|
-
rows.childReservations.write,
|
|
3510
|
-
Effect.mapError(internalFailure(operation)),
|
|
3511
|
-
);
|
|
3512
|
-
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
3513
|
-
|
|
3514
|
-
if (Option.isNone(updated)) {
|
|
3515
|
-
return yield* corruptionFailure(
|
|
3516
|
-
operation,
|
|
3517
|
-
"effect_agent_child_reservations",
|
|
3518
|
-
validated.reservationId,
|
|
3519
|
-
"An updated child reservation row is missing inside its own transaction.",
|
|
3520
|
-
);
|
|
3521
|
-
}
|
|
3522
|
-
|
|
3523
|
-
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
3524
|
-
}),
|
|
3525
|
-
);
|
|
3526
|
-
|
|
3527
|
-
yield* hitFailpoint("ledger:child-release-pending:after", operation);
|
|
3528
|
-
|
|
3529
|
-
return frozen;
|
|
3530
|
-
});
|
|
3531
|
-
|
|
3532
|
-
const releaseChildBudget: SubmissionLedger["Service"]["releaseChildBudget"] = Effect.fn(
|
|
3533
|
-
"DoSubmissionLedger.releaseChildBudget",
|
|
3534
|
-
)(function* (request: ReleaseChildBudgetRequest) {
|
|
3535
|
-
const operation = "ledger release child budget";
|
|
3536
|
-
|
|
3537
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(ReleaseChildBudgetRequest))(
|
|
3538
|
-
request,
|
|
3539
|
-
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3540
|
-
|
|
3541
|
-
yield* hitFailpoint("ledger:child-release:before", operation);
|
|
3542
|
-
|
|
3543
|
-
const released = yield* inWriteTransaction(
|
|
3544
|
-
operation,
|
|
3545
|
-
Effect.gen(function* () {
|
|
3546
|
-
const existing = yield* readChildReservation(operation, validated.reservationId);
|
|
3547
|
-
|
|
3548
|
-
if (Option.isNone(existing)) {
|
|
3549
|
-
return yield* LedgerError.make({
|
|
3550
|
-
operation,
|
|
3551
|
-
message: `Unknown child reservation ${validated.reservationId}.`,
|
|
3552
|
-
});
|
|
3553
|
-
}
|
|
3554
|
-
// Applied exactly once: replaying a released reservation returns the stored row
|
|
3555
|
-
// unchanged (spec §12: "never available twice").
|
|
3556
|
-
if (existing.value.status === "released") {
|
|
3557
|
-
return yield* childReservationSnapshotFromRow(operation, existing.value);
|
|
3558
|
-
}
|
|
3559
|
-
if (existing.value.status !== "releasePending") {
|
|
3560
|
-
return yield* ChildReservationConflict.make({
|
|
3561
|
-
reservationId: validated.reservationId,
|
|
3562
|
-
status: existing.value.status,
|
|
3563
|
-
message: "Cannot release a reservation whose accounting decision is not frozen.",
|
|
3564
|
-
});
|
|
3565
|
-
}
|
|
3566
|
-
const now = yield* currentInstant;
|
|
3567
|
-
|
|
3568
|
-
yield* sql`
|
|
3569
|
-
UPDATE effect_agent_child_reservations
|
|
3570
|
-
SET status = 'released', released_at = ${now.iso}
|
|
3571
|
-
WHERE reservation_id = ${validated.reservationId}
|
|
3572
|
-
RETURNING *`.pipe(
|
|
3573
|
-
rows.childReservations.write,
|
|
3574
|
-
Effect.mapError(internalFailure(operation)),
|
|
3575
|
-
);
|
|
3576
|
-
const updated = yield* readChildReservation(operation, validated.reservationId);
|
|
3403
|
+
yield* hitFailpoint("ledger:child-release:after", operation);
|
|
3577
3404
|
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
"effect_agent_child_reservations",
|
|
3582
|
-
validated.reservationId,
|
|
3583
|
-
"An updated child reservation row is missing inside its own transaction.",
|
|
3584
|
-
);
|
|
3585
|
-
}
|
|
3586
|
-
|
|
3587
|
-
return yield* childReservationSnapshotFromRow(operation, updated.value);
|
|
3588
|
-
}),
|
|
3589
|
-
);
|
|
3590
|
-
|
|
3591
|
-
yield* hitFailpoint("ledger:child-release:after", operation);
|
|
3592
|
-
|
|
3593
|
-
return released;
|
|
3594
|
-
});
|
|
3405
|
+
return released;
|
|
3406
|
+
},
|
|
3407
|
+
);
|
|
3595
3408
|
|
|
3596
3409
|
interface ScanCursor {
|
|
3597
3410
|
readonly threadId: string;
|
|
3598
3411
|
readonly queueSequence: number;
|
|
3599
3412
|
}
|
|
3600
3413
|
|
|
3601
|
-
const scanPage = Effect.
|
|
3414
|
+
const scanPage = Effect.fnUntraced(function* (
|
|
3602
3415
|
cursor: ScanCursor | undefined,
|
|
3603
3416
|
): Effect.fn.Return<
|
|
3604
3417
|
readonly [ReadonlyArray<SubmissionWorkItem>, Option.Option<ScanCursor | undefined>],
|
|
@@ -3698,264 +3511,226 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
3698
3511
|
),
|
|
3699
3512
|
);
|
|
3700
3513
|
|
|
3701
|
-
const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] =
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
const operation = "ledger read abort intent";
|
|
3705
|
-
|
|
3706
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
|
|
3707
|
-
Effect.mapError(internalFailure(operation)),
|
|
3708
|
-
);
|
|
3709
|
-
|
|
3710
|
-
return yield* state.read(
|
|
3711
|
-
Effect.gen(function* () {
|
|
3712
|
-
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
3713
|
-
const intent = yield* readAbortIntent(operation, validated.submissionId);
|
|
3514
|
+
const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] =
|
|
3515
|
+
Effect.fnUntraced(function* (request) {
|
|
3516
|
+
const operation = "ledger read abort intent";
|
|
3714
3517
|
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
}),
|
|
3719
|
-
);
|
|
3720
|
-
});
|
|
3518
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
|
|
3519
|
+
Effect.mapError(internalFailure(operation)),
|
|
3520
|
+
);
|
|
3721
3521
|
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3522
|
+
return yield* state.read(
|
|
3523
|
+
Effect.gen(function* () {
|
|
3524
|
+
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
3525
|
+
const intent = yield* readAbortIntent(operation, validated.submissionId);
|
|
3726
3526
|
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3527
|
+
return Option.isNone(intent)
|
|
3528
|
+
? undefined
|
|
3529
|
+
: yield* abortIntentFromRow(
|
|
3530
|
+
operation,
|
|
3531
|
+
submission,
|
|
3532
|
+
validated.submissionId,
|
|
3533
|
+
intent.value,
|
|
3534
|
+
);
|
|
3535
|
+
}),
|
|
3536
|
+
);
|
|
3537
|
+
});
|
|
3730
3538
|
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
const submission = yield* decodeSubmissionSnapshot(operation, submissionRow);
|
|
3735
|
-
|
|
3736
|
-
let ownership: OwnershipSnapshot | undefined;
|
|
3737
|
-
const ownershipRow = yield* readOwnership(operation, validated.submissionId);
|
|
3738
|
-
|
|
3739
|
-
if (Option.isSome(ownershipRow)) {
|
|
3740
|
-
ownership = yield* decodeOwnershipSnapshot({
|
|
3741
|
-
attemptId: ownershipRow.value.attempt_id,
|
|
3742
|
-
ownerProducerId: ownershipRow.value.owner_producer_id,
|
|
3743
|
-
producerEpoch: ownershipRow.value.producer_epoch,
|
|
3744
|
-
leaseExpiresAt: ownershipRow.value.lease_expires_at,
|
|
3745
|
-
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
3746
|
-
}
|
|
3539
|
+
const loadRecoverySnapshot: SubmissionLedger["Service"]["loadRecoverySnapshot"] =
|
|
3540
|
+
Effect.fnUntraced(function* (request: RecoverySnapshotRequest) {
|
|
3541
|
+
const operation = "ledger load recovery snapshot";
|
|
3747
3542
|
|
|
3748
|
-
|
|
3543
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
|
|
3544
|
+
request,
|
|
3545
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3749
3546
|
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
submissionRow
|
|
3753
|
-
|
|
3754
|
-
inputApplied = yield* decodeInputAppliedMarker({
|
|
3755
|
-
recordId: submissionRow.input_applied_record_id,
|
|
3756
|
-
sequence: submissionRow.input_applied_sequence,
|
|
3757
|
-
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
3758
|
-
}
|
|
3547
|
+
return yield* state.read(
|
|
3548
|
+
Effect.gen(function* () {
|
|
3549
|
+
const submissionRow = yield* requireSubmission(operation, validated.submissionId);
|
|
3550
|
+
const submission = yield* decodeSubmissionSnapshot(operation, submissionRow);
|
|
3759
3551
|
|
|
3760
|
-
|
|
3761
|
-
const reservationRow = yield* readReservation(operation, validated.submissionId);
|
|
3552
|
+
let inputApplied: InputAppliedMarker | undefined;
|
|
3762
3553
|
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
),
|
|
3773
|
-
);
|
|
3554
|
+
if (
|
|
3555
|
+
submissionRow.input_applied_record_id !== null &&
|
|
3556
|
+
submissionRow.input_applied_sequence !== null
|
|
3557
|
+
) {
|
|
3558
|
+
inputApplied = yield* decodeInputAppliedMarker({
|
|
3559
|
+
recordId: submissionRow.input_applied_record_id,
|
|
3560
|
+
sequence: submissionRow.input_applied_sequence,
|
|
3561
|
+
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
3562
|
+
}
|
|
3774
3563
|
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
)(reservationRow.value.settlement_id).pipe(Effect.mapError(internalFailure(operation)));
|
|
3564
|
+
let abortIntent: AbortIntent | undefined;
|
|
3565
|
+
const abortRow = yield* readAbortIntent(operation, validated.submissionId);
|
|
3778
3566
|
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3567
|
+
if (Option.isSome(abortRow)) {
|
|
3568
|
+
abortIntent = yield* abortIntentFromRow(
|
|
3569
|
+
operation,
|
|
3570
|
+
submissionRow,
|
|
3571
|
+
validated.submissionId,
|
|
3572
|
+
abortRow.value,
|
|
3573
|
+
);
|
|
3574
|
+
}
|
|
3787
3575
|
|
|
3788
|
-
|
|
3789
|
-
|
|
3576
|
+
// Host-side view: every Submission whose host linkage points here, in queue order
|
|
3577
|
+
// (the terminalize loop settles them with the host outcome, DUR-002).
|
|
3578
|
+
const joinRows = (yield* cached(
|
|
3579
|
+
rows.submissions.by("joined_host_submission_id", validated.submissionId),
|
|
3580
|
+
operation,
|
|
3581
|
+
))
|
|
3582
|
+
.filter((row) => row.joined_host_submission_id === validated.submissionId)
|
|
3583
|
+
.sort((a, b) => a.queue_sequence - b.queue_sequence);
|
|
3790
3584
|
|
|
3791
|
-
|
|
3792
|
-
abortIntent = yield* abortIntentFromRow(
|
|
3585
|
+
const joinSubmissions = yield* decodeSubmissionRows(
|
|
3793
3586
|
operation,
|
|
3794
|
-
submissionRow,
|
|
3795
3587
|
validated.submissionId,
|
|
3796
|
-
|
|
3588
|
+
joinRows,
|
|
3797
3589
|
);
|
|
3798
|
-
}
|
|
3799
3590
|
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
.sort((a, b) => a.queue_sequence - b.queue_sequence);
|
|
3808
|
-
|
|
3809
|
-
const joinSubmissions = yield* decodeSubmissionRows(
|
|
3810
|
-
operation,
|
|
3811
|
-
validated.submissionId,
|
|
3812
|
-
joinRows,
|
|
3813
|
-
);
|
|
3814
|
-
|
|
3815
|
-
const joins = yield* Effect.forEach(joinSubmissions, (row) =>
|
|
3816
|
-
decodeJoinSnapshot({
|
|
3817
|
-
submissionId: row.submission_id,
|
|
3818
|
-
state: row.state,
|
|
3819
|
-
hostSubmissionId: validated.submissionId,
|
|
3820
|
-
}).pipe(Effect.mapError(internalFailure(operation))),
|
|
3821
|
-
);
|
|
3822
|
-
|
|
3823
|
-
let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
|
|
3591
|
+
const joins = yield* Effect.forEach(joinSubmissions, (row) =>
|
|
3592
|
+
decodeJoinSnapshot({
|
|
3593
|
+
submissionId: row.submission_id,
|
|
3594
|
+
state: row.state,
|
|
3595
|
+
hostSubmissionId: validated.submissionId,
|
|
3596
|
+
}).pipe(Effect.mapError(internalFailure(operation))),
|
|
3597
|
+
);
|
|
3824
3598
|
|
|
3825
|
-
|
|
3826
|
-
hostSubmissionId = yield* decodeSubmissionId(
|
|
3827
|
-
submissionRow.joined_host_submission_id,
|
|
3828
|
-
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3829
|
-
}
|
|
3599
|
+
let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
|
|
3830
3600
|
|
|
3831
|
-
|
|
3601
|
+
if (submissionRow.joined_host_submission_id !== null) {
|
|
3602
|
+
hostSubmissionId = yield* decodeSubmissionId(
|
|
3603
|
+
submissionRow.joined_host_submission_id,
|
|
3604
|
+
).pipe(Effect.mapError(internalFailure(operation)));
|
|
3605
|
+
}
|
|
3832
3606
|
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3607
|
+
let suspension: SuspensionSnapshot | undefined;
|
|
3608
|
+
|
|
3609
|
+
if (submissionRow.suspended_reason_json !== null && submissionRow.suspended_at !== null) {
|
|
3610
|
+
const reason = yield* parseStoredJsonText(submissionRow.suspended_reason_json).pipe(
|
|
3611
|
+
Effect.mapError((error) =>
|
|
3612
|
+
corruptionFailure(
|
|
3613
|
+
operation,
|
|
3614
|
+
"effect_agent_submissions",
|
|
3615
|
+
validated.submissionId,
|
|
3616
|
+
error.message,
|
|
3617
|
+
),
|
|
3841
3618
|
),
|
|
3842
|
-
)
|
|
3843
|
-
);
|
|
3619
|
+
);
|
|
3844
3620
|
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3621
|
+
suspension = yield* decodeSuspensionSnapshot({
|
|
3622
|
+
reason,
|
|
3623
|
+
suspendedAt: submissionRow.suspended_at,
|
|
3624
|
+
}).pipe(
|
|
3625
|
+
Effect.mapError((error) =>
|
|
3626
|
+
corruptionFailure(
|
|
3627
|
+
operation,
|
|
3628
|
+
"effect_agent_submissions",
|
|
3629
|
+
validated.submissionId,
|
|
3630
|
+
error.message,
|
|
3631
|
+
),
|
|
3855
3632
|
),
|
|
3856
|
-
)
|
|
3857
|
-
|
|
3858
|
-
}
|
|
3859
|
-
|
|
3860
|
-
const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
3633
|
+
);
|
|
3634
|
+
}
|
|
3861
3635
|
|
|
3862
|
-
|
|
3863
|
-
approvalIntentFromRow(operation, row),
|
|
3864
|
-
);
|
|
3636
|
+
const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
|
|
3865
3637
|
|
|
3866
|
-
|
|
3638
|
+
const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
|
|
3639
|
+
approvalIntentFromRow(operation, row),
|
|
3640
|
+
);
|
|
3867
3641
|
|
|
3868
|
-
|
|
3869
|
-
unknownResolutionIntentFromRow(operation, row),
|
|
3870
|
-
);
|
|
3642
|
+
const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
|
|
3871
3643
|
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
// state comes from its local row when this store holds it, and otherwise from the
|
|
3876
|
-
// durable cross-store settlement marker; a child that is neither local nor marked
|
|
3877
|
-
// settled is enriched by the routed per-child lookup one layer out (plan §1.3).
|
|
3878
|
-
const childReservationRows = (yield* cached(
|
|
3879
|
-
rows.childReservations.by("parent_submission_id", validated.submissionId),
|
|
3880
|
-
operation,
|
|
3881
|
-
))
|
|
3882
|
-
.filter((row) => row.parent_submission_id === validated.submissionId)
|
|
3883
|
-
.sort((a, b) => a.parent_tool_call_id.localeCompare(b.parent_tool_call_id));
|
|
3644
|
+
const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
|
|
3645
|
+
unknownResolutionIntentFromRow(operation, row),
|
|
3646
|
+
);
|
|
3884
3647
|
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3648
|
+
// Parent-side subagent view: this Submission's child budget reservations in parent
|
|
3649
|
+
// Tool Call order, plus each attached child's current lane state (a disposable
|
|
3650
|
+
// derived view; canonical records stay the recovery truth, DUR-015). The child's
|
|
3651
|
+
// state comes from its local row when this store holds it, and otherwise from the
|
|
3652
|
+
// durable cross-store settlement marker; a child that is neither local nor marked
|
|
3653
|
+
// settled is enriched by the routed per-child lookup one layer out (plan §1.3).
|
|
3654
|
+
const childReservationRows = (yield* cached(
|
|
3655
|
+
rows.childReservations.by("parent_submission_id", validated.submissionId),
|
|
3656
|
+
operation,
|
|
3657
|
+
))
|
|
3658
|
+
.filter((row) => row.parent_submission_id === validated.submissionId)
|
|
3659
|
+
.sort((a, b) => a.parent_tool_call_id.localeCompare(b.parent_tool_call_id));
|
|
3890
3660
|
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3661
|
+
const decodedChildReservations = yield* decodeChildReservationRows(
|
|
3662
|
+
operation,
|
|
3663
|
+
validated.submissionId,
|
|
3664
|
+
childReservationRows,
|
|
3665
|
+
);
|
|
3894
3666
|
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3667
|
+
const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
|
|
3668
|
+
childReservationSnapshotFromRow(operation, row),
|
|
3669
|
+
);
|
|
3898
3670
|
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
const
|
|
3671
|
+
const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
|
|
3672
|
+
const markersByChild = new Map(markers.map((row) => [row.child_submission_id, row]));
|
|
3673
|
+
const childAttachments: Array<ChildAttachmentSnapshot> = [];
|
|
3674
|
+
|
|
3675
|
+
for (const row of decodedChildReservations) {
|
|
3676
|
+
if (row.child_submission_id === null) continue;
|
|
3677
|
+
const child = yield* readSubmission(operation, row.child_submission_id);
|
|
3678
|
+
|
|
3679
|
+
if (Option.isSome(child)) {
|
|
3680
|
+
childAttachments.push(
|
|
3681
|
+
yield* decodeChildAttachmentSnapshot({
|
|
3682
|
+
toolCallId: row.parent_tool_call_id,
|
|
3683
|
+
childSubmissionId: row.child_submission_id,
|
|
3684
|
+
childState: child.value.state,
|
|
3685
|
+
...(child.value.settled_outcome === null
|
|
3686
|
+
? {}
|
|
3687
|
+
: { childOutcome: child.value.settled_outcome }),
|
|
3688
|
+
}).pipe(Effect.mapError(internalFailure(operation))),
|
|
3689
|
+
);
|
|
3690
|
+
continue;
|
|
3691
|
+
}
|
|
3692
|
+
const marker = markersByChild.get(row.child_submission_id);
|
|
3902
3693
|
|
|
3903
|
-
|
|
3694
|
+
if (marker === undefined) continue;
|
|
3904
3695
|
childAttachments.push(
|
|
3905
3696
|
yield* decodeChildAttachmentSnapshot({
|
|
3906
3697
|
toolCallId: row.parent_tool_call_id,
|
|
3907
3698
|
childSubmissionId: row.child_submission_id,
|
|
3908
|
-
childState:
|
|
3909
|
-
...(
|
|
3910
|
-
? {}
|
|
3911
|
-
: { childOutcome: child.value.settled_outcome }),
|
|
3699
|
+
childState: "settled",
|
|
3700
|
+
...(marker.child_outcome === null ? {} : { childOutcome: marker.child_outcome }),
|
|
3912
3701
|
}).pipe(Effect.mapError(internalFailure(operation))),
|
|
3913
3702
|
);
|
|
3914
|
-
continue;
|
|
3915
3703
|
}
|
|
3916
|
-
const marker = markersByChild.get(row.child_submission_id);
|
|
3917
|
-
|
|
3918
|
-
if (marker === undefined) continue;
|
|
3919
|
-
childAttachments.push(
|
|
3920
|
-
yield* decodeChildAttachmentSnapshot({
|
|
3921
|
-
toolCallId: row.parent_tool_call_id,
|
|
3922
|
-
childSubmissionId: row.child_submission_id,
|
|
3923
|
-
childState: "settled",
|
|
3924
|
-
...(marker.child_outcome === null ? {} : { childOutcome: marker.child_outcome }),
|
|
3925
|
-
}).pipe(Effect.mapError(internalFailure(operation))),
|
|
3926
|
-
);
|
|
3927
|
-
}
|
|
3928
3704
|
|
|
3929
|
-
|
|
3705
|
+
let parentLinkage: ParentLinkage | undefined;
|
|
3930
3706
|
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3707
|
+
if (
|
|
3708
|
+
submissionRow.parent_submission_id !== null &&
|
|
3709
|
+
submissionRow.parent_tool_call_id !== null
|
|
3710
|
+
) {
|
|
3711
|
+
parentLinkage = yield* decodeParentLinkage({
|
|
3712
|
+
parentSubmissionId: submissionRow.parent_submission_id,
|
|
3713
|
+
parentToolCallId: submissionRow.parent_tool_call_id,
|
|
3714
|
+
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
3715
|
+
}
|
|
3940
3716
|
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
})
|
|
3956
|
-
|
|
3957
|
-
);
|
|
3958
|
-
});
|
|
3717
|
+
return RecoverySnapshot.make({
|
|
3718
|
+
submission,
|
|
3719
|
+
joins,
|
|
3720
|
+
approvalDecisions,
|
|
3721
|
+
unknownResolutions,
|
|
3722
|
+
childReservations,
|
|
3723
|
+
childAttachments,
|
|
3724
|
+
...(parentLinkage === undefined ? {} : { parentLinkage }),
|
|
3725
|
+
...(hostSubmissionId === undefined ? {} : { hostSubmissionId }),
|
|
3726
|
+
...(suspension === undefined ? {} : { suspension }),
|
|
3727
|
+
hasOwner: owner.hasOwner(submissionRow.thread_id, submissionRow.submission_id),
|
|
3728
|
+
...(inputApplied === undefined ? {} : { inputApplied }),
|
|
3729
|
+
...(abortIntent === undefined ? {} : { abortIntent }),
|
|
3730
|
+
});
|
|
3731
|
+
}),
|
|
3732
|
+
);
|
|
3733
|
+
});
|
|
3959
3734
|
|
|
3960
3735
|
return Context.make(
|
|
3961
3736
|
SubmissionLedger,
|
|
@@ -3964,20 +3739,18 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
3964
3739
|
admit,
|
|
3965
3740
|
markReady,
|
|
3966
3741
|
lookup,
|
|
3742
|
+
resolveFundingOwner,
|
|
3967
3743
|
resolveAdmission,
|
|
3968
|
-
|
|
3969
|
-
renewOwnership,
|
|
3970
|
-
releaseOwnership,
|
|
3744
|
+
selectHead,
|
|
3971
3745
|
markInputApplied,
|
|
3972
|
-
|
|
3973
|
-
finalizeSettlement,
|
|
3746
|
+
finalizeSettlement: (request) => owner.withMutation(finalizeSettlement(request)),
|
|
3974
3747
|
requestAbort,
|
|
3975
3748
|
stopWorker,
|
|
3976
3749
|
inspectWorker,
|
|
3977
3750
|
claimJoining,
|
|
3978
3751
|
markJoined,
|
|
3979
3752
|
revertJoining,
|
|
3980
|
-
suspend,
|
|
3753
|
+
suspend: (request) => owner.withMutation(suspend(request)),
|
|
3981
3754
|
recordApprovalDecision,
|
|
3982
3755
|
markUnknown,
|
|
3983
3756
|
recordUnknownResolution,
|
|
@@ -3990,27 +3763,30 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
3990
3763
|
loadRecoverySnapshot,
|
|
3991
3764
|
readAbortIntent: readAbortIntentForSubmission,
|
|
3992
3765
|
}),
|
|
3993
|
-
);
|
|
3766
|
+
).pipe(Context.add(StorageOwner, owner), Context.add(SettlementPublisher, { publish }));
|
|
3994
3767
|
});
|
|
3995
3768
|
|
|
3996
3769
|
/**
|
|
3997
3770
|
* Durable Object SubmissionLedger implementation sharing the journal's private SQLite
|
|
3998
|
-
* database, storage-backed
|
|
3771
|
+
* database, storage-backed transactions, and scoped local owner.
|
|
3999
3772
|
* Configuration, failpoint, SQL, and Crypto authority stay visible in the input channel.
|
|
4000
3773
|
*/
|
|
4001
3774
|
export const submissionLedgerLayer: Layer.Layer<
|
|
4002
|
-
SubmissionLedger,
|
|
3775
|
+
SubmissionLedger | SettlementPublisher | StorageOwner,
|
|
4003
3776
|
DoStorageInitializationError,
|
|
4004
3777
|
DoStorageConfig | DoStorageFailpoint | SqlClientService.SqlClient | Crypto.Crypto
|
|
4005
3778
|
> = Layer.effectContext(makeServices());
|
|
4006
3779
|
|
|
4007
3780
|
/**
|
|
4008
3781
|
* A composition-root convenience Layer for the durable Submission Ledger. Point it at the
|
|
4009
|
-
* same `ctx.storage` as the ThreadStore so
|
|
3782
|
+
* same `ctx.storage` as the ThreadStore so both share the same scoped storage owner.
|
|
4010
3783
|
*/
|
|
4011
3784
|
export const ledgerLayer = (
|
|
4012
3785
|
options: DoStorageOptions,
|
|
4013
|
-
): Layer.Layer<
|
|
3786
|
+
): Layer.Layer<
|
|
3787
|
+
SubmissionLedger | SettlementPublisher | StorageOwner,
|
|
3788
|
+
DoStorageInitializationError
|
|
3789
|
+
> =>
|
|
4014
3790
|
Layer.unwrap(
|
|
4015
3791
|
Effect.map(DoStorageConfig, (config) =>
|
|
4016
3792
|
submissionLedgerLayer.pipe(
|