effect-agent 0.1.0-beta.113 → 0.1.0-beta.115
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/Agent-1DMSO_Fl.mjs.map +1 -1
- package/dist/{Agent-DriBcXBN.d.mts → Agent-D9cXqfF3.d.mts} +9 -2
- package/dist/{AgentRuntime-DLwPoEVH.mjs → AgentRuntime-Cp1hAb-W.mjs} +8 -4
- package/dist/AgentRuntime-Cp1hAb-W.mjs.map +1 -0
- package/dist/{AgentRuntime-CWkHuzyt.d.mts → AgentRuntime-DyZv8uu2.d.mts} +6 -6
- package/dist/{DurableAgentRuntime-BIrgxwRl.mjs → DurableAgentRuntime-tNrkJZNo.mjs} +254 -162
- package/dist/DurableAgentRuntime-tNrkJZNo.mjs.map +1 -0
- package/dist/{DurableFailpoint-mx3-nsxU.d.mts → DurableFailpoint-Cnr3wx1L.d.mts} +3 -3
- package/dist/{EventSource-ARs56eBq.d.mts → EventSource-DPRdlvhV.d.mts} +3 -3
- package/dist/{Messaging-DaZDlBqh.d.mts → Messaging-JYYN3oYS.d.mts} +3 -3
- package/dist/{MessagingHost-e1Sbzfao.d.mts → MessagingHost-CqzpxbGz.d.mts} +3 -3
- package/dist/{MessagingHost-D89X7ma8.mjs → MessagingHost-DfsbMPvq.mjs} +2 -2
- package/dist/{MessagingHost-D89X7ma8.mjs.map → MessagingHost-DfsbMPvq.mjs.map} +1 -1
- package/dist/{Output-CAFJTAbP.d.mts → Output-V-6YL1_Z.d.mts} +2 -2
- package/dist/{PreparedInputAdmission-_WYg-D0f.d.mts → PreparedInputAdmission-DqQUjA9k.d.mts} +4 -4
- package/dist/{Records-BtxMy8xD.d.mts → Records-Byeb70tt.d.mts} +89 -31
- package/dist/{RunOptions-BoESJrxw.d.mts → RunOptions-CSBsSj0X.d.mts} +3 -3
- package/dist/{Schedule-DGPyXBzK.d.mts → Schedule-krSnBcbU.d.mts} +2 -2
- package/dist/{Subagent-D9eYdGTl.mjs → Subagent-Di8EEjWZ.mjs} +67 -105
- package/dist/Subagent-Di8EEjWZ.mjs.map +1 -0
- package/dist/{Subagent-F72tuaxy.d.mts → Subagent-gw9KD6eb.d.mts} +310 -35
- package/dist/{SubagentHost-CmiNTOBZ.d.mts → SubagentHost-P9zB_TfV.d.mts} +24 -18
- package/dist/{Subscription-B9VszlF9.d.mts → Subscription-Xnwd42Sm.d.mts} +6 -3
- package/dist/{SubscriptionInput-BJWZqCWP.d.mts → SubscriptionInput-CqtJ58vl.d.mts} +3 -3
- package/dist/{Worker-T_3RbaKc.mjs → Worker-CfwzMZEK.mjs} +66 -5
- package/dist/Worker-CfwzMZEK.mjs.map +1 -0
- package/dist/{Worker-Du9dhVnp.d.mts → Worker-fwWkBg12.d.mts} +199 -6
- package/dist/{agent-registration-B5CI-Ygk.mjs → agent-registration-CKNsrpwo.mjs} +7 -7
- package/dist/agent-registration-CKNsrpwo.mjs.map +1 -0
- package/dist/capabilities/Commands.d.mts +1 -1
- package/dist/capabilities/Messaging.d.mts +1 -1
- package/dist/capabilities/Messaging.mjs +2 -2
- package/dist/capabilities/RunHooks.d.mts +1 -1
- package/dist/capabilities/Subagent.d.mts +3 -3
- package/dist/capabilities/Subagent.mjs +3 -3
- package/dist/core/Agent.d.mts +1 -1
- package/dist/core/Worker.d.mts +2 -2
- package/dist/core/Worker.mjs +2 -2
- package/dist/durable/ActivityStore.d.mts +1 -1
- package/dist/durable/Admin.d.mts +1 -1
- package/dist/durable/AgentRegistration.d.mts +1 -1
- package/dist/durable/AgentRegistration.mjs +1 -1
- package/dist/durable/Certification.d.mts +2 -2
- package/dist/durable/CommittedActivity.d.mts +1 -1
- package/dist/durable/Digest.d.mts +1 -1
- package/dist/durable/DurableAgentRuntime.d.mts +1 -1
- package/dist/durable/DurableAgentRuntime.mjs +1 -1
- package/dist/durable/DurableFailpoint.d.mts +1 -1
- package/dist/durable/DurableFailpoint.mjs +4 -0
- package/dist/durable/DurableFailpoint.mjs.map +1 -1
- package/dist/durable/DurableFailpointTestControl.d.mts +1 -1
- package/dist/durable/EventSource.d.mts +1 -1
- package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
- package/dist/durable/MessageDelivery.d.mts +20 -3
- package/dist/durable/MessageDelivery.mjs +12 -4
- package/dist/durable/MessageDelivery.mjs.map +1 -1
- package/dist/durable/MessageDeliveryStoreConformance.d.mts +41 -39
- package/dist/durable/MessageDeliveryStoreConformance.mjs +1 -1
- package/dist/durable/PersistentHistory.d.mts +1 -1
- package/dist/durable/PreparedInputAdmission.d.mts +1 -1
- package/dist/durable/Records.d.mts +2 -2
- package/dist/durable/Records.mjs +12 -5
- package/dist/durable/Records.mjs.map +1 -1
- package/dist/durable/Recovery.d.mts +1 -1
- package/dist/durable/RunJournal.d.mts +1 -1
- package/dist/durable/Schedule.d.mts +1 -1
- package/dist/durable/ScheduleStoreConformance.d.mts +1 -1
- package/dist/durable/ScheduleTransition.d.mts +1 -1
- package/dist/durable/Scheduling.d.mts +2 -2
- package/dist/durable/SqlMessageDeliveryStore.mjs +7 -1
- package/dist/durable/SqlMessageDeliveryStore.mjs.map +1 -1
- package/dist/durable/SqlStorageV2Upgrade.d.mts +2 -2
- package/dist/durable/SqlSubscriptionStore.d.mts +2 -2
- package/dist/durable/SqlThreadNativeReads.d.mts +5 -1
- package/dist/durable/SqlThreadNativeReads.mjs +6 -0
- package/dist/durable/SqlThreadNativeReads.mjs.map +1 -1
- package/dist/durable/SubmissionLedger.d.mts +2 -2
- package/dist/durable/SubmissionLedger.mjs +34 -4
- package/dist/durable/SubmissionLedger.mjs.map +1 -1
- package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
- package/dist/durable/SubmissionLedgerConformance.mjs +899 -706
- package/dist/durable/SubmissionLedgerConformance.mjs.map +1 -1
- package/dist/durable/SubmissionStatus.d.mts +1 -1
- package/dist/durable/Subscription.d.mts +1 -1
- package/dist/durable/Subscription.mjs +1 -1
- package/dist/durable/SubscriptionInput.d.mts +1 -1
- package/dist/durable/SubscriptionInput.mjs +1 -1
- package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
- package/dist/durable/SubscriptionTools.d.mts +3 -3
- package/dist/durable/SubscriptionTransition.d.mts +1 -1
- package/dist/durable/Subscriptions.d.mts +5 -5
- package/dist/durable/ThreadContextHistory.d.mts +1 -1
- package/dist/durable/ThreadContextHistoryProjection.d.mts +1 -1
- package/dist/durable/ThreadInvariants.d.mts +1 -1
- package/dist/durable/ThreadProjection.d.mts +1 -1
- package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
- package/dist/durable/ThreadStore.d.mts +1 -1
- package/dist/durable/ThreadStore.mjs +5 -0
- package/dist/durable/ThreadStore.mjs.map +1 -1
- package/dist/durable/ThreadStoreConformance.d.mts +1 -1
- package/dist/durable/WorkerHost.d.mts +3 -3
- package/dist/durable/WorkerHost.mjs +1 -1
- package/dist/engine/AgentRuntime.d.mts +1 -1
- package/dist/engine/AgentRuntime.mjs +1 -1
- package/dist/engine/MessagingHost.d.mts +1 -1
- package/dist/engine/MessagingHost.mjs +1 -1
- package/dist/engine/Output.d.mts +1 -1
- package/dist/engine/RunOptions.d.mts +1 -1
- package/dist/engine/SubagentHost.d.mts +2 -2
- package/dist/engine/SubagentHost.mjs +5 -1
- package/dist/engine/SubagentHost.mjs.map +1 -1
- package/dist/index.d.mts +16 -16
- package/dist/index.mjs +5 -5
- package/package.json +1 -1
- package/src/capabilities/Subagent.ts +2 -1
- package/src/capabilities/internal/subagent-background.ts +86 -57
- package/src/capabilities/internal/subagent-reporting.ts +3 -128
- package/src/core/Agent.ts +8 -1
- package/src/core/Worker.ts +58 -2
- package/src/durable/DurableAgentRuntime.ts +2 -0
- package/src/durable/DurableFailpoint.ts +4 -0
- package/src/durable/MessageDelivery.ts +17 -2
- package/src/durable/Records.ts +12 -2
- package/src/durable/SqlMessageDeliveryStore.ts +15 -1
- package/src/durable/SqlThreadNativeReads.ts +14 -0
- package/src/durable/SubmissionLedger.ts +48 -1
- package/src/durable/SubmissionLedgerConformance.ts +260 -42
- package/src/durable/ThreadStore.ts +5 -0
- package/src/durable/internal/agent-registration.ts +13 -48
- package/src/durable/internal/worker-host.ts +490 -341
- package/src/engine/SubagentHost.ts +37 -13
- package/src/engine/internal/agent-runtime.ts +17 -2
- package/dist/AgentRuntime-DLwPoEVH.mjs.map +0 -1
- package/dist/DurableAgentRuntime-BIrgxwRl.mjs.map +0 -1
- package/dist/Subagent-D9eYdGTl.mjs.map +0 -1
- package/dist/Worker-T_3RbaKc.mjs.map +0 -1
- package/dist/agent-registration-B5CI-Ygk.mjs.map +0 -1
|
@@ -2,11 +2,12 @@ import { AgentId, DelegationId, SubmissionId, ThreadId, ToolCallId } from "../co
|
|
|
2
2
|
import { AgentPolicy } from "../core/AgentPolicy.mjs";
|
|
3
3
|
import { IdempotencyKey, JoinedToHost, Principal } from "../core/Receipt.mjs";
|
|
4
4
|
import { SubagentDelegationCaps, SubagentGrant, SubagentReservationAmounts } from "../core/SubagentContract.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import { d as MessageAdmission, l as InputMessage } from "../MessagingHost-
|
|
5
|
+
import { b as WorkerUpdate, o as WorkerCompletion } from "../Worker-CfwzMZEK.mjs";
|
|
6
|
+
import { d as MessageAdmission, l as InputMessage } from "../MessagingHost-DfsbMPvq.mjs";
|
|
7
7
|
import { CanonicalSequence, DefinitionDigests, DeploymentId, Digest, PersistedJson, ProducerId, RecordEnvelope, SubmissionSettled, SubmissionSettledRecord, WorkerAdmission } from "./Records.mjs";
|
|
8
|
-
import { AbortCommand, AbortIntentRequest, AdmissionConflict, AdmissionPolicyError, AdmissionRequest, ApprovalConflict, ApprovalDecisionCommand, ApprovalPendingSuspension, AttachChildToReservationRequest, BeginChildBudgetReleaseRequest, ChildBudgetReservationRequest, ChildReservationConflict, ChildReservationId, ChildSettledNotification, ClaimJoiningRequest, ClaimRequest, LedgerError, MarkInputAppliedRequest, MarkJoinedRequest, MarkReadyRequest, MarkUnknownRequest, OwnershipLost, OwnershipToken, ParentLinkage, RecoverySnapshotRequest, ReleaseChildBudgetRequest, ReleaseOwnershipRequest, RenewOwnershipRequest, ResolutionCompletedWithResult, ResolutionNeverHappened, ResolutionSafeToRetry, RevertJoiningRequest, SettlementConflict, SettlementFinalization, SettlementReservation, SubmissionLedger, SubmissionLookupById, SubmissionLookupByKey, SuspendRequest, UnknownResolutionCommand, UnknownResolutionConflict, WaitingChild, WaitingForChildSuspension, submissionInputRecordId, submissionSettlementId, submissionSettlementRecordId } from "./SubmissionLedger.mjs";
|
|
8
|
+
import { AbortCommand, AbortIntentRequest, AdmissionConflict, AdmissionPolicyError, AdmissionRequest, ApprovalConflict, ApprovalDecisionCommand, ApprovalPendingSuspension, AttachChildToReservationRequest, BeginChildBudgetReleaseRequest, ChildBudgetReservationRequest, ChildReservationConflict, ChildReservationId, ChildSettledNotification, ClaimJoiningRequest, ClaimRequest, LedgerError, MarkInputAppliedRequest, MarkJoinedRequest, MarkReadyRequest, MarkUnknownRequest, OwnershipLost, OwnershipToken, ParentLinkage, RecoverySnapshotRequest, ReleaseChildBudgetRequest, ReleaseOwnershipRequest, RenewOwnershipRequest, ResolutionCompletedWithResult, ResolutionNeverHappened, ResolutionSafeToRetry, RevertJoiningRequest, Settlement, SettlementConflict, SettlementFinalization, SettlementReservation, SubmissionLedger, SubmissionLookupById, SubmissionLookupByKey, SuspendRequest, UnknownResolutionCommand, UnknownResolutionConflict, WaitingChild, WaitingForChildSuspension, submissionInputRecordId, submissionSettlementId, submissionSettlementRecordId } from "./SubmissionLedger.mjs";
|
|
9
9
|
import { digestJson } from "./Digest.mjs";
|
|
10
|
+
import { S as runIdForSubmission } from "../RunJournal-BXrq0VIK.mjs";
|
|
10
11
|
import { Clock, DateTime, Duration, Effect, Option, Result, Schema, Stream } from "effect";
|
|
11
12
|
import { TestClock } from "effect/testing";
|
|
12
13
|
//#region src/durable/SubmissionLedgerConformance.ts
|
|
@@ -220,54 +221,55 @@ const admissionIdempotency = conformanceCase("replays identical admissions and r
|
|
|
220
221
|
const second = yield* ledger.admit(yield* admissionRequest(threadId, "admission-key-2", { city: "Faro" }));
|
|
221
222
|
yield* ensure(second.submissionId !== first.submissionId && second.queueSequence !== first.queueSequence, "A different key on the same lane must mint fresh identities and a fresh queue sequence");
|
|
222
223
|
}));
|
|
224
|
+
const workerMetadata = (base) => WorkerAdmission.make({
|
|
225
|
+
messageId: base.idempotencyKey,
|
|
226
|
+
sourceSubmissionId: Schema.decodeSync(SubmissionId)("source-input"),
|
|
227
|
+
deliveryPrincipal: base.principal,
|
|
228
|
+
parameters: { prompt: "original projection parameters" },
|
|
229
|
+
createdAtMillis: 1,
|
|
230
|
+
origin: {
|
|
231
|
+
worker: {
|
|
232
|
+
schemaVersion: 1,
|
|
233
|
+
threadId: base.threadId,
|
|
234
|
+
targetAgentId: CONFORMANCE_AGENT,
|
|
235
|
+
delegationId: Schema.decodeSync(DelegationId)("worker-research")
|
|
236
|
+
},
|
|
237
|
+
source: {
|
|
238
|
+
_tag: "programmatic",
|
|
239
|
+
threadId: decodeThreadId("worker-source"),
|
|
240
|
+
agentId: CONFORMANCE_AGENT
|
|
241
|
+
},
|
|
242
|
+
targetDigests: CONFORMANCE_DIGESTS,
|
|
243
|
+
policy: AgentPolicy.resolve(),
|
|
244
|
+
budget: {
|
|
245
|
+
caps: SubagentDelegationCaps.make({
|
|
246
|
+
maxConcurrentChildren: 1,
|
|
247
|
+
maxTotalChildInvocations: 2
|
|
248
|
+
}),
|
|
249
|
+
allocation: SubagentReservationAmounts.make({
|
|
250
|
+
turns: 12,
|
|
251
|
+
toolCalls: 24,
|
|
252
|
+
durationMillis: 3e5,
|
|
253
|
+
inputTokens: 0,
|
|
254
|
+
outputTokens: 0,
|
|
255
|
+
costMicrousd: 0,
|
|
256
|
+
resultBytes: 1e3
|
|
257
|
+
})
|
|
258
|
+
},
|
|
259
|
+
grant: SubagentGrant.make({
|
|
260
|
+
maxDepth: 1,
|
|
261
|
+
allowedToolNames: []
|
|
262
|
+
}),
|
|
263
|
+
depth: 1,
|
|
264
|
+
firstMessageId: base.idempotencyKey,
|
|
265
|
+
createdAtMillis: 1,
|
|
266
|
+
expiresAtMillis: 1e6
|
|
267
|
+
}
|
|
268
|
+
});
|
|
223
269
|
const workerAdmissionIdentity = conformanceCase("retains immutable worker admission metadata and rejects same-key changes or omission", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
224
270
|
const ledger = yield* SubmissionLedger;
|
|
225
271
|
const base = yield* admissionRequest(decodeThreadId("ledger-conformance-worker"), "worker-message", { text: "first" });
|
|
226
|
-
const metadata =
|
|
227
|
-
messageId: base.idempotencyKey,
|
|
228
|
-
sourceSubmissionId: Schema.decodeSync(SubmissionId)("source-input"),
|
|
229
|
-
deliveryPrincipal: base.principal,
|
|
230
|
-
parameters: { prompt: "original projection parameters" },
|
|
231
|
-
createdAtMillis: 1,
|
|
232
|
-
origin: {
|
|
233
|
-
worker: {
|
|
234
|
-
schemaVersion: 1,
|
|
235
|
-
threadId: base.threadId,
|
|
236
|
-
targetAgentId: CONFORMANCE_AGENT,
|
|
237
|
-
delegationId: Schema.decodeSync(DelegationId)("worker-research")
|
|
238
|
-
},
|
|
239
|
-
source: {
|
|
240
|
-
_tag: "programmatic",
|
|
241
|
-
threadId: decodeThreadId("worker-source"),
|
|
242
|
-
agentId: CONFORMANCE_AGENT
|
|
243
|
-
},
|
|
244
|
-
targetDigests: CONFORMANCE_DIGESTS,
|
|
245
|
-
policy: AgentPolicy.resolve(),
|
|
246
|
-
budget: {
|
|
247
|
-
caps: SubagentDelegationCaps.make({
|
|
248
|
-
maxConcurrentChildren: 1,
|
|
249
|
-
maxTotalChildInvocations: 2
|
|
250
|
-
}),
|
|
251
|
-
allocation: SubagentReservationAmounts.make({
|
|
252
|
-
turns: 12,
|
|
253
|
-
toolCalls: 24,
|
|
254
|
-
durationMillis: 3e5,
|
|
255
|
-
inputTokens: 0,
|
|
256
|
-
outputTokens: 0,
|
|
257
|
-
costMicrousd: 0,
|
|
258
|
-
resultBytes: 1e3
|
|
259
|
-
})
|
|
260
|
-
},
|
|
261
|
-
grant: SubagentGrant.make({
|
|
262
|
-
maxDepth: 1,
|
|
263
|
-
allowedToolNames: []
|
|
264
|
-
}),
|
|
265
|
-
depth: 1,
|
|
266
|
-
firstMessageId: base.idempotencyKey,
|
|
267
|
-
createdAtMillis: 1,
|
|
268
|
-
expiresAtMillis: 1e6
|
|
269
|
-
}
|
|
270
|
-
});
|
|
272
|
+
const metadata = workerMetadata(base);
|
|
271
273
|
const admitted = yield* ledger.admit(AdmissionRequest.make({
|
|
272
274
|
...base,
|
|
273
275
|
workerAdmission: metadata
|
|
@@ -1297,6 +1299,664 @@ const unknownAbortClaim = conformanceCase("a durable abort makes an unknown head
|
|
|
1297
1299
|
yield* ensure((yield* expectSome("follower claim", yield* claimLane(threadId, PRODUCER_B))).submissionId === follower.submissionId, "Settlement must release the follower");
|
|
1298
1300
|
}
|
|
1299
1301
|
}));
|
|
1302
|
+
const unknownResolutionLifecycle = conformanceCase("unknown resolutions reopen the lane only when no open call remains", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1303
|
+
const threadId = decodeThreadId("ledger-conformance-unknown");
|
|
1304
|
+
const ledger = yield* SubmissionLedger;
|
|
1305
|
+
const admitted = yield* admitReady(threadId, "unknown-key-1", { work: "unknown" });
|
|
1306
|
+
const claim = yield* expectSome("the claim before the unknown marking", yield* claimLane(threadId, PRODUCER_A));
|
|
1307
|
+
const call1 = decodeToolCallId("call-unknown-1");
|
|
1308
|
+
const call2 = decodeToolCallId("call-unknown-2");
|
|
1309
|
+
const call3 = decodeToolCallId("call-unknown-3");
|
|
1310
|
+
const marking = MarkUnknownRequest.make({
|
|
1311
|
+
submissionId: admitted.submissionId,
|
|
1312
|
+
toolCallIds: [call1, call2],
|
|
1313
|
+
reason: "the worker died during two supplier calls"
|
|
1314
|
+
});
|
|
1315
|
+
yield* ledger.markUnknown(marking);
|
|
1316
|
+
yield* ledger.markUnknown(marking);
|
|
1317
|
+
yield* ensure((yield* expectSome("lookup after marking unknown", yield* lookupById(admitted.submissionId))).state === "unknown", "Marking unknown must be idempotent and block the lane");
|
|
1318
|
+
yield* advancePastLease(claim.leaseExpiresAt);
|
|
1319
|
+
yield* ensure(Option.isNone(yield* claimLane(threadId, PRODUCER_B)), "An unknown lane must consume no worker permit");
|
|
1320
|
+
const first = yield* ledger.recordUnknownResolution(UnknownResolutionCommand.make({
|
|
1321
|
+
submissionId: admitted.submissionId,
|
|
1322
|
+
toolCallId: call1,
|
|
1323
|
+
author: "conformance-operator",
|
|
1324
|
+
reason: "supplier store shows no booking",
|
|
1325
|
+
resolution: ResolutionNeverHappened.make()
|
|
1326
|
+
}));
|
|
1327
|
+
yield* ensure((yield* expectSome("lookup after a partial resolution", yield* lookupById(admitted.submissionId))).state === "unknown" && Option.isNone(yield* claimLane(threadId, PRODUCER_B)), "A partial resolution must keep the lane blocked while calls remain open");
|
|
1328
|
+
yield* TestClock.adjust("1 second");
|
|
1329
|
+
const divergent = yield* expectFailure("re-resolving the same call divergently", ledger.recordUnknownResolution(UnknownResolutionCommand.make({
|
|
1330
|
+
submissionId: admitted.submissionId,
|
|
1331
|
+
toolCallId: call1,
|
|
1332
|
+
author: "conformance-operator",
|
|
1333
|
+
reason: "changed my mind",
|
|
1334
|
+
resolution: ResolutionSafeToRetry.make()
|
|
1335
|
+
})));
|
|
1336
|
+
yield* ensure(isUnknownResolutionConflict(divergent) && divergent.toolCallId === call1, "A divergent re-resolution must conflict");
|
|
1337
|
+
const replayed = yield* ledger.recordUnknownResolution(UnknownResolutionCommand.make({
|
|
1338
|
+
submissionId: admitted.submissionId,
|
|
1339
|
+
toolCallId: call1,
|
|
1340
|
+
author: "conformance-operator-second",
|
|
1341
|
+
reason: "a different reason text",
|
|
1342
|
+
resolution: ResolutionNeverHappened.make()
|
|
1343
|
+
}));
|
|
1344
|
+
yield* ensure(sameInstant(replayed.resolvedAt, first.resolvedAt) && replayed.reason === first.reason, "Repeating the same resolution must replay the recorded intent unchanged");
|
|
1345
|
+
yield* ledger.markUnknown(MarkUnknownRequest.make({
|
|
1346
|
+
submissionId: admitted.submissionId,
|
|
1347
|
+
toolCallIds: [call3],
|
|
1348
|
+
reason: "a second wave of uncertainty"
|
|
1349
|
+
}));
|
|
1350
|
+
yield* ledger.recordUnknownResolution(UnknownResolutionCommand.make({
|
|
1351
|
+
submissionId: admitted.submissionId,
|
|
1352
|
+
toolCallId: call2,
|
|
1353
|
+
author: "conformance-operator",
|
|
1354
|
+
reason: "supplier confirmed the booking",
|
|
1355
|
+
resolution: ResolutionCompletedWithResult.make({
|
|
1356
|
+
result: { bookingRef: "booking-1" },
|
|
1357
|
+
isFailure: false
|
|
1358
|
+
})
|
|
1359
|
+
}));
|
|
1360
|
+
yield* ensure((yield* expectSome("lookup while the extended set stays open", yield* lookupById(admitted.submissionId))).state === "unknown" && Option.isNone(yield* claimLane(threadId, PRODUCER_B)), "The lane must stay blocked while any marked call lacks a resolution");
|
|
1361
|
+
yield* ledger.recordUnknownResolution(UnknownResolutionCommand.make({
|
|
1362
|
+
submissionId: admitted.submissionId,
|
|
1363
|
+
toolCallId: call3,
|
|
1364
|
+
author: "conformance-operator",
|
|
1365
|
+
reason: "idempotency key covers a repeat",
|
|
1366
|
+
resolution: ResolutionSafeToRetry.make()
|
|
1367
|
+
}));
|
|
1368
|
+
yield* ensure((yield* expectSome("lookup after the covering resolution", yield* lookupById(admitted.submissionId))).state === "input-applied", "Covering every marked call must reopen the lane as input-applied");
|
|
1369
|
+
yield* ensure((yield* recoverySnapshot(admitted.submissionId)).unknownResolutions.length === 3, "The recovery snapshot must expose every recorded resolution intent");
|
|
1370
|
+
const reclaim = yield* expectSome("the claim after the lane reopened", yield* claimLane(threadId, PRODUCER_B));
|
|
1371
|
+
yield* ensure(reclaim.submissionId === admitted.submissionId && reclaim.producerEpoch > claim.producerEpoch, "The reopened lane must grant a fresh fenced Attempt");
|
|
1372
|
+
yield* settleClaimed(admitted, reclaim.ownershipToken);
|
|
1373
|
+
const late = yield* expectFailure("resolving an unknown call for a settled Submission", ledger.recordUnknownResolution(UnknownResolutionCommand.make({
|
|
1374
|
+
submissionId: admitted.submissionId,
|
|
1375
|
+
toolCallId: decodeToolCallId("call-unknown-late"),
|
|
1376
|
+
author: "conformance-operator",
|
|
1377
|
+
reason: "too late",
|
|
1378
|
+
resolution: ResolutionNeverHappened.make()
|
|
1379
|
+
})));
|
|
1380
|
+
yield* ensure(isSettlementConflict(late), "A resolution must never land on a settled Submission");
|
|
1381
|
+
}));
|
|
1382
|
+
const suspendResumesImmediatelyWhenDecided = conformanceCase("suspend with an already-present decision resumes immediately", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1383
|
+
const threadId = decodeThreadId("ledger-conformance-suspend");
|
|
1384
|
+
const ledger = yield* SubmissionLedger;
|
|
1385
|
+
const admitted = yield* admitReady(threadId, "suspend-key-1", { work: "suspend" });
|
|
1386
|
+
const claim = yield* expectSome("the claim before suspension", yield* claimLane(threadId, PRODUCER_A));
|
|
1387
|
+
const callA = decodeToolCallId("call-suspend-a");
|
|
1388
|
+
const callB = decodeToolCallId("call-suspend-b");
|
|
1389
|
+
const foreign = yield* expectFailure("suspending without owning the lane", ledger.suspend(SuspendRequest.make({
|
|
1390
|
+
submissionId: admitted.submissionId,
|
|
1391
|
+
ownershipToken: BOGUS_TOKEN,
|
|
1392
|
+
reason: ApprovalPendingSuspension.make({ toolCallIds: [callA] })
|
|
1393
|
+
})));
|
|
1394
|
+
yield* ensure(isOwnershipLost(foreign), "suspend must be fenced by the owning token");
|
|
1395
|
+
yield* ledger.recordApprovalDecision(ApprovalDecisionCommand.make({
|
|
1396
|
+
submissionId: admitted.submissionId,
|
|
1397
|
+
toolCallId: callA,
|
|
1398
|
+
decision: "approved",
|
|
1399
|
+
resolver: "conformance-approver",
|
|
1400
|
+
reason: "decided before the suspend committed"
|
|
1401
|
+
}));
|
|
1402
|
+
yield* ensure((yield* ledger.suspend(SuspendRequest.make({
|
|
1403
|
+
submissionId: admitted.submissionId,
|
|
1404
|
+
ownershipToken: claim.ownershipToken,
|
|
1405
|
+
reason: ApprovalPendingSuspension.make({ toolCallIds: [callA] })
|
|
1406
|
+
}))) === "resume-immediately", "A fully-decided suspension reason must resume immediately");
|
|
1407
|
+
yield* ensure((yield* expectSome("lookup after the immediate resume", yield* lookupById(admitted.submissionId))).state === "running", "An immediate resume must not transition the Submission to suspended");
|
|
1408
|
+
yield* ledger.renewOwnership(RenewOwnershipRequest.make({
|
|
1409
|
+
submissionId: admitted.submissionId,
|
|
1410
|
+
ownershipToken: claim.ownershipToken
|
|
1411
|
+
}));
|
|
1412
|
+
yield* ensure((yield* ledger.suspend(SuspendRequest.make({
|
|
1413
|
+
submissionId: admitted.submissionId,
|
|
1414
|
+
ownershipToken: claim.ownershipToken,
|
|
1415
|
+
reason: ApprovalPendingSuspension.make({ toolCallIds: [callA, callB] })
|
|
1416
|
+
}))) === "suspended", "An undecided call in the reason must suspend durably");
|
|
1417
|
+
yield* ensure((yield* expectSome("lookup after the suspension", yield* lookupById(admitted.submissionId))).state === "suspended", "The suspension must be durable");
|
|
1418
|
+
const suspendedSnapshot = yield* recoverySnapshot(admitted.submissionId);
|
|
1419
|
+
yield* ensure(suspendedSnapshot.suspension !== void 0 && suspendedSnapshot.suspension.reason._tag === "ApprovalPending" && suspendedSnapshot.suspension.reason.toolCallIds.length === 2 && suspendedSnapshot.approvalDecisions.length === 1, "The recovery snapshot must expose the suspension reason and prior decisions");
|
|
1420
|
+
const ended = yield* expectFailure("renewing after the suspension ended the ownership period", ledger.renewOwnership(RenewOwnershipRequest.make({
|
|
1421
|
+
submissionId: admitted.submissionId,
|
|
1422
|
+
ownershipToken: claim.ownershipToken
|
|
1423
|
+
})));
|
|
1424
|
+
yield* ensure(isOwnershipLost(ended), "Suspension must end the ownership period without settling");
|
|
1425
|
+
yield* ledger.recordApprovalDecision(ApprovalDecisionCommand.make({
|
|
1426
|
+
submissionId: admitted.submissionId,
|
|
1427
|
+
toolCallId: callB,
|
|
1428
|
+
decision: "denied",
|
|
1429
|
+
resolver: "conformance-approver",
|
|
1430
|
+
reason: "the covering decision"
|
|
1431
|
+
}));
|
|
1432
|
+
yield* ensure((yield* expectSome("lookup after the covering decision", yield* lookupById(admitted.submissionId))).state === "input-applied", "The covering decision must wake the suspended lane");
|
|
1433
|
+
yield* ensure((yield* recoverySnapshot(admitted.submissionId)).suspension === void 0, "Waking must clear the durable suspension");
|
|
1434
|
+
const reclaim = yield* expectSome("the claim after waking", yield* claimLane(threadId, PRODUCER_B));
|
|
1435
|
+
yield* settleClaimed(admitted, reclaim.ownershipToken);
|
|
1436
|
+
const terminal = yield* expectFailure("suspending a settled Submission", ledger.suspend(SuspendRequest.make({
|
|
1437
|
+
submissionId: admitted.submissionId,
|
|
1438
|
+
ownershipToken: reclaim.ownershipToken,
|
|
1439
|
+
reason: ApprovalPendingSuspension.make({ toolCallIds: [callA] })
|
|
1440
|
+
})));
|
|
1441
|
+
yield* ensure(isSettlementConflict(terminal) && terminal.existingOutcome === "completed", "Suspension must never land on a settled Submission");
|
|
1442
|
+
}));
|
|
1443
|
+
const joinedSettlementLinkageAuthority = conformanceCase("a joined Submission's settlement reservation is authorized by host linkage", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1444
|
+
const threadId = decodeThreadId("ledger-conformance-joined-settlement");
|
|
1445
|
+
const ledger = yield* SubmissionLedger;
|
|
1446
|
+
const host = yield* admitReady(threadId, "joined-settle-host", { work: "host" });
|
|
1447
|
+
const queued = yield* admitReady(threadId, "joined-settle-2", { queued: 2 });
|
|
1448
|
+
const hostClaim = yield* expectSome("the host claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1449
|
+
const claims = yield* ledger.claimJoining(ClaimJoiningRequest.make({
|
|
1450
|
+
threadId,
|
|
1451
|
+
hostSubmissionId: host.submissionId,
|
|
1452
|
+
ownershipToken: hostClaim.ownershipToken,
|
|
1453
|
+
maxCount: 1
|
|
1454
|
+
}));
|
|
1455
|
+
yield* ensure(claims.length === 1 && claims[0]?.submissionId === queued.submissionId, "The queued Submission must join the host's contiguous prefix");
|
|
1456
|
+
const joiningReservation = yield* settlementReservation({
|
|
1457
|
+
submissionId: queued.submissionId,
|
|
1458
|
+
ownershipToken: BOGUS_TOKEN,
|
|
1459
|
+
receiptId: queued.receiptId,
|
|
1460
|
+
outcome: "completed"
|
|
1461
|
+
});
|
|
1462
|
+
const fenced = yield* expectFailure("reserving a joining Submission's settlement without lane ownership", ledger.reserveSettlement(joiningReservation));
|
|
1463
|
+
yield* ensure(isOwnershipLost(fenced), "A joining Submission's settlement reservation must stay ownership-fenced");
|
|
1464
|
+
yield* ledger.markJoined(MarkJoinedRequest.make({
|
|
1465
|
+
submissionId: queued.submissionId,
|
|
1466
|
+
ownershipToken: hostClaim.ownershipToken,
|
|
1467
|
+
recordId: submissionInputRecordId(queued.submissionId),
|
|
1468
|
+
sequence: decodeSequence(7)
|
|
1469
|
+
}));
|
|
1470
|
+
const joinedReservation = yield* settlementReservation({
|
|
1471
|
+
submissionId: queued.submissionId,
|
|
1472
|
+
ownershipToken: BOGUS_TOKEN,
|
|
1473
|
+
receiptId: queued.receiptId,
|
|
1474
|
+
outcome: "completed"
|
|
1475
|
+
});
|
|
1476
|
+
yield* ledger.reserveSettlement(joinedReservation);
|
|
1477
|
+
yield* ensure((yield* ledger.finalizeSettlement(SettlementFinalization.make({
|
|
1478
|
+
submissionId: queued.submissionId,
|
|
1479
|
+
settlementId: submissionSettlementId(queued.submissionId)
|
|
1480
|
+
}))).outcome === "completed", "The joined settlement must finalize with the reserved outcome");
|
|
1481
|
+
const settled = yield* expectSome("lookup after the joined settlement", yield* lookupById(queued.submissionId));
|
|
1482
|
+
yield* ensure(settled.state === "settled" && settled.settledOutcome === "completed", "The joined Submission must settle terminally");
|
|
1483
|
+
}));
|
|
1484
|
+
const childReservationIdempotency = conformanceCase("replays an identical child reservation and rejects a divergent allocation digest", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1485
|
+
const threadId = decodeThreadId("ledger-conformance-child-reserve");
|
|
1486
|
+
const ledger = yield* SubmissionLedger;
|
|
1487
|
+
const parent = yield* admitReady(threadId, "child-reserve-parent", { work: "parent" });
|
|
1488
|
+
const claim = yield* expectSome("the parent claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1489
|
+
const allocation = {
|
|
1490
|
+
turns: 4,
|
|
1491
|
+
toolCalls: 8
|
|
1492
|
+
};
|
|
1493
|
+
const allocationDigest = yield* digestJson(allocation);
|
|
1494
|
+
const requestFields = {
|
|
1495
|
+
reservationId: decodeChildReservationId("child-reservation:run-reserve:call-1"),
|
|
1496
|
+
parentSubmissionId: parent.submissionId,
|
|
1497
|
+
parentToolCallId: decodeToolCallId("call-child-reserve"),
|
|
1498
|
+
ownershipToken: claim.ownershipToken,
|
|
1499
|
+
allocation,
|
|
1500
|
+
allocationDigest
|
|
1501
|
+
};
|
|
1502
|
+
const request = ChildBudgetReservationRequest.make(requestFields);
|
|
1503
|
+
const first = yield* ledger.reserveChildBudget(request);
|
|
1504
|
+
yield* ensure(!first.replayed && first.reservation.status === "reserved" && first.reservation.allocationDigest === allocationDigest && first.reservation.childSubmissionId === void 0 && persistedJsonEquivalent(first.reservation.allocation, allocation), "The first reservation must create a reserved row carrying the exact allocation");
|
|
1505
|
+
yield* TestClock.adjust("1 second");
|
|
1506
|
+
const replayed = yield* ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1507
|
+
...requestFields,
|
|
1508
|
+
allocation: {
|
|
1509
|
+
toolCalls: 8,
|
|
1510
|
+
turns: 4
|
|
1511
|
+
}
|
|
1512
|
+
}));
|
|
1513
|
+
yield* ensure(replayed.replayed && replayed.reservation.status === "reserved" && persistedJsonEquivalent(replayed.reservation.allocation, allocation) && sameInstant(replayed.reservation.reservedAt, first.reservation.reservedAt), "An identical reservation must replay the stored row unchanged");
|
|
1514
|
+
const divergentAllocation = {
|
|
1515
|
+
turns: 64,
|
|
1516
|
+
toolCalls: 8
|
|
1517
|
+
};
|
|
1518
|
+
const divergent = yield* expectFailure("a divergent allocation for the same reservation id", ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1519
|
+
...requestFields,
|
|
1520
|
+
allocation: divergentAllocation,
|
|
1521
|
+
allocationDigest: yield* digestJson(divergentAllocation)
|
|
1522
|
+
})));
|
|
1523
|
+
yield* ensure(isChildReservationConflict(divergent) && divergent.status === "reserved", "A divergent allocation must conflict with the recorded reservation");
|
|
1524
|
+
const secondId = yield* expectFailure("a second reservation id for the same parent Tool Call", ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1525
|
+
...requestFields,
|
|
1526
|
+
reservationId: decodeChildReservationId("child-reservation:run-reserve:call-1-other")
|
|
1527
|
+
})));
|
|
1528
|
+
yield* ensure(isChildReservationConflict(secondId), "One parent Tool Call must never own two reservations");
|
|
1529
|
+
const snapshot = yield* recoverySnapshot(parent.submissionId);
|
|
1530
|
+
yield* ensure(snapshot.childReservations.length === 1 && snapshot.childReservations[0].reservationId === request.reservationId && snapshot.childReservations[0].status === "reserved" && snapshot.childAttachments.length === 0, "The parent recovery snapshot must expose the reservation before any attachment");
|
|
1531
|
+
}));
|
|
1532
|
+
const childReservationFencing = conformanceCase("a stale parent token cannot transition a child reservation", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1533
|
+
const threadId = decodeThreadId("ledger-conformance-child-fence");
|
|
1534
|
+
const childLane = decodeThreadId("ledger-conformance-child-fence-child");
|
|
1535
|
+
const ledger = yield* SubmissionLedger;
|
|
1536
|
+
const parent = yield* admitReady(threadId, "child-fence-parent", { work: "parent" });
|
|
1537
|
+
const child = yield* admitReady(childLane, "child-fence-child", { work: "child" });
|
|
1538
|
+
const firstClaim = yield* expectSome("the first parent claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1539
|
+
const allocation = { turns: 2 };
|
|
1540
|
+
const allocationDigest = yield* digestJson(allocation);
|
|
1541
|
+
const requestFields = {
|
|
1542
|
+
reservationId: decodeChildReservationId("child-reservation:run-fence:call-1"),
|
|
1543
|
+
parentSubmissionId: parent.submissionId,
|
|
1544
|
+
parentToolCallId: decodeToolCallId("call-child-fence"),
|
|
1545
|
+
ownershipToken: BOGUS_TOKEN,
|
|
1546
|
+
allocation,
|
|
1547
|
+
allocationDigest
|
|
1548
|
+
};
|
|
1549
|
+
const request = ChildBudgetReservationRequest.make(requestFields);
|
|
1550
|
+
const foreign = yield* expectFailure("creating a reservation without owning the parent lane", ledger.reserveChildBudget(request));
|
|
1551
|
+
yield* ensure(isOwnershipLost(foreign), "Reservation creation must be fenced by the parent's live ownership token");
|
|
1552
|
+
yield* ensure((yield* recoverySnapshot(parent.submissionId)).childReservations.length === 0, "A fenced reservation attempt must leave no row behind");
|
|
1553
|
+
yield* ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1554
|
+
...requestFields,
|
|
1555
|
+
ownershipToken: firstClaim.ownershipToken
|
|
1556
|
+
}));
|
|
1557
|
+
yield* ledger.releaseOwnership(ReleaseOwnershipRequest.make({
|
|
1558
|
+
submissionId: parent.submissionId,
|
|
1559
|
+
ownershipToken: firstClaim.ownershipToken
|
|
1560
|
+
}));
|
|
1561
|
+
const reclaim = yield* expectSome("the replacement parent claim", yield* claimLane(threadId, PRODUCER_B));
|
|
1562
|
+
yield* ensure(reclaim.producerEpoch > firstClaim.producerEpoch, "The replacement claim must fence the stale parent Attempt");
|
|
1563
|
+
const staleAttach = yield* expectFailure("attaching a child with the superseded parent token", ledger.attachChildToReservation(AttachChildToReservationRequest.make({
|
|
1564
|
+
reservationId: request.reservationId,
|
|
1565
|
+
ownershipToken: firstClaim.ownershipToken,
|
|
1566
|
+
childSubmissionId: child.submissionId
|
|
1567
|
+
})));
|
|
1568
|
+
yield* ensure(isOwnershipLost(staleAttach), "A superseded parent token must not attach a child");
|
|
1569
|
+
const staleCreate = yield* expectFailure("creating a second-call reservation with the superseded parent token", ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1570
|
+
...requestFields,
|
|
1571
|
+
reservationId: decodeChildReservationId("child-reservation:run-fence:call-2"),
|
|
1572
|
+
parentToolCallId: decodeToolCallId("call-child-fence-second"),
|
|
1573
|
+
ownershipToken: firstClaim.ownershipToken
|
|
1574
|
+
})));
|
|
1575
|
+
yield* ensure(isOwnershipLost(staleCreate), "A superseded parent token must not create new reservation state");
|
|
1576
|
+
const staleReplay = yield* ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1577
|
+
...requestFields,
|
|
1578
|
+
ownershipToken: firstClaim.ownershipToken
|
|
1579
|
+
}));
|
|
1580
|
+
yield* ensure(staleReplay.replayed && staleReplay.reservation.status === "reserved", "An identical reservation replay must return the recorded row even from a stale caller");
|
|
1581
|
+
yield* ensure((yield* ledger.attachChildToReservation(AttachChildToReservationRequest.make({
|
|
1582
|
+
reservationId: request.reservationId,
|
|
1583
|
+
ownershipToken: reclaim.ownershipToken,
|
|
1584
|
+
childSubmissionId: child.submissionId
|
|
1585
|
+
}))).childSubmissionId === child.submissionId, "The live replacement token must attach the child");
|
|
1586
|
+
}));
|
|
1587
|
+
const attachChildIdempotency = conformanceCase("attachChildToReservation is idempotent and rejects a divergent child", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1588
|
+
const threadId = decodeThreadId("ledger-conformance-child-attach");
|
|
1589
|
+
const childLane = decodeThreadId("ledger-conformance-child-attach-child");
|
|
1590
|
+
const ledger = yield* SubmissionLedger;
|
|
1591
|
+
const parent = yield* admitReady(threadId, "child-attach-parent", { work: "parent" });
|
|
1592
|
+
const child = yield* admitReady(childLane, "child-attach-child", { queued: 1 });
|
|
1593
|
+
const otherChild = yield* admitReady(childLane, "child-attach-other", { queued: 2 });
|
|
1594
|
+
const claim = yield* expectSome("the parent claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1595
|
+
const allocation = { turns: 3 };
|
|
1596
|
+
const reservationId = decodeChildReservationId("child-reservation:run-attach:call-1");
|
|
1597
|
+
yield* ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1598
|
+
reservationId,
|
|
1599
|
+
parentSubmissionId: parent.submissionId,
|
|
1600
|
+
parentToolCallId: decodeToolCallId("call-child-attach"),
|
|
1601
|
+
ownershipToken: claim.ownershipToken,
|
|
1602
|
+
allocation,
|
|
1603
|
+
allocationDigest: yield* digestJson(allocation)
|
|
1604
|
+
}));
|
|
1605
|
+
const unknownReservation = yield* expectFailure("attaching to a reservation that was never created", ledger.attachChildToReservation(AttachChildToReservationRequest.make({
|
|
1606
|
+
reservationId: decodeChildReservationId("child-reservation:run-attach:missing"),
|
|
1607
|
+
ownershipToken: claim.ownershipToken,
|
|
1608
|
+
childSubmissionId: child.submissionId
|
|
1609
|
+
})));
|
|
1610
|
+
yield* ensure(isLedgerError(unknownReservation), "Attaching to an unknown reservation must fail as a ledger error");
|
|
1611
|
+
const attached = yield* ledger.attachChildToReservation(AttachChildToReservationRequest.make({
|
|
1612
|
+
reservationId,
|
|
1613
|
+
ownershipToken: claim.ownershipToken,
|
|
1614
|
+
childSubmissionId: child.submissionId
|
|
1615
|
+
}));
|
|
1616
|
+
yield* ensure(attached.childSubmissionId === child.submissionId && attached.status === "reserved", "Attaching must record the child on the reservation row");
|
|
1617
|
+
yield* ensure((yield* ledger.attachChildToReservation(AttachChildToReservationRequest.make({
|
|
1618
|
+
reservationId,
|
|
1619
|
+
ownershipToken: claim.ownershipToken,
|
|
1620
|
+
childSubmissionId: child.submissionId
|
|
1621
|
+
}))).childSubmissionId === child.submissionId, "Repeating the identical attachment must be a no-op");
|
|
1622
|
+
const divergent = yield* expectFailure("attaching a different child to the same reservation", ledger.attachChildToReservation(AttachChildToReservationRequest.make({
|
|
1623
|
+
reservationId,
|
|
1624
|
+
ownershipToken: claim.ownershipToken,
|
|
1625
|
+
childSubmissionId: otherChild.submissionId
|
|
1626
|
+
})));
|
|
1627
|
+
yield* ensure(isChildReservationConflict(divergent), "A divergent child attachment must conflict with the recorded child");
|
|
1628
|
+
const snapshot = yield* recoverySnapshot(parent.submissionId);
|
|
1629
|
+
yield* ensure(snapshot.childAttachments.length === 1 && snapshot.childAttachments[0].childSubmissionId === child.submissionId && snapshot.childAttachments[0].toolCallId === decodeToolCallId("call-child-attach") && snapshot.childAttachments[0].childState === "ready" && snapshot.childAttachments[0].childOutcome === void 0, "The parent recovery snapshot must expose the attachment with the child's lane state");
|
|
1630
|
+
}));
|
|
1631
|
+
const beginReleaseFreezesAccountingOnce = conformanceCase("beginRelease freezes the accounting decision exactly once", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1632
|
+
const threadId = decodeThreadId("ledger-conformance-child-freeze");
|
|
1633
|
+
const ledger = yield* SubmissionLedger;
|
|
1634
|
+
const parent = yield* admitReady(threadId, "child-freeze-parent", { work: "parent" });
|
|
1635
|
+
const claim = yield* expectSome("the parent claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1636
|
+
const allocation = { turns: 4 };
|
|
1637
|
+
const reservationId = decodeChildReservationId("child-reservation:run-freeze:call-1");
|
|
1638
|
+
yield* ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1639
|
+
reservationId,
|
|
1640
|
+
parentSubmissionId: parent.submissionId,
|
|
1641
|
+
parentToolCallId: decodeToolCallId("call-child-freeze"),
|
|
1642
|
+
ownershipToken: claim.ownershipToken,
|
|
1643
|
+
allocation,
|
|
1644
|
+
allocationDigest: yield* digestJson(allocation)
|
|
1645
|
+
}));
|
|
1646
|
+
const accounting = {
|
|
1647
|
+
consumed: { turns: 1 },
|
|
1648
|
+
released: { turns: 3 }
|
|
1649
|
+
};
|
|
1650
|
+
const frozen = yield* ledger.beginChildBudgetRelease(BeginChildBudgetReleaseRequest.make({
|
|
1651
|
+
reservationId,
|
|
1652
|
+
accounting
|
|
1653
|
+
}));
|
|
1654
|
+
yield* ensure(frozen.status === "releasePending" && frozen.accounting !== void 0 && persistedJsonEquivalent(frozen.accounting, accounting) && frozen.releaseBeganAt !== void 0, "The first beginRelease must freeze the accounting and move to releasePending");
|
|
1655
|
+
yield* TestClock.adjust("1 second");
|
|
1656
|
+
const replayed = yield* ledger.beginChildBudgetRelease(BeginChildBudgetReleaseRequest.make({
|
|
1657
|
+
reservationId,
|
|
1658
|
+
accounting: {
|
|
1659
|
+
released: { turns: 3 },
|
|
1660
|
+
consumed: { turns: 1 }
|
|
1661
|
+
}
|
|
1662
|
+
}));
|
|
1663
|
+
yield* ensure(replayed.status === "releasePending" && replayed.releaseBeganAt !== void 0 && frozen.releaseBeganAt !== void 0 && sameInstant(replayed.releaseBeganAt, frozen.releaseBeganAt), "Replaying the identical accounting must be a no-op with the frozen decision retained");
|
|
1664
|
+
const divergent = yield* expectFailure("freezing a different accounting decision", ledger.beginChildBudgetRelease(BeginChildBudgetReleaseRequest.make({
|
|
1665
|
+
reservationId,
|
|
1666
|
+
accounting: {
|
|
1667
|
+
consumed: { turns: 4 },
|
|
1668
|
+
released: { turns: 0 }
|
|
1669
|
+
}
|
|
1670
|
+
})));
|
|
1671
|
+
yield* ensure(isChildReservationConflict(divergent) && divergent.status === "releasePending", "A divergent accounting freeze must conflict with the frozen decision");
|
|
1672
|
+
yield* ledger.releaseChildBudget(ReleaseChildBudgetRequest.make({ reservationId }));
|
|
1673
|
+
const afterRelease = yield* ledger.beginChildBudgetRelease(BeginChildBudgetReleaseRequest.make({
|
|
1674
|
+
reservationId,
|
|
1675
|
+
accounting
|
|
1676
|
+
}));
|
|
1677
|
+
yield* ensure(afterRelease.status === "released" && afterRelease.accounting !== void 0 && persistedJsonEquivalent(afterRelease.accounting, accounting), "Replaying the identical accounting after release must return the released row");
|
|
1678
|
+
const divergentAfterRelease = yield* expectFailure("freezing a different accounting decision after release", ledger.beginChildBudgetRelease(BeginChildBudgetReleaseRequest.make({
|
|
1679
|
+
reservationId,
|
|
1680
|
+
accounting: {
|
|
1681
|
+
consumed: { turns: 2 },
|
|
1682
|
+
released: { turns: 2 }
|
|
1683
|
+
}
|
|
1684
|
+
})));
|
|
1685
|
+
yield* ensure(isChildReservationConflict(divergentAfterRelease) && divergentAfterRelease.status === "released", "The frozen decision must stay immutable after release");
|
|
1686
|
+
}));
|
|
1687
|
+
const releaseAppliedExactlyOnce = conformanceCase("release returns unused allocation exactly once", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1688
|
+
const threadId = decodeThreadId("ledger-conformance-child-release");
|
|
1689
|
+
const ledger = yield* SubmissionLedger;
|
|
1690
|
+
const parent = yield* admitReady(threadId, "child-release-parent", { work: "parent" });
|
|
1691
|
+
const claim = yield* expectSome("the parent claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1692
|
+
const allocation = { turns: 4 };
|
|
1693
|
+
const reservationId = decodeChildReservationId("child-reservation:run-release:call-1");
|
|
1694
|
+
yield* ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1695
|
+
reservationId,
|
|
1696
|
+
parentSubmissionId: parent.submissionId,
|
|
1697
|
+
parentToolCallId: decodeToolCallId("call-child-release"),
|
|
1698
|
+
ownershipToken: claim.ownershipToken,
|
|
1699
|
+
allocation,
|
|
1700
|
+
allocationDigest: yield* digestJson(allocation)
|
|
1701
|
+
}));
|
|
1702
|
+
const early = yield* expectFailure("releasing before the accounting decision is frozen", ledger.releaseChildBudget(ReleaseChildBudgetRequest.make({ reservationId })));
|
|
1703
|
+
yield* ensure(isChildReservationConflict(early) && early.status === "reserved", "Release must never skip the releasePending freeze");
|
|
1704
|
+
yield* ledger.beginChildBudgetRelease(BeginChildBudgetReleaseRequest.make({
|
|
1705
|
+
reservationId,
|
|
1706
|
+
accounting: {
|
|
1707
|
+
consumed: {},
|
|
1708
|
+
released: { turns: 4 }
|
|
1709
|
+
}
|
|
1710
|
+
}));
|
|
1711
|
+
const released = yield* ledger.releaseChildBudget(ReleaseChildBudgetRequest.make({ reservationId }));
|
|
1712
|
+
yield* ensure(released.status === "released" && released.releasedAt !== void 0, "Release must transition releasePending to released");
|
|
1713
|
+
yield* TestClock.adjust("1 second");
|
|
1714
|
+
const replayed = yield* ledger.releaseChildBudget(ReleaseChildBudgetRequest.make({ reservationId }));
|
|
1715
|
+
yield* ensure(replayed.status === "released" && replayed.releasedAt !== void 0 && released.releasedAt !== void 0 && sameInstant(replayed.releasedAt, released.releasedAt), "Replaying the release must return the stored row unchanged — never applied twice");
|
|
1716
|
+
const snapshot = yield* recoverySnapshot(parent.submissionId);
|
|
1717
|
+
yield* ensure(snapshot.childReservations.length === 1 && snapshot.childReservations[0].status === "released", "The recovery snapshot must expose the released reservation");
|
|
1718
|
+
}));
|
|
1719
|
+
const recordChildSettledWake = conformanceCase("recordChildSettled accepts canonical terminalizing prefixes and wakes only when all are covered", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1720
|
+
const parentLane = decodeThreadId("ledger-conformance-child-wake");
|
|
1721
|
+
const childLaneA = decodeThreadId("ledger-conformance-child-wake-a");
|
|
1722
|
+
const childLaneB = decodeThreadId("ledger-conformance-child-wake-b");
|
|
1723
|
+
const ledger = yield* SubmissionLedger;
|
|
1724
|
+
const parent = yield* admitReady(parentLane, "child-wake-parent", { work: "parent" });
|
|
1725
|
+
const childA = yield* admitReady(childLaneA, "child-wake-a", { child: "a" });
|
|
1726
|
+
const childB = yield* admitReady(childLaneB, "child-wake-b", { child: "b" });
|
|
1727
|
+
const parentClaim = yield* expectSome("the parent claim", yield* claimLane(parentLane, PRODUCER_A));
|
|
1728
|
+
yield* ensure((yield* ledger.suspend(SuspendRequest.make({
|
|
1729
|
+
submissionId: parent.submissionId,
|
|
1730
|
+
ownershipToken: parentClaim.ownershipToken,
|
|
1731
|
+
reason: WaitingForChildSuspension.make({ children: [WaitingChild.make({
|
|
1732
|
+
toolCallId: decodeToolCallId("call-wake-a"),
|
|
1733
|
+
childSubmissionId: childA.submissionId
|
|
1734
|
+
}), WaitingChild.make({
|
|
1735
|
+
toolCallId: decodeToolCallId("call-wake-b"),
|
|
1736
|
+
childSubmissionId: childB.submissionId
|
|
1737
|
+
})] })
|
|
1738
|
+
}))) === "suspended", "Unsettled children must suspend the parent durably");
|
|
1739
|
+
const ended = yield* expectFailure("renewing after the waitingForChild suspension ended the ownership period", ledger.renewOwnership(RenewOwnershipRequest.make({
|
|
1740
|
+
submissionId: parent.submissionId,
|
|
1741
|
+
ownershipToken: parentClaim.ownershipToken
|
|
1742
|
+
})));
|
|
1743
|
+
yield* ensure(isOwnershipLost(ended), "waitingForChild must end the ownership period without settling (SUB-030)");
|
|
1744
|
+
yield* ensure(Option.isNone(yield* claimLane(parentLane, PRODUCER_B)), "A waitingForChild head must produce no claim and consume no worker permit");
|
|
1745
|
+
const childClaimA = yield* expectSome("the first child claim", yield* claimLane(childLaneA, PRODUCER_A));
|
|
1746
|
+
const childReservationA = yield* settlementReservation({
|
|
1747
|
+
submissionId: childA.submissionId,
|
|
1748
|
+
ownershipToken: childClaimA.ownershipToken,
|
|
1749
|
+
receiptId: childA.receiptId,
|
|
1750
|
+
outcome: "completed"
|
|
1751
|
+
});
|
|
1752
|
+
yield* ledger.reserveSettlement(childReservationA);
|
|
1753
|
+
yield* ensure((yield* ledger.recordChildSettled(ChildSettledNotification.make({
|
|
1754
|
+
parentSubmissionId: parent.submissionId,
|
|
1755
|
+
childSubmissionId: childA.submissionId
|
|
1756
|
+
}))) === "still-waiting", "A settlement notification must not wake the parent while a listed child is unsettled");
|
|
1757
|
+
yield* ledger.finalizeSettlement(SettlementFinalization.make({
|
|
1758
|
+
submissionId: childA.submissionId,
|
|
1759
|
+
settlementId: childReservationA.settlementId
|
|
1760
|
+
}));
|
|
1761
|
+
yield* ensure((yield* expectSome("lookup while one child is outstanding", yield* lookupById(parent.submissionId))).state === "suspended" && Option.isNone(yield* claimLane(parentLane, PRODUCER_B)), "The parent lane must stay suspended until every listed child settled");
|
|
1762
|
+
const childClaimB = yield* expectSome("the second child claim", yield* claimLane(childLaneB, PRODUCER_A));
|
|
1763
|
+
const childReservationB = yield* settlementReservation({
|
|
1764
|
+
submissionId: childB.submissionId,
|
|
1765
|
+
ownershipToken: childClaimB.ownershipToken,
|
|
1766
|
+
receiptId: childB.receiptId,
|
|
1767
|
+
outcome: "completed"
|
|
1768
|
+
});
|
|
1769
|
+
yield* ledger.reserveSettlement(childReservationB);
|
|
1770
|
+
yield* ensure((yield* ledger.recordChildSettled(ChildSettledNotification.make({
|
|
1771
|
+
parentSubmissionId: parent.submissionId,
|
|
1772
|
+
childSubmissionId: childB.submissionId
|
|
1773
|
+
}))) === "woken", "The covering canonical settlement prefix must wake the parent before child finalization");
|
|
1774
|
+
yield* ledger.finalizeSettlement(SettlementFinalization.make({
|
|
1775
|
+
submissionId: childB.submissionId,
|
|
1776
|
+
settlementId: childReservationB.settlementId
|
|
1777
|
+
}));
|
|
1778
|
+
yield* ensure((yield* expectSome("lookup after the covering settlement", yield* lookupById(parent.submissionId))).state === "input-applied", "The woken parent must transition suspended(WaitingForChild) to input-applied");
|
|
1779
|
+
yield* ensure((yield* recoverySnapshot(parent.submissionId)).suspension === void 0, "Waking must clear the durable suspension");
|
|
1780
|
+
yield* ensure((yield* ledger.recordChildSettled(ChildSettledNotification.make({
|
|
1781
|
+
parentSubmissionId: parent.submissionId,
|
|
1782
|
+
childSubmissionId: childA.submissionId
|
|
1783
|
+
}))) === "not-waiting", "Replaying a notification after the wake must be an idempotent no-op");
|
|
1784
|
+
const reclaim = yield* expectSome("the parent claim after waking", yield* claimLane(parentLane, PRODUCER_B));
|
|
1785
|
+
yield* ensure(reclaim.submissionId === parent.submissionId && reclaim.producerEpoch > parentClaim.producerEpoch, "The woken lane must grant a fresh fenced Attempt");
|
|
1786
|
+
yield* settleClaimed(parent, reclaim.ownershipToken);
|
|
1787
|
+
yield* ensure((yield* ledger.recordChildSettled(ChildSettledNotification.make({
|
|
1788
|
+
parentSubmissionId: parent.submissionId,
|
|
1789
|
+
childSubmissionId: childB.submissionId
|
|
1790
|
+
}))) === "not-waiting", "A notification for a settled parent must answer not-waiting");
|
|
1791
|
+
}));
|
|
1792
|
+
const suspendResumesImmediatelyForSettledChildren = conformanceCase("suspend returns resume-immediately when children already settled", ({ ensure, expectSome }) => Effect.gen(function* () {
|
|
1793
|
+
const parentLane = decodeThreadId("ledger-conformance-child-raced");
|
|
1794
|
+
const childLaneA = decodeThreadId("ledger-conformance-child-raced-a");
|
|
1795
|
+
const childLaneB = decodeThreadId("ledger-conformance-child-raced-b");
|
|
1796
|
+
const ledger = yield* SubmissionLedger;
|
|
1797
|
+
const settledChild = yield* admitReady(childLaneA, "child-raced-a", { child: "a" });
|
|
1798
|
+
const settledClaim = yield* expectSome("the settled child's claim", yield* claimLane(childLaneA, PRODUCER_A));
|
|
1799
|
+
yield* settleClaimed(settledChild, settledClaim.ownershipToken);
|
|
1800
|
+
const pendingChild = yield* admitReady(childLaneB, "child-raced-b", { child: "b" });
|
|
1801
|
+
const parent = yield* admitReady(parentLane, "child-raced-parent", { work: "parent" });
|
|
1802
|
+
const parentClaim = yield* expectSome("the parent claim", yield* claimLane(parentLane, PRODUCER_A));
|
|
1803
|
+
yield* ensure((yield* ledger.suspend(SuspendRequest.make({
|
|
1804
|
+
submissionId: parent.submissionId,
|
|
1805
|
+
ownershipToken: parentClaim.ownershipToken,
|
|
1806
|
+
reason: WaitingForChildSuspension.make({ children: [WaitingChild.make({
|
|
1807
|
+
toolCallId: decodeToolCallId("call-raced-a"),
|
|
1808
|
+
childSubmissionId: settledChild.submissionId
|
|
1809
|
+
})] })
|
|
1810
|
+
}))) === "resume-immediately", "A suspend listing only settled children must resume immediately");
|
|
1811
|
+
yield* ensure((yield* expectSome("lookup after the immediate resume", yield* lookupById(parent.submissionId))).state === "running", "An immediate resume must not transition the parent to suspended");
|
|
1812
|
+
yield* ledger.renewOwnership(RenewOwnershipRequest.make({
|
|
1813
|
+
submissionId: parent.submissionId,
|
|
1814
|
+
ownershipToken: parentClaim.ownershipToken
|
|
1815
|
+
}));
|
|
1816
|
+
yield* ensure((yield* ledger.suspend(SuspendRequest.make({
|
|
1817
|
+
submissionId: parent.submissionId,
|
|
1818
|
+
ownershipToken: parentClaim.ownershipToken,
|
|
1819
|
+
reason: WaitingForChildSuspension.make({ children: [WaitingChild.make({
|
|
1820
|
+
toolCallId: decodeToolCallId("call-raced-a"),
|
|
1821
|
+
childSubmissionId: settledChild.submissionId
|
|
1822
|
+
}), WaitingChild.make({
|
|
1823
|
+
toolCallId: decodeToolCallId("call-raced-b"),
|
|
1824
|
+
childSubmissionId: pendingChild.submissionId
|
|
1825
|
+
})] })
|
|
1826
|
+
}))) === "suspended", "One unsettled listed child must suspend the parent durably");
|
|
1827
|
+
const snapshot = yield* recoverySnapshot(parent.submissionId);
|
|
1828
|
+
yield* ensure(snapshot.suspension !== void 0 && snapshot.suspension.reason._tag === "WaitingForChild" && snapshot.suspension.reason.children.length === 2, "The recovery snapshot must expose the WaitingForChild reason with its children");
|
|
1829
|
+
}));
|
|
1830
|
+
const admissionParentLinkage = conformanceCase("admit records and replays parent linkage", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1831
|
+
const parentLane = decodeThreadId("ledger-conformance-linkage");
|
|
1832
|
+
const childLane = decodeThreadId("ledger-conformance-linkage-child");
|
|
1833
|
+
const ledger = yield* SubmissionLedger;
|
|
1834
|
+
const parent = yield* admitReady(parentLane, "linkage-parent", { work: "parent" });
|
|
1835
|
+
const linkage = ParentLinkage.make({
|
|
1836
|
+
parentSubmissionId: parent.submissionId,
|
|
1837
|
+
parentToolCallId: decodeToolCallId("call-linkage")
|
|
1838
|
+
});
|
|
1839
|
+
const request = yield* admissionRequest(childLane, "linkage-child-key", { task: "research" }, linkage);
|
|
1840
|
+
const admitted = yield* ledger.admit(request);
|
|
1841
|
+
yield* ensure(!admitted.replayed, "The first linked admission must create the child");
|
|
1842
|
+
const byId = yield* expectSome("lookup of the linked child", yield* lookupById(admitted.submissionId));
|
|
1843
|
+
yield* ensure(byId.parentLinkage !== void 0 && byId.parentLinkage.parentSubmissionId === parent.submissionId && byId.parentLinkage.parentToolCallId === linkage.parentToolCallId, "The child snapshot must expose its immutable parent linkage");
|
|
1844
|
+
const childSnapshot = yield* recoverySnapshot(admitted.submissionId);
|
|
1845
|
+
yield* ensure(childSnapshot.parentLinkage !== void 0 && childSnapshot.parentLinkage.parentSubmissionId === parent.submissionId, "The child recovery snapshot must expose its parent linkage");
|
|
1846
|
+
const replayed = yield* ledger.admit(request);
|
|
1847
|
+
yield* ensure(replayed.replayed && replayed.submissionId === admitted.submissionId && replayed.receiptId === admitted.receiptId, "An identical linked admission must replay the original identities (SUB-016)");
|
|
1848
|
+
const divergentLinkage = yield* expectFailure("replaying the admission with a different parent Tool Call", ledger.admit(yield* admissionRequest(childLane, "linkage-child-key", { task: "research" }, ParentLinkage.make({
|
|
1849
|
+
parentSubmissionId: parent.submissionId,
|
|
1850
|
+
parentToolCallId: decodeToolCallId("call-linkage-other")
|
|
1851
|
+
}))));
|
|
1852
|
+
yield* ensure(isAdmissionConflict(divergentLinkage), "A divergent parent linkage must conflict even when the input digest matches");
|
|
1853
|
+
const droppedLinkage = yield* expectFailure("replaying the admission without its parent linkage", ledger.admit(yield* admissionRequest(childLane, "linkage-child-key", { task: "research" })));
|
|
1854
|
+
yield* ensure(isAdmissionConflict(droppedLinkage), "Dropping the recorded linkage on replay must conflict");
|
|
1855
|
+
const plain = yield* ledger.admit(yield* admissionRequest(childLane, "linkage-plain-key", { task: "plain" }));
|
|
1856
|
+
const addedLinkage = yield* expectFailure("replaying an unlinked admission with a parent linkage", ledger.admit(yield* admissionRequest(childLane, "linkage-plain-key", { task: "plain" }, linkage)));
|
|
1857
|
+
yield* ensure(isAdmissionConflict(addedLinkage), "Adding a linkage to an unlinked admission on replay must conflict");
|
|
1858
|
+
yield* ensure((yield* expectSome("lookup of the unlinked Submission", yield* lookupById(plain.submissionId))).parentLinkage === void 0, "An unlinked Submission must expose no parent linkage");
|
|
1859
|
+
}));
|
|
1860
|
+
const resolveAdmissionAuthority = conformanceCase("resolveAdmission distinguishes notAdmitted from admitted authoritatively", ({ ensure, expectSome }) => Effect.gen(function* () {
|
|
1861
|
+
const threadId = decodeThreadId("ledger-conformance-resolve");
|
|
1862
|
+
const ledger = yield* SubmissionLedger;
|
|
1863
|
+
const key = SubmissionLookupByKey.make({
|
|
1864
|
+
threadId,
|
|
1865
|
+
principal: CONFORMANCE_PRINCIPAL,
|
|
1866
|
+
idempotencyKey: decodeIdempotencyKey("resolve-key-1")
|
|
1867
|
+
});
|
|
1868
|
+
yield* ensure((yield* ledger.resolveAdmission(key))._tag === "NotAdmitted", "The authoritative store must prove absence before admission (SUB-031)");
|
|
1869
|
+
const admitted = yield* ledger.admit(yield* admissionRequest(threadId, "resolve-key-1", { work: "resolve" }));
|
|
1870
|
+
const after = yield* ledger.resolveAdmission(key);
|
|
1871
|
+
yield* ensure(after._tag === "Admitted" && after.submission.submissionId === admitted.submissionId && after.submission.receiptId === admitted.receiptId && after.submission.state === "admitted", "An admitted key must resolve to the full authoritative snapshot");
|
|
1872
|
+
yield* ensure((yield* ledger.resolveAdmission(SubmissionLookupByKey.make({
|
|
1873
|
+
threadId,
|
|
1874
|
+
principal: OTHER_PRINCIPAL,
|
|
1875
|
+
idempotencyKey: decodeIdempotencyKey("resolve-key-1")
|
|
1876
|
+
})))._tag === "NotAdmitted", "Admission resolution must stay scoped to the requesting principal");
|
|
1877
|
+
yield* ledger.markReady(MarkReadyRequest.make({ submissionId: admitted.submissionId }));
|
|
1878
|
+
const claim = yield* expectSome("the claim before terminal resolution", yield* claimLane(threadId, PRODUCER_A));
|
|
1879
|
+
yield* settleClaimed(admitted, claim.ownershipToken);
|
|
1880
|
+
const settled = yield* ledger.resolveAdmission(key);
|
|
1881
|
+
yield* ensure(settled._tag === "Admitted" && settled.submission.state === "settled" && settled.submission.settledOutcome === "completed", "A settled Submission still resolves as admitted — terminality never becomes absence");
|
|
1882
|
+
}));
|
|
1883
|
+
const queuedAbortSettlementAuthority = conformanceCase("reserveSettlement authorizes an aborted, unowned queued settlement by its durable intent", ({ ensure, expectFailure }) => Effect.gen(function* () {
|
|
1884
|
+
const threadId = decodeThreadId("ledger-conformance-queued-abort");
|
|
1885
|
+
const ledger = yield* SubmissionLedger;
|
|
1886
|
+
const head = yield* admitReady(threadId, "queued-abort-head", { work: "head" });
|
|
1887
|
+
const second = yield* admitReady(threadId, "queued-abort-2", { queued: 2 });
|
|
1888
|
+
const third = yield* admitReady(threadId, "queued-abort-3", { queued: 3 });
|
|
1889
|
+
yield* ledger.requestAbort(AbortCommand.make({
|
|
1890
|
+
submissionId: second.submissionId,
|
|
1891
|
+
author: "operator",
|
|
1892
|
+
reason: "cancelled while queued"
|
|
1893
|
+
}));
|
|
1894
|
+
const unaborted = yield* expectFailure("reserving an aborted settlement for a queued row without an abort intent", settlementReservation({
|
|
1895
|
+
submissionId: third.submissionId,
|
|
1896
|
+
ownershipToken: BOGUS_TOKEN,
|
|
1897
|
+
receiptId: third.receiptId,
|
|
1898
|
+
outcome: "aborted"
|
|
1899
|
+
}).pipe(Effect.flatMap((reservation) => ledger.reserveSettlement(reservation))));
|
|
1900
|
+
yield* ensure(isOwnershipLost(unaborted), "A queued reservation without a durable abort intent must stay fenced (OwnershipLost)");
|
|
1901
|
+
const wrongOutcome = yield* expectFailure("reserving a completed settlement for the aborted queued row", settlementReservation({
|
|
1902
|
+
submissionId: second.submissionId,
|
|
1903
|
+
ownershipToken: BOGUS_TOKEN,
|
|
1904
|
+
receiptId: second.receiptId,
|
|
1905
|
+
outcome: "completed",
|
|
1906
|
+
result: { fabricated: true }
|
|
1907
|
+
}).pipe(Effect.flatMap((reservation) => ledger.reserveSettlement(reservation))));
|
|
1908
|
+
yield* ensure(isOwnershipLost(wrongOutcome), "An abort intent must never authorize a non-aborted settlement outcome");
|
|
1909
|
+
const reservation = yield* settlementReservation({
|
|
1910
|
+
submissionId: second.submissionId,
|
|
1911
|
+
ownershipToken: BOGUS_TOKEN,
|
|
1912
|
+
receiptId: second.receiptId,
|
|
1913
|
+
outcome: "aborted"
|
|
1914
|
+
});
|
|
1915
|
+
const reserved = yield* ledger.reserveSettlement(reservation);
|
|
1916
|
+
yield* ensure(reserved.replayed === false && reserved.outcome === "aborted", "The abort intent must authorize the aborted reservation without lane ownership");
|
|
1917
|
+
yield* ensure((yield* ledger.reserveSettlement(reservation)).replayed === true, "Replaying the identical aborted reservation must short-circuit idempotently");
|
|
1918
|
+
yield* ledger.finalizeSettlement(SettlementFinalization.make({
|
|
1919
|
+
submissionId: second.submissionId,
|
|
1920
|
+
settlementId: submissionSettlementId(second.submissionId)
|
|
1921
|
+
}));
|
|
1922
|
+
const settled = yield* recoverySnapshot(second.submissionId);
|
|
1923
|
+
yield* ensure(settled.submission.state === "settled" && settled.submission.settledOutcome === "aborted", "The aborted queued Submission must settle while the head is still unsettled");
|
|
1924
|
+
yield* ensure((yield* recoverySnapshot(head.submissionId)).submission.state === "ready", "Settling the aborted queued row must not disturb the unclaimed head");
|
|
1925
|
+
}));
|
|
1926
|
+
const abortedSettledRowIsNotAJoiningGap = conformanceCase("claimJoining treats an aborted-settled row as a non-gap", ({ ensure, expectSome }) => Effect.gen(function* () {
|
|
1927
|
+
const threadId = decodeThreadId("ledger-conformance-aborted-non-gap");
|
|
1928
|
+
const ledger = yield* SubmissionLedger;
|
|
1929
|
+
const host = yield* admitReady(threadId, "aborted-gap-host", { work: "host" });
|
|
1930
|
+
const second = yield* admitReady(threadId, "aborted-gap-2", { queued: 2 });
|
|
1931
|
+
const third = yield* admitReady(threadId, "aborted-gap-3", { queued: 3 });
|
|
1932
|
+
yield* ledger.requestAbort(AbortCommand.make({
|
|
1933
|
+
submissionId: second.submissionId,
|
|
1934
|
+
author: "operator",
|
|
1935
|
+
reason: "cancelled while queued"
|
|
1936
|
+
}));
|
|
1937
|
+
const reservation = yield* settlementReservation({
|
|
1938
|
+
submissionId: second.submissionId,
|
|
1939
|
+
ownershipToken: BOGUS_TOKEN,
|
|
1940
|
+
receiptId: second.receiptId,
|
|
1941
|
+
outcome: "aborted"
|
|
1942
|
+
});
|
|
1943
|
+
yield* ledger.reserveSettlement(reservation);
|
|
1944
|
+
yield* ledger.finalizeSettlement(SettlementFinalization.make({
|
|
1945
|
+
submissionId: second.submissionId,
|
|
1946
|
+
settlementId: submissionSettlementId(second.submissionId)
|
|
1947
|
+
}));
|
|
1948
|
+
const hostClaim = yield* expectSome("the host claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1949
|
+
yield* ensure(hostClaim.submissionId === host.submissionId, "The host must head the lane (the aborted-settled row is out of the queue)");
|
|
1950
|
+
const claims = yield* ledger.claimJoining(ClaimJoiningRequest.make({
|
|
1951
|
+
threadId,
|
|
1952
|
+
hostSubmissionId: host.submissionId,
|
|
1953
|
+
ownershipToken: hostClaim.ownershipToken,
|
|
1954
|
+
maxCount: 8
|
|
1955
|
+
}));
|
|
1956
|
+
yield* ensure(claims.length === 1 && claims[0].submissionId === third.submissionId, "The joining prefix must skip the aborted-settled row and claim the later ready work");
|
|
1957
|
+
const settled = yield* recoverySnapshot(second.submissionId);
|
|
1958
|
+
yield* ensure(settled.submission.state === "settled" && settled.submission.settledOutcome === "aborted", "Walking the prefix must never disturb the aborted-settled row");
|
|
1959
|
+
}));
|
|
1300
1960
|
/**
|
|
1301
1961
|
* The shared, adapter-parameterized SubmissionLedger contract suite (STORE-010). Every durable
|
|
1302
1962
|
* ledger adapter test suite must execute each case against its own ledger provisioning, inside
|
|
@@ -1305,6 +1965,183 @@ const unknownAbortClaim = conformanceCase("a durable abort makes an unknown head
|
|
|
1305
1965
|
const submissionLedgerConformanceCases = [
|
|
1306
1966
|
admissionIdempotency,
|
|
1307
1967
|
workerAdmissionIdentity,
|
|
1968
|
+
conformanceCase("seals terminal assignments atomically while waiting and unapplied corrections remain steerable", ({ ensure, expectSome, expectFailure }) => Effect.gen(function* () {
|
|
1969
|
+
const ledger = yield* SubmissionLedger;
|
|
1970
|
+
if (ledger.inspectWorker === void 0) return;
|
|
1971
|
+
for (const scenario of [
|
|
1972
|
+
{
|
|
1973
|
+
name: "completed",
|
|
1974
|
+
outcome: "completed",
|
|
1975
|
+
disposition: "completed",
|
|
1976
|
+
terminal: "completed"
|
|
1977
|
+
},
|
|
1978
|
+
{
|
|
1979
|
+
name: "waiting",
|
|
1980
|
+
outcome: "completed",
|
|
1981
|
+
disposition: "waiting",
|
|
1982
|
+
terminal: void 0
|
|
1983
|
+
},
|
|
1984
|
+
{
|
|
1985
|
+
name: "failure",
|
|
1986
|
+
outcome: "failed",
|
|
1987
|
+
queued: true,
|
|
1988
|
+
terminal: "failed"
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
name: "failure-joining",
|
|
1992
|
+
outcome: "failed",
|
|
1993
|
+
queued: true,
|
|
1994
|
+
joining: true,
|
|
1995
|
+
terminal: "failed"
|
|
1996
|
+
},
|
|
1997
|
+
{
|
|
1998
|
+
name: "exhaustion",
|
|
1999
|
+
outcome: "completed",
|
|
2000
|
+
exhausted: true,
|
|
2001
|
+
terminal: "failed"
|
|
2002
|
+
},
|
|
2003
|
+
{
|
|
2004
|
+
name: "active-abort",
|
|
2005
|
+
outcome: "aborted",
|
|
2006
|
+
terminal: "cancelled"
|
|
2007
|
+
},
|
|
2008
|
+
{
|
|
2009
|
+
name: "queued-abort",
|
|
2010
|
+
outcome: "aborted",
|
|
2011
|
+
noRun: true,
|
|
2012
|
+
terminal: void 0
|
|
2013
|
+
},
|
|
2014
|
+
{
|
|
2015
|
+
name: "reusable",
|
|
2016
|
+
outcome: "completed",
|
|
2017
|
+
disposition: "completed",
|
|
2018
|
+
reusable: true,
|
|
2019
|
+
terminal: void 0
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
name: "late-correction",
|
|
2023
|
+
outcome: "completed",
|
|
2024
|
+
disposition: "completed",
|
|
2025
|
+
queued: true,
|
|
2026
|
+
terminal: void 0
|
|
2027
|
+
},
|
|
2028
|
+
{
|
|
2029
|
+
name: "applied-correction",
|
|
2030
|
+
outcome: "completed",
|
|
2031
|
+
disposition: "completed",
|
|
2032
|
+
queued: true,
|
|
2033
|
+
joined: true,
|
|
2034
|
+
terminal: "completed"
|
|
2035
|
+
}
|
|
2036
|
+
]) {
|
|
2037
|
+
const threadId = decodeThreadId(`assignment-${scenario.name}`);
|
|
2038
|
+
const base = yield* admissionRequest(threadId, "first", { text: "first" });
|
|
2039
|
+
const metadata = workerMetadata(base);
|
|
2040
|
+
const workerAdmission = WorkerAdmission.make({
|
|
2041
|
+
...metadata,
|
|
2042
|
+
origin: {
|
|
2043
|
+
...metadata.origin,
|
|
2044
|
+
..."reusable" in scenario ? {} : { lifecycle: "assignment" }
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
const request = AdmissionRequest.make({
|
|
2048
|
+
...base,
|
|
2049
|
+
workerAdmission
|
|
2050
|
+
});
|
|
2051
|
+
const first = yield* ledger.admit(request);
|
|
2052
|
+
yield* ledger.markReady(MarkReadyRequest.make({ submissionId: first.submissionId }));
|
|
2053
|
+
const claim = yield* expectSome("assignment claim", yield* claimLane(threadId, PRODUCER_A));
|
|
2054
|
+
const runId = runIdForSubmission(first.submissionId);
|
|
2055
|
+
const payload = yield* Schema.decodeEffect(SubmissionSettledRecord)(SubmissionSettled.make({
|
|
2056
|
+
submissionId: first.submissionId,
|
|
2057
|
+
settlementId: submissionSettlementId(first.submissionId),
|
|
2058
|
+
receiptId: first.receiptId,
|
|
2059
|
+
outcome: scenario.outcome,
|
|
2060
|
+
..."noRun" in scenario ? {} : { runId },
|
|
2061
|
+
...scenario.outcome === "aborted" ? {} : { result: scenario.outcome === "failed" ? {
|
|
2062
|
+
errorTag: "TestFailure",
|
|
2063
|
+
message: "Failed task"
|
|
2064
|
+
} : { answer: "done" } },
|
|
2065
|
+
..."disposition" in scenario ? { runDisposition: scenario.disposition } : {},
|
|
2066
|
+
..."exhausted" in scenario ? {
|
|
2067
|
+
finishReason: "budget-exhausted",
|
|
2068
|
+
exhausted: "turns"
|
|
2069
|
+
} : {}
|
|
2070
|
+
})).pipe(Effect.orDie);
|
|
2071
|
+
const record = RecordEnvelope.make({
|
|
2072
|
+
recordId: submissionSettlementRecordId(first.submissionId),
|
|
2073
|
+
family: "thread",
|
|
2074
|
+
schemaVersion: 1,
|
|
2075
|
+
createdAt: CONFORMANCE_CREATED_AT,
|
|
2076
|
+
deploymentId: CONFORMANCE_DEPLOYMENT,
|
|
2077
|
+
payload
|
|
2078
|
+
});
|
|
2079
|
+
let queued;
|
|
2080
|
+
if ("queued" in scenario) {
|
|
2081
|
+
const next = yield* admissionRequest(threadId, "correction", { text: "correction" });
|
|
2082
|
+
queued = yield* ledger.admit(AdmissionRequest.make({
|
|
2083
|
+
...next,
|
|
2084
|
+
workerAdmission: {
|
|
2085
|
+
...workerAdmission,
|
|
2086
|
+
messageId: next.idempotencyKey
|
|
2087
|
+
}
|
|
2088
|
+
}));
|
|
2089
|
+
yield* ledger.markReady(MarkReadyRequest.make({ submissionId: queued.submissionId }));
|
|
2090
|
+
if ("joined" in scenario || "joining" in scenario) yield* ledger.claimJoining(ClaimJoiningRequest.make({
|
|
2091
|
+
threadId,
|
|
2092
|
+
hostSubmissionId: first.submissionId,
|
|
2093
|
+
ownershipToken: claim.ownershipToken,
|
|
2094
|
+
maxCount: 1
|
|
2095
|
+
}));
|
|
2096
|
+
if ("joined" in scenario) yield* ledger.markJoined(MarkJoinedRequest.make({
|
|
2097
|
+
submissionId: queued.submissionId,
|
|
2098
|
+
ownershipToken: claim.ownershipToken,
|
|
2099
|
+
recordId: submissionInputRecordId(queued.submissionId),
|
|
2100
|
+
sequence: Schema.decodeSync(CanonicalSequence)(2)
|
|
2101
|
+
}));
|
|
2102
|
+
}
|
|
2103
|
+
yield* ledger.reserveSettlement(SettlementReservation.make({
|
|
2104
|
+
submissionId: first.submissionId,
|
|
2105
|
+
ownershipToken: claim.ownershipToken,
|
|
2106
|
+
settlementId: payload.settlementId,
|
|
2107
|
+
outcome: scenario.outcome,
|
|
2108
|
+
record,
|
|
2109
|
+
recordDigest: yield* digestJson(yield* Schema.encodeEffect(RecordEnvelope)(record).pipe(Effect.orDie))
|
|
2110
|
+
}));
|
|
2111
|
+
const finalization = SettlementFinalization.make({
|
|
2112
|
+
submissionId: first.submissionId,
|
|
2113
|
+
settlementId: payload.settlementId
|
|
2114
|
+
});
|
|
2115
|
+
const settled = yield* ledger.finalizeSettlement(finalization);
|
|
2116
|
+
yield* ensure(Schema.toEquivalence(Settlement)(settled, yield* ledger.finalizeSettlement(finalization)), "Finalization replay must preserve its exact outcome");
|
|
2117
|
+
const control = yield* ledger.inspectWorker(threadId);
|
|
2118
|
+
yield* ensure(control.terminal === scenario.terminal && control.stopped === (scenario.terminal !== void 0), `Wrong assignment state for ${scenario.name}`);
|
|
2119
|
+
yield* ensure((yield* ledger.admit(request)).receiptId === first.receiptId, "A seal must preserve same-command receipt replay");
|
|
2120
|
+
const next = yield* admissionRequest(threadId, "later", { text: "later" });
|
|
2121
|
+
const later = AdmissionRequest.make({
|
|
2122
|
+
...next,
|
|
2123
|
+
workerAdmission: {
|
|
2124
|
+
...workerAdmission,
|
|
2125
|
+
messageId: next.idempotencyKey
|
|
2126
|
+
}
|
|
2127
|
+
});
|
|
2128
|
+
if (scenario.terminal !== void 0) {
|
|
2129
|
+
const refusal = yield* expectFailure("terminal admission", ledger.admit(later));
|
|
2130
|
+
yield* ensure(Schema.is(AdmissionPolicyError)(refusal) && refusal.code === "worker-stopped", "New instructions cannot reopen a terminal assignment");
|
|
2131
|
+
} else yield* ledger.admit(later);
|
|
2132
|
+
if (queued !== void 0) {
|
|
2133
|
+
if ("joining" in scenario) yield* ledger.revertJoining(RevertJoiningRequest.make({ submissionId: queued.submissionId }));
|
|
2134
|
+
yield* ensure((yield* recoverySnapshot(queued.submissionId)).abortIntent !== void 0 === (scenario.outcome === "failed"), "Failure aborts queued inputs; completion never cancels an accepted correction or joined member");
|
|
2135
|
+
}
|
|
2136
|
+
if (scenario.terminal !== void 0 && ledger.stopWorker !== void 0) {
|
|
2137
|
+
yield* ledger.stopWorker({
|
|
2138
|
+
threadId,
|
|
2139
|
+
author: CONFORMANCE_PRINCIPAL
|
|
2140
|
+
});
|
|
2141
|
+
yield* ensure((yield* ledger.inspectWorker(threadId)).terminal === scenario.terminal, "Explicit stop cannot replace an assignment's first terminal outcome");
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
})),
|
|
1308
2145
|
messageAdmissionIdentity,
|
|
1309
2146
|
workerCompletionIdentity,
|
|
1310
2147
|
workerUpdateIdentity,
|
|
@@ -1328,665 +2165,21 @@ const submissionLedgerConformanceCases = [
|
|
|
1328
2165
|
markJoinedIdempotency,
|
|
1329
2166
|
claimNeverGrantsBlockedHead,
|
|
1330
2167
|
approvalDecisionIdempotency,
|
|
1331
|
-
|
|
1332
|
-
const threadId = decodeThreadId("ledger-conformance-unknown");
|
|
1333
|
-
const ledger = yield* SubmissionLedger;
|
|
1334
|
-
const admitted = yield* admitReady(threadId, "unknown-key-1", { work: "unknown" });
|
|
1335
|
-
const claim = yield* expectSome("the claim before the unknown marking", yield* claimLane(threadId, PRODUCER_A));
|
|
1336
|
-
const call1 = decodeToolCallId("call-unknown-1");
|
|
1337
|
-
const call2 = decodeToolCallId("call-unknown-2");
|
|
1338
|
-
const call3 = decodeToolCallId("call-unknown-3");
|
|
1339
|
-
const marking = MarkUnknownRequest.make({
|
|
1340
|
-
submissionId: admitted.submissionId,
|
|
1341
|
-
toolCallIds: [call1, call2],
|
|
1342
|
-
reason: "the worker died during two supplier calls"
|
|
1343
|
-
});
|
|
1344
|
-
yield* ledger.markUnknown(marking);
|
|
1345
|
-
yield* ledger.markUnknown(marking);
|
|
1346
|
-
yield* ensure((yield* expectSome("lookup after marking unknown", yield* lookupById(admitted.submissionId))).state === "unknown", "Marking unknown must be idempotent and block the lane");
|
|
1347
|
-
yield* advancePastLease(claim.leaseExpiresAt);
|
|
1348
|
-
yield* ensure(Option.isNone(yield* claimLane(threadId, PRODUCER_B)), "An unknown lane must consume no worker permit");
|
|
1349
|
-
const first = yield* ledger.recordUnknownResolution(UnknownResolutionCommand.make({
|
|
1350
|
-
submissionId: admitted.submissionId,
|
|
1351
|
-
toolCallId: call1,
|
|
1352
|
-
author: "conformance-operator",
|
|
1353
|
-
reason: "supplier store shows no booking",
|
|
1354
|
-
resolution: ResolutionNeverHappened.make()
|
|
1355
|
-
}));
|
|
1356
|
-
yield* ensure((yield* expectSome("lookup after a partial resolution", yield* lookupById(admitted.submissionId))).state === "unknown" && Option.isNone(yield* claimLane(threadId, PRODUCER_B)), "A partial resolution must keep the lane blocked while calls remain open");
|
|
1357
|
-
yield* TestClock.adjust("1 second");
|
|
1358
|
-
const divergent = yield* expectFailure("re-resolving the same call divergently", ledger.recordUnknownResolution(UnknownResolutionCommand.make({
|
|
1359
|
-
submissionId: admitted.submissionId,
|
|
1360
|
-
toolCallId: call1,
|
|
1361
|
-
author: "conformance-operator",
|
|
1362
|
-
reason: "changed my mind",
|
|
1363
|
-
resolution: ResolutionSafeToRetry.make()
|
|
1364
|
-
})));
|
|
1365
|
-
yield* ensure(isUnknownResolutionConflict(divergent) && divergent.toolCallId === call1, "A divergent re-resolution must conflict");
|
|
1366
|
-
const replayed = yield* ledger.recordUnknownResolution(UnknownResolutionCommand.make({
|
|
1367
|
-
submissionId: admitted.submissionId,
|
|
1368
|
-
toolCallId: call1,
|
|
1369
|
-
author: "conformance-operator-second",
|
|
1370
|
-
reason: "a different reason text",
|
|
1371
|
-
resolution: ResolutionNeverHappened.make()
|
|
1372
|
-
}));
|
|
1373
|
-
yield* ensure(sameInstant(replayed.resolvedAt, first.resolvedAt) && replayed.reason === first.reason, "Repeating the same resolution must replay the recorded intent unchanged");
|
|
1374
|
-
yield* ledger.markUnknown(MarkUnknownRequest.make({
|
|
1375
|
-
submissionId: admitted.submissionId,
|
|
1376
|
-
toolCallIds: [call3],
|
|
1377
|
-
reason: "a second wave of uncertainty"
|
|
1378
|
-
}));
|
|
1379
|
-
yield* ledger.recordUnknownResolution(UnknownResolutionCommand.make({
|
|
1380
|
-
submissionId: admitted.submissionId,
|
|
1381
|
-
toolCallId: call2,
|
|
1382
|
-
author: "conformance-operator",
|
|
1383
|
-
reason: "supplier confirmed the booking",
|
|
1384
|
-
resolution: ResolutionCompletedWithResult.make({
|
|
1385
|
-
result: { bookingRef: "booking-1" },
|
|
1386
|
-
isFailure: false
|
|
1387
|
-
})
|
|
1388
|
-
}));
|
|
1389
|
-
yield* ensure((yield* expectSome("lookup while the extended set stays open", yield* lookupById(admitted.submissionId))).state === "unknown" && Option.isNone(yield* claimLane(threadId, PRODUCER_B)), "The lane must stay blocked while any marked call lacks a resolution");
|
|
1390
|
-
yield* ledger.recordUnknownResolution(UnknownResolutionCommand.make({
|
|
1391
|
-
submissionId: admitted.submissionId,
|
|
1392
|
-
toolCallId: call3,
|
|
1393
|
-
author: "conformance-operator",
|
|
1394
|
-
reason: "idempotency key covers a repeat",
|
|
1395
|
-
resolution: ResolutionSafeToRetry.make()
|
|
1396
|
-
}));
|
|
1397
|
-
yield* ensure((yield* expectSome("lookup after the covering resolution", yield* lookupById(admitted.submissionId))).state === "input-applied", "Covering every marked call must reopen the lane as input-applied");
|
|
1398
|
-
yield* ensure((yield* recoverySnapshot(admitted.submissionId)).unknownResolutions.length === 3, "The recovery snapshot must expose every recorded resolution intent");
|
|
1399
|
-
const reclaim = yield* expectSome("the claim after the lane reopened", yield* claimLane(threadId, PRODUCER_B));
|
|
1400
|
-
yield* ensure(reclaim.submissionId === admitted.submissionId && reclaim.producerEpoch > claim.producerEpoch, "The reopened lane must grant a fresh fenced Attempt");
|
|
1401
|
-
yield* settleClaimed(admitted, reclaim.ownershipToken);
|
|
1402
|
-
const late = yield* expectFailure("resolving an unknown call for a settled Submission", ledger.recordUnknownResolution(UnknownResolutionCommand.make({
|
|
1403
|
-
submissionId: admitted.submissionId,
|
|
1404
|
-
toolCallId: decodeToolCallId("call-unknown-late"),
|
|
1405
|
-
author: "conformance-operator",
|
|
1406
|
-
reason: "too late",
|
|
1407
|
-
resolution: ResolutionNeverHappened.make()
|
|
1408
|
-
})));
|
|
1409
|
-
yield* ensure(isSettlementConflict(late), "A resolution must never land on a settled Submission");
|
|
1410
|
-
})),
|
|
2168
|
+
unknownResolutionLifecycle,
|
|
1411
2169
|
unknownAbortClaim,
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
yield* ledger.recordApprovalDecision(ApprovalDecisionCommand.make({
|
|
1426
|
-
submissionId: admitted.submissionId,
|
|
1427
|
-
toolCallId: callA,
|
|
1428
|
-
decision: "approved",
|
|
1429
|
-
resolver: "conformance-approver",
|
|
1430
|
-
reason: "decided before the suspend committed"
|
|
1431
|
-
}));
|
|
1432
|
-
yield* ensure((yield* ledger.suspend(SuspendRequest.make({
|
|
1433
|
-
submissionId: admitted.submissionId,
|
|
1434
|
-
ownershipToken: claim.ownershipToken,
|
|
1435
|
-
reason: ApprovalPendingSuspension.make({ toolCallIds: [callA] })
|
|
1436
|
-
}))) === "resume-immediately", "A fully-decided suspension reason must resume immediately");
|
|
1437
|
-
yield* ensure((yield* expectSome("lookup after the immediate resume", yield* lookupById(admitted.submissionId))).state === "running", "An immediate resume must not transition the Submission to suspended");
|
|
1438
|
-
yield* ledger.renewOwnership(RenewOwnershipRequest.make({
|
|
1439
|
-
submissionId: admitted.submissionId,
|
|
1440
|
-
ownershipToken: claim.ownershipToken
|
|
1441
|
-
}));
|
|
1442
|
-
yield* ensure((yield* ledger.suspend(SuspendRequest.make({
|
|
1443
|
-
submissionId: admitted.submissionId,
|
|
1444
|
-
ownershipToken: claim.ownershipToken,
|
|
1445
|
-
reason: ApprovalPendingSuspension.make({ toolCallIds: [callA, callB] })
|
|
1446
|
-
}))) === "suspended", "An undecided call in the reason must suspend durably");
|
|
1447
|
-
yield* ensure((yield* expectSome("lookup after the suspension", yield* lookupById(admitted.submissionId))).state === "suspended", "The suspension must be durable");
|
|
1448
|
-
const suspendedSnapshot = yield* recoverySnapshot(admitted.submissionId);
|
|
1449
|
-
yield* ensure(suspendedSnapshot.suspension !== void 0 && suspendedSnapshot.suspension.reason._tag === "ApprovalPending" && suspendedSnapshot.suspension.reason.toolCallIds.length === 2 && suspendedSnapshot.approvalDecisions.length === 1, "The recovery snapshot must expose the suspension reason and prior decisions");
|
|
1450
|
-
const ended = yield* expectFailure("renewing after the suspension ended the ownership period", ledger.renewOwnership(RenewOwnershipRequest.make({
|
|
1451
|
-
submissionId: admitted.submissionId,
|
|
1452
|
-
ownershipToken: claim.ownershipToken
|
|
1453
|
-
})));
|
|
1454
|
-
yield* ensure(isOwnershipLost(ended), "Suspension must end the ownership period without settling");
|
|
1455
|
-
yield* ledger.recordApprovalDecision(ApprovalDecisionCommand.make({
|
|
1456
|
-
submissionId: admitted.submissionId,
|
|
1457
|
-
toolCallId: callB,
|
|
1458
|
-
decision: "denied",
|
|
1459
|
-
resolver: "conformance-approver",
|
|
1460
|
-
reason: "the covering decision"
|
|
1461
|
-
}));
|
|
1462
|
-
yield* ensure((yield* expectSome("lookup after the covering decision", yield* lookupById(admitted.submissionId))).state === "input-applied", "The covering decision must wake the suspended lane");
|
|
1463
|
-
yield* ensure((yield* recoverySnapshot(admitted.submissionId)).suspension === void 0, "Waking must clear the durable suspension");
|
|
1464
|
-
const reclaim = yield* expectSome("the claim after waking", yield* claimLane(threadId, PRODUCER_B));
|
|
1465
|
-
yield* settleClaimed(admitted, reclaim.ownershipToken);
|
|
1466
|
-
const terminal = yield* expectFailure("suspending a settled Submission", ledger.suspend(SuspendRequest.make({
|
|
1467
|
-
submissionId: admitted.submissionId,
|
|
1468
|
-
ownershipToken: reclaim.ownershipToken,
|
|
1469
|
-
reason: ApprovalPendingSuspension.make({ toolCallIds: [callA] })
|
|
1470
|
-
})));
|
|
1471
|
-
yield* ensure(isSettlementConflict(terminal) && terminal.existingOutcome === "completed", "Suspension must never land on a settled Submission");
|
|
1472
|
-
})),
|
|
1473
|
-
conformanceCase("a joined Submission's settlement reservation is authorized by host linkage", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1474
|
-
const threadId = decodeThreadId("ledger-conformance-joined-settlement");
|
|
1475
|
-
const ledger = yield* SubmissionLedger;
|
|
1476
|
-
const host = yield* admitReady(threadId, "joined-settle-host", { work: "host" });
|
|
1477
|
-
const queued = yield* admitReady(threadId, "joined-settle-2", { queued: 2 });
|
|
1478
|
-
const hostClaim = yield* expectSome("the host claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1479
|
-
const claims = yield* ledger.claimJoining(ClaimJoiningRequest.make({
|
|
1480
|
-
threadId,
|
|
1481
|
-
hostSubmissionId: host.submissionId,
|
|
1482
|
-
ownershipToken: hostClaim.ownershipToken,
|
|
1483
|
-
maxCount: 1
|
|
1484
|
-
}));
|
|
1485
|
-
yield* ensure(claims.length === 1 && claims[0]?.submissionId === queued.submissionId, "The queued Submission must join the host's contiguous prefix");
|
|
1486
|
-
const joiningReservation = yield* settlementReservation({
|
|
1487
|
-
submissionId: queued.submissionId,
|
|
1488
|
-
ownershipToken: BOGUS_TOKEN,
|
|
1489
|
-
receiptId: queued.receiptId,
|
|
1490
|
-
outcome: "completed"
|
|
1491
|
-
});
|
|
1492
|
-
const fenced = yield* expectFailure("reserving a joining Submission's settlement without lane ownership", ledger.reserveSettlement(joiningReservation));
|
|
1493
|
-
yield* ensure(isOwnershipLost(fenced), "A joining Submission's settlement reservation must stay ownership-fenced");
|
|
1494
|
-
yield* ledger.markJoined(MarkJoinedRequest.make({
|
|
1495
|
-
submissionId: queued.submissionId,
|
|
1496
|
-
ownershipToken: hostClaim.ownershipToken,
|
|
1497
|
-
recordId: submissionInputRecordId(queued.submissionId),
|
|
1498
|
-
sequence: decodeSequence(7)
|
|
1499
|
-
}));
|
|
1500
|
-
const joinedReservation = yield* settlementReservation({
|
|
1501
|
-
submissionId: queued.submissionId,
|
|
1502
|
-
ownershipToken: BOGUS_TOKEN,
|
|
1503
|
-
receiptId: queued.receiptId,
|
|
1504
|
-
outcome: "completed"
|
|
1505
|
-
});
|
|
1506
|
-
yield* ledger.reserveSettlement(joinedReservation);
|
|
1507
|
-
yield* ensure((yield* ledger.finalizeSettlement(SettlementFinalization.make({
|
|
1508
|
-
submissionId: queued.submissionId,
|
|
1509
|
-
settlementId: submissionSettlementId(queued.submissionId)
|
|
1510
|
-
}))).outcome === "completed", "The joined settlement must finalize with the reserved outcome");
|
|
1511
|
-
const settled = yield* expectSome("lookup after the joined settlement", yield* lookupById(queued.submissionId));
|
|
1512
|
-
yield* ensure(settled.state === "settled" && settled.settledOutcome === "completed", "The joined Submission must settle terminally");
|
|
1513
|
-
})),
|
|
1514
|
-
conformanceCase("replays an identical child reservation and rejects a divergent allocation digest", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1515
|
-
const threadId = decodeThreadId("ledger-conformance-child-reserve");
|
|
1516
|
-
const ledger = yield* SubmissionLedger;
|
|
1517
|
-
const parent = yield* admitReady(threadId, "child-reserve-parent", { work: "parent" });
|
|
1518
|
-
const claim = yield* expectSome("the parent claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1519
|
-
const allocation = {
|
|
1520
|
-
turns: 4,
|
|
1521
|
-
toolCalls: 8
|
|
1522
|
-
};
|
|
1523
|
-
const allocationDigest = yield* digestJson(allocation);
|
|
1524
|
-
const requestFields = {
|
|
1525
|
-
reservationId: decodeChildReservationId("child-reservation:run-reserve:call-1"),
|
|
1526
|
-
parentSubmissionId: parent.submissionId,
|
|
1527
|
-
parentToolCallId: decodeToolCallId("call-child-reserve"),
|
|
1528
|
-
ownershipToken: claim.ownershipToken,
|
|
1529
|
-
allocation,
|
|
1530
|
-
allocationDigest
|
|
1531
|
-
};
|
|
1532
|
-
const request = ChildBudgetReservationRequest.make(requestFields);
|
|
1533
|
-
const first = yield* ledger.reserveChildBudget(request);
|
|
1534
|
-
yield* ensure(!first.replayed && first.reservation.status === "reserved" && first.reservation.allocationDigest === allocationDigest && first.reservation.childSubmissionId === void 0 && persistedJsonEquivalent(first.reservation.allocation, allocation), "The first reservation must create a reserved row carrying the exact allocation");
|
|
1535
|
-
yield* TestClock.adjust("1 second");
|
|
1536
|
-
const replayed = yield* ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1537
|
-
...requestFields,
|
|
1538
|
-
allocation: {
|
|
1539
|
-
toolCalls: 8,
|
|
1540
|
-
turns: 4
|
|
1541
|
-
}
|
|
1542
|
-
}));
|
|
1543
|
-
yield* ensure(replayed.replayed && replayed.reservation.status === "reserved" && persistedJsonEquivalent(replayed.reservation.allocation, allocation) && sameInstant(replayed.reservation.reservedAt, first.reservation.reservedAt), "An identical reservation must replay the stored row unchanged");
|
|
1544
|
-
const divergentAllocation = {
|
|
1545
|
-
turns: 64,
|
|
1546
|
-
toolCalls: 8
|
|
1547
|
-
};
|
|
1548
|
-
const divergent = yield* expectFailure("a divergent allocation for the same reservation id", ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1549
|
-
...requestFields,
|
|
1550
|
-
allocation: divergentAllocation,
|
|
1551
|
-
allocationDigest: yield* digestJson(divergentAllocation)
|
|
1552
|
-
})));
|
|
1553
|
-
yield* ensure(isChildReservationConflict(divergent) && divergent.status === "reserved", "A divergent allocation must conflict with the recorded reservation");
|
|
1554
|
-
const secondId = yield* expectFailure("a second reservation id for the same parent Tool Call", ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1555
|
-
...requestFields,
|
|
1556
|
-
reservationId: decodeChildReservationId("child-reservation:run-reserve:call-1-other")
|
|
1557
|
-
})));
|
|
1558
|
-
yield* ensure(isChildReservationConflict(secondId), "One parent Tool Call must never own two reservations");
|
|
1559
|
-
const snapshot = yield* recoverySnapshot(parent.submissionId);
|
|
1560
|
-
yield* ensure(snapshot.childReservations.length === 1 && snapshot.childReservations[0].reservationId === request.reservationId && snapshot.childReservations[0].status === "reserved" && snapshot.childAttachments.length === 0, "The parent recovery snapshot must expose the reservation before any attachment");
|
|
1561
|
-
})),
|
|
1562
|
-
conformanceCase("a stale parent token cannot transition a child reservation", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1563
|
-
const threadId = decodeThreadId("ledger-conformance-child-fence");
|
|
1564
|
-
const childLane = decodeThreadId("ledger-conformance-child-fence-child");
|
|
1565
|
-
const ledger = yield* SubmissionLedger;
|
|
1566
|
-
const parent = yield* admitReady(threadId, "child-fence-parent", { work: "parent" });
|
|
1567
|
-
const child = yield* admitReady(childLane, "child-fence-child", { work: "child" });
|
|
1568
|
-
const firstClaim = yield* expectSome("the first parent claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1569
|
-
const allocation = { turns: 2 };
|
|
1570
|
-
const allocationDigest = yield* digestJson(allocation);
|
|
1571
|
-
const requestFields = {
|
|
1572
|
-
reservationId: decodeChildReservationId("child-reservation:run-fence:call-1"),
|
|
1573
|
-
parentSubmissionId: parent.submissionId,
|
|
1574
|
-
parentToolCallId: decodeToolCallId("call-child-fence"),
|
|
1575
|
-
ownershipToken: BOGUS_TOKEN,
|
|
1576
|
-
allocation,
|
|
1577
|
-
allocationDigest
|
|
1578
|
-
};
|
|
1579
|
-
const request = ChildBudgetReservationRequest.make(requestFields);
|
|
1580
|
-
const foreign = yield* expectFailure("creating a reservation without owning the parent lane", ledger.reserveChildBudget(request));
|
|
1581
|
-
yield* ensure(isOwnershipLost(foreign), "Reservation creation must be fenced by the parent's live ownership token");
|
|
1582
|
-
yield* ensure((yield* recoverySnapshot(parent.submissionId)).childReservations.length === 0, "A fenced reservation attempt must leave no row behind");
|
|
1583
|
-
yield* ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1584
|
-
...requestFields,
|
|
1585
|
-
ownershipToken: firstClaim.ownershipToken
|
|
1586
|
-
}));
|
|
1587
|
-
yield* ledger.releaseOwnership(ReleaseOwnershipRequest.make({
|
|
1588
|
-
submissionId: parent.submissionId,
|
|
1589
|
-
ownershipToken: firstClaim.ownershipToken
|
|
1590
|
-
}));
|
|
1591
|
-
const reclaim = yield* expectSome("the replacement parent claim", yield* claimLane(threadId, PRODUCER_B));
|
|
1592
|
-
yield* ensure(reclaim.producerEpoch > firstClaim.producerEpoch, "The replacement claim must fence the stale parent Attempt");
|
|
1593
|
-
const staleAttach = yield* expectFailure("attaching a child with the superseded parent token", ledger.attachChildToReservation(AttachChildToReservationRequest.make({
|
|
1594
|
-
reservationId: request.reservationId,
|
|
1595
|
-
ownershipToken: firstClaim.ownershipToken,
|
|
1596
|
-
childSubmissionId: child.submissionId
|
|
1597
|
-
})));
|
|
1598
|
-
yield* ensure(isOwnershipLost(staleAttach), "A superseded parent token must not attach a child");
|
|
1599
|
-
const staleCreate = yield* expectFailure("creating a second-call reservation with the superseded parent token", ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1600
|
-
...requestFields,
|
|
1601
|
-
reservationId: decodeChildReservationId("child-reservation:run-fence:call-2"),
|
|
1602
|
-
parentToolCallId: decodeToolCallId("call-child-fence-second"),
|
|
1603
|
-
ownershipToken: firstClaim.ownershipToken
|
|
1604
|
-
})));
|
|
1605
|
-
yield* ensure(isOwnershipLost(staleCreate), "A superseded parent token must not create new reservation state");
|
|
1606
|
-
const staleReplay = yield* ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1607
|
-
...requestFields,
|
|
1608
|
-
ownershipToken: firstClaim.ownershipToken
|
|
1609
|
-
}));
|
|
1610
|
-
yield* ensure(staleReplay.replayed && staleReplay.reservation.status === "reserved", "An identical reservation replay must return the recorded row even from a stale caller");
|
|
1611
|
-
yield* ensure((yield* ledger.attachChildToReservation(AttachChildToReservationRequest.make({
|
|
1612
|
-
reservationId: request.reservationId,
|
|
1613
|
-
ownershipToken: reclaim.ownershipToken,
|
|
1614
|
-
childSubmissionId: child.submissionId
|
|
1615
|
-
}))).childSubmissionId === child.submissionId, "The live replacement token must attach the child");
|
|
1616
|
-
})),
|
|
1617
|
-
conformanceCase("attachChildToReservation is idempotent and rejects a divergent child", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1618
|
-
const threadId = decodeThreadId("ledger-conformance-child-attach");
|
|
1619
|
-
const childLane = decodeThreadId("ledger-conformance-child-attach-child");
|
|
1620
|
-
const ledger = yield* SubmissionLedger;
|
|
1621
|
-
const parent = yield* admitReady(threadId, "child-attach-parent", { work: "parent" });
|
|
1622
|
-
const child = yield* admitReady(childLane, "child-attach-child", { queued: 1 });
|
|
1623
|
-
const otherChild = yield* admitReady(childLane, "child-attach-other", { queued: 2 });
|
|
1624
|
-
const claim = yield* expectSome("the parent claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1625
|
-
const allocation = { turns: 3 };
|
|
1626
|
-
const reservationId = decodeChildReservationId("child-reservation:run-attach:call-1");
|
|
1627
|
-
yield* ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1628
|
-
reservationId,
|
|
1629
|
-
parentSubmissionId: parent.submissionId,
|
|
1630
|
-
parentToolCallId: decodeToolCallId("call-child-attach"),
|
|
1631
|
-
ownershipToken: claim.ownershipToken,
|
|
1632
|
-
allocation,
|
|
1633
|
-
allocationDigest: yield* digestJson(allocation)
|
|
1634
|
-
}));
|
|
1635
|
-
const unknownReservation = yield* expectFailure("attaching to a reservation that was never created", ledger.attachChildToReservation(AttachChildToReservationRequest.make({
|
|
1636
|
-
reservationId: decodeChildReservationId("child-reservation:run-attach:missing"),
|
|
1637
|
-
ownershipToken: claim.ownershipToken,
|
|
1638
|
-
childSubmissionId: child.submissionId
|
|
1639
|
-
})));
|
|
1640
|
-
yield* ensure(isLedgerError(unknownReservation), "Attaching to an unknown reservation must fail as a ledger error");
|
|
1641
|
-
const attached = yield* ledger.attachChildToReservation(AttachChildToReservationRequest.make({
|
|
1642
|
-
reservationId,
|
|
1643
|
-
ownershipToken: claim.ownershipToken,
|
|
1644
|
-
childSubmissionId: child.submissionId
|
|
1645
|
-
}));
|
|
1646
|
-
yield* ensure(attached.childSubmissionId === child.submissionId && attached.status === "reserved", "Attaching must record the child on the reservation row");
|
|
1647
|
-
yield* ensure((yield* ledger.attachChildToReservation(AttachChildToReservationRequest.make({
|
|
1648
|
-
reservationId,
|
|
1649
|
-
ownershipToken: claim.ownershipToken,
|
|
1650
|
-
childSubmissionId: child.submissionId
|
|
1651
|
-
}))).childSubmissionId === child.submissionId, "Repeating the identical attachment must be a no-op");
|
|
1652
|
-
const divergent = yield* expectFailure("attaching a different child to the same reservation", ledger.attachChildToReservation(AttachChildToReservationRequest.make({
|
|
1653
|
-
reservationId,
|
|
1654
|
-
ownershipToken: claim.ownershipToken,
|
|
1655
|
-
childSubmissionId: otherChild.submissionId
|
|
1656
|
-
})));
|
|
1657
|
-
yield* ensure(isChildReservationConflict(divergent), "A divergent child attachment must conflict with the recorded child");
|
|
1658
|
-
const snapshot = yield* recoverySnapshot(parent.submissionId);
|
|
1659
|
-
yield* ensure(snapshot.childAttachments.length === 1 && snapshot.childAttachments[0].childSubmissionId === child.submissionId && snapshot.childAttachments[0].toolCallId === decodeToolCallId("call-child-attach") && snapshot.childAttachments[0].childState === "ready" && snapshot.childAttachments[0].childOutcome === void 0, "The parent recovery snapshot must expose the attachment with the child's lane state");
|
|
1660
|
-
})),
|
|
1661
|
-
conformanceCase("beginRelease freezes the accounting decision exactly once", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1662
|
-
const threadId = decodeThreadId("ledger-conformance-child-freeze");
|
|
1663
|
-
const ledger = yield* SubmissionLedger;
|
|
1664
|
-
const parent = yield* admitReady(threadId, "child-freeze-parent", { work: "parent" });
|
|
1665
|
-
const claim = yield* expectSome("the parent claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1666
|
-
const allocation = { turns: 4 };
|
|
1667
|
-
const reservationId = decodeChildReservationId("child-reservation:run-freeze:call-1");
|
|
1668
|
-
yield* ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1669
|
-
reservationId,
|
|
1670
|
-
parentSubmissionId: parent.submissionId,
|
|
1671
|
-
parentToolCallId: decodeToolCallId("call-child-freeze"),
|
|
1672
|
-
ownershipToken: claim.ownershipToken,
|
|
1673
|
-
allocation,
|
|
1674
|
-
allocationDigest: yield* digestJson(allocation)
|
|
1675
|
-
}));
|
|
1676
|
-
const accounting = {
|
|
1677
|
-
consumed: { turns: 1 },
|
|
1678
|
-
released: { turns: 3 }
|
|
1679
|
-
};
|
|
1680
|
-
const frozen = yield* ledger.beginChildBudgetRelease(BeginChildBudgetReleaseRequest.make({
|
|
1681
|
-
reservationId,
|
|
1682
|
-
accounting
|
|
1683
|
-
}));
|
|
1684
|
-
yield* ensure(frozen.status === "releasePending" && frozen.accounting !== void 0 && persistedJsonEquivalent(frozen.accounting, accounting) && frozen.releaseBeganAt !== void 0, "The first beginRelease must freeze the accounting and move to releasePending");
|
|
1685
|
-
yield* TestClock.adjust("1 second");
|
|
1686
|
-
const replayed = yield* ledger.beginChildBudgetRelease(BeginChildBudgetReleaseRequest.make({
|
|
1687
|
-
reservationId,
|
|
1688
|
-
accounting: {
|
|
1689
|
-
released: { turns: 3 },
|
|
1690
|
-
consumed: { turns: 1 }
|
|
1691
|
-
}
|
|
1692
|
-
}));
|
|
1693
|
-
yield* ensure(replayed.status === "releasePending" && replayed.releaseBeganAt !== void 0 && frozen.releaseBeganAt !== void 0 && sameInstant(replayed.releaseBeganAt, frozen.releaseBeganAt), "Replaying the identical accounting must be a no-op with the frozen decision retained");
|
|
1694
|
-
const divergent = yield* expectFailure("freezing a different accounting decision", ledger.beginChildBudgetRelease(BeginChildBudgetReleaseRequest.make({
|
|
1695
|
-
reservationId,
|
|
1696
|
-
accounting: {
|
|
1697
|
-
consumed: { turns: 4 },
|
|
1698
|
-
released: { turns: 0 }
|
|
1699
|
-
}
|
|
1700
|
-
})));
|
|
1701
|
-
yield* ensure(isChildReservationConflict(divergent) && divergent.status === "releasePending", "A divergent accounting freeze must conflict with the frozen decision");
|
|
1702
|
-
yield* ledger.releaseChildBudget(ReleaseChildBudgetRequest.make({ reservationId }));
|
|
1703
|
-
const afterRelease = yield* ledger.beginChildBudgetRelease(BeginChildBudgetReleaseRequest.make({
|
|
1704
|
-
reservationId,
|
|
1705
|
-
accounting
|
|
1706
|
-
}));
|
|
1707
|
-
yield* ensure(afterRelease.status === "released" && afterRelease.accounting !== void 0 && persistedJsonEquivalent(afterRelease.accounting, accounting), "Replaying the identical accounting after release must return the released row");
|
|
1708
|
-
const divergentAfterRelease = yield* expectFailure("freezing a different accounting decision after release", ledger.beginChildBudgetRelease(BeginChildBudgetReleaseRequest.make({
|
|
1709
|
-
reservationId,
|
|
1710
|
-
accounting: {
|
|
1711
|
-
consumed: { turns: 2 },
|
|
1712
|
-
released: { turns: 2 }
|
|
1713
|
-
}
|
|
1714
|
-
})));
|
|
1715
|
-
yield* ensure(isChildReservationConflict(divergentAfterRelease) && divergentAfterRelease.status === "released", "The frozen decision must stay immutable after release");
|
|
1716
|
-
})),
|
|
1717
|
-
conformanceCase("release returns unused allocation exactly once", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1718
|
-
const threadId = decodeThreadId("ledger-conformance-child-release");
|
|
1719
|
-
const ledger = yield* SubmissionLedger;
|
|
1720
|
-
const parent = yield* admitReady(threadId, "child-release-parent", { work: "parent" });
|
|
1721
|
-
const claim = yield* expectSome("the parent claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1722
|
-
const allocation = { turns: 4 };
|
|
1723
|
-
const reservationId = decodeChildReservationId("child-reservation:run-release:call-1");
|
|
1724
|
-
yield* ledger.reserveChildBudget(ChildBudgetReservationRequest.make({
|
|
1725
|
-
reservationId,
|
|
1726
|
-
parentSubmissionId: parent.submissionId,
|
|
1727
|
-
parentToolCallId: decodeToolCallId("call-child-release"),
|
|
1728
|
-
ownershipToken: claim.ownershipToken,
|
|
1729
|
-
allocation,
|
|
1730
|
-
allocationDigest: yield* digestJson(allocation)
|
|
1731
|
-
}));
|
|
1732
|
-
const early = yield* expectFailure("releasing before the accounting decision is frozen", ledger.releaseChildBudget(ReleaseChildBudgetRequest.make({ reservationId })));
|
|
1733
|
-
yield* ensure(isChildReservationConflict(early) && early.status === "reserved", "Release must never skip the releasePending freeze");
|
|
1734
|
-
yield* ledger.beginChildBudgetRelease(BeginChildBudgetReleaseRequest.make({
|
|
1735
|
-
reservationId,
|
|
1736
|
-
accounting: {
|
|
1737
|
-
consumed: {},
|
|
1738
|
-
released: { turns: 4 }
|
|
1739
|
-
}
|
|
1740
|
-
}));
|
|
1741
|
-
const released = yield* ledger.releaseChildBudget(ReleaseChildBudgetRequest.make({ reservationId }));
|
|
1742
|
-
yield* ensure(released.status === "released" && released.releasedAt !== void 0, "Release must transition releasePending to released");
|
|
1743
|
-
yield* TestClock.adjust("1 second");
|
|
1744
|
-
const replayed = yield* ledger.releaseChildBudget(ReleaseChildBudgetRequest.make({ reservationId }));
|
|
1745
|
-
yield* ensure(replayed.status === "released" && replayed.releasedAt !== void 0 && released.releasedAt !== void 0 && sameInstant(replayed.releasedAt, released.releasedAt), "Replaying the release must return the stored row unchanged — never applied twice");
|
|
1746
|
-
const snapshot = yield* recoverySnapshot(parent.submissionId);
|
|
1747
|
-
yield* ensure(snapshot.childReservations.length === 1 && snapshot.childReservations[0].status === "released", "The recovery snapshot must expose the released reservation");
|
|
1748
|
-
})),
|
|
1749
|
-
conformanceCase("recordChildSettled accepts canonical terminalizing prefixes and wakes only when all are covered", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1750
|
-
const parentLane = decodeThreadId("ledger-conformance-child-wake");
|
|
1751
|
-
const childLaneA = decodeThreadId("ledger-conformance-child-wake-a");
|
|
1752
|
-
const childLaneB = decodeThreadId("ledger-conformance-child-wake-b");
|
|
1753
|
-
const ledger = yield* SubmissionLedger;
|
|
1754
|
-
const parent = yield* admitReady(parentLane, "child-wake-parent", { work: "parent" });
|
|
1755
|
-
const childA = yield* admitReady(childLaneA, "child-wake-a", { child: "a" });
|
|
1756
|
-
const childB = yield* admitReady(childLaneB, "child-wake-b", { child: "b" });
|
|
1757
|
-
const parentClaim = yield* expectSome("the parent claim", yield* claimLane(parentLane, PRODUCER_A));
|
|
1758
|
-
yield* ensure((yield* ledger.suspend(SuspendRequest.make({
|
|
1759
|
-
submissionId: parent.submissionId,
|
|
1760
|
-
ownershipToken: parentClaim.ownershipToken,
|
|
1761
|
-
reason: WaitingForChildSuspension.make({ children: [WaitingChild.make({
|
|
1762
|
-
toolCallId: decodeToolCallId("call-wake-a"),
|
|
1763
|
-
childSubmissionId: childA.submissionId
|
|
1764
|
-
}), WaitingChild.make({
|
|
1765
|
-
toolCallId: decodeToolCallId("call-wake-b"),
|
|
1766
|
-
childSubmissionId: childB.submissionId
|
|
1767
|
-
})] })
|
|
1768
|
-
}))) === "suspended", "Unsettled children must suspend the parent durably");
|
|
1769
|
-
const ended = yield* expectFailure("renewing after the waitingForChild suspension ended the ownership period", ledger.renewOwnership(RenewOwnershipRequest.make({
|
|
1770
|
-
submissionId: parent.submissionId,
|
|
1771
|
-
ownershipToken: parentClaim.ownershipToken
|
|
1772
|
-
})));
|
|
1773
|
-
yield* ensure(isOwnershipLost(ended), "waitingForChild must end the ownership period without settling (SUB-030)");
|
|
1774
|
-
yield* ensure(Option.isNone(yield* claimLane(parentLane, PRODUCER_B)), "A waitingForChild head must produce no claim and consume no worker permit");
|
|
1775
|
-
const childClaimA = yield* expectSome("the first child claim", yield* claimLane(childLaneA, PRODUCER_A));
|
|
1776
|
-
const childReservationA = yield* settlementReservation({
|
|
1777
|
-
submissionId: childA.submissionId,
|
|
1778
|
-
ownershipToken: childClaimA.ownershipToken,
|
|
1779
|
-
receiptId: childA.receiptId,
|
|
1780
|
-
outcome: "completed"
|
|
1781
|
-
});
|
|
1782
|
-
yield* ledger.reserveSettlement(childReservationA);
|
|
1783
|
-
yield* ensure((yield* ledger.recordChildSettled(ChildSettledNotification.make({
|
|
1784
|
-
parentSubmissionId: parent.submissionId,
|
|
1785
|
-
childSubmissionId: childA.submissionId
|
|
1786
|
-
}))) === "still-waiting", "A settlement notification must not wake the parent while a listed child is unsettled");
|
|
1787
|
-
yield* ledger.finalizeSettlement(SettlementFinalization.make({
|
|
1788
|
-
submissionId: childA.submissionId,
|
|
1789
|
-
settlementId: childReservationA.settlementId
|
|
1790
|
-
}));
|
|
1791
|
-
yield* ensure((yield* expectSome("lookup while one child is outstanding", yield* lookupById(parent.submissionId))).state === "suspended" && Option.isNone(yield* claimLane(parentLane, PRODUCER_B)), "The parent lane must stay suspended until every listed child settled");
|
|
1792
|
-
const childClaimB = yield* expectSome("the second child claim", yield* claimLane(childLaneB, PRODUCER_A));
|
|
1793
|
-
const childReservationB = yield* settlementReservation({
|
|
1794
|
-
submissionId: childB.submissionId,
|
|
1795
|
-
ownershipToken: childClaimB.ownershipToken,
|
|
1796
|
-
receiptId: childB.receiptId,
|
|
1797
|
-
outcome: "completed"
|
|
1798
|
-
});
|
|
1799
|
-
yield* ledger.reserveSettlement(childReservationB);
|
|
1800
|
-
yield* ensure((yield* ledger.recordChildSettled(ChildSettledNotification.make({
|
|
1801
|
-
parentSubmissionId: parent.submissionId,
|
|
1802
|
-
childSubmissionId: childB.submissionId
|
|
1803
|
-
}))) === "woken", "The covering canonical settlement prefix must wake the parent before child finalization");
|
|
1804
|
-
yield* ledger.finalizeSettlement(SettlementFinalization.make({
|
|
1805
|
-
submissionId: childB.submissionId,
|
|
1806
|
-
settlementId: childReservationB.settlementId
|
|
1807
|
-
}));
|
|
1808
|
-
yield* ensure((yield* expectSome("lookup after the covering settlement", yield* lookupById(parent.submissionId))).state === "input-applied", "The woken parent must transition suspended(WaitingForChild) to input-applied");
|
|
1809
|
-
yield* ensure((yield* recoverySnapshot(parent.submissionId)).suspension === void 0, "Waking must clear the durable suspension");
|
|
1810
|
-
yield* ensure((yield* ledger.recordChildSettled(ChildSettledNotification.make({
|
|
1811
|
-
parentSubmissionId: parent.submissionId,
|
|
1812
|
-
childSubmissionId: childA.submissionId
|
|
1813
|
-
}))) === "not-waiting", "Replaying a notification after the wake must be an idempotent no-op");
|
|
1814
|
-
const reclaim = yield* expectSome("the parent claim after waking", yield* claimLane(parentLane, PRODUCER_B));
|
|
1815
|
-
yield* ensure(reclaim.submissionId === parent.submissionId && reclaim.producerEpoch > parentClaim.producerEpoch, "The woken lane must grant a fresh fenced Attempt");
|
|
1816
|
-
yield* settleClaimed(parent, reclaim.ownershipToken);
|
|
1817
|
-
yield* ensure((yield* ledger.recordChildSettled(ChildSettledNotification.make({
|
|
1818
|
-
parentSubmissionId: parent.submissionId,
|
|
1819
|
-
childSubmissionId: childB.submissionId
|
|
1820
|
-
}))) === "not-waiting", "A notification for a settled parent must answer not-waiting");
|
|
1821
|
-
})),
|
|
1822
|
-
conformanceCase("suspend returns resume-immediately when children already settled", ({ ensure, expectSome }) => Effect.gen(function* () {
|
|
1823
|
-
const parentLane = decodeThreadId("ledger-conformance-child-raced");
|
|
1824
|
-
const childLaneA = decodeThreadId("ledger-conformance-child-raced-a");
|
|
1825
|
-
const childLaneB = decodeThreadId("ledger-conformance-child-raced-b");
|
|
1826
|
-
const ledger = yield* SubmissionLedger;
|
|
1827
|
-
const settledChild = yield* admitReady(childLaneA, "child-raced-a", { child: "a" });
|
|
1828
|
-
const settledClaim = yield* expectSome("the settled child's claim", yield* claimLane(childLaneA, PRODUCER_A));
|
|
1829
|
-
yield* settleClaimed(settledChild, settledClaim.ownershipToken);
|
|
1830
|
-
const pendingChild = yield* admitReady(childLaneB, "child-raced-b", { child: "b" });
|
|
1831
|
-
const parent = yield* admitReady(parentLane, "child-raced-parent", { work: "parent" });
|
|
1832
|
-
const parentClaim = yield* expectSome("the parent claim", yield* claimLane(parentLane, PRODUCER_A));
|
|
1833
|
-
yield* ensure((yield* ledger.suspend(SuspendRequest.make({
|
|
1834
|
-
submissionId: parent.submissionId,
|
|
1835
|
-
ownershipToken: parentClaim.ownershipToken,
|
|
1836
|
-
reason: WaitingForChildSuspension.make({ children: [WaitingChild.make({
|
|
1837
|
-
toolCallId: decodeToolCallId("call-raced-a"),
|
|
1838
|
-
childSubmissionId: settledChild.submissionId
|
|
1839
|
-
})] })
|
|
1840
|
-
}))) === "resume-immediately", "A suspend listing only settled children must resume immediately");
|
|
1841
|
-
yield* ensure((yield* expectSome("lookup after the immediate resume", yield* lookupById(parent.submissionId))).state === "running", "An immediate resume must not transition the parent to suspended");
|
|
1842
|
-
yield* ledger.renewOwnership(RenewOwnershipRequest.make({
|
|
1843
|
-
submissionId: parent.submissionId,
|
|
1844
|
-
ownershipToken: parentClaim.ownershipToken
|
|
1845
|
-
}));
|
|
1846
|
-
yield* ensure((yield* ledger.suspend(SuspendRequest.make({
|
|
1847
|
-
submissionId: parent.submissionId,
|
|
1848
|
-
ownershipToken: parentClaim.ownershipToken,
|
|
1849
|
-
reason: WaitingForChildSuspension.make({ children: [WaitingChild.make({
|
|
1850
|
-
toolCallId: decodeToolCallId("call-raced-a"),
|
|
1851
|
-
childSubmissionId: settledChild.submissionId
|
|
1852
|
-
}), WaitingChild.make({
|
|
1853
|
-
toolCallId: decodeToolCallId("call-raced-b"),
|
|
1854
|
-
childSubmissionId: pendingChild.submissionId
|
|
1855
|
-
})] })
|
|
1856
|
-
}))) === "suspended", "One unsettled listed child must suspend the parent durably");
|
|
1857
|
-
const snapshot = yield* recoverySnapshot(parent.submissionId);
|
|
1858
|
-
yield* ensure(snapshot.suspension !== void 0 && snapshot.suspension.reason._tag === "WaitingForChild" && snapshot.suspension.reason.children.length === 2, "The recovery snapshot must expose the WaitingForChild reason with its children");
|
|
1859
|
-
})),
|
|
1860
|
-
conformanceCase("admit records and replays parent linkage", ({ ensure, expectFailure, expectSome }) => Effect.gen(function* () {
|
|
1861
|
-
const parentLane = decodeThreadId("ledger-conformance-linkage");
|
|
1862
|
-
const childLane = decodeThreadId("ledger-conformance-linkage-child");
|
|
1863
|
-
const ledger = yield* SubmissionLedger;
|
|
1864
|
-
const parent = yield* admitReady(parentLane, "linkage-parent", { work: "parent" });
|
|
1865
|
-
const linkage = ParentLinkage.make({
|
|
1866
|
-
parentSubmissionId: parent.submissionId,
|
|
1867
|
-
parentToolCallId: decodeToolCallId("call-linkage")
|
|
1868
|
-
});
|
|
1869
|
-
const request = yield* admissionRequest(childLane, "linkage-child-key", { task: "research" }, linkage);
|
|
1870
|
-
const admitted = yield* ledger.admit(request);
|
|
1871
|
-
yield* ensure(!admitted.replayed, "The first linked admission must create the child");
|
|
1872
|
-
const byId = yield* expectSome("lookup of the linked child", yield* lookupById(admitted.submissionId));
|
|
1873
|
-
yield* ensure(byId.parentLinkage !== void 0 && byId.parentLinkage.parentSubmissionId === parent.submissionId && byId.parentLinkage.parentToolCallId === linkage.parentToolCallId, "The child snapshot must expose its immutable parent linkage");
|
|
1874
|
-
const childSnapshot = yield* recoverySnapshot(admitted.submissionId);
|
|
1875
|
-
yield* ensure(childSnapshot.parentLinkage !== void 0 && childSnapshot.parentLinkage.parentSubmissionId === parent.submissionId, "The child recovery snapshot must expose its parent linkage");
|
|
1876
|
-
const replayed = yield* ledger.admit(request);
|
|
1877
|
-
yield* ensure(replayed.replayed && replayed.submissionId === admitted.submissionId && replayed.receiptId === admitted.receiptId, "An identical linked admission must replay the original identities (SUB-016)");
|
|
1878
|
-
const divergentLinkage = yield* expectFailure("replaying the admission with a different parent Tool Call", ledger.admit(yield* admissionRequest(childLane, "linkage-child-key", { task: "research" }, ParentLinkage.make({
|
|
1879
|
-
parentSubmissionId: parent.submissionId,
|
|
1880
|
-
parentToolCallId: decodeToolCallId("call-linkage-other")
|
|
1881
|
-
}))));
|
|
1882
|
-
yield* ensure(isAdmissionConflict(divergentLinkage), "A divergent parent linkage must conflict even when the input digest matches");
|
|
1883
|
-
const droppedLinkage = yield* expectFailure("replaying the admission without its parent linkage", ledger.admit(yield* admissionRequest(childLane, "linkage-child-key", { task: "research" })));
|
|
1884
|
-
yield* ensure(isAdmissionConflict(droppedLinkage), "Dropping the recorded linkage on replay must conflict");
|
|
1885
|
-
const plain = yield* ledger.admit(yield* admissionRequest(childLane, "linkage-plain-key", { task: "plain" }));
|
|
1886
|
-
const addedLinkage = yield* expectFailure("replaying an unlinked admission with a parent linkage", ledger.admit(yield* admissionRequest(childLane, "linkage-plain-key", { task: "plain" }, linkage)));
|
|
1887
|
-
yield* ensure(isAdmissionConflict(addedLinkage), "Adding a linkage to an unlinked admission on replay must conflict");
|
|
1888
|
-
yield* ensure((yield* expectSome("lookup of the unlinked Submission", yield* lookupById(plain.submissionId))).parentLinkage === void 0, "An unlinked Submission must expose no parent linkage");
|
|
1889
|
-
})),
|
|
1890
|
-
conformanceCase("resolveAdmission distinguishes notAdmitted from admitted authoritatively", ({ ensure, expectSome }) => Effect.gen(function* () {
|
|
1891
|
-
const threadId = decodeThreadId("ledger-conformance-resolve");
|
|
1892
|
-
const ledger = yield* SubmissionLedger;
|
|
1893
|
-
const key = SubmissionLookupByKey.make({
|
|
1894
|
-
threadId,
|
|
1895
|
-
principal: CONFORMANCE_PRINCIPAL,
|
|
1896
|
-
idempotencyKey: decodeIdempotencyKey("resolve-key-1")
|
|
1897
|
-
});
|
|
1898
|
-
yield* ensure((yield* ledger.resolveAdmission(key))._tag === "NotAdmitted", "The authoritative store must prove absence before admission (SUB-031)");
|
|
1899
|
-
const admitted = yield* ledger.admit(yield* admissionRequest(threadId, "resolve-key-1", { work: "resolve" }));
|
|
1900
|
-
const after = yield* ledger.resolveAdmission(key);
|
|
1901
|
-
yield* ensure(after._tag === "Admitted" && after.submission.submissionId === admitted.submissionId && after.submission.receiptId === admitted.receiptId && after.submission.state === "admitted", "An admitted key must resolve to the full authoritative snapshot");
|
|
1902
|
-
yield* ensure((yield* ledger.resolveAdmission(SubmissionLookupByKey.make({
|
|
1903
|
-
threadId,
|
|
1904
|
-
principal: OTHER_PRINCIPAL,
|
|
1905
|
-
idempotencyKey: decodeIdempotencyKey("resolve-key-1")
|
|
1906
|
-
})))._tag === "NotAdmitted", "Admission resolution must stay scoped to the requesting principal");
|
|
1907
|
-
yield* ledger.markReady(MarkReadyRequest.make({ submissionId: admitted.submissionId }));
|
|
1908
|
-
const claim = yield* expectSome("the claim before terminal resolution", yield* claimLane(threadId, PRODUCER_A));
|
|
1909
|
-
yield* settleClaimed(admitted, claim.ownershipToken);
|
|
1910
|
-
const settled = yield* ledger.resolveAdmission(key);
|
|
1911
|
-
yield* ensure(settled._tag === "Admitted" && settled.submission.state === "settled" && settled.submission.settledOutcome === "completed", "A settled Submission still resolves as admitted — terminality never becomes absence");
|
|
1912
|
-
})),
|
|
1913
|
-
conformanceCase("reserveSettlement authorizes an aborted, unowned queued settlement by its durable intent", ({ ensure, expectFailure }) => Effect.gen(function* () {
|
|
1914
|
-
const threadId = decodeThreadId("ledger-conformance-queued-abort");
|
|
1915
|
-
const ledger = yield* SubmissionLedger;
|
|
1916
|
-
const head = yield* admitReady(threadId, "queued-abort-head", { work: "head" });
|
|
1917
|
-
const second = yield* admitReady(threadId, "queued-abort-2", { queued: 2 });
|
|
1918
|
-
const third = yield* admitReady(threadId, "queued-abort-3", { queued: 3 });
|
|
1919
|
-
yield* ledger.requestAbort(AbortCommand.make({
|
|
1920
|
-
submissionId: second.submissionId,
|
|
1921
|
-
author: "operator",
|
|
1922
|
-
reason: "cancelled while queued"
|
|
1923
|
-
}));
|
|
1924
|
-
const unaborted = yield* expectFailure("reserving an aborted settlement for a queued row without an abort intent", settlementReservation({
|
|
1925
|
-
submissionId: third.submissionId,
|
|
1926
|
-
ownershipToken: BOGUS_TOKEN,
|
|
1927
|
-
receiptId: third.receiptId,
|
|
1928
|
-
outcome: "aborted"
|
|
1929
|
-
}).pipe(Effect.flatMap((reservation) => ledger.reserveSettlement(reservation))));
|
|
1930
|
-
yield* ensure(isOwnershipLost(unaborted), "A queued reservation without a durable abort intent must stay fenced (OwnershipLost)");
|
|
1931
|
-
const wrongOutcome = yield* expectFailure("reserving a completed settlement for the aborted queued row", settlementReservation({
|
|
1932
|
-
submissionId: second.submissionId,
|
|
1933
|
-
ownershipToken: BOGUS_TOKEN,
|
|
1934
|
-
receiptId: second.receiptId,
|
|
1935
|
-
outcome: "completed",
|
|
1936
|
-
result: { fabricated: true }
|
|
1937
|
-
}).pipe(Effect.flatMap((reservation) => ledger.reserveSettlement(reservation))));
|
|
1938
|
-
yield* ensure(isOwnershipLost(wrongOutcome), "An abort intent must never authorize a non-aborted settlement outcome");
|
|
1939
|
-
const reservation = yield* settlementReservation({
|
|
1940
|
-
submissionId: second.submissionId,
|
|
1941
|
-
ownershipToken: BOGUS_TOKEN,
|
|
1942
|
-
receiptId: second.receiptId,
|
|
1943
|
-
outcome: "aborted"
|
|
1944
|
-
});
|
|
1945
|
-
const reserved = yield* ledger.reserveSettlement(reservation);
|
|
1946
|
-
yield* ensure(reserved.replayed === false && reserved.outcome === "aborted", "The abort intent must authorize the aborted reservation without lane ownership");
|
|
1947
|
-
yield* ensure((yield* ledger.reserveSettlement(reservation)).replayed === true, "Replaying the identical aborted reservation must short-circuit idempotently");
|
|
1948
|
-
yield* ledger.finalizeSettlement(SettlementFinalization.make({
|
|
1949
|
-
submissionId: second.submissionId,
|
|
1950
|
-
settlementId: submissionSettlementId(second.submissionId)
|
|
1951
|
-
}));
|
|
1952
|
-
const settled = yield* recoverySnapshot(second.submissionId);
|
|
1953
|
-
yield* ensure(settled.submission.state === "settled" && settled.submission.settledOutcome === "aborted", "The aborted queued Submission must settle while the head is still unsettled");
|
|
1954
|
-
yield* ensure((yield* recoverySnapshot(head.submissionId)).submission.state === "ready", "Settling the aborted queued row must not disturb the unclaimed head");
|
|
1955
|
-
})),
|
|
1956
|
-
conformanceCase("claimJoining treats an aborted-settled row as a non-gap", ({ ensure, expectSome }) => Effect.gen(function* () {
|
|
1957
|
-
const threadId = decodeThreadId("ledger-conformance-aborted-non-gap");
|
|
1958
|
-
const ledger = yield* SubmissionLedger;
|
|
1959
|
-
const host = yield* admitReady(threadId, "aborted-gap-host", { work: "host" });
|
|
1960
|
-
const second = yield* admitReady(threadId, "aborted-gap-2", { queued: 2 });
|
|
1961
|
-
const third = yield* admitReady(threadId, "aborted-gap-3", { queued: 3 });
|
|
1962
|
-
yield* ledger.requestAbort(AbortCommand.make({
|
|
1963
|
-
submissionId: second.submissionId,
|
|
1964
|
-
author: "operator",
|
|
1965
|
-
reason: "cancelled while queued"
|
|
1966
|
-
}));
|
|
1967
|
-
const reservation = yield* settlementReservation({
|
|
1968
|
-
submissionId: second.submissionId,
|
|
1969
|
-
ownershipToken: BOGUS_TOKEN,
|
|
1970
|
-
receiptId: second.receiptId,
|
|
1971
|
-
outcome: "aborted"
|
|
1972
|
-
});
|
|
1973
|
-
yield* ledger.reserveSettlement(reservation);
|
|
1974
|
-
yield* ledger.finalizeSettlement(SettlementFinalization.make({
|
|
1975
|
-
submissionId: second.submissionId,
|
|
1976
|
-
settlementId: submissionSettlementId(second.submissionId)
|
|
1977
|
-
}));
|
|
1978
|
-
const hostClaim = yield* expectSome("the host claim", yield* claimLane(threadId, PRODUCER_A));
|
|
1979
|
-
yield* ensure(hostClaim.submissionId === host.submissionId, "The host must head the lane (the aborted-settled row is out of the queue)");
|
|
1980
|
-
const claims = yield* ledger.claimJoining(ClaimJoiningRequest.make({
|
|
1981
|
-
threadId,
|
|
1982
|
-
hostSubmissionId: host.submissionId,
|
|
1983
|
-
ownershipToken: hostClaim.ownershipToken,
|
|
1984
|
-
maxCount: 8
|
|
1985
|
-
}));
|
|
1986
|
-
yield* ensure(claims.length === 1 && claims[0].submissionId === third.submissionId, "The joining prefix must skip the aborted-settled row and claim the later ready work");
|
|
1987
|
-
const settled = yield* recoverySnapshot(second.submissionId);
|
|
1988
|
-
yield* ensure(settled.submission.state === "settled" && settled.submission.settledOutcome === "aborted", "Walking the prefix must never disturb the aborted-settled row");
|
|
1989
|
-
}))
|
|
2170
|
+
suspendResumesImmediatelyWhenDecided,
|
|
2171
|
+
joinedSettlementLinkageAuthority,
|
|
2172
|
+
childReservationIdempotency,
|
|
2173
|
+
childReservationFencing,
|
|
2174
|
+
attachChildIdempotency,
|
|
2175
|
+
beginReleaseFreezesAccountingOnce,
|
|
2176
|
+
releaseAppliedExactlyOnce,
|
|
2177
|
+
recordChildSettledWake,
|
|
2178
|
+
suspendResumesImmediatelyForSettledChildren,
|
|
2179
|
+
admissionParentLinkage,
|
|
2180
|
+
resolveAdmissionAuthority,
|
|
2181
|
+
queuedAbortSettlementAuthority,
|
|
2182
|
+
abortedSettledRowIsNotAJoiningGap
|
|
1990
2183
|
];
|
|
1991
2184
|
//#endregion
|
|
1992
2185
|
export { SubmissionLedgerConformanceViolation, submissionLedgerConformanceCases };
|