mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 20:21:50 +01:00
## Thinking Path > - Paperclip manages AI agents and their work. > - Native runs preserve provider sessions across controller restarts. > - A completion event can reach durable storage before the next session checkpoint. > - Recovery advanced the event cursor but lost the saved completion proposal. > - The completed turn then appeared to need another provider turn to report its result. > - This change restores the exact active turn's proposal before advancing the cursor. > - Recovery can finalize completed work without repeating its external actions. ## Linked Issues or Issue Description **What happened?** After a controller restart, a native run could fail with `native_finalization_missing` even though `run.result.proposed` was already durable. The recovered driver skipped that event when its cursor advanced, so the result was absent from its state. Automatic disposition retries could then encounter outstanding tool receipts and exhaust recovery. **Expected behavior** Restore the saved result for the exact active turn. Wait for provider termination, then use normal finalization. Do not start another provider turn to repeat completed work. **Steps to reproduce** 1. Start a native Codex task and save its active-turn checkpoint. 2. Persist its completion proposal, then lose the controller before another checkpoint. 3. Recover the same run after the provider completes. Before this change, finalization reports a missing result. **Paperclip version or commit** Reproduced on `92b238137b`. The repair is based on `7c247243e5`. **Deployment mode** Native Runner with Codex. The incident used a remote sandbox; the regression covers controller event recovery. Related: #12845 and #15297. The older #12241 addresses provider process continuity; this change repairs the controller's saved event projection. ## What Changed - Restore valid completion proposals for the checkpoint's exact active turn during event replay. - Reject conflicting, invalid, or incorrectly bound proposals. Ignore proposals from another turn and prevent adoption after a newer turn starts. - Keep provider termination and normal finalization as separate requirements. - Add eight behavioral cases using the real Codex driver and a simulated transport, and document the recovery contract and verification scope. ## Verification - Red: the new restart regression failed with `native_finalization_missing` before the production change. - Green: 254 tests passed across the native runtime, backend adapter, and Codex recovery suites. The new cases check no repeated provider work, no early finalization, duplicate proposals, conflicts, invalid proposals, and stale turn/session bindings. - `node scripts/check-maps.mjs` and `git diff --check` passed. - `pnpm -r typecheck` and `pnpm build` passed locally. All CI gates passed on `2ac025a15c`; Greptile rated that commit 5/5 with no outstanding findings. The full local `pnpm test:run` was started after completing the embedded PostgreSQL installation, then stopped while still running once the complete CI test matrix passed; it is not reported as a completed local run. - Real local acceptance passed on `2ac025a15c` with the production Rust Runner transport, Codex CLI `0.160.0`, and `gpt-6.1-sol` on macOS arm64. Persisted the completion proposal while retaining the older active-turn checkpoint; let the provider finish without persisting its later controller events; stopped the exact owned runner/provider; recovered the same provider session and turn. Observed one accepted result, one durable `turn.start` command, and one line in the receipt file. Hosted sandbox restart and recovery of the original stopped task have not been performed. - Initial setup failures are retained: dependency drift caused diff-viewer type errors, and skipped PostgreSQL postinstall links prevented database startup. Dependency alignment and the package postinstall repaired those local prerequisites. Probe setup iterations failed on a direct-provider response schema, invalid probe input fields, and an original provider that still held its writer lock. Acceptance uses the production transport, validated input, and exact-owner shutdown. No production code changes were made for those probe setup failures. ## Risks - A stale proposal could finalize the wrong work. Recovery requires the checkpoint's active turn and matching run, session, and source. It rejects conflicting results and waits for provider termination. - The change uses the existing paginated recovery scan. It adds no database query per event or new retry allowance. - No schema migration or change to task authorization is included. ## Model Used OpenAI GPT-6, running as Codex with reasoning, tool use, and code execution. The exact served model snapshot and context-window size are not exposed in this session. The live acceptance probe uses `gpt-6.1-sol`. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
9010 lines
301 KiB
TypeScript
9010 lines
301 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { resolveQualifiedAcpxProfile } from "./drivers/acpx/qualified-profiles.js";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { createCodexNativeSessionBackend } from "./backends/codex-native-backend.js";
|
|
import { FakeCodexTransport, WORKSPACE } from "./drivers/codex/codex-app-server-driver.test-support.js";
|
|
|
|
import type { ControlPlanePort } from "./contracts/control-plane-port.js";
|
|
import type { NativeExecutionInputV1, NativeExecutionInputV5 } from "./contracts/native-execution.js";
|
|
import type { NativeRunIdentity } from "./contracts/types.js";
|
|
import type {
|
|
NativeSession,
|
|
NativeSessionBackend,
|
|
PersistedNativeSession,
|
|
} from "./contracts/native-session-backend.js";
|
|
import {
|
|
NativeSessionCloseUnrecoverableError,
|
|
NativeSessionCleanupQuarantinedError,
|
|
NativeSessionProtocolIntegrityError,
|
|
} from "./contracts/native-session-backend.js";
|
|
import type {
|
|
PrpEvent,
|
|
PrpStructuredRunResult,
|
|
PrpTerminalState,
|
|
} from "./protocol/replay-contract.js";
|
|
import {
|
|
NATIVE_RUNTIME_ASSET_SCHEMA,
|
|
PAPERCLIP_EXECUTION_PROMPT,
|
|
PAPERCLIP_EXECUTION_PROMPT_REVISION,
|
|
canonicalNativeRuntimeContextDigest,
|
|
nativeRuntimePromptDigest,
|
|
} from "./contracts/runtime-context.js";
|
|
import {
|
|
executeNativeSession,
|
|
completeTerminatedRemoteNativeSessionCleanup,
|
|
completeTerminatedLocalNativeSessionCleanup,
|
|
type ExecuteNativeSessionOptions,
|
|
} from "./native-session-runtime.js";
|
|
|
|
const identity = {
|
|
runId: "run-recovery",
|
|
sessionId: "session-recovery",
|
|
companyId: "company-recovery",
|
|
issueId: "issue-recovery",
|
|
agentId: "agent-recovery",
|
|
};
|
|
|
|
const result: PrpStructuredRunResult = {
|
|
schema: "paperclip.run_result.v1",
|
|
reportedWorkDisposition: "done",
|
|
summary: "Recovered native work completed.",
|
|
completionClaim: {
|
|
contractRevision: "1",
|
|
objectiveSatisfied: true,
|
|
criteria: [
|
|
{ criterionId: "objective", status: "satisfied", evidenceRefs: [] },
|
|
],
|
|
remainingWork: [],
|
|
},
|
|
evidence: [],
|
|
verification: [{ commandOrCheck: "recovery", status: "passed" }],
|
|
attentionRequests: [],
|
|
artifacts: [],
|
|
};
|
|
|
|
const terminal: PrpTerminalState = {
|
|
schema: "paperclip.prp.terminal.v1",
|
|
turnTerminalState: "completed",
|
|
runTerminalState: "succeeded",
|
|
reportedWorkDisposition: "done",
|
|
};
|
|
|
|
const yieldedResult: PrpStructuredRunResult = {
|
|
schema: "paperclip.run_result.v1",
|
|
reportedWorkDisposition: "yielded",
|
|
summary: "Waiting for the requested response.",
|
|
completionClaim: {
|
|
contractRevision: "1",
|
|
objectiveSatisfied: false,
|
|
criteria: [
|
|
{
|
|
criterionId: "objective",
|
|
status: "unknown",
|
|
evidenceRefs: ["interaction:pending"],
|
|
},
|
|
],
|
|
remainingWork: [
|
|
{ description: "Resume after the response.", blocksCompletion: true },
|
|
],
|
|
},
|
|
evidence: [{ ref: "interaction:pending" }],
|
|
verification: [],
|
|
attentionRequests: [],
|
|
artifacts: [{ kind: "issue_thread_interaction", ref: "interaction:pending" }],
|
|
continuation: {
|
|
kind: "response_wake",
|
|
summary: "Resume from the answer.",
|
|
idempotencyKey: "interaction-response:pending",
|
|
},
|
|
};
|
|
|
|
const input: NativeExecutionInputV1 = {
|
|
schema: "paperclip.native-execution-input.v1",
|
|
binding: {
|
|
companyId: identity.companyId,
|
|
runId: identity.runId,
|
|
issueId: identity.issueId,
|
|
agentId: identity.agentId,
|
|
executionWorkspaceId: "workspace-recovery",
|
|
},
|
|
task: {
|
|
identifier: "PAP-RECOVERY",
|
|
title: "Recover native work",
|
|
description: null,
|
|
prompt: "# PAP-RECOVERY: Recover native work",
|
|
workMode: "standard",
|
|
},
|
|
workspace: {
|
|
cwd: "/workspace",
|
|
repoUrl: null,
|
|
repoRef: null,
|
|
branchName: null,
|
|
},
|
|
session: {
|
|
normalizedSessionId: identity.sessionId,
|
|
driverKind: "codex_app_server",
|
|
protocolVersion: 1,
|
|
},
|
|
provider: { kind: "codex", model: null },
|
|
completionContract: {
|
|
id: "contract-recovery",
|
|
sha256: "contract-recovery-sha",
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
contract: {
|
|
revision: "1",
|
|
objective: "Recover native work",
|
|
criteria: [{ id: "objective", requirement: "Complete after recovery" }],
|
|
},
|
|
},
|
|
interactionResponses: [],
|
|
credentialBindings: [],
|
|
};
|
|
|
|
function preparedInput(): NativeExecutionInputV5 {
|
|
const digest = "0".repeat(64);
|
|
const context = {
|
|
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
|
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
|
|
skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null },
|
|
} as const;
|
|
const requirement = input.completionContract.contract.criteria[0]!.requirement;
|
|
const prompt = `${input.task.prompt}\n\n${requirement}`;
|
|
return {
|
|
...input, schema: "paperclip.native-execution-input.v5", executionMode: "default", planningContext: null,
|
|
task: { ...input.task, description: requirement, prompt },
|
|
provider: { kind: "codex", model: null, approvalPolicy: "never" },
|
|
runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) },
|
|
completionSources: {
|
|
promptSha256: createHash("sha256").update(prompt).digest("hex"),
|
|
contractRevision: input.completionContract.contract.revision,
|
|
criteria: [{ id: "objective", source: { kind: "description", id: identity.issueId, revision: createHash("sha256").update(requirement).digest("hex") } }],
|
|
},
|
|
};
|
|
}
|
|
|
|
function controlEvent(
|
|
sourceSeq: number,
|
|
eventType: PrpEvent["eventType"],
|
|
payload: Record<string, unknown>,
|
|
): PrpEvent {
|
|
return {
|
|
schema: "paperclip.prp.event.v1",
|
|
sourceEventId: `control-recovery:${identity.runId}:${sourceSeq}`,
|
|
sourceSeq,
|
|
sourceInstanceId: "control-recovery",
|
|
sourceKind: "control_plane",
|
|
runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId,
|
|
turnId: "turn-recovery",
|
|
eventType,
|
|
schemaVersion: 1,
|
|
priority: 0,
|
|
emittedAt: "2026-08-09T00:00:00.000Z",
|
|
payload,
|
|
};
|
|
}
|
|
|
|
function canonicalTestJson(value: unknown): string {
|
|
if (Array.isArray(value)) {
|
|
return `[${value.map(canonicalTestJson).join(",")}]`;
|
|
}
|
|
if (typeof value === "object" && value !== null) {
|
|
const record = value as Record<string, unknown>;
|
|
return `{${Object.keys(record)
|
|
.sort()
|
|
.map((key) => `${JSON.stringify(key)}:${canonicalTestJson(record[key])}`)
|
|
.join(",")}}`;
|
|
}
|
|
return JSON.stringify(value) ?? "undefined";
|
|
}
|
|
|
|
function runnerEvent(
|
|
sourceSeq: number,
|
|
eventType: PrpEvent["eventType"],
|
|
payload: Record<string, unknown> = {},
|
|
): PrpEvent {
|
|
return {
|
|
schema: "paperclip.prp.event.v1",
|
|
sourceEventId: `runner-recovery:${identity.runId}:${sourceSeq}`,
|
|
sourceSeq,
|
|
sourceInstanceId: "runner-recovery",
|
|
sourceKind: "runner",
|
|
runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId,
|
|
turnId: "turn-recovery",
|
|
eventType,
|
|
schemaVersion: 1,
|
|
priority: 0,
|
|
emittedAt: "2026-08-09T00:00:00.000Z",
|
|
payload,
|
|
};
|
|
}
|
|
|
|
function highestContiguous(events: PrpEvent[]): number {
|
|
const sequences = new Set(events.map((event) => event.sourceSeq));
|
|
let cursor = 0;
|
|
while (sequences.has(cursor + 1)) cursor += 1;
|
|
return cursor;
|
|
}
|
|
|
|
describe("executeNativeSession recovery", () => {
|
|
it.each([
|
|
"completed", "pending", "duplicate", "conflicting", "other turn", "newer turn",
|
|
"wrong session", "invalid",
|
|
])("recovers a journaled completion proposal after restart: %s", async (scenario) => {
|
|
const originalTransport = new FakeCodexTransport();
|
|
const recoveredTransport = new FakeCodexTransport();
|
|
const transports = [originalTransport, recoveredTransport];
|
|
const executionInput = { ...input, workspace: { ...input.workspace, cwd: WORKSPACE } };
|
|
const backend = createCodexNativeSessionBackend(executionInput, {
|
|
runnerInstanceId: "runner-recovery",
|
|
transportFactory: () => transports.shift()!,
|
|
});
|
|
const original = await backend.openSession({ identity, workingDirectory: WORKSPACE });
|
|
const journal: PrpEvent[] = [];
|
|
const iterator = original.events()[Symbol.asyncIterator]();
|
|
const drainThrough = async (eventType: string) => {
|
|
while (true) {
|
|
const next = await iterator.next();
|
|
if (next.done) throw new Error(`stream closed before ${eventType}`);
|
|
journal.push(structuredClone(next.value));
|
|
if (next.value.eventType === eventType) return;
|
|
}
|
|
};
|
|
await original.startTurn({ message: { role: "user", text: "Finish the assigned review." } });
|
|
originalTransport.push("turn/started", {
|
|
threadId: "thread-1", turn: { id: "turn-1", status: "inProgress" },
|
|
});
|
|
await drainThrough("turn.started");
|
|
const checkpoint = await original.snapshot();
|
|
expect(checkpoint.semanticResult).toBeNull();
|
|
originalTransport.push("paperclip/runResult", {
|
|
threadId: "thread-1", turnId: "turn-1", result,
|
|
});
|
|
await drainThrough("run.result.proposed");
|
|
expect((await original.snapshot()).semanticResult).toEqual(result);
|
|
const proposal = journal.at(-1)!;
|
|
if (scenario === "other turn") proposal.turnId = "prior-turn";
|
|
if (scenario === "wrong session") proposal.normalizedSessionId = "other-session";
|
|
if (scenario === "invalid") proposal.payload = { reportedWorkDisposition: "done" };
|
|
if (["duplicate", "conflicting", "newer turn"].includes(scenario)) {
|
|
journal.push({
|
|
...proposal, sourceSeq: proposal.sourceSeq + 1,
|
|
sourceEventId: `${proposal.sourceEventId}:next`,
|
|
...(scenario === "newer turn"
|
|
? { eventType: "turn.started" as const, turnId: "newer-turn", payload: {} }
|
|
: { payload: scenario === "conflicting" ? { ...result, summary: "A different result." } : result }),
|
|
});
|
|
}
|
|
const persistedProposalCount = journal.filter(event => event.eventType === "run.result.proposed").length;
|
|
await original.close({ reason: "controller disconnected after persisting the proposal" });
|
|
recoveredTransport.readResponse = { thread: {
|
|
id: "thread-1", sessionId: "provider-session-1", cwd: WORKSPACE,
|
|
turns: [{ id: "turn-1", status: scenario === "pending" ? "inProgress" : "completed", items: [
|
|
{ id: "answer", type: "agentMessage", text: "The review was published." },
|
|
] }],
|
|
} };
|
|
const completeRun = vi.fn(async () => undefined);
|
|
const checkpoints: PersistedNativeSession[] = [];
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async loadSessionCheckpoint() { return structuredClone(checkpoint); },
|
|
async checkpointSession(value) { checkpoints.push(structuredClone(value)); },
|
|
async appendEvent(event) {
|
|
const existing = journal.find(value => value.sourceEventId === event.sourceEventId);
|
|
if (existing) expect(event).toEqual(existing);
|
|
else journal.push(structuredClone(event as PrpEvent));
|
|
return { cursor: journal.length, highestContiguousSourceSeq: event.sourceSeq,
|
|
disposition: existing ? "duplicate" : "committed" };
|
|
},
|
|
async replayEvents(query) {
|
|
const events = journal.filter(event => event.sourceInstanceId === query.sourceInstanceId);
|
|
return {
|
|
events: structuredClone(events.filter(event => event.sourceSeq > query.afterSourceSeq).slice(0, query.limit)),
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
};
|
|
},
|
|
completeRun,
|
|
};
|
|
const execution = executeNativeSession({
|
|
input: executionInput, backend, controlPlane: port,
|
|
runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
if (["conflicting", "wrong session", "invalid"].includes(scenario)) {
|
|
await expect(execution).rejects.toBeInstanceOf(NativeSessionProtocolIntegrityError);
|
|
expect(completeRun).not.toHaveBeenCalled();
|
|
expect(recoveredTransport.calls).toEqual([]);
|
|
return;
|
|
}
|
|
if (["other turn", "newer turn"].includes(scenario)) {
|
|
await expect(execution).rejects.toThrow("native_finalization_missing");
|
|
expect(completeRun).not.toHaveBeenCalled();
|
|
expect(recoveredTransport.calls.filter(call => call.method === "turn/start")).toEqual([]);
|
|
return;
|
|
}
|
|
if (scenario === "pending") {
|
|
await vi.waitFor(() => expect(checkpoints[0]?.semanticResult).toEqual(result));
|
|
expect(completeRun).not.toHaveBeenCalled();
|
|
recoveredTransport.push("turn/completed", {
|
|
threadId: "thread-1", turn: { id: "turn-1", status: "completed", items: [] },
|
|
});
|
|
}
|
|
await expect(execution).resolves.toMatchObject({ result, turnId: "turn-1", terminal });
|
|
expect(completeRun).toHaveBeenCalledExactlyOnceWith(
|
|
expect.objectContaining({ result, turnId: "turn-1" }),
|
|
expect.anything(),
|
|
);
|
|
expect(recoveredTransport.calls.filter(call => call.method === "turn/start")).toEqual([]);
|
|
expect(journal.filter(event => event.eventType === "run.result.proposed")).toHaveLength(persistedProposalCount);
|
|
expect(checkpoints.at(-1)).toMatchObject({ semanticResult: result, activeTurnId: null });
|
|
});
|
|
|
|
it.each([
|
|
{ label: "continues a restart-lost Codex turn", allowed: true, error: { code: "provider_turn_lost_on_restore", recoverable: true } },
|
|
{ label: "requires the control plane restart claim", allowed: false, error: { code: "provider_turn_lost_on_restore", recoverable: true } },
|
|
{ label: "preserves real provider errors", allowed: true, error: { code: "model_not_found", recoverable: false } },
|
|
{ label: "does not reopen historical error-null failures", allowed: true, error: null },
|
|
])("$label without replaying the task", async ({ label, allowed, error }) => {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "runner", driverKind: "codex_app_server", sessionId: "thread-recovery", identity,
|
|
providerSessionId: "provider-recovery", cursor: "0", activeTurnId: "turn-recovery",
|
|
terminalTurns: [], semanticResult: null, pendingRuntimeRequests: [], lineage: [],
|
|
};
|
|
const failure = error;
|
|
const interrupted: PersistedNativeSession = {
|
|
...checkpoint, activeTurnId: null,
|
|
terminalTurns: [{ turnId: "turn-recovery", fingerprint: JSON.stringify({ terminalState: "failed", error: failure, result: null }) }],
|
|
};
|
|
const startTurn = vi.fn<NativeSession["startTurn"]>(async () => ({ turnId: "turn-continuation" }));
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() { return { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.failed", { status: "failed", error: failure });
|
|
if (!startTurn.mock.calls.length) return;
|
|
yield { ...runnerEvent(2, "turn.completed", { status: "completed" }), turnId: "turn-continuation" };
|
|
},
|
|
startTurn,
|
|
async result() { return startTurn.mock.calls.length ? { result, terminal, turnId: "turn-continuation" } : null; },
|
|
async snapshot() { return structuredClone(interrupted); },
|
|
async close() {},
|
|
};
|
|
const events: PrpEvent[] = [];
|
|
const checkpoints: PersistedNativeSession[] = [];
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() { return { kind: "runner", name: "restart", version: "1", capabilities: await session.capabilities() }; },
|
|
async openSession() { throw new Error("must preserve the provider session"); },
|
|
async recoverSession() { return { recovered: true, session }; },
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession(snapshot) { checkpoints.push(structuredClone(snapshot)); },
|
|
async appendEvent(event) { events.push(event); return { cursor: events.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
|
async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; },
|
|
async completeRun() {},
|
|
};
|
|
const execution = executeNativeSession({
|
|
input, backend, controlPlane: port, persistedSession: checkpoint,
|
|
runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery",
|
|
resumeInterruptedTurn: allowed,
|
|
});
|
|
if (label !== "continues a restart-lost Codex turn") {
|
|
await expect(execution).rejects.toMatchObject({ code: "native_provider_terminal_failed" });
|
|
expect(startTurn).not.toHaveBeenCalled();
|
|
return;
|
|
}
|
|
await expect(execution).resolves.toMatchObject({ result, turnId: "turn-continuation", terminal: { runTerminalState: "succeeded" } });
|
|
expect(startTurn).toHaveBeenCalledOnce();
|
|
const submitted = startTurn.mock.calls[0]![0] as Parameters<NativeSession["startTurn"]>[0];
|
|
expect(submitted.continuation).toBe(true);
|
|
expect(submitted.message.text).toContain("Reconcile any unfinished tool or command");
|
|
expect(submitted.message.text).not.toContain(input.task.prompt);
|
|
expect(events.filter(event => event.eventType === "turn.failed")).toHaveLength(1);
|
|
expect(events.filter(event => event.eventType === "run.terminal")).toHaveLength(1);
|
|
expect(checkpoints.some(snapshot => snapshot.terminalTurns?.some(turn => turn.turnId === "turn-recovery"))).toBe(true);
|
|
});
|
|
|
|
it.each([undefined, 0, 7 * 24 * 60 * 60 * 1000, 30 * 24 * 60 * 60 * 1000])(
|
|
"honors long-lived turn duration independently of operation bounds (%s)",
|
|
async (turnTimeoutMs) => {
|
|
vi.useFakeTimers();
|
|
let finish = () => {};
|
|
const waiting = new Promise<void>((resolve) => { finish = resolve; });
|
|
const capabilities = {
|
|
resume: true, typedEvents: true, steering: false,
|
|
interruption: true, structuredResult: true,
|
|
};
|
|
const cancel = vi.fn(async () => { finish(); });
|
|
const close = vi.fn(async () => { finish(); });
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() { return capabilities; },
|
|
async *events() {
|
|
yield runnerEvent(1, "tool.execution.started", { name: "wait for CI", status: "running" });
|
|
await waiting;
|
|
yield runnerEvent(2, "turn.completed");
|
|
},
|
|
async startTurn() { return { turnId: "turn-recovery" }; },
|
|
async result() { return { result, terminal, turnId: "turn-recovery" }; },
|
|
async snapshot() {
|
|
return { backendKind: "mock", sessionId: "driver-recovery", identity,
|
|
providerSessionId: "provider-recovery", cursor: null, activeTurnId: null,
|
|
pendingRuntimeRequests: [], lineage: [] };
|
|
},
|
|
cancel, close,
|
|
};
|
|
const appendEvent = vi.fn<ControlPlanePort["appendEvent"]>(async event => ({
|
|
cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq,
|
|
disposition: "committed",
|
|
}));
|
|
const completeRun = vi.fn(async () => {});
|
|
try {
|
|
const execution = executeNativeSession({
|
|
input, turnTimeoutMs,
|
|
backend: {
|
|
async descriptor() { return { kind: "mock", name: "long-lived", version: "1", capabilities }; },
|
|
async openSession() { return session; },
|
|
},
|
|
controlPlane: {
|
|
async openRun() {}, async checkpointSession() {}, appendEvent,
|
|
async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; },
|
|
completeRun,
|
|
},
|
|
runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
// Observe rejection before advancing the clock, including on the old implementation.
|
|
let failure: unknown;
|
|
const observed = execution.catch(error => { failure = error; return null; });
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
expect(appendEvent).toHaveBeenCalledOnce();
|
|
await vi.advanceTimersByTimeAsync(6 * 24 * 60 * 60 * 1000);
|
|
expect(failure).toBeUndefined();
|
|
expect(cancel).not.toHaveBeenCalled();
|
|
expect(close).not.toHaveBeenCalled();
|
|
expect(completeRun).not.toHaveBeenCalled();
|
|
if (turnTimeoutMs) {
|
|
// Includes a 30-day bound beyond Node's single-timer maximum.
|
|
await vi.advanceTimersByTimeAsync(turnTimeoutMs - 6 * 24 * 60 * 60 * 1000 - 1);
|
|
expect(failure).toBeUndefined();
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
await observed;
|
|
expect(failure).toBeInstanceOf(Error);
|
|
expect((failure as Error).message).toContain(`native session timed out after ${turnTimeoutMs}ms`);
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
expect(completeRun).not.toHaveBeenCalled();
|
|
} else {
|
|
await vi.advanceTimersByTimeAsync(2 * 24 * 60 * 60 * 1000);
|
|
finish();
|
|
expect(await observed).toMatchObject({ result });
|
|
expect(completeRun).toHaveBeenCalledOnce();
|
|
expect(cancel).not.toHaveBeenCalled();
|
|
}
|
|
} finally {
|
|
finish();
|
|
vi.useRealTimers();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([false, true])("preserves a durable session failure when its stream closes (throws=%s)", async throws => {
|
|
const capabilities = { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true };
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() { return capabilities; },
|
|
async *events() {
|
|
yield runnerEvent(1, "session.failed", { error: { code: "notification_transport_failed" }, recoverable: false });
|
|
if (throws) throw new Error("provider stdout closed");
|
|
},
|
|
async startTurn() { return { turnId: "turn-recovery" }; },
|
|
async result() { return null; },
|
|
async snapshot() { return { backendKind: "mock", sessionId: "driver-recovery", identity, providerSessionId: "provider-recovery", cursor: null, activeTurnId: null, pendingRuntimeRequests: [], lineage: [] }; },
|
|
async close() {},
|
|
};
|
|
const completeRun = vi.fn();
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() { return { kind: "mock", name: "failure-fixture", version: "1", capabilities }; },
|
|
async openSession() { return session; },
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {}, async checkpointSession() {},
|
|
async appendEvent() { return { cursor: 1, highestContiguousSourceSeq: 1, disposition: "committed" }; },
|
|
async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; }, completeRun,
|
|
};
|
|
await expect(executeNativeSession({ input, backend, controlPlane: port, runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery" }))
|
|
.rejects.toMatchObject({ code: "native_provider_terminal_failed", providerCode: "notification_transport_failed", recoverable: false });
|
|
expect(completeRun).not.toHaveBeenCalled();
|
|
});
|
|
it.each((["complete", "paused", "blocked", "limited", "usageLimited", "budgetLimited"] as const)
|
|
.flatMap((status) => [false, true].map((snapshotBeforeUpdate) => ({ status, snapshotBeforeUpdate }))))(
|
|
"handles a new chat turn instead of completing it from an existing $status goal (snapshot: $snapshotBeforeUpdate)", async ({ status, snapshotBeforeUpdate }) => {
|
|
const oldGoal = {
|
|
threadId: "provider-recovery",
|
|
objective: "Say hello",
|
|
status,
|
|
tokenBudget: null,
|
|
tokensUsed: 500,
|
|
timeUsedSeconds: 2,
|
|
createdAt: Date.parse("2026-08-09T00:00:00.000Z"),
|
|
updatedAt: Date.parse("2026-08-09T00:00:02.000Z"),
|
|
};
|
|
const reply = { ...result, summary: "Said bye in response to the new message." };
|
|
const capabilities = {
|
|
resume: true, typedEvents: true, steering: false,
|
|
interruption: true, structuredResult: true,
|
|
};
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: oldGoal.threadId,
|
|
activeTurnId: null,
|
|
semanticResult: null,
|
|
terminal: null,
|
|
terminalTurns: [],
|
|
pendingRuntimeRequests: [],
|
|
goal: { ...oldGoal, createdAt: oldGoal.createdAt / 1000, updatedAt: oldGoal.updatedAt / 1000 },
|
|
};
|
|
const startTurn = vi.fn<NativeSession["startTurn"]>(async () => ({ turnId: "turn-recovery" }));
|
|
const goal = vi.fn(async () => oldGoal);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() { return capabilities; },
|
|
async *events() {
|
|
let seq = 0;
|
|
// The resume snapshot is durable UI state, not work for this prompt.
|
|
if (snapshotBeforeUpdate) yield runnerEvent(++seq, "session.goal.snapshot", {
|
|
goal: {
|
|
...oldGoal,
|
|
createdAt: new Date(oldGoal.createdAt).toISOString(),
|
|
elapsedSeconds: oldGoal.timeUsedSeconds,
|
|
},
|
|
workingNow: false,
|
|
});
|
|
// Codex replays the unchanged goal as an update during resume too.
|
|
// Usage-only changes do not make an inactive goal own a new prompt.
|
|
yield runnerEvent(++seq, "session.goal.updated", {
|
|
goal: {
|
|
...oldGoal,
|
|
createdAt: new Date(oldGoal.createdAt).toISOString(),
|
|
tokensUsed: 600,
|
|
updatedAt: new Date(oldGoal.updatedAt + 1000).toISOString(),
|
|
},
|
|
workingNow: false,
|
|
});
|
|
yield runnerEvent(++seq, "turn.started");
|
|
yield runnerEvent(++seq, "run.result.proposed", reply);
|
|
yield runnerEvent(++seq, "turn.completed");
|
|
},
|
|
startTurn,
|
|
goal,
|
|
async result() { return { result: reply, terminal, turnId: "turn-recovery" }; },
|
|
async snapshot() { return checkpoint; },
|
|
async close() {},
|
|
};
|
|
const appended: PrpEvent[] = [];
|
|
const digest = "0".repeat(64);
|
|
const context = {
|
|
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
|
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
|
|
skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null },
|
|
} as const;
|
|
const completed = await executeNativeSession({
|
|
input: snapshotBeforeUpdate ? {
|
|
...input, schema: "paperclip.native-execution-input.v4", executionMode: "default", planningContext: null,
|
|
provider: { kind: "codex", model: null, approvalPolicy: "never" },
|
|
runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) },
|
|
continuationPrompt: "Say bye",
|
|
} : { ...input, task: { ...input.task, prompt: "Say bye" } },
|
|
backend: {
|
|
async descriptor() {
|
|
return { kind: "mock", name: "chat-after-goal", version: "1", capabilities,
|
|
runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" } };
|
|
},
|
|
async openSession() { throw new Error("must resume the same provider session"); },
|
|
async recoverSession() { return { recovered: true, session }; },
|
|
},
|
|
persistedSession: checkpoint,
|
|
controlPlane: {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
appended.push(event);
|
|
return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" };
|
|
},
|
|
async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; },
|
|
async completeRun() {},
|
|
},
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 1000,
|
|
});
|
|
expect(startTurn).toHaveBeenCalledOnce();
|
|
const submitted = startTurn.mock.calls[0]![0]!;
|
|
if (snapshotBeforeUpdate) {
|
|
expect(submitted.continuation).toBe(true);
|
|
expect(JSON.parse(submitted.message.text)).toMatchObject({ schema: "paperclip.native-continuation.v1", events: "Say bye",
|
|
completion: { instruction: expect.stringContaining("Earlier reports belong to earlier turns") } });
|
|
} else {
|
|
expect(submitted).not.toHaveProperty("continuation");
|
|
expect(JSON.parse(submitted.message.text).task.prompt).toBe("Say bye");
|
|
}
|
|
expect(goal).not.toHaveBeenCalled();
|
|
expect(completed.providerSessionId).toBe(oldGoal.threadId);
|
|
expect(completed.result).toEqual(reply);
|
|
expect(appended.map((event) => event.eventType)).toEqual([
|
|
...(snapshotBeforeUpdate ? ["session.goal.snapshot"] : []),
|
|
"session.goal.updated", "turn.started", "run.result.proposed", "turn.completed",
|
|
"run.result.accepted", "run.terminal",
|
|
]);
|
|
});
|
|
|
|
it("applies a session goal control without starting an ordinary turn", async () => {
|
|
const activeGoal = {
|
|
threadId: "provider-recovery",
|
|
objective: "Verify goal mode",
|
|
status: "active" as const,
|
|
tokenBudget: 12_000,
|
|
tokensUsed: 100,
|
|
timeUsedSeconds: 1,
|
|
createdAt: Date.parse("2026-08-09T00:00:00.000Z"),
|
|
updatedAt: Date.parse("2026-08-09T00:00:01.000Z"),
|
|
};
|
|
const completeGoal = {
|
|
...activeGoal,
|
|
status: "complete" as const,
|
|
tokensUsed: 500,
|
|
timeUsedSeconds: 2,
|
|
updatedAt: Date.parse("2026-08-09T00:00:02.000Z"),
|
|
};
|
|
const startTurn = vi.fn(async () => ({ turnId: "turn-recovery" }));
|
|
const goal = vi.fn(async (operation: Parameters<NonNullable<NativeSession["goal"]>>[0]) =>
|
|
operation.action === "get" ? completeGoal : activeGoal,
|
|
);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "session.goal.snapshot", {
|
|
goal: null,
|
|
workingNow: false,
|
|
});
|
|
yield runnerEvent(2, "session.goal.updated", {
|
|
requestId: "goal-create",
|
|
goal: {
|
|
objective: activeGoal.objective,
|
|
status: activeGoal.status,
|
|
tokenBudget: activeGoal.tokenBudget,
|
|
tokensUsed: activeGoal.tokensUsed,
|
|
elapsedSeconds: activeGoal.timeUsedSeconds,
|
|
},
|
|
workingNow: false,
|
|
});
|
|
yield runnerEvent(3, "turn.started");
|
|
yield runnerEvent(4, "run.result.proposed", result);
|
|
yield runnerEvent(5, "turn.completed");
|
|
yield runnerEvent(6, "session.goal.snapshot", {
|
|
goal: {
|
|
objective: completeGoal.objective,
|
|
status: completeGoal.status,
|
|
tokenBudget: completeGoal.tokenBudget,
|
|
tokensUsed: completeGoal.tokensUsed,
|
|
elapsedSeconds: completeGoal.timeUsedSeconds,
|
|
},
|
|
workingNow: false,
|
|
});
|
|
},
|
|
startTurn,
|
|
goal,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
goal: completeGoal,
|
|
lineage: [],
|
|
};
|
|
},
|
|
async close() {},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "goal-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const appended: PrpEvent[] = [];
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
appended.push(event);
|
|
return {
|
|
cursor: event.sourceSeq,
|
|
highestContiguousSourceSeq: event.sourceSeq,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const completed = await executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
sessionGoalControl: {
|
|
requestId: "goal-create",
|
|
action: "create",
|
|
objective: activeGoal.objective,
|
|
tokenBudget: activeGoal.tokenBudget,
|
|
},
|
|
});
|
|
|
|
expect(startTurn).not.toHaveBeenCalled();
|
|
expect(goal).toHaveBeenNthCalledWith(1, {
|
|
action: "set",
|
|
objective: activeGoal.objective,
|
|
status: "active",
|
|
requestId: "goal-create",
|
|
tokenBudget: activeGoal.tokenBudget,
|
|
});
|
|
expect(goal).toHaveBeenNthCalledWith(2, { action: "get" });
|
|
expect(appended.map((event) => event.eventType)).toContain("session.goal.snapshot");
|
|
expect(completed.result).toMatchObject({
|
|
reportedWorkDisposition: "done",
|
|
summary: result.summary,
|
|
completionClaim: { objectiveSatisfied: true },
|
|
});
|
|
});
|
|
|
|
it.each((["complete", "paused", "blocked", "limited", "usageLimited", "budgetLimited"] as const)
|
|
.flatMap((status) => [false, true].map((lateGoal) => ({ status, lateGoal }))))(
|
|
"reconciles an out-of-band $status goal (after semantic result: $lateGoal)", async ({ status, lateGoal }) => {
|
|
const activeGoal = {
|
|
threadId: "provider-agent-goal",
|
|
objective: "Finish autonomous work",
|
|
status: "active" as const,
|
|
tokenBudget: null,
|
|
tokensUsed: 100,
|
|
timeUsedSeconds: 1,
|
|
createdAt: Date.parse("2026-08-09T00:00:00.000Z"),
|
|
updatedAt: Date.parse("2026-08-09T00:00:01.000Z"),
|
|
};
|
|
const completeGoal = {
|
|
...activeGoal,
|
|
status,
|
|
tokensUsed: 500,
|
|
timeUsedSeconds: 2,
|
|
updatedAt: Date.parse("2026-08-09T00:00:02.000Z"),
|
|
};
|
|
const startTurn = vi.fn(async () => ({ turnId: "turn-agent-goal" }));
|
|
const goal = vi.fn(async () => completeGoal);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
if (lateGoal) yield runnerEvent(1, "run.result.proposed", result);
|
|
yield runnerEvent(lateGoal ? 2 : 1, "session.goal.updated", {
|
|
goal: {
|
|
objective: activeGoal.objective,
|
|
status: activeGoal.status,
|
|
tokenBudget: activeGoal.tokenBudget,
|
|
tokensUsed: activeGoal.tokensUsed,
|
|
elapsedSeconds: activeGoal.timeUsedSeconds,
|
|
},
|
|
workingNow: true,
|
|
});
|
|
if (!lateGoal) yield runnerEvent(2, "run.result.proposed", result);
|
|
// A newly observed goal owns the lifetime even when the completion
|
|
// proposal arrived first.
|
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
yield runnerEvent(3, "session.goal.updated", {
|
|
goal: {
|
|
objective: completeGoal.objective,
|
|
status: completeGoal.status,
|
|
tokenBudget: completeGoal.tokenBudget,
|
|
tokensUsed: completeGoal.tokensUsed,
|
|
elapsedSeconds: completeGoal.timeUsedSeconds,
|
|
},
|
|
workingNow: true,
|
|
});
|
|
yield runnerEvent(4, "turn.completed");
|
|
},
|
|
startTurn,
|
|
goal,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-agent-goal",
|
|
identity,
|
|
providerSessionId: activeGoal.threadId,
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
goal: activeGoal,
|
|
lineage: [],
|
|
};
|
|
},
|
|
async close() {},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "agent-goal-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const appended: PrpEvent[] = [];
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
appended.push(event);
|
|
return {
|
|
cursor: event.sourceSeq,
|
|
highestContiguousSourceSeq: event.sourceSeq,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const completed = await executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-agent-goal",
|
|
controlPlaneInstanceId: "control-agent-goal",
|
|
});
|
|
|
|
expect(startTurn).toHaveBeenCalledOnce();
|
|
expect(goal).not.toHaveBeenCalled();
|
|
expect(appended.map((event) => event.eventType)).toEqual([
|
|
...(lateGoal ? ["run.result.proposed", "session.goal.updated"] : ["session.goal.updated", "run.result.proposed"]),
|
|
"session.goal.updated",
|
|
"turn.completed",
|
|
"run.result.accepted",
|
|
"run.terminal",
|
|
]);
|
|
expect(completed.result).toMatchObject({
|
|
reportedWorkDisposition: status === "complete" ? "done" : status === "blocked" ? "blocked" : "yielded",
|
|
...(status === "complete" ? { summary: result.summary } : {}),
|
|
completionClaim: { objectiveSatisfied: status === "complete" },
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{ keepSessionOpen: false, recoveryOnly: false },
|
|
{ keepSessionOpen: true, recoveryOnly: false },
|
|
{ keepSessionOpen: false, recoveryOnly: true },
|
|
{ keepSessionOpen: true, recoveryOnly: true },
|
|
].flatMap((options) => [false, true].map((cleared) => ({ ...options, cleared }))))("reconciles goal recovery without replaying completed controls (warm=$keepSessionOpen, recovery=$recoveryOnly, cleared=$cleared)", async ({ keepSessionOpen, recoveryOnly, cleared }) => {
|
|
const pausedGoal = {
|
|
threadId: "provider-recovery",
|
|
objective: "Verify recovered goal control",
|
|
status: recoveryOnly ? "complete" as const : "paused" as const,
|
|
tokenBudget: null,
|
|
tokensUsed: 250,
|
|
timeUsedSeconds: 2,
|
|
createdAt: Date.parse("2026-08-09T00:00:00.000Z"),
|
|
updatedAt: Date.parse("2026-08-09T00:00:02.000Z"),
|
|
};
|
|
const startTurn = vi.fn(async () => ({ turnId: "turn-recovery" }));
|
|
const goal = vi.fn(async () => cleared ? null : pausedGoal);
|
|
const requestId = recoveryOnly ? `recovery_${input.binding.runId}` : cleared ? "goal-clear" : "goal-pause";
|
|
const close = vi.fn(async () => {});
|
|
let snapshotCount = 0;
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, cleared ? "session.goal.cleared" : "session.goal.updated", {
|
|
requestId,
|
|
goal: cleared ? null : {
|
|
objective: pausedGoal.objective,
|
|
status: pausedGoal.status,
|
|
tokenBudget: pausedGoal.tokenBudget,
|
|
tokensUsed: pausedGoal.tokensUsed,
|
|
elapsedSeconds: pausedGoal.timeUsedSeconds,
|
|
},
|
|
workingNow: !recoveryOnly,
|
|
});
|
|
yield runnerEvent(2, "turn.completed");
|
|
yield runnerEvent(3, "session.goal.snapshot", {
|
|
goal: cleared ? null : {
|
|
objective: pausedGoal.objective,
|
|
status: pausedGoal.status,
|
|
tokenBudget: pausedGoal.tokenBudget,
|
|
tokensUsed: pausedGoal.tokensUsed,
|
|
elapsedSeconds: pausedGoal.timeUsedSeconds,
|
|
},
|
|
workingNow: false,
|
|
});
|
|
},
|
|
startTurn,
|
|
goal,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
snapshotCount += 1;
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: !recoveryOnly && snapshotCount === 1 ? "turn-recovery" : null,
|
|
pendingRuntimeRequests: [],
|
|
goal: cleared ? null : pausedGoal,
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const persistedSession: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: "turn-recovery",
|
|
pendingRuntimeRequests: [],
|
|
goal: { ...pausedGoal, status: "active" },
|
|
lineage: [],
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "goal-recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("fresh session must not be opened");
|
|
},
|
|
async recoverSession() {
|
|
return { recovered: true, session };
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
return {
|
|
cursor: event.sourceSeq,
|
|
highestContiguousSourceSeq: event.sourceSeq,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const completed = await executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
persistedSession,
|
|
keepSessionOpen,
|
|
requireSessionCloseBeforeReturn: true,
|
|
resumeSessionGoalHeartbeat: recoveryOnly,
|
|
sessionGoalControl: recoveryOnly ? null : {
|
|
requestId,
|
|
action: cleared ? "clear" : "pause",
|
|
},
|
|
});
|
|
|
|
expect(startTurn).not.toHaveBeenCalled();
|
|
expect(goal).toHaveBeenNthCalledWith(1, {
|
|
action: recoveryOnly ? "get" : cleared ? "clear" : "pause",
|
|
requestId,
|
|
});
|
|
expect(goal).toHaveBeenCalledTimes(cleared && !recoveryOnly ? 2 : 1);
|
|
if (cleared && !recoveryOnly) {
|
|
expect(goal).toHaveBeenNthCalledWith(2, { action: "get" });
|
|
}
|
|
expect(close).toHaveBeenCalledTimes(1);
|
|
expect(completed.result).toMatchObject({
|
|
reportedWorkDisposition: recoveryOnly && !cleared ? "done" : "yielded",
|
|
completionClaim: { objectiveSatisfied: recoveryOnly && !cleared },
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
recoverable: false,
|
|
message:
|
|
"There's an issue with the selected model (custom-model). It may not exist or you may not have access to it.",
|
|
modelRejected: true,
|
|
},
|
|
{
|
|
recoverable: true,
|
|
message:
|
|
"There's an issue with the selected model (custom-model). It may not exist or you may not have access to it.",
|
|
modelRejected: false,
|
|
},
|
|
{
|
|
recoverable: false,
|
|
message: "The model service failed while processing output.",
|
|
modelRejected: false,
|
|
},
|
|
])(
|
|
"preserves structured provider failure and model retry classification ($recoverable, $modelRejected)",
|
|
async ({ recoverable, message, modelRejected }) => {
|
|
const capabilities = {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
};
|
|
const close = vi.fn(async () => {});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return capabilities;
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.failed", {
|
|
error: { code: "RUNTIME", recoverable, message },
|
|
});
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "model-rejection",
|
|
version: "1",
|
|
capabilities,
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
const result = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
await expect(result).rejects.toThrow(message);
|
|
await expect(result).rejects.toMatchObject({
|
|
code: "native_provider_terminal_failed",
|
|
providerCode: "RUNTIME",
|
|
recoverable,
|
|
});
|
|
if (modelRejected) {
|
|
await expect(result).rejects.toThrow("native_provider_model_rejected:");
|
|
} else {
|
|
await expect(result).rejects.not.toThrow(
|
|
"native_provider_model_rejected",
|
|
);
|
|
}
|
|
expect(close).toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it("keeps governed-wait discovery synchronous", () => {
|
|
type GovernedWaitResolver = NonNullable<
|
|
ExecuteNativeSessionOptions["resolveGovernedWait"]
|
|
>;
|
|
const resolver: GovernedWaitResolver = () => null;
|
|
// An async resolver could retain control-plane mutation authority after
|
|
// execution settles, so the public boundary rejects it at compile time.
|
|
// @ts-expect-error governed-wait discovery must not return a promise
|
|
const asynchronousResolver: GovernedWaitResolver = async () => null;
|
|
|
|
expect(resolver).toBeTypeOf("function");
|
|
void asynchronousResolver;
|
|
});
|
|
|
|
it("preserves durable success while quarantined cleanup stays bounded", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
let executionCloseCount = 0;
|
|
let quarantineAttempt = 0;
|
|
const close = vi.fn(({ reason }: { reason: string }) => {
|
|
if (reason === "native session quarantined cleanup recovery") {
|
|
quarantineAttempt += 1;
|
|
const attempt = quarantineAttempt;
|
|
return new Promise<void>((resolve, reject) => {
|
|
setTimeout(() => {
|
|
if (attempt < 3)
|
|
reject(new Error("transient quarantine failure"));
|
|
else resolve();
|
|
}, 6_500);
|
|
});
|
|
}
|
|
if (reason === "native session execution complete") {
|
|
executionCloseCount += 1;
|
|
if (executionCloseCount > 1) return Promise.resolve();
|
|
}
|
|
return Promise.reject(new Error("persistent close failure"));
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const openSession = vi.fn(async () => session);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
const execute = () =>
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
|
|
await expect(execute()).resolves.toMatchObject({ result });
|
|
expect(close).toHaveBeenCalledOnce();
|
|
await vi.advanceTimersByTimeAsync(3_000);
|
|
expect(close).toHaveBeenCalledTimes(5);
|
|
|
|
// Admission inherits the already-running three-attempt recovery and
|
|
// waits through its bounded attempts instead of timing out after one.
|
|
const recoveredExecution = execute();
|
|
let admissionSettled = false;
|
|
void recoveredExecution.then(
|
|
() => {
|
|
admissionSettled = true;
|
|
},
|
|
() => {
|
|
admissionSettled = true;
|
|
},
|
|
);
|
|
await vi.advanceTimersByTimeAsync(20_000);
|
|
expect(admissionSettled).toBe(false);
|
|
expect(openSession).toHaveBeenCalledTimes(1);
|
|
expect(close).toHaveBeenCalledTimes(7);
|
|
await vi.advanceTimersByTimeAsync(2_000);
|
|
await expect(recoveredExecution).resolves.toMatchObject({ result });
|
|
expect(quarantineAttempt).toBe(3);
|
|
expect(close).toHaveBeenCalledTimes(8);
|
|
expect(openSession).toHaveBeenCalledTimes(2);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("awaits the quarantine owner that replaces an exhausted close recovery", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
let executionCloseCount = 0;
|
|
const close = vi.fn(({ reason }: { reason: string }) => {
|
|
if (reason === "native session execution complete") {
|
|
executionCloseCount += 1;
|
|
return executionCloseCount === 1
|
|
? Promise.reject(new Error("initial close failed"))
|
|
: Promise.resolve();
|
|
}
|
|
if (
|
|
reason.startsWith(
|
|
"native session cleanup recovery after close failure",
|
|
)
|
|
) {
|
|
return Promise.reject(new Error("bounded close recovery failed"));
|
|
}
|
|
if (reason === "native session quarantined cleanup recovery") {
|
|
return new Promise<void>((resolve) => setTimeout(resolve, 50));
|
|
}
|
|
return Promise.resolve();
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const openSession = vi.fn(async () => session);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
const execute = () =>
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
|
|
await expect(execute()).resolves.toMatchObject({ result });
|
|
const admitted = execute();
|
|
await vi.advanceTimersByTimeAsync(3_100);
|
|
await expect(admitted).resolves.toMatchObject({ result });
|
|
expect(openSession).toHaveBeenCalledTimes(2);
|
|
expect(close).toHaveBeenCalledWith({
|
|
reason: "native session quarantined cleanup recovery",
|
|
});
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("covers maximum-duration retained and replacement recovery phases", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
let executionCloseCount = 0;
|
|
let retainedRecoveryAttempt = 0;
|
|
let quarantineRecoveryAttempt = 0;
|
|
const close = vi.fn(({ reason }: { reason: string }) => {
|
|
if (reason === "native session execution complete") {
|
|
executionCloseCount += 1;
|
|
if (executionCloseCount > 1) return Promise.resolve();
|
|
return new Promise<void>((_resolve, reject) => {
|
|
setTimeout(() => reject(new Error("initial close failed")), 6_900);
|
|
});
|
|
}
|
|
if (
|
|
reason.startsWith(
|
|
"native session cleanup recovery after close failure",
|
|
)
|
|
) {
|
|
retainedRecoveryAttempt += 1;
|
|
return new Promise<void>((_resolve, reject) => {
|
|
setTimeout(
|
|
() => reject(new Error("bounded retained recovery failed")),
|
|
6_900,
|
|
);
|
|
});
|
|
}
|
|
if (reason === "native session quarantined cleanup recovery") {
|
|
quarantineRecoveryAttempt += 1;
|
|
const attempt = quarantineRecoveryAttempt;
|
|
return new Promise<void>((resolve, reject) => {
|
|
setTimeout(() => {
|
|
if (attempt < 3)
|
|
reject(new Error("transient quarantine recovery failure"));
|
|
else resolve();
|
|
}, 6_500);
|
|
});
|
|
}
|
|
return Promise.resolve();
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const openSession = vi.fn(async () => session);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
const execute = () =>
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
|
|
const firstExecution = execute();
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
await expect(firstExecution).resolves.toMatchObject({ result });
|
|
const admitted = execute();
|
|
let admissionSettled = false;
|
|
void admitted.then(
|
|
() => {
|
|
admissionSettled = true;
|
|
},
|
|
() => {
|
|
admissionSettled = true;
|
|
},
|
|
);
|
|
|
|
// The initial close plus three near-bound retries exceed the old 23s
|
|
// admission grace but remain within the configured 31s owner phase.
|
|
await vi.advanceTimersByTimeAsync(23_100);
|
|
expect(retainedRecoveryAttempt).toBe(2);
|
|
expect(quarantineRecoveryAttempt).toBe(0);
|
|
expect(admissionSettled).toBe(false);
|
|
expect(openSession).toHaveBeenCalledOnce();
|
|
|
|
await vi.advanceTimersByTimeAsync(7_600);
|
|
expect(retainedRecoveryAttempt).toBe(3);
|
|
expect(quarantineRecoveryAttempt).toBe(1);
|
|
expect(admissionSettled).toBe(false);
|
|
expect(openSession).toHaveBeenCalledOnce();
|
|
|
|
// The replacement then receives its complete three-attempt bound rather
|
|
// than inheriting only the remainder of the retained owner's deadline.
|
|
await vi.advanceTimersByTimeAsync(21_600);
|
|
await expect(admitted).resolves.toMatchObject({ result });
|
|
expect(quarantineRecoveryAttempt).toBe(3);
|
|
expect(openSession).toHaveBeenCalledTimes(2);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("runs a full admission batch after an inherited scheduled attempt fails", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
let executionCloseCount = 0;
|
|
let scheduledRecoveryAttempt = 0;
|
|
let admissionRecoveryAttempt = 0;
|
|
const close = vi.fn(({ reason }: { reason: string }) => {
|
|
if (reason === "native session execution complete") {
|
|
executionCloseCount += 1;
|
|
return executionCloseCount === 1
|
|
? Promise.reject(new Error("initial close failed"))
|
|
: Promise.resolve();
|
|
}
|
|
if (
|
|
reason.startsWith(
|
|
"native session cleanup recovery after close failure",
|
|
)
|
|
) {
|
|
return Promise.reject(new Error("bounded retained recovery failed"));
|
|
}
|
|
if (reason === "native session quarantined cleanup recovery") {
|
|
return Promise.reject(
|
|
new Error("bounded quarantine recovery failed"),
|
|
);
|
|
}
|
|
if (
|
|
reason === "native session scheduled quarantined cleanup recovery"
|
|
) {
|
|
scheduledRecoveryAttempt += 1;
|
|
return new Promise<void>((_resolve, reject) => {
|
|
setTimeout(
|
|
() => reject(new Error("scheduled cleanup failed")),
|
|
6_500,
|
|
);
|
|
});
|
|
}
|
|
if (reason === "native session quarantined admission recovery") {
|
|
admissionRecoveryAttempt += 1;
|
|
const attempt = admissionRecoveryAttempt;
|
|
return new Promise<void>((resolve, reject) => {
|
|
setTimeout(() => {
|
|
if (attempt === 1)
|
|
reject(new Error("transient admission cleanup failure"));
|
|
else resolve();
|
|
}, 6_500);
|
|
});
|
|
}
|
|
return Promise.resolve();
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const openSession = vi.fn(async () => session);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
const execute = () =>
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
|
|
await expect(execute()).resolves.toMatchObject({ result });
|
|
// Exhaust retained and initial quarantine batches, then enter the slow
|
|
// autonomous one-attempt recovery scheduled sixty seconds later.
|
|
await vi.advanceTimersByTimeAsync(65_001);
|
|
expect(scheduledRecoveryAttempt).toBe(1);
|
|
|
|
const admitted = execute();
|
|
let admissionSettled = false;
|
|
void admitted.then(
|
|
() => {
|
|
admissionSettled = true;
|
|
},
|
|
() => {
|
|
admissionSettled = true;
|
|
},
|
|
);
|
|
await vi.advanceTimersByTimeAsync(6_600);
|
|
expect(admissionRecoveryAttempt).toBe(1);
|
|
expect(admissionSettled).toBe(false);
|
|
expect(openSession).toHaveBeenCalledOnce();
|
|
|
|
await vi.advanceTimersByTimeAsync(14_100);
|
|
await expect(admitted).resolves.toMatchObject({ result });
|
|
expect(admissionRecoveryAttempt).toBe(2);
|
|
expect(openSession).toHaveBeenCalledTimes(2);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("stops scheduled cleanup after the quarantine lifetime budget", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
let executionCloseCount = 0;
|
|
let admissionAttempt = 0;
|
|
let scheduledAttempt = 0;
|
|
const lifetimeIdentity = {
|
|
...identity,
|
|
companyId: "company-cleanup-lifetime",
|
|
};
|
|
const close = vi.fn(({ reason }: { reason: string }) => {
|
|
if (reason === "native session execution complete") {
|
|
executionCloseCount += 1;
|
|
return executionCloseCount === 1
|
|
? Promise.reject(new Error("initial close failed"))
|
|
: Promise.resolve();
|
|
}
|
|
if (
|
|
reason.startsWith(
|
|
"native session cleanup recovery after close failure",
|
|
)
|
|
) {
|
|
return Promise.reject(new Error("bounded close recovery failed"));
|
|
}
|
|
if (reason === "native session quarantined cleanup recovery") {
|
|
return Promise.reject(new Error("quarantined close recovery failed"));
|
|
}
|
|
if (reason === "native session quarantined admission recovery") {
|
|
admissionAttempt += 1;
|
|
return Promise.reject(new Error("admission cleanup failed"));
|
|
}
|
|
if (
|
|
reason === "native session scheduled quarantined cleanup recovery"
|
|
) {
|
|
scheduledAttempt += 1;
|
|
return Promise.reject(new Error("scheduled cleanup failed"));
|
|
}
|
|
return Promise.resolve();
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => lifetimeIdentity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity: lifetimeIdentity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const openSession = vi.fn(async () => session);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
const execute = () =>
|
|
executeNativeSession({
|
|
input: {
|
|
...input,
|
|
binding: {
|
|
...input.binding,
|
|
companyId: lifetimeIdentity.companyId,
|
|
},
|
|
},
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
|
|
await expect(execute()).resolves.toMatchObject({ result });
|
|
await vi.advanceTimersByTimeAsync(6_000);
|
|
|
|
const recoveredExecution = expect(execute()).rejects.toThrow(
|
|
"prior session cleanup remains incomplete",
|
|
);
|
|
await vi.advanceTimersByTimeAsync(2_100);
|
|
await recoveredExecution;
|
|
|
|
expect(admissionAttempt).toBe(3);
|
|
expect(scheduledAttempt).toBe(0);
|
|
await vi.advanceTimersByTimeAsync(180_000);
|
|
expect(scheduledAttempt).toBe(3);
|
|
await vi.advanceTimersByTimeAsync(300_000);
|
|
expect(scheduledAttempt).toBe(3);
|
|
expect(openSession).toHaveBeenCalledOnce();
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("fails admission within a bound when quarantined close never settles", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
let releaseBlockedClose = () => {};
|
|
let blockClose = false;
|
|
const close = vi.fn(() => {
|
|
if (blockClose) {
|
|
return new Promise<void>((resolve) => {
|
|
releaseBlockedClose = resolve;
|
|
});
|
|
}
|
|
return Promise.reject(new Error("persistent close failure"));
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const openSession = vi.fn(async () => session);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
const execute = () =>
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
|
|
await expect(execute()).resolves.toMatchObject({ result });
|
|
await vi.advanceTimersByTimeAsync(6_000);
|
|
blockClose = true;
|
|
const blockedAdmission = execute();
|
|
const blockedResult = expect(blockedAdmission).rejects.toThrow(
|
|
"prior session cleanup exceeded the admission grace",
|
|
);
|
|
await vi.advanceTimersByTimeAsync(31_100);
|
|
await blockedResult;
|
|
expect(openSession).toHaveBeenCalledOnce();
|
|
|
|
const isolatedSession: NativeSession = {
|
|
...session,
|
|
identity: () => ({ ...identity, companyId: "company-isolated" }),
|
|
close: vi.fn(async () => undefined),
|
|
};
|
|
const isolatedOpenSession = vi.fn(async () => isolatedSession);
|
|
await expect(
|
|
executeNativeSession({
|
|
input: {
|
|
...input,
|
|
binding: {
|
|
...input.binding,
|
|
companyId: "company-isolated",
|
|
},
|
|
},
|
|
backend: { ...backend, openSession: isolatedOpenSession },
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
}),
|
|
).resolves.toMatchObject({ result });
|
|
expect(isolatedOpenSession).toHaveBeenCalledOnce();
|
|
|
|
blockClose = false;
|
|
releaseBlockedClose();
|
|
close.mockResolvedValue(undefined);
|
|
await vi.runAllTimersAsync();
|
|
await expect(execute()).resolves.toMatchObject({ result });
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("quarantines a pending first close before another provider session can open", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
let releaseClose = () => {};
|
|
const pendingClose = new Promise<void>((resolve) => {
|
|
releaseClose = resolve;
|
|
});
|
|
const close = vi.fn(() => pendingClose);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const openSession = vi.fn(async () => session);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
const execute = () =>
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
|
|
const firstExecution = execute();
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
await expect(firstExecution).resolves.toMatchObject({ result });
|
|
expect(close).toHaveBeenCalledOnce();
|
|
|
|
const blockedAdmission = execute();
|
|
const blockedResult = expect(blockedAdmission).rejects.toThrow(
|
|
"prior session cleanup exceeded the admission grace",
|
|
);
|
|
await vi.advanceTimersByTimeAsync(31_100);
|
|
await blockedResult;
|
|
expect(openSession).toHaveBeenCalledOnce();
|
|
expect(close).toHaveBeenCalledOnce();
|
|
|
|
releaseClose();
|
|
await vi.runAllTimersAsync();
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("fails closed before launch when a v3 driver does not declare complete native context realization", async () => {
|
|
const digest = "0".repeat(64);
|
|
const context = {
|
|
prompt: {
|
|
revision: PAPERCLIP_EXECUTION_PROMPT_REVISION,
|
|
text: PAPERCLIP_EXECUTION_PROMPT,
|
|
digest: nativeRuntimePromptDigest(),
|
|
},
|
|
instructions: {
|
|
entryPath: "AGENTS.md",
|
|
bundle: {
|
|
schema: NATIVE_RUNTIME_ASSET_SCHEMA,
|
|
digest,
|
|
manifestDigest: digest,
|
|
rootPath: "/paperclip/context/instructions",
|
|
fileCount: 1,
|
|
totalBytes: 1,
|
|
},
|
|
},
|
|
skills: [],
|
|
mcp: { assignmentSetId: "none", digest, bindingId: null },
|
|
} as const;
|
|
const openSession = vi.fn();
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "future-provider",
|
|
name: "future-provider",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input: {
|
|
...input,
|
|
schema: "paperclip.native-execution-input.v3",
|
|
executionMode: "default",
|
|
planningContext: null,
|
|
runtimeContext: {
|
|
...context,
|
|
aggregateDigest: canonicalNativeRuntimeContextDigest(context),
|
|
},
|
|
},
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
}),
|
|
).rejects.toThrow("does not natively realize instructions, skills, mcp");
|
|
expect(openSession).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not admit a fresh run when provider session initialization fails", async () => {
|
|
const providerFailure = new Error("provider initialization failed");
|
|
const openSession = vi.fn(async () => {
|
|
throw providerFailure;
|
|
});
|
|
const openRun = vi.fn(async () => undefined);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "fresh-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
}),
|
|
).rejects.toBe(providerFailure);
|
|
|
|
expect(openSession).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
identity,
|
|
workingDirectory: input.workspace.cwd,
|
|
signal: expect.any(AbortSignal),
|
|
}),
|
|
);
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("retains late bootstrap cleanup through the next admission", async () => {
|
|
vi.useFakeTimers();
|
|
let releaseClose = () => {};
|
|
try {
|
|
let resolveBootstrap = (_value: NativeSession) => {};
|
|
const stalledBootstrap = new Promise<NativeSession>((resolve) => {
|
|
resolveBootstrap = resolve;
|
|
});
|
|
let markBootstrapStarted = () => {};
|
|
const bootstrapStarted = new Promise<void>((resolve) => {
|
|
markBootstrapStarted = resolve;
|
|
});
|
|
let markCloseStarted = () => {};
|
|
const closeStarted = new Promise<void>((resolve) => {
|
|
markCloseStarted = resolve;
|
|
});
|
|
const closeReleased = new Promise<void>((resolve) => {
|
|
releaseClose = resolve;
|
|
});
|
|
const close = vi.fn(async () => {
|
|
markCloseStarted();
|
|
await closeReleased;
|
|
});
|
|
const lateSession: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
};
|
|
},
|
|
async *events() {},
|
|
async startTurn() {
|
|
throw new Error("late fresh session must not start");
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
throw new Error("late fresh session must not snapshot");
|
|
},
|
|
close,
|
|
};
|
|
let bootstrapSignal: AbortSignal | undefined;
|
|
let bootstrapCount = 0;
|
|
const openSession = vi.fn(
|
|
(bootstrapInput: {
|
|
identity: NativeRunIdentity;
|
|
workingDirectory?: string;
|
|
signal?: AbortSignal;
|
|
}) => {
|
|
bootstrapCount += 1;
|
|
if (bootstrapCount > 1) {
|
|
throw new Error("replacement bootstrap launched");
|
|
}
|
|
bootstrapSignal = bootstrapInput.signal;
|
|
bootstrapInput.signal?.addEventListener(
|
|
"abort",
|
|
() => resolveBootstrap(lateSession),
|
|
{ once: true },
|
|
);
|
|
markBootstrapStarted();
|
|
return stalledBootstrap;
|
|
},
|
|
);
|
|
const openRun = vi.fn(async () => undefined);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "fresh-stalled-backend",
|
|
version: "1",
|
|
capabilities: await lateSession.capabilities(),
|
|
};
|
|
},
|
|
openSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
const execute = () =>
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 5,
|
|
});
|
|
const execution = execute();
|
|
const rejection = expect(execution).rejects.toThrow(
|
|
"native session bootstrap timed out after 5ms",
|
|
);
|
|
|
|
await bootstrapStarted;
|
|
expect(bootstrapSignal?.aborted).toBe(false);
|
|
await vi.advanceTimersByTimeAsync(5);
|
|
await rejection;
|
|
expect(bootstrapSignal?.aborted).toBe(true);
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
|
|
await closeStarted;
|
|
expect(close).toHaveBeenCalledWith({
|
|
reason: "native session bootstrap timed out",
|
|
});
|
|
|
|
// Even after the detached disposer exhausts its short settlement grace,
|
|
// the exact close remains admission-visible until it releases provider
|
|
// resources. A replacement bootstrap cannot start concurrently.
|
|
await vi.advanceTimersByTimeAsync(101);
|
|
const blockedAdmission = execute();
|
|
const blockedAdmissionRejection = expect(
|
|
blockedAdmission,
|
|
).rejects.toThrow("replacement bootstrap launched");
|
|
await vi.advanceTimersByTimeAsync(1_000);
|
|
expect(openSession).toHaveBeenCalledOnce();
|
|
|
|
releaseClose();
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
await blockedAdmissionRejection;
|
|
expect(openSession).toHaveBeenCalledTimes(2);
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
} finally {
|
|
releaseClose();
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("waits for controller ownership publication before dispatching a turn", async () => {
|
|
let release!: () => void;
|
|
const published = new Promise<void>((resolve) => { release = resolve; });
|
|
const snapshotFailure = new Error("stop after ownership publication");
|
|
const snapshot = vi.fn(async () => { throw snapshotFailure; });
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() { return { resume: false, typedEvents: true, steering: false, interruption: true }; },
|
|
async *events() {},
|
|
async startTurn() { throw new Error("unexpected turn"); },
|
|
async result() { return null; },
|
|
snapshot,
|
|
close: vi.fn(async () => undefined),
|
|
};
|
|
const onSession = vi.fn(async (current: NativeSession | null) => { if (current) await published; });
|
|
const running = executeNativeSession({
|
|
input,
|
|
backend: { async descriptor() { return { kind: "mock", name: "owner-barrier", version: "1", capabilities: await session.capabilities() }; }, async openSession() { return session; } },
|
|
controlPlane: { async openRun() {}, async checkpointSession() {}, async appendEvent() { throw new Error("unexpected event"); }, async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; }, async completeRun() {} },
|
|
runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery", onSession,
|
|
});
|
|
const rejected = expect(running).rejects.toBe(snapshotFailure);
|
|
await vi.waitFor(() => expect(onSession).toHaveBeenCalledWith(session));
|
|
expect(snapshot).not.toHaveBeenCalled();
|
|
release();
|
|
await rejected;
|
|
});
|
|
|
|
it("closes the provider when owner quarantine notification throws", async () => {
|
|
const snapshotFailure = new Error("snapshot failed");
|
|
const close = vi.fn(async () => undefined);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
};
|
|
},
|
|
async *events() {},
|
|
async startTurn() {
|
|
throw new Error("unexpected turn");
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
throw snapshotFailure;
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "owner-notification-backend",
|
|
version: "1",
|
|
capabilities: await session.capabilities(),
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
const retainedSessions: Array<NativeSession | null> = [];
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
keepSessionOpen: true,
|
|
onSession(current) {
|
|
retainedSessions.push(current);
|
|
if (current === null) throw new Error("owner notification failed");
|
|
},
|
|
}),
|
|
).rejects.toBe(snapshotFailure);
|
|
|
|
expect(retainedSessions).toEqual([session, null]);
|
|
expect(close).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it.each(["control-plane checkpoint", "owner checkpoint"] as const)(
|
|
"aborts consumption and closes the provider when the startup %s never settles",
|
|
async (stalledBoundary) => {
|
|
vi.useFakeTimers();
|
|
let releaseStream = () => {};
|
|
try {
|
|
const streamReleased = new Promise<void>((resolve) => {
|
|
releaseStream = resolve;
|
|
});
|
|
const never = new Promise<void>(() => undefined);
|
|
let markCheckpointStalled = () => {};
|
|
const checkpointStalled = new Promise<void>((resolve) => {
|
|
markCheckpointStalled = resolve;
|
|
});
|
|
let checkpointSignal: AbortSignal | undefined;
|
|
let controlPlaneCheckpointCount = 0;
|
|
const checkpointSession: NonNullable<
|
|
ControlPlanePort["checkpointSession"]
|
|
> = async (_snapshot, checkpointOptions) => {
|
|
controlPlaneCheckpointCount += 1;
|
|
if (
|
|
stalledBoundary === "control-plane checkpoint" &&
|
|
controlPlaneCheckpointCount === 2
|
|
) {
|
|
checkpointSignal = checkpointOptions?.signal;
|
|
markCheckpointStalled();
|
|
await never;
|
|
}
|
|
};
|
|
let ownerCheckpointCount = 0;
|
|
const onCheckpoint: NonNullable<
|
|
ExecuteNativeSessionOptions["onCheckpoint"]
|
|
> = async (_snapshot, checkpointOptions) => {
|
|
ownerCheckpointCount += 1;
|
|
if (
|
|
stalledBoundary === "owner checkpoint" &&
|
|
ownerCheckpointCount === 2
|
|
) {
|
|
checkpointSignal = checkpointOptions?.signal;
|
|
markCheckpointStalled();
|
|
await never;
|
|
}
|
|
};
|
|
const startTurn = vi.fn(async () => ({
|
|
turnId: "turn-checkpoint-stalled",
|
|
}));
|
|
const close = vi.fn(async () => {
|
|
releaseStream();
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
await streamReleased;
|
|
},
|
|
startTurn,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-checkpoint-stalled",
|
|
cursor: "0",
|
|
activeTurnId: "turn-checkpoint-stalled",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "checkpoint-stalled-backend",
|
|
version: "1",
|
|
capabilities: await session.capabilities(),
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const openRun = vi.fn(async () => undefined);
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
checkpointSession,
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
const retainedSessions: Array<NativeSession | null> = [];
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 1_000,
|
|
checkpointTimeoutMs: 1,
|
|
keepSessionOpen: true,
|
|
onCheckpoint,
|
|
onSession: (current) => retainedSessions.push(current),
|
|
});
|
|
const rejection = expect(execution).rejects.toThrow(
|
|
"native session checkpoint timed out after 1ms",
|
|
);
|
|
await checkpointStalled;
|
|
expect(checkpointSignal?.aborted).toBe(false);
|
|
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
await rejection;
|
|
|
|
expect(checkpointSignal?.aborted).toBe(true);
|
|
expect(openRun).toHaveBeenCalledOnce();
|
|
expect(startTurn).toHaveBeenCalledOnce();
|
|
expect(close).toHaveBeenCalledOnce();
|
|
expect(retainedSessions).toEqual([session, null]);
|
|
} finally {
|
|
releaseStream();
|
|
vi.useRealTimers();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"provider result",
|
|
"completion checkpoint",
|
|
"control-plane replay",
|
|
"final event append",
|
|
"run completion",
|
|
] as const)(
|
|
"bounds post-terminal finalization when %s never settles",
|
|
async (stalledBoundary) => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const never = new Promise<never>(() => undefined);
|
|
let markFinalizationStalled = () => {};
|
|
const finalizationStalled = new Promise<void>((resolve) => {
|
|
markFinalizationStalled = resolve;
|
|
});
|
|
let stalledSignal: AbortSignal | undefined;
|
|
let resultCalls = 0;
|
|
let resultResolved = false;
|
|
let completionCheckpointCalls = 0;
|
|
const close = vi.fn(async () => undefined);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
resultCalls += 1;
|
|
if (stalledBoundary === "provider result") {
|
|
markFinalizationStalled();
|
|
return await never;
|
|
}
|
|
resultResolved = true;
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "1",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "finalization-timeout-backend",
|
|
version: "1",
|
|
capabilities: await session.capabilities(),
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession(_snapshot, operationOptions) {
|
|
if (stalledBoundary === "completion checkpoint" && resultResolved) {
|
|
completionCheckpointCalls += 1;
|
|
stalledSignal = operationOptions?.signal;
|
|
markFinalizationStalled();
|
|
await never;
|
|
}
|
|
},
|
|
async appendEvent(event, operationOptions) {
|
|
if (
|
|
stalledBoundary === "final event append" &&
|
|
(event as PrpEvent).sourceKind === "control_plane"
|
|
) {
|
|
stalledSignal = operationOptions?.signal;
|
|
markFinalizationStalled();
|
|
return await never;
|
|
}
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: (event as PrpEvent).sourceSeq,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(_replay, operationOptions) {
|
|
if (stalledBoundary === "control-plane replay") {
|
|
stalledSignal = operationOptions?.signal;
|
|
markFinalizationStalled();
|
|
return await never;
|
|
}
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun(_completion, operationOptions) {
|
|
if (stalledBoundary === "run completion") {
|
|
stalledSignal = operationOptions?.signal;
|
|
markFinalizationStalled();
|
|
await never;
|
|
}
|
|
},
|
|
};
|
|
const retainedSessions: Array<NativeSession | null> = [];
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 10,
|
|
keepSessionOpen: true,
|
|
onSession: (current) => retainedSessions.push(current),
|
|
});
|
|
const rejection = expect(execution).rejects.toThrow(
|
|
"native session finalization timed out after 10ms",
|
|
);
|
|
await finalizationStalled;
|
|
if (stalledBoundary !== "provider result") {
|
|
expect(stalledSignal?.aborted).toBe(false);
|
|
}
|
|
|
|
await vi.advanceTimersByTimeAsync(20);
|
|
await rejection;
|
|
|
|
if (stalledBoundary !== "provider result") {
|
|
expect(stalledSignal?.aborted).toBe(true);
|
|
}
|
|
expect(resultCalls).toBe(1);
|
|
if (stalledBoundary === "completion checkpoint") {
|
|
expect(completionCheckpointCalls).toBe(1);
|
|
}
|
|
expect(close).toHaveBeenCalledOnce();
|
|
expect(retainedSessions).toEqual([session, null]);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each(["final event append", "run completion"] as const)(
|
|
"confirms durable completion when %s commits before its acknowledgement stalls",
|
|
async (stalledBoundary) => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const never = new Promise<never>(() => undefined);
|
|
let markFinalizationStalled = () => {};
|
|
const finalizationStalled = new Promise<void>((resolve) => {
|
|
markFinalizationStalled = resolve;
|
|
});
|
|
let stalledOnce = false;
|
|
let stalledSignal: AbortSignal | undefined;
|
|
const events: PrpEvent[] = [];
|
|
let durableCompletion: unknown = null;
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "1",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
async close() {},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "durable-finalization-backend",
|
|
version: "1",
|
|
capabilities: await session.capabilities(),
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event, operationOptions) {
|
|
const appended = structuredClone(event as PrpEvent);
|
|
const existing = events.find(
|
|
(candidate) =>
|
|
candidate.sourceInstanceId === appended.sourceInstanceId &&
|
|
candidate.sourceSeq === appended.sourceSeq,
|
|
);
|
|
if (existing === undefined) events.push(appended);
|
|
if (
|
|
stalledBoundary === "final event append" &&
|
|
appended.sourceKind === "control_plane" &&
|
|
!stalledOnce
|
|
) {
|
|
stalledOnce = true;
|
|
stalledSignal = operationOptions?.signal;
|
|
markFinalizationStalled();
|
|
return await never;
|
|
}
|
|
const sourceEvents = events.filter(
|
|
(candidate) =>
|
|
candidate.sourceInstanceId === appended.sourceInstanceId,
|
|
);
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(sourceEvents),
|
|
disposition: existing === undefined ? "committed" : "duplicate",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
const sourceEvents = events.filter(
|
|
(event) => event.sourceInstanceId === replay.sourceInstanceId,
|
|
);
|
|
return {
|
|
events: structuredClone(
|
|
sourceEvents.filter(
|
|
(event) => event.sourceSeq > replay.afterSourceSeq,
|
|
),
|
|
),
|
|
highestContiguousSourceSeq: highestContiguous(sourceEvents),
|
|
};
|
|
},
|
|
async completeRun(completion, operationOptions) {
|
|
if (durableCompletion === null) {
|
|
durableCompletion = structuredClone(completion);
|
|
} else {
|
|
expect(completion).toEqual(durableCompletion);
|
|
}
|
|
if (stalledBoundary === "run completion" && !stalledOnce) {
|
|
stalledOnce = true;
|
|
stalledSignal = operationOptions?.signal;
|
|
markFinalizationStalled();
|
|
await never;
|
|
}
|
|
},
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 10,
|
|
});
|
|
await finalizationStalled;
|
|
expect(stalledSignal?.aborted).toBe(false);
|
|
|
|
await vi.advanceTimersByTimeAsync(10);
|
|
|
|
await expect(execution).resolves.toMatchObject({
|
|
result,
|
|
terminal,
|
|
nativeEventCount: 3,
|
|
});
|
|
expect(stalledSignal?.aborted).toBe(true);
|
|
expect(durableCompletion).toMatchObject({ result, terminal });
|
|
expect(
|
|
events.filter((event) => event.sourceKind === "control_plane"),
|
|
).toHaveLength(2);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ boundary: "control-plane replay", fault: "typed", admitted: false },
|
|
{ boundary: "final event append", fault: "typed", admitted: false },
|
|
{
|
|
boundary: "lost final event acknowledgement",
|
|
fault: "typed",
|
|
admitted: false,
|
|
},
|
|
{ boundary: "control-plane replay", fault: "lookalike", admitted: true },
|
|
{ boundary: "control-plane replay", fault: "generic", admitted: true },
|
|
{
|
|
boundary: "completion invoked before commit",
|
|
fault: "typed",
|
|
admitted: true,
|
|
},
|
|
{
|
|
boundary: "lost completion acknowledgement",
|
|
fault: "typed",
|
|
admitted: true,
|
|
},
|
|
] as const)(
|
|
"observes integrity before completion admission without revoking admitted completion (%j)",
|
|
async ({ boundary, fault, admitted }) => {
|
|
vi.useFakeTimers();
|
|
let releaseBoundary = () => {};
|
|
const boundaryReleased = new Promise<void>((resolve) => {
|
|
releaseBoundary = resolve;
|
|
});
|
|
let markBoundaryReached = () => {};
|
|
const boundaryReached = new Promise<void>((resolve) => {
|
|
markBoundaryReached = resolve;
|
|
});
|
|
const failure =
|
|
fault === "typed"
|
|
? new NativeSessionProtocolIntegrityError(
|
|
"semantic_input_digest_mismatch",
|
|
)
|
|
: fault === "lookalike"
|
|
? Object.assign(new Error("untrusted transport error"), {
|
|
code: "native_event_replay_conflict",
|
|
reason: "semantic_input_digest_mismatch",
|
|
})
|
|
: new Error("snapshot temporarily unavailable");
|
|
let latchedFailure: Error | null = null;
|
|
let boundaryBlocked = false;
|
|
const waitAtBoundary = async () => {
|
|
if (boundaryBlocked) return;
|
|
boundaryBlocked = true;
|
|
markBoundaryReached();
|
|
await boundaryReleased;
|
|
};
|
|
const events: PrpEvent[] = [];
|
|
let durableCompletion: unknown = null;
|
|
const close = vi.fn(async () => undefined);
|
|
const resolveResult = vi.fn(async () => ({
|
|
result,
|
|
terminal,
|
|
turnId: "turn-recovery",
|
|
}));
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
result: resolveResult,
|
|
async snapshot() {
|
|
if (latchedFailure !== null) throw latchedFailure;
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "1",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: `completion-integrity-${boundary}-${fault}`,
|
|
version: "1",
|
|
capabilities: await session.capabilities(),
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const completeRun = vi.fn<ControlPlanePort["completeRun"]>(
|
|
async (completion) => {
|
|
if (boundary === "completion invoked before commit")
|
|
await waitAtBoundary();
|
|
if (durableCompletion === null)
|
|
durableCompletion = structuredClone(completion);
|
|
else expect(completion).toEqual(durableCompletion);
|
|
if (
|
|
boundary === "lost completion acknowledgement" &&
|
|
!boundaryBlocked
|
|
) {
|
|
await waitAtBoundary();
|
|
await new Promise<never>(() => undefined);
|
|
}
|
|
},
|
|
);
|
|
const checkpointSession = vi.fn<ControlPlanePort["checkpointSession"]>(
|
|
async () => undefined,
|
|
);
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
checkpointSession,
|
|
async appendEvent(rawEvent) {
|
|
const event = structuredClone(rawEvent as PrpEvent);
|
|
const existing = events.some(
|
|
(candidate) =>
|
|
candidate.sourceInstanceId === event.sourceInstanceId &&
|
|
candidate.sourceSeq === event.sourceSeq,
|
|
);
|
|
if (!existing) events.push(event);
|
|
if (
|
|
boundary === "final event append" &&
|
|
event.eventType === "run.terminal"
|
|
) {
|
|
await waitAtBoundary();
|
|
}
|
|
if (
|
|
boundary === "lost final event acknowledgement" &&
|
|
event.eventType === "run.terminal" &&
|
|
!boundaryBlocked
|
|
) {
|
|
await waitAtBoundary();
|
|
await new Promise<never>(() => undefined);
|
|
}
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(
|
|
events.filter(
|
|
(candidate) =>
|
|
candidate.sourceInstanceId === event.sourceInstanceId,
|
|
),
|
|
),
|
|
disposition: existing ? "duplicate" : "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
if (
|
|
boundary === "control-plane replay" &&
|
|
replay.sourceInstanceId === "control-recovery"
|
|
) {
|
|
await waitAtBoundary();
|
|
}
|
|
const sourceEvents = events.filter(
|
|
(event) => event.sourceInstanceId === replay.sourceInstanceId,
|
|
);
|
|
return {
|
|
events: structuredClone(
|
|
sourceEvents.filter(
|
|
(event) => event.sourceSeq > replay.afterSourceSeq,
|
|
),
|
|
),
|
|
highestContiguousSourceSeq: highestContiguous(sourceEvents),
|
|
};
|
|
},
|
|
completeRun,
|
|
};
|
|
const outcome = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 10,
|
|
requireSessionCloseBeforeReturn: true,
|
|
}).then(
|
|
(value) => ({ value, error: null }),
|
|
(error: unknown) => ({ value: null, error }),
|
|
);
|
|
try {
|
|
await boundaryReached;
|
|
const checkpointsBeforeFault = checkpointSession.mock.calls.length;
|
|
if (boundary === "completion invoked before commit") {
|
|
expect(completeRun).toHaveBeenCalledOnce();
|
|
expect(durableCompletion).toBeNull();
|
|
}
|
|
latchedFailure = failure;
|
|
releaseBoundary();
|
|
if (
|
|
boundary === "lost completion acknowledgement" ||
|
|
boundary === "lost final event acknowledgement"
|
|
) {
|
|
await vi.advanceTimersByTimeAsync(10);
|
|
}
|
|
const settled = await outcome;
|
|
if (!admitted) {
|
|
expect(settled.error).toBe(failure);
|
|
expect(settled.value).toBeNull();
|
|
expect(completeRun).not.toHaveBeenCalled();
|
|
expect(durableCompletion).toBeNull();
|
|
} else {
|
|
expect(settled.error).toBeNull();
|
|
expect(settled.value).toMatchObject({
|
|
result,
|
|
terminal,
|
|
nativeEventCount: 3,
|
|
});
|
|
expect(durableCompletion).toMatchObject({ result, terminal });
|
|
expect(completeRun).toHaveBeenCalledTimes(
|
|
boundary === "lost completion acknowledgement" ? 2 : 1,
|
|
);
|
|
}
|
|
expect(
|
|
events.filter((event) => event.sourceKind === "control_plane"),
|
|
).toHaveLength(2);
|
|
expect(checkpointSession).toHaveBeenCalledTimes(checkpointsBeforeFault);
|
|
expect(resolveResult).toHaveBeenCalledOnce();
|
|
expect(close).toHaveBeenCalledOnce();
|
|
} finally {
|
|
releaseBoundary();
|
|
await vi.advanceTimersByTimeAsync(30);
|
|
await outcome;
|
|
vi.useRealTimers();
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"provider snapshot",
|
|
"post-completion checkpoint",
|
|
"provider usage",
|
|
] as const)(
|
|
"preserves durable completion when %s never settles",
|
|
async (stalledBoundary) => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const never = new Promise<never>(() => undefined);
|
|
let markEnrichmentStalled = () => {};
|
|
const enrichmentStalled = new Promise<void>((resolve) => {
|
|
markEnrichmentStalled = resolve;
|
|
});
|
|
let stalledSignal: AbortSignal | undefined;
|
|
let runCompleted = false;
|
|
const close = vi.fn(async () => undefined);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot(snapshotOptions) {
|
|
if (stalledBoundary === "provider snapshot" && runCompleted) {
|
|
stalledSignal = snapshotOptions?.signal;
|
|
markEnrichmentStalled();
|
|
return await never;
|
|
}
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "1",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
async usage() {
|
|
if (stalledBoundary === "provider usage" && runCompleted) {
|
|
markEnrichmentStalled();
|
|
return await never;
|
|
}
|
|
return { driverVersion: "2" };
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "post-completion-enrichment-backend",
|
|
version: "1",
|
|
capabilities: await session.capabilities(),
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const completeRun = vi.fn(async () => {
|
|
runCompleted = true;
|
|
});
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession(_snapshot, operationOptions) {
|
|
if (
|
|
stalledBoundary === "post-completion checkpoint" &&
|
|
runCompleted
|
|
) {
|
|
stalledSignal = operationOptions?.signal;
|
|
markEnrichmentStalled();
|
|
await never;
|
|
}
|
|
},
|
|
async appendEvent(event) {
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: (event as PrpEvent).sourceSeq,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
completeRun,
|
|
};
|
|
const retainedSessions: Array<NativeSession | null> = [];
|
|
const enrichmentFailures: Array<"checkpoint" | "usage"> = [];
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 10,
|
|
keepSessionOpen: true,
|
|
onSession: (current) => retainedSessions.push(current),
|
|
onPostCompletionEnrichmentFailure: ({ stage }) =>
|
|
enrichmentFailures.push(stage),
|
|
});
|
|
await enrichmentStalled;
|
|
if (stalledSignal !== undefined)
|
|
expect(stalledSignal.aborted).toBe(false);
|
|
|
|
await vi.advanceTimersByTimeAsync(10);
|
|
|
|
await expect(execution).resolves.toMatchObject({
|
|
result,
|
|
terminal,
|
|
providerSessionId: "provider-recovery",
|
|
driverVersion: "1",
|
|
usage: null,
|
|
});
|
|
if (stalledSignal !== undefined)
|
|
expect(stalledSignal.aborted).toBe(true);
|
|
expect(completeRun).toHaveBeenCalledOnce();
|
|
expect(enrichmentFailures).toEqual([
|
|
stalledBoundary === "provider usage" ? "usage" : "checkpoint",
|
|
]);
|
|
if (stalledBoundary === "provider usage") {
|
|
expect(close).not.toHaveBeenCalled();
|
|
expect(retainedSessions).toEqual([session]);
|
|
} else {
|
|
expect(close).toHaveBeenCalledOnce();
|
|
expect(retainedSessions).toEqual([session, null]);
|
|
}
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
},
|
|
);
|
|
|
|
it("contains a consumer rejection when starting the turn fails first", async () => {
|
|
let markAppendStarted = () => {};
|
|
const appendStarted = new Promise<void>((resolve) => {
|
|
markAppendStarted = resolve;
|
|
});
|
|
let releaseAppend = () => {};
|
|
const appendReleased = new Promise<void>((resolve) => {
|
|
releaseAppend = resolve;
|
|
});
|
|
let appendCommitted = false;
|
|
const close = vi.fn(async () => undefined);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.started");
|
|
},
|
|
async startTurn() {
|
|
await appendStarted;
|
|
throw new Error("start turn failed");
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(_event, options) {
|
|
markAppendStarted();
|
|
await Promise.race([
|
|
appendReleased,
|
|
new Promise<never>((_resolve, reject) => {
|
|
const rejectAbort = () =>
|
|
reject(options?.signal.reason ?? new Error("append aborted"));
|
|
if (options?.signal.aborted) rejectAbort();
|
|
else
|
|
options?.signal.addEventListener("abort", rejectAbort, {
|
|
once: true,
|
|
});
|
|
}),
|
|
]);
|
|
appendCommitted = true;
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed" as const,
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
await appendStarted;
|
|
await expect(execution).rejects.toThrow("start turn failed");
|
|
expect(close).toHaveBeenCalled();
|
|
expect(appendCommitted).toBe(false);
|
|
releaseAppend();
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
expect(appendCommitted).toBe(false);
|
|
});
|
|
|
|
it("stops and closes a timed-out consumer even when the caller requested a warm session", async () => {
|
|
let markAppendStarted = () => {};
|
|
const appendStarted = new Promise<void>((resolve) => {
|
|
markAppendStarted = resolve;
|
|
});
|
|
let releaseAppend = () => {};
|
|
const appendReleased = new Promise<void>((resolve) => {
|
|
releaseAppend = resolve;
|
|
});
|
|
let releaseTeardown = () => {};
|
|
const teardownReleased = new Promise<void>((resolve) => {
|
|
releaseTeardown = resolve;
|
|
});
|
|
const iteratorTeardown = vi.fn();
|
|
let appendCommitted = false;
|
|
const appendEvent = vi.fn(
|
|
async (_event: PrpEvent, options?: { signal: AbortSignal }) => {
|
|
markAppendStarted();
|
|
await Promise.race([
|
|
appendReleased,
|
|
new Promise<never>((_resolve, reject) => {
|
|
const rejectAbort = () =>
|
|
reject(options?.signal.reason ?? new Error("append aborted"));
|
|
if (options?.signal.aborted) rejectAbort();
|
|
else
|
|
options?.signal.addEventListener("abort", rejectAbort, {
|
|
once: true,
|
|
});
|
|
}),
|
|
]);
|
|
appendCommitted = true;
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed" as const,
|
|
};
|
|
},
|
|
);
|
|
const cancel = vi.fn(() => {
|
|
releaseTeardown();
|
|
return { cleanup: Promise.resolve() };
|
|
});
|
|
const close = vi.fn(async () => {
|
|
releaseTeardown();
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
try {
|
|
yield runnerEvent(1, "turn.completed");
|
|
} finally {
|
|
iteratorTeardown();
|
|
await teardownReleased;
|
|
}
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
cancel,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
appendEvent,
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 1,
|
|
keepSessionOpen: true,
|
|
});
|
|
const rejection = expect(execution).rejects.toThrow(
|
|
"native session timed out",
|
|
);
|
|
await appendStarted;
|
|
await vi.waitFor(() => expect(iteratorTeardown).toHaveBeenCalledOnce());
|
|
await rejection;
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
expect(close).toHaveBeenCalled();
|
|
expect(appendEvent).toHaveBeenCalledOnce();
|
|
expect(appendCommitted).toBe(false);
|
|
releaseAppend();
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
expect(appendCommitted).toBe(false);
|
|
});
|
|
|
|
it("closes a failed session while retaining an uncancellable event read", async () => {
|
|
let releaseStream = () => {};
|
|
const streamReleased = new Promise<void>((resolve) => {
|
|
releaseStream = resolve;
|
|
});
|
|
const close = vi.fn(async () => {
|
|
releaseStream();
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
await streamReleased;
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 1,
|
|
keepSessionOpen: true,
|
|
}),
|
|
).rejects.toThrow("native session timed out");
|
|
expect(close).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("commits cancellation before bounding failed provider cleanup", async () => {
|
|
let releaseStream = () => {};
|
|
const streamReleased = new Promise<void>((resolve) => {
|
|
releaseStream = resolve;
|
|
});
|
|
let releaseCancellation = () => {};
|
|
const cancellationReleased = new Promise<void>((resolve) => {
|
|
releaseCancellation = resolve;
|
|
});
|
|
const interrupt = vi.fn(() => cancellationReleased);
|
|
let cancellationCommitted = false;
|
|
const cancel = vi.fn(() => {
|
|
cancellationCommitted = true;
|
|
return { cleanup: cancellationReleased };
|
|
});
|
|
const close = vi.fn(async () => {
|
|
releaseStream();
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
await streamReleased;
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
interrupt,
|
|
cancel,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 1,
|
|
keepSessionOpen: true,
|
|
});
|
|
await vi.waitFor(() => expect(close).toHaveBeenCalledOnce());
|
|
expect(interrupt).not.toHaveBeenCalled();
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
await expect(execution).rejects.toThrow("native session timed out");
|
|
expect(cancellationCommitted).toBe(true);
|
|
releaseCancellation();
|
|
});
|
|
|
|
it("bounds failure when iterator teardown and provider close never settle", async () => {
|
|
const never = new Promise<void>(() => undefined);
|
|
let releaseClose = () => {};
|
|
const pendingClose = new Promise<void>((resolve) => {
|
|
releaseClose = resolve;
|
|
});
|
|
const close = vi.fn(() => pendingClose);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
await never;
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 1,
|
|
keepSessionOpen: true,
|
|
}),
|
|
).rejects.toThrow("native session timed out");
|
|
expect(close).toHaveBeenCalledOnce();
|
|
releaseClose();
|
|
await pendingClose;
|
|
});
|
|
|
|
it("preserves durable success when provider close never settles", async () => {
|
|
let releaseClose = () => {};
|
|
const pendingClose = new Promise<void>((resolve) => {
|
|
releaseClose = resolve;
|
|
});
|
|
const close = vi.fn(() => pendingClose);
|
|
const completeRun = vi.fn(async () => undefined);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "1",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const events: PrpEvent[] = [];
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
events.push(structuredClone(event as PrpEvent));
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
completeRun,
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
}),
|
|
).resolves.toMatchObject({ result, terminal });
|
|
expect(completeRun).toHaveBeenCalledOnce();
|
|
expect(close).toHaveBeenCalledOnce();
|
|
releaseClose();
|
|
await pendingClose;
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"waits for a required backend checkpoint close when enrichment failure=%s",
|
|
async (enrichmentFails) => {
|
|
let releaseClose = () => {};
|
|
const pendingClose = new Promise<void>((resolve) => {
|
|
releaseClose = resolve;
|
|
});
|
|
const close = vi.fn(() => pendingClose);
|
|
let runCompleted = false;
|
|
let enrichmentFailureObserved = false;
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
// Exercise only the best-effort enrichment snapshot after the
|
|
// control plane has durably committed the run result.
|
|
if (enrichmentFails && runCompleted) {
|
|
enrichmentFailureObserved = true;
|
|
throw new Error("checkpoint enrichment failed");
|
|
}
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "1",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const events: PrpEvent[] = [];
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
events.push(structuredClone(event as PrpEvent));
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {
|
|
runCompleted = true;
|
|
},
|
|
};
|
|
|
|
let resolved = false;
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
requireSessionCloseBeforeReturn: true,
|
|
}).then((value) => {
|
|
resolved = true;
|
|
return value;
|
|
});
|
|
await vi.waitFor(() => expect(close).toHaveBeenCalledOnce());
|
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
expect(resolved).toBe(false);
|
|
releaseClose();
|
|
await expect(execution).resolves.toMatchObject({ result, terminal });
|
|
expect(resolved).toBe(true);
|
|
expect(enrichmentFailureObserved).toBe(enrichmentFails);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ typed: true, closeFails: false },
|
|
{ typed: true, closeFails: true },
|
|
{ typed: false, closeFails: true },
|
|
{ typed: true, closeFails: true, startupRace: true },
|
|
])(
|
|
"preserves the primary execution failure through required cleanup (%j)",
|
|
async ({ typed, closeFails, startupRace = false }) => {
|
|
const failure = typed
|
|
? new NativeSessionProtocolIntegrityError(
|
|
"semantic_input_digest_mismatch",
|
|
)
|
|
: Object.assign(new Error("ordinary provider connection failed"), {
|
|
code: "native_event_replay_conflict",
|
|
recovery: "operator_required",
|
|
});
|
|
const closeFailure = new NativeSessionCloseUnrecoverableError();
|
|
let observeClose = () => {};
|
|
const closeStarted = new Promise<void>((resolve) => {
|
|
observeClose = resolve;
|
|
});
|
|
const close = vi.fn(async () => {
|
|
observeClose();
|
|
if (closeFails) throw closeFailure;
|
|
});
|
|
const capabilities = {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
};
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
capabilities: async () => capabilities,
|
|
async *events() {
|
|
throw failure;
|
|
},
|
|
startTurn: async () => {
|
|
if (startupRace) {
|
|
await closeStarted;
|
|
throw new Error(
|
|
"provider_transport_failed: startup raced with close",
|
|
);
|
|
}
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
result: vi.fn(async () => null),
|
|
snapshot: async () => ({
|
|
backendKind: "mock",
|
|
sessionId: "driver-integrity",
|
|
identity,
|
|
providerSessionId: "provider-integrity",
|
|
cursor: "0",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
}),
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
descriptor: async () => ({
|
|
kind: "mock",
|
|
name: `integrity-${typed}-${closeFails}-${startupRace}`,
|
|
version: "1",
|
|
capabilities,
|
|
}),
|
|
openSession: vi.fn(async () => session),
|
|
};
|
|
const events: PrpEvent[] = [];
|
|
const port: ControlPlanePort = {
|
|
openRun: async () => {},
|
|
checkpointSession: async () => {},
|
|
appendEvent: async (event) => {
|
|
events.push(structuredClone(event as PrpEvent));
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
replayEvents: async () => ({
|
|
events: [],
|
|
highestContiguousSourceSeq: 0,
|
|
}),
|
|
completeRun: vi.fn(async () => {}),
|
|
};
|
|
const onSession = vi.fn();
|
|
const onSessionAdmission = vi.fn(async () => {});
|
|
const options: ExecuteNativeSessionOptions = {
|
|
onSessionAdmission,
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
onSession,
|
|
requireSessionCloseBeforeReturn: true,
|
|
timeoutMs: 900_000,
|
|
};
|
|
await expect(executeNativeSession(options)).rejects.toBe(
|
|
failure,
|
|
);
|
|
if (closeFails) expect(failure).toHaveProperty("cleanupError", closeFailure);
|
|
expect(close).toHaveBeenCalledOnce();
|
|
expect(onSession).toHaveBeenLastCalledWith(null);
|
|
expect(port.completeRun).not.toHaveBeenCalled();
|
|
expect(session.result).not.toHaveBeenCalled();
|
|
expect(events.some((event) => event.sourceKind === "runner")).toBe(false);
|
|
if (closeFails) {
|
|
await expect(executeNativeSession(options)).rejects.toBeInstanceOf(
|
|
NativeSessionCleanupQuarantinedError,
|
|
);
|
|
expect(backend.openSession).toHaveBeenCalledOnce();
|
|
expect(onSessionAdmission).toHaveBeenCalledOnce();
|
|
expect(close).toHaveBeenCalledOnce();
|
|
}
|
|
},
|
|
);
|
|
|
|
it("collects a failed run's private instructions only after its owned provider close joins", async () => {
|
|
const scopedIdentity = { ...identity, companyId: "instruction-close-company", runId: "instruction-close-run" };
|
|
let finishClose!: () => void;
|
|
const closed = new Promise<void>((resolve) => { finishClose = resolve; });
|
|
const order: string[] = [];
|
|
const capabilities = { resume: true, typedEvents: true, steering: false, interruption: false, structuredResult: true };
|
|
const session: NativeSession = {
|
|
identity: () => scopedIdentity,
|
|
capabilities: async () => capabilities,
|
|
async *events() { throw new Error("provider failed after editing instructions"); },
|
|
startTurn: async () => ({ turnId: "instruction-turn" }),
|
|
result: async () => null,
|
|
snapshot: async () => ({ backendKind: "local", sessionId: "instruction-session", identity: scopedIdentity,
|
|
providerSessionId: "instruction-provider", cursor: null, activeTurnId: null, pendingRuntimeRequests: [], lineage: [] }),
|
|
close: async () => { order.push("closing"); await closed; order.push("stopped"); },
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
descriptor: async () => ({ kind: "local", name: "instruction-close-test", version: "1", capabilities }),
|
|
openSession: async () => session,
|
|
};
|
|
const controlPlane: ControlPlanePort = {
|
|
openRun: async () => {}, checkpointSession: async () => {},
|
|
appendEvent: async () => ({ cursor: 0, highestContiguousSourceSeq: 0, disposition: "committed" }),
|
|
replayEvents: async () => ({ events: [], highestContiguousSourceSeq: 0 }), completeRun: async () => {},
|
|
};
|
|
const options = {
|
|
input: { ...input, binding: { ...input.binding, companyId: scopedIdentity.companyId, runId: scopedIdentity.runId } },
|
|
backend, controlPlane, runnerInstanceId: "instruction-runner", controlPlaneInstanceId: "control",
|
|
requireSessionCloseBeforeReturn: true,
|
|
onSessionClosed: async () => { order.push("collected"); },
|
|
};
|
|
const execution = executeNativeSession(options);
|
|
const failed = expect(execution).rejects.toThrow("provider failed after editing instructions");
|
|
await vi.waitFor(() => expect(order).toEqual(["closing"]));
|
|
finishClose();
|
|
await failed;
|
|
expect(order).toEqual(["closing", "stopped", "collected"]);
|
|
});
|
|
|
|
it("retires only the terminated remote resource, including two sandboxes for one run", async () => {
|
|
const scopedIdentity = { ...identity, companyId: "remote-stop-company", runId: "remote-stop-run" };
|
|
const binding = { ...scopedIdentity, remoteCleanupScope: "first-sandbox" };
|
|
const scopedInput = { ...input, binding: { ...input.binding, companyId: scopedIdentity.companyId, runId: scopedIdentity.runId } };
|
|
const failure = new NativeSessionCloseUnrecoverableError();
|
|
const capabilities = { resume: true, typedEvents: true, steering: false, interruption: false, structuredResult: true };
|
|
const session: NativeSession = {
|
|
identity: () => scopedIdentity,
|
|
capabilities: async () => capabilities,
|
|
async *events() { throw new Error("cancelled remote transport"); },
|
|
startTurn: async () => ({ turnId: "remote-turn" }),
|
|
result: async () => null,
|
|
async snapshot() { return { backendKind: "mock", sessionId: "stop-session", identity: this.identity(),
|
|
providerSessionId: "stop-provider", cursor: null, activeTurnId: null, pendingRuntimeRequests: [], lineage: [] }; },
|
|
close: vi.fn(async () => { throw failure; }),
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
descriptor: async () => ({ kind: "remote", name: "remote-stop-test", version: "1", capabilities }),
|
|
openSession: vi.fn(async () => session),
|
|
};
|
|
const controlPlane: ControlPlanePort = {
|
|
openRun: async () => {}, checkpointSession: async () => {},
|
|
appendEvent: async () => ({ cursor: 0, highestContiguousSourceSeq: 0, disposition: "committed" }),
|
|
replayEvents: async () => ({ events: [], highestContiguousSourceSeq: 0 }),
|
|
completeRun: vi.fn(async () => {}),
|
|
};
|
|
const options = { input: scopedInput, backend, controlPlane, runnerInstanceId: "remote-runner",
|
|
controlPlaneInstanceId: "control", requireSessionCloseBeforeReturn: true,
|
|
remoteCleanupScope: binding.remoteCleanupScope };
|
|
await expect(executeNativeSession(options)).rejects.toMatchObject({ message: "cancelled remote transport", cleanupError: failure });
|
|
expect(completeTerminatedRemoteNativeSessionCleanup({ ...binding, runId: "other-run" })).toBe(true);
|
|
expect(completeTerminatedRemoteNativeSessionCleanup({ ...binding, companyId: "other-company" })).toBe(true);
|
|
await expect(executeNativeSession(options)).rejects.toBeInstanceOf(NativeSessionCleanupQuarantinedError);
|
|
expect(backend.openSession).toHaveBeenCalledOnce();
|
|
// A separate sandbox can start without inheriting this process quarantine.
|
|
const independent = { ...scopedIdentity, sessionId: "other-sandbox-session" };
|
|
const independentSession = { ...session, identity: () => independent };
|
|
const independentBackend = { ...backend, openSession: vi.fn(async () => independentSession) };
|
|
await expect(executeNativeSession({ ...options, remoteCleanupScope: "other-sandbox",
|
|
input: { ...scopedInput, binding: { ...scopedInput.binding, runId: independent.runId } },
|
|
backend: independentBackend })).rejects.toMatchObject({ message: "cancelled remote transport", cleanupError: failure });
|
|
expect(independentBackend.openSession).toHaveBeenCalledOnce();
|
|
expect(completeTerminatedRemoteNativeSessionCleanup(binding)).toBe(true);
|
|
// Same company/run, different sandbox: its quarantine must remain intact.
|
|
await expect(executeNativeSession({ ...options, remoteCleanupScope: "other-sandbox",
|
|
backend: independentBackend })).rejects.toBeInstanceOf(NativeSessionCleanupQuarantinedError);
|
|
expect(independentBackend.openSession).toHaveBeenCalledOnce();
|
|
// Reopening is now possible; the old failure/result was never rewritten.
|
|
await expect(executeNativeSession(options)).rejects.toMatchObject({ message: "cancelled remote transport", cleanupError: failure });
|
|
expect(backend.openSession).toHaveBeenCalledTimes(2);
|
|
expect(controlPlane.completeRun).not.toHaveBeenCalled();
|
|
completeTerminatedRemoteNativeSessionCleanup(binding);
|
|
completeTerminatedRemoteNativeSessionCleanup({ ...binding, remoteCleanupScope: "other-sandbox" });
|
|
});
|
|
|
|
it("retires local quarantine only for the stopped run and runner instance", async () => {
|
|
const scopedIdentity = { ...identity, companyId: "local-stop-company", runId: "local-stop-run" };
|
|
const binding = { ...scopedIdentity, runnerInstanceId: "local-runner" };
|
|
const scopedInput = { ...input, binding: { ...input.binding, companyId: scopedIdentity.companyId, runId: scopedIdentity.runId } };
|
|
const failure = new NativeSessionCloseUnrecoverableError();
|
|
const capabilities = { resume: true, typedEvents: true, steering: false, interruption: false, structuredResult: true };
|
|
const session: NativeSession = {
|
|
identity: () => scopedIdentity, capabilities: async () => capabilities,
|
|
async *events() { throw new Error("local transport stopped"); },
|
|
startTurn: async () => ({ turnId: "local-turn" }), result: async () => null,
|
|
async snapshot() { return { backendKind: "mock", sessionId: "stop-session", identity: this.identity(),
|
|
providerSessionId: "stop-provider", cursor: null, activeTurnId: null, pendingRuntimeRequests: [], lineage: [] }; },
|
|
close: vi.fn(async () => { throw failure; }),
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
descriptor: async () => ({ kind: "local", name: "local-stop-test", version: "1", capabilities }),
|
|
openSession: vi.fn(async () => session),
|
|
};
|
|
const controlPlane: ControlPlanePort = {
|
|
openRun: async () => {}, checkpointSession: async () => {},
|
|
appendEvent: async () => ({ cursor: 0, highestContiguousSourceSeq: 0, disposition: "committed" }),
|
|
replayEvents: async () => ({ events: [], highestContiguousSourceSeq: 0 }), completeRun: vi.fn(async () => {}),
|
|
};
|
|
const options = { input: scopedInput, backend, controlPlane, runnerInstanceId: binding.runnerInstanceId,
|
|
controlPlaneInstanceId: "control", requireSessionCloseBeforeReturn: true };
|
|
await expect(executeNativeSession(options)).rejects.toMatchObject({ message: "local transport stopped", cleanupError: failure });
|
|
completeTerminatedLocalNativeSessionCleanup({ ...binding, companyId: "other-company" });
|
|
completeTerminatedLocalNativeSessionCleanup({ ...binding, runId: "other-run" });
|
|
expect(completeTerminatedLocalNativeSessionCleanup({ ...binding, runnerInstanceId: "other-runner" })).toBe(false);
|
|
await expect(executeNativeSession(options)).rejects.toBeInstanceOf(NativeSessionCleanupQuarantinedError);
|
|
expect(backend.openSession).toHaveBeenCalledOnce();
|
|
expect(completeTerminatedLocalNativeSessionCleanup(binding)).toBe(true);
|
|
await expect(executeNativeSession(options)).rejects.toMatchObject({ message: "local transport stopped", cleanupError: failure });
|
|
expect(backend.openSession).toHaveBeenCalledTimes(2);
|
|
expect(controlPlane.completeRun).not.toHaveBeenCalled();
|
|
completeTerminatedLocalNativeSessionCleanup(binding);
|
|
});
|
|
|
|
it("propagates an exhausted required backend checkpoint close", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const closeFailure = new Error("required remote checkpoint close failed");
|
|
const onSessionClosed = vi.fn(async () => {});
|
|
const close = vi.fn(({ reason }: { reason: string }) =>
|
|
reason === "native session quarantined cleanup recovery"
|
|
? Promise.resolve()
|
|
: Promise.reject(closeFailure),
|
|
);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "1",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: false,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const events: PrpEvent[] = [];
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
events.push(structuredClone(event as PrpEvent));
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const execution = expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
requireSessionCloseBeforeReturn: true,
|
|
onSessionClosed,
|
|
}),
|
|
).rejects.toThrow(closeFailure);
|
|
await vi.advanceTimersByTimeAsync(3_000);
|
|
await execution;
|
|
expect(close).toHaveBeenCalledTimes(5);
|
|
expect(onSessionClosed).not.toHaveBeenCalled();
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("closes after a synchronous governed-wait probe returns no result", async () => {
|
|
const resolveGovernedWait = vi.fn(() => null);
|
|
const lifecycle: string[] = [];
|
|
const close = vi.fn(async () => {
|
|
lifecycle.push("closed");
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "item.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: "turn-recovery",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 5,
|
|
resolveGovernedWait,
|
|
});
|
|
await expect(execution).rejects.toThrow("before a turn terminal fact");
|
|
expect(resolveGovernedWait).toHaveBeenCalledOnce();
|
|
expect(close).toHaveBeenCalled();
|
|
expect(lifecycle).toEqual(["closed"]);
|
|
});
|
|
|
|
it("commits a proved governed wait without waiting for abort-insensitive provider cleanup", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const lifecycle: string[] = [];
|
|
let cancellationSignal: AbortSignal | undefined;
|
|
const cancel = vi.fn(({ signal }: { signal: AbortSignal }) => {
|
|
cancellationSignal = signal;
|
|
lifecycle.push("cancelled");
|
|
return { cleanup: new Promise<void>(() => undefined) };
|
|
});
|
|
const close = vi.fn(async () => {
|
|
lifecycle.push("closed");
|
|
});
|
|
const events: PrpEvent[] = [];
|
|
const retainedSessions: Array<NativeSession | null> = [];
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "item.completed");
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
yield runnerEvent(2, "turn.interrupted", { providerTerminalObserved: true });
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
cancel,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: null,
|
|
activeTurnId: "turn-recovery",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
events.push(structuredClone(event as PrpEvent));
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: event.sourceSeq,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 5,
|
|
resolveGovernedWait: () => yieldedResult,
|
|
keepSessionOpen: true,
|
|
onSession: (current) => retainedSessions.push(current),
|
|
});
|
|
let settled = false;
|
|
const completed = execution.then(value => { settled = true; return value; });
|
|
void completed.catch(() => undefined);
|
|
await vi.advanceTimersByTimeAsync(9_900);
|
|
expect(settled).toBe(false);
|
|
await vi.advanceTimersByTimeAsync(200);
|
|
await expect(completed).resolves.toMatchObject({ result: yieldedResult });
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
expect(cancellationSignal?.aborted).toBe(true);
|
|
expect(close).toHaveBeenCalledOnce();
|
|
expect(lifecycle).toEqual(["cancelled", "closed"]);
|
|
expect(retainedSessions.at(-1)).toBeNull();
|
|
expect(events.map((event) => event.eventType)).toEqual([
|
|
"item.completed",
|
|
"turn.interrupted",
|
|
"run.result.accepted",
|
|
"run.terminal",
|
|
]);
|
|
} finally { vi.useRealTimers(); }
|
|
});
|
|
|
|
it("uses the execution timeout when a completion report has no provider terminal", async () => {
|
|
const lifecycle: string[] = [];
|
|
let releaseProvider = () => {};
|
|
const providerReleased = new Promise<void>((resolve) => {
|
|
releaseProvider = resolve;
|
|
});
|
|
const cancel = vi.fn(() => {
|
|
lifecycle.push("cancelled");
|
|
return {
|
|
cleanup: new Promise<void>((resolve) =>
|
|
setTimeout(() => {
|
|
releaseProvider();
|
|
resolve();
|
|
}, 150),
|
|
),
|
|
};
|
|
});
|
|
const providerResult = vi.fn(async () => null);
|
|
const close = vi.fn(async () => {
|
|
lifecycle.push("closed");
|
|
});
|
|
const events: PrpEvent[] = [];
|
|
const retainedSessions: Array<NativeSession | null> = [];
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "run.result.proposed", result);
|
|
await providerReleased;
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
cancel,
|
|
result: providerResult,
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "1",
|
|
activeTurnId: "turn-recovery",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "semantic-result-terminal-stall-backend",
|
|
version: "1",
|
|
capabilities: await session.capabilities(),
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
events.push(structuredClone(event as PrpEvent));
|
|
const sourceEvents = events.filter(
|
|
(candidate) => candidate.sourceInstanceId === event.sourceInstanceId,
|
|
);
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(sourceEvents),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
const sourceEvents = events.filter(
|
|
(event) => event.sourceInstanceId === replay.sourceInstanceId,
|
|
);
|
|
return {
|
|
events: structuredClone(
|
|
sourceEvents.filter(
|
|
(event) => event.sourceSeq > replay.afterSourceSeq,
|
|
),
|
|
),
|
|
highestContiguousSourceSeq: highestContiguous(sourceEvents),
|
|
};
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
timeoutMs: 20,
|
|
keepSessionOpen: true,
|
|
onSession: (current) => retainedSessions.push(current),
|
|
}),
|
|
).rejects.toThrow("native session timed out after 20ms");
|
|
|
|
expect(cancel).toHaveBeenCalledWith({
|
|
reason: "Native session event consumption failed.",
|
|
signal: expect.any(AbortSignal),
|
|
});
|
|
expect(providerResult).not.toHaveBeenCalled();
|
|
expect(events.map((event) => event.eventType)).toEqual([
|
|
"run.result.proposed",
|
|
]);
|
|
expect(lifecycle).toContain("cancelled");
|
|
expect(close).toHaveBeenCalledOnce();
|
|
expect(retainedSessions.at(-1)).toBeNull();
|
|
});
|
|
|
|
it.each([false, true])("retains a reusable session while terminal teardown settles (governed=%s)", async (governed) => {
|
|
const close = vi.fn(async () => undefined);
|
|
const retainedSessions: Array<NativeSession | null> = [];
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
try {
|
|
yield { ...runnerEvent(1, governed ? "item.completed" : "run.result.proposed", governed ? { kind: "dynamicToolCall" } : result), turnId: "turn-recovery" };
|
|
yield {
|
|
...runnerEvent(2, governed ? "turn.interrupted" : "turn.completed"),
|
|
turnId: "turn-recovery",
|
|
};
|
|
} finally {
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 150));
|
|
}
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "2",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
cancel: () => ({ cleanup: Promise.resolve() }),
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "semantic-terminal-subscription-backend",
|
|
version: "1",
|
|
capabilities: await session.capabilities(),
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const events: PrpEvent[] = [];
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
events.push(structuredClone(event as PrpEvent));
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
resolveGovernedWait: governed ? () => yieldedResult : undefined,
|
|
keepSessionOpen: true,
|
|
onSession: (current) => retainedSessions.push(current),
|
|
}),
|
|
).resolves.toMatchObject({ result: governed ? yieldedResult : result });
|
|
|
|
expect(close).not.toHaveBeenCalled();
|
|
expect(retainedSessions).toEqual([session]);
|
|
});
|
|
|
|
it.each([
|
|
["turn.completed", "succeeded"],
|
|
["turn.failed", "failed"],
|
|
["turn.cancelled", "cancelled"],
|
|
["turn.interrupted", "cancelled"],
|
|
["stream.closed", null],
|
|
] as const)("persists a delayed final answer before settling %s", async (eventType, runTerminalState) => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
let releasePersistence!: () => void;
|
|
const persistence = new Promise<void>((resolve) => { releasePersistence = resolve; });
|
|
let persistingAnswer = false;
|
|
const cancel = vi.fn(() => ({ cleanup: Promise.resolve() }));
|
|
const close = vi.fn(async () => undefined);
|
|
const events: PrpEvent[] = [];
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "run.result.proposed", result);
|
|
await new Promise((resolve) => setTimeout(resolve, 6_000));
|
|
yield runnerEvent(2, "item.completed", {
|
|
kind: "agentMessage", channel: "final", text: "Final response.",
|
|
});
|
|
if (eventType !== "stream.closed") yield runnerEvent(3, eventType);
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
cancel,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "3",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "semantic-result-final-response-backend",
|
|
version: "1",
|
|
capabilities: await session.capabilities(),
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
if (event.eventType === "item.completed") {
|
|
persistingAnswer = true;
|
|
await persistence;
|
|
}
|
|
events.push(structuredClone(event as PrpEvent));
|
|
const sourceEvents = events.filter(
|
|
(candidate) => candidate.sourceInstanceId === event.sourceInstanceId,
|
|
);
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(sourceEvents),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
const sourceEvents = events.filter(
|
|
(event) => event.sourceInstanceId === replay.sourceInstanceId,
|
|
);
|
|
return {
|
|
events: structuredClone(
|
|
sourceEvents.filter(
|
|
(event) => event.sourceSeq > replay.afterSourceSeq,
|
|
),
|
|
),
|
|
highestContiguousSourceSeq: highestContiguous(sourceEvents),
|
|
};
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
let completed = false;
|
|
const execution = executeNativeSession({
|
|
input, backend, controlPlane: port,
|
|
runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery",
|
|
}).then((value) => { completed = true; return value; });
|
|
await vi.waitFor(() => expect(events).toHaveLength(1));
|
|
await vi.advanceTimersByTimeAsync(6_001);
|
|
expect(persistingAnswer).toBe(true);
|
|
expect(completed).toBe(false);
|
|
expect(events.map((event) => event.eventType)).toEqual(["run.result.proposed"]);
|
|
expect(cancel).not.toHaveBeenCalled();
|
|
releasePersistence();
|
|
if (eventType === "stream.closed") {
|
|
await expect(execution).rejects.toThrow("native event stream closed before a turn terminal fact");
|
|
expect(events.map((event) => event.eventType)).toEqual(["run.result.proposed", "item.completed"]);
|
|
return;
|
|
}
|
|
await expect(execution).resolves.toMatchObject({ result, terminal: { runTerminalState } });
|
|
|
|
expect(cancel).not.toHaveBeenCalled();
|
|
expect(close).toHaveBeenCalledOnce();
|
|
expect(events.map((event) => event.eventType)).toEqual([
|
|
"run.result.proposed",
|
|
"item.completed",
|
|
eventType,
|
|
"run.result.accepted",
|
|
"run.terminal",
|
|
]);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("rejects a mismatched checkpoint before it mutates control-plane state", async () => {
|
|
const openRun = vi.fn(async () => undefined);
|
|
const checkpointSession = vi.fn(async () => undefined);
|
|
const openSession = vi.fn();
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async loadSessionCheckpoint() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity: { ...identity, companyId: "other-company" },
|
|
};
|
|
},
|
|
checkpointSession,
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
}),
|
|
).rejects.toThrow("native_session_checkpoint_binding_mismatch");
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
expect(checkpointSession).not.toHaveBeenCalled();
|
|
expect(openSession).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects a mismatched existing session before opening control-plane state", async () => {
|
|
const openRun = vi.fn(async () => undefined);
|
|
const attachRun = vi.fn(async () => undefined);
|
|
const existingSession: NativeSession = {
|
|
identity: () => ({ ...identity, companyId: "other-company" }),
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
attachRun,
|
|
async *events() {},
|
|
async startTurn() {
|
|
return { turnId: "unexpected" };
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
throw new Error("unexpected snapshot");
|
|
},
|
|
async close() {},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "existing-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("unexpected open");
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
existingSession,
|
|
}),
|
|
).rejects.toThrow("native_session_attach_binding_mismatch");
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
expect(attachRun).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each(["codex", "copilot"] as const)("quarantines a retained %s session when attachment partially mutates then fails", async (provider) => {
|
|
const qualified = resolveQualifiedAcpxProfile("copilot", "selected-model");
|
|
const executionInput: NativeExecutionInputV1 = provider === "copilot" ? {
|
|
...input,
|
|
session: { ...input.session, driverKind: "acpx_runtime" },
|
|
provider: { kind: "acpx", agent: "copilot", model: "selected-model", permissionPolicy: "interactive", profile: {
|
|
driverKind: qualified.driverKind, protocolVersion: qualified.protocolVersion, acpxVersion: qualified.acpxVersion,
|
|
agent: qualified.agent, agentProfileVersion: qualified.agentProfileVersion,
|
|
agentServerPackage: qualified.agentServerPackage, agentServerVersion: qualified.agentServerVersion,
|
|
agentRuntimePackage: qualified.agentRuntimePackage, agentRuntimeVersion: qualified.agentRuntimeVersion,
|
|
commandDigest: qualified.commandDigest,
|
|
} },
|
|
} : input;
|
|
const attachmentFailure = new Error("provider attachment failed");
|
|
const openRun = vi.fn(async () => undefined);
|
|
let retainedIdentity = { ...identity, runId: "run-previous" };
|
|
const attachRun = vi.fn(async (input: { identity: NativeRunIdentity }) => {
|
|
retainedIdentity = structuredClone(input.identity);
|
|
throw attachmentFailure;
|
|
});
|
|
const close = vi.fn(() => new Promise<void>(() => {}));
|
|
const startTurn = vi.fn(async () => ({ turnId: "unexpected" }));
|
|
const onSession = vi.fn();
|
|
const existingSession: NativeSession = {
|
|
identity: () => structuredClone(retainedIdentity),
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
attachRun,
|
|
async *events() {},
|
|
startTurn,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
throw new Error("unexpected snapshot");
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: `attachment-failure-backend-${provider}`,
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("unexpected open");
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input: executionInput,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
existingSession,
|
|
onSession,
|
|
}),
|
|
).rejects.toBe(attachmentFailure);
|
|
expect(attachRun).toHaveBeenCalledWith(provider === "copilot"
|
|
? { identity, currentRunGrant: { runtimeContext: null, instructions: expect.any(String) } }
|
|
: { identity });
|
|
expect(retainedIdentity).toEqual(identity);
|
|
expect(onSession).toHaveBeenCalledOnce();
|
|
expect(onSession).toHaveBeenCalledWith(null);
|
|
expect(close).toHaveBeenCalledWith({
|
|
reason: "native session attachment failed",
|
|
});
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
expect(startTurn).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("quarantines an attached session when control-plane run admission fails", async () => {
|
|
const admissionFailure = new Error("control-plane admission failed");
|
|
const openRun = vi.fn(async () => {
|
|
throw admissionFailure;
|
|
});
|
|
const attachRun = vi.fn(async () => undefined);
|
|
const close = vi.fn(async () => undefined);
|
|
const startTurn = vi.fn(async () => ({ turnId: "unexpected" }));
|
|
const onSession = vi.fn();
|
|
const existingSession: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
attachRun,
|
|
async *events() {},
|
|
startTurn,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
throw new Error("unexpected snapshot");
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "control-plane-admission-failure-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("unexpected open");
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
existingSession,
|
|
onSession,
|
|
}),
|
|
).rejects.toBe(admissionFailure);
|
|
expect(attachRun).toHaveBeenCalledWith({ identity });
|
|
expect(openRun).toHaveBeenCalledOnce();
|
|
expect(onSession).toHaveBeenCalledOnce();
|
|
expect(onSession).toHaveBeenCalledWith(null);
|
|
expect(close).toHaveBeenCalledWith({
|
|
reason: "native control-plane run admission failed",
|
|
});
|
|
expect(startTurn).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects checkpoint adoption when the requested session id is absent", async () => {
|
|
const openRun = vi.fn(async () => undefined);
|
|
const openSession = vi.fn();
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async loadSessionCheckpoint() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: "driver-other-session",
|
|
identity: { ...identity, sessionId: "other-session" },
|
|
};
|
|
},
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input: {
|
|
...input,
|
|
session: { ...input.session, normalizedSessionId: null },
|
|
},
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
}),
|
|
).rejects.toThrow("native_session_checkpoint_binding_mismatch");
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
expect(openSession).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("proves required provider recovery before re-opening the durable run", async () => {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-unrecoverable",
|
|
identity,
|
|
providerSessionId: "provider-unrecoverable",
|
|
providerRecoveryPolicy: "same_session_only",
|
|
cursor: "0",
|
|
activeTurnId: "turn-unrecoverable",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const openRun = vi.fn(async () => undefined);
|
|
const completeRun = vi.fn(async () => undefined);
|
|
const recoverSession = vi.fn(async () => ({
|
|
recovered: false as const,
|
|
reason: "provider session no longer exists",
|
|
}));
|
|
const openSession = vi.fn(async () => {
|
|
throw new Error("replacement is forbidden");
|
|
});
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
recoverSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
completeRun,
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
}),
|
|
).rejects.toThrow(
|
|
"native_session_recovery_failed: provider session no longer exists",
|
|
);
|
|
|
|
expect(recoverSession).toHaveBeenCalledOnce();
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
expect(completeRun).not.toHaveBeenCalled();
|
|
expect(openSession).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("bounds paginated recovery replay with one signal and observes a late rejection", async () => {
|
|
vi.useFakeTimers();
|
|
let rejectStalledReplay = (_error: Error) => {};
|
|
try {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-replay-stalled",
|
|
identity,
|
|
providerSessionId: "provider-replay-stalled",
|
|
providerRecoveryPolicy: "same_session_only",
|
|
cursor: "0",
|
|
activeTurnId: "turn-replay-stalled",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const stalledReplay = new Promise<never>((_resolve, reject) => {
|
|
rejectStalledReplay = reject;
|
|
});
|
|
let markSecondPageStarted = () => {};
|
|
const secondPageStarted = new Promise<void>((resolve) => {
|
|
markSecondPageStarted = resolve;
|
|
});
|
|
const replaySignals: AbortSignal[] = [];
|
|
const replayEvents = vi.fn<ControlPlanePort["replayEvents"]>(
|
|
async (replay, operationOptions) => {
|
|
expect(operationOptions?.signal).toBeInstanceOf(AbortSignal);
|
|
replaySignals.push(operationOptions!.signal);
|
|
if (replay.afterSourceSeq === 0) {
|
|
return {
|
|
events: [runnerEvent(1, "item.completed", { kind: "progress" })],
|
|
highestContiguousSourceSeq: 1,
|
|
};
|
|
}
|
|
markSecondPageStarted();
|
|
return await stalledReplay;
|
|
},
|
|
);
|
|
const recoverSession = vi.fn();
|
|
const openRun = vi.fn(async () => undefined);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("unexpected replacement");
|
|
},
|
|
recoverSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
replayEvents,
|
|
async completeRun() {},
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
persistedSession: structuredClone(checkpoint),
|
|
timeoutMs: 5,
|
|
});
|
|
const rejection = expect(execution).rejects.toThrow(
|
|
"native session recovery replay timed out after 5ms",
|
|
);
|
|
|
|
await secondPageStarted;
|
|
expect(replayEvents).toHaveBeenCalledTimes(2);
|
|
expect(replaySignals).toHaveLength(2);
|
|
expect(replaySignals[1]).toBe(replaySignals[0]);
|
|
expect(replaySignals[0]?.aborted).toBe(false);
|
|
|
|
await vi.advanceTimersByTimeAsync(5);
|
|
await rejection;
|
|
|
|
expect(replaySignals[0]?.aborted).toBe(true);
|
|
expect(recoverSession).not.toHaveBeenCalled();
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
|
|
// A broken adapter may ignore abort and reject later. The bounded helper
|
|
// keeps that losing operation observed after execution already rejected.
|
|
rejectStalledReplay(new Error("late recovery replay failure"));
|
|
await Promise.resolve();
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("bounds provider recovery and closes a session returned after timeout", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery-stalled",
|
|
identity,
|
|
providerSessionId: "provider-recovery-stalled",
|
|
providerRecoveryPolicy: "same_session_only",
|
|
cursor: "0",
|
|
activeTurnId: "turn-recovery-stalled",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
let resolveRecovery = (_value: {
|
|
recovered: true;
|
|
session: NativeSession;
|
|
}) => {};
|
|
const stalledRecovery = new Promise<{
|
|
recovered: true;
|
|
session: NativeSession;
|
|
}>((resolve) => {
|
|
resolveRecovery = resolve;
|
|
});
|
|
let markRecoveryStarted = () => {};
|
|
const recoveryStarted = new Promise<void>((resolve) => {
|
|
markRecoveryStarted = resolve;
|
|
});
|
|
let markCloseStarted = () => {};
|
|
const closeStarted = new Promise<void>((resolve) => {
|
|
markCloseStarted = resolve;
|
|
});
|
|
const close = vi.fn(async () => {
|
|
markCloseStarted();
|
|
});
|
|
const lateSession: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
};
|
|
},
|
|
async *events() {},
|
|
async startTurn() {
|
|
throw new Error("late recovery session must not start");
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
close,
|
|
};
|
|
let recoverySignal: AbortSignal | undefined;
|
|
const recoverSession = vi.fn(
|
|
(
|
|
_checkpoint: PersistedNativeSession,
|
|
recoveryOptions: { signal: AbortSignal },
|
|
) => {
|
|
recoverySignal = recoveryOptions.signal;
|
|
recoveryOptions.signal.addEventListener(
|
|
"abort",
|
|
() => resolveRecovery({ recovered: true, session: lateSession }),
|
|
{ once: true },
|
|
);
|
|
markRecoveryStarted();
|
|
return stalledRecovery;
|
|
},
|
|
);
|
|
const openRun = vi.fn(async () => undefined);
|
|
const onSession = vi.fn();
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("unexpected replacement");
|
|
},
|
|
recoverSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
persistedSession: structuredClone(checkpoint),
|
|
timeoutMs: 5,
|
|
onSession,
|
|
});
|
|
const rejection = expect(execution).rejects.toThrow(
|
|
"native session provider recovery timed out after 5ms",
|
|
);
|
|
|
|
await recoveryStarted;
|
|
expect(recoverySignal?.aborted).toBe(false);
|
|
await vi.advanceTimersByTimeAsync(5);
|
|
await rejection;
|
|
expect(recoverySignal?.aborted).toBe(true);
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
expect(onSession).not.toHaveBeenCalled();
|
|
|
|
await closeStarted;
|
|
expect(close).toHaveBeenCalledWith({
|
|
reason: "native session provider recovery timed out",
|
|
});
|
|
expect(onSession).not.toHaveBeenCalled();
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("bounds replacement bootstrap and closes a session returned after timeout", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-replacement-stalled",
|
|
identity,
|
|
providerSessionId: "provider-replacement-stalled",
|
|
providerRecoveryPolicy: "allow_replacement_after_resume_failure",
|
|
cursor: "0",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
let resolveReplacement = (_value: NativeSession) => {};
|
|
const stalledReplacement = new Promise<NativeSession>((resolve) => {
|
|
resolveReplacement = resolve;
|
|
});
|
|
let markReplacementStarted = () => {};
|
|
const replacementStarted = new Promise<void>((resolve) => {
|
|
markReplacementStarted = resolve;
|
|
});
|
|
let markCloseStarted = () => {};
|
|
const closeStarted = new Promise<void>((resolve) => {
|
|
markCloseStarted = resolve;
|
|
});
|
|
const close = vi.fn(async () => {
|
|
markCloseStarted();
|
|
});
|
|
const lateSession: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
};
|
|
},
|
|
async *events() {},
|
|
async startTurn() {
|
|
throw new Error("late replacement session must not start");
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
close,
|
|
};
|
|
let replacementSignal: AbortSignal | undefined;
|
|
const openReplacementSession = vi.fn(
|
|
(replacementInput: {
|
|
identity: NativeRunIdentity;
|
|
workingDirectory?: string;
|
|
signal?: AbortSignal;
|
|
}) => {
|
|
replacementSignal = replacementInput.signal;
|
|
replacementInput.signal?.addEventListener(
|
|
"abort",
|
|
() => resolveReplacement(lateSession),
|
|
{ once: true },
|
|
);
|
|
markReplacementStarted();
|
|
return stalledReplacement;
|
|
},
|
|
);
|
|
const openRun = vi.fn(async () => undefined);
|
|
const onSession = vi.fn();
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "replacement-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("replacement seam must be used");
|
|
},
|
|
async recoverSession() {
|
|
return { recovered: false, reason: "provider session is missing" };
|
|
},
|
|
openReplacementSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
persistedSession: structuredClone(checkpoint),
|
|
timeoutMs: 5,
|
|
onSession,
|
|
});
|
|
const rejection = expect(execution).rejects.toThrow(
|
|
"native session replacement bootstrap timed out after 5ms",
|
|
);
|
|
|
|
await replacementStarted;
|
|
expect(replacementSignal?.aborted).toBe(false);
|
|
await vi.advanceTimersByTimeAsync(5);
|
|
await rejection;
|
|
expect(replacementSignal?.aborted).toBe(true);
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
expect(onSession).not.toHaveBeenCalled();
|
|
|
|
await closeStarted;
|
|
expect(close).toHaveBeenCalledWith({
|
|
reason: "native session replacement bootstrap timed out",
|
|
});
|
|
expect(onSession).not.toHaveBeenCalled();
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("does not persist a reconciled recovery cursor when provider recovery rejects", async () => {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery-rejects",
|
|
identity,
|
|
providerSessionId: "provider-recovery-rejects",
|
|
providerRecoveryPolicy: "same_session_only",
|
|
cursor: "0",
|
|
activeTurnId: "turn-recovery-rejects",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const recoveryFailure = new Error("provider recovery rejected");
|
|
const openRun = vi.fn(async () => undefined);
|
|
const checkpointSession = vi.fn(async () => undefined);
|
|
const onCheckpoint = vi.fn(async () => undefined);
|
|
const recoverSession = vi.fn(
|
|
async (recoveryCheckpoint: PersistedNativeSession) => {
|
|
expect(recoveryCheckpoint.cursor).toBe("1");
|
|
throw recoveryFailure;
|
|
},
|
|
);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("unexpected replacement");
|
|
},
|
|
recoverSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
checkpointSession,
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents(replay) {
|
|
return {
|
|
events: replay.afterSourceSeq === 0 ? [runnerEvent(1)] : [],
|
|
highestContiguousSourceSeq: 1,
|
|
};
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
onCheckpoint,
|
|
}),
|
|
).rejects.toBe(recoveryFailure);
|
|
|
|
expect(recoverSession).toHaveBeenCalledOnce();
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
expect(checkpointSession).not.toHaveBeenCalled();
|
|
expect(onCheckpoint).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not persist a reconciled recovery cursor when run admission rejects", async () => {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-admission-rejects",
|
|
identity,
|
|
providerSessionId: "provider-admission-rejects",
|
|
providerRecoveryPolicy: "same_session_only",
|
|
cursor: "0",
|
|
activeTurnId: "turn-admission-rejects",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const admissionFailure = new Error("control-plane admission rejected");
|
|
const openRun = vi.fn(async () => {
|
|
throw admissionFailure;
|
|
});
|
|
const checkpointSession = vi.fn(async () => undefined);
|
|
const onCheckpoint = vi.fn(async () => undefined);
|
|
const onSession = vi.fn();
|
|
const close = vi.fn(async () => undefined);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {},
|
|
async startTurn() {
|
|
throw new Error("unexpected turn");
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
throw new Error("unexpected snapshot");
|
|
},
|
|
close,
|
|
};
|
|
const recoverSession = vi.fn(
|
|
async (recoveryCheckpoint: PersistedNativeSession) => {
|
|
expect(recoveryCheckpoint.cursor).toBe("1");
|
|
return { recovered: true as const, session };
|
|
},
|
|
);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("unexpected replacement");
|
|
},
|
|
recoverSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
checkpointSession,
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents(replay) {
|
|
return {
|
|
events: replay.afterSourceSeq === 0 ? [runnerEvent(1)] : [],
|
|
highestContiguousSourceSeq: 1,
|
|
};
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
onCheckpoint,
|
|
onSession,
|
|
}),
|
|
).rejects.toBe(admissionFailure);
|
|
|
|
expect(recoverSession).toHaveBeenCalledOnce();
|
|
expect(openRun).toHaveBeenCalledOnce();
|
|
expect(checkpointSession).not.toHaveBeenCalled();
|
|
expect(onCheckpoint).not.toHaveBeenCalled();
|
|
expect(onSession).toHaveBeenCalledOnce();
|
|
expect(onSession).toHaveBeenCalledWith(null);
|
|
expect(close).toHaveBeenCalledWith({
|
|
reason: "native control-plane run admission failed",
|
|
});
|
|
});
|
|
|
|
it("continues a provider-reported active turn without starting a duplicate turn", async () => {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "0",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const providerSnapshot: PersistedNativeSession = {
|
|
...checkpoint,
|
|
cursor: "1",
|
|
activeTurnId: "turn-recovery",
|
|
};
|
|
const terminalEvent: PrpEvent = {
|
|
schema: "paperclip.prp.event.v1",
|
|
sourceEventId: "provider-recovery:1",
|
|
sourceSeq: 1,
|
|
sourceInstanceId: "provider-recovery",
|
|
sourceKind: "provider",
|
|
runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId,
|
|
turnId: "turn-recovery",
|
|
eventType: "turn.completed",
|
|
schemaVersion: 1,
|
|
priority: 0,
|
|
emittedAt: "2026-08-09T00:00:00.000Z",
|
|
payload: {},
|
|
};
|
|
const bySource = new Map<string, PrpEvent[]>();
|
|
const startTurn = vi.fn(async () => ({ turnId: "duplicate-turn" }));
|
|
const openSession = vi.fn(async () => {
|
|
throw new Error("must recover the provider session");
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield terminalEvent;
|
|
},
|
|
startTurn,
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return structuredClone(providerSnapshot);
|
|
},
|
|
async close() {},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
async recoverSession() {
|
|
return { recovered: true, session };
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
const list = bySource.get(event.sourceInstanceId) ?? [];
|
|
list.push(structuredClone(event));
|
|
bySource.set(event.sourceInstanceId, list);
|
|
return {
|
|
cursor: list.length,
|
|
highestContiguousSourceSeq: highestContiguous(list),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
const list = bySource.get(replay.sourceInstanceId) ?? [];
|
|
return {
|
|
events: structuredClone(
|
|
list.filter((event) => event.sourceSeq > replay.afterSourceSeq),
|
|
),
|
|
highestContiguousSourceSeq: highestContiguous(list),
|
|
};
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
turnId: "turn-recovery",
|
|
providerSessionId: "provider-recovery",
|
|
});
|
|
expect(openSession).not.toHaveBeenCalled();
|
|
expect(startTurn).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
{ checkpointCursor: "12", expectedCursor: "41", terminalSequence: 42 },
|
|
{ checkpointCursor: "50", expectedCursor: "50", terminalSequence: 51 },
|
|
])(
|
|
"seeds recovery from the larger of checkpoint $checkpointCursor and the persisted source high-water mark",
|
|
async ({ checkpointCursor, expectedCursor, terminalSequence }) => {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: checkpointCursor,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const runnerEvents = [
|
|
runnerEvent(13, "item.completed", { kind: "progress" }),
|
|
runnerEvent(41, "item.completed", { kind: "progress" }),
|
|
];
|
|
const terminalEvent = runnerEvent(terminalSequence, "turn.completed");
|
|
const controlEvents: PrpEvent[] = [];
|
|
const checkpoints: PersistedNativeSession[] = [];
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield terminalEvent;
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-recovery" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
...checkpoint,
|
|
cursor: String(terminalSequence),
|
|
activeTurnId: null,
|
|
};
|
|
},
|
|
async close() {},
|
|
};
|
|
const recoverSession = vi.fn(
|
|
async (recoveryCheckpoint: PersistedNativeSession) => {
|
|
expect(recoveryCheckpoint.cursor).toBe(expectedCursor);
|
|
return { recovered: true, session };
|
|
},
|
|
);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("must recover the provider session");
|
|
},
|
|
recoverSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
async checkpointSession(snapshot) {
|
|
checkpoints.push(structuredClone(snapshot));
|
|
},
|
|
async appendEvent(event) {
|
|
const target =
|
|
event.sourceInstanceId === "runner-recovery"
|
|
? runnerEvents
|
|
: controlEvents;
|
|
if (
|
|
target.some((existing) => existing.sourceSeq === event.sourceSeq)
|
|
) {
|
|
throw new Error(`native_event_replay_conflict:${event.sourceSeq}`);
|
|
}
|
|
target.push(structuredClone(event));
|
|
return {
|
|
cursor: target.length,
|
|
highestContiguousSourceSeq: highestContiguous(target),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
const source =
|
|
replay.sourceInstanceId === "runner-recovery"
|
|
? runnerEvents
|
|
: controlEvents;
|
|
const events = source
|
|
.filter((event) => event.sourceSeq > replay.afterSourceSeq)
|
|
.sort((left, right) => left.sourceSeq - right.sourceSeq)
|
|
.slice(0, replay.limit);
|
|
return {
|
|
events: structuredClone(events),
|
|
highestContiguousSourceSeq: highestContiguous(source),
|
|
};
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const getFreshSessionHandoff = vi.fn(async () => "FRESH_HANDOFF_ONLY");
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
getFreshSessionHandoff,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
}),
|
|
).resolves.toMatchObject({ turnId: "turn-recovery" });
|
|
|
|
expect(getFreshSessionHandoff).not.toHaveBeenCalled();
|
|
expect(recoverSession).toHaveBeenCalledOnce();
|
|
expect(
|
|
runnerEvents.some((event) => event.sourceSeq === terminalSequence),
|
|
).toBe(true);
|
|
if (checkpointCursor === "12") {
|
|
expect(checkpoints[0]).toMatchObject({ cursor: "41" });
|
|
}
|
|
},
|
|
);
|
|
|
|
it("attempts exact recovery before opening an observable replacement session", async () => {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-old",
|
|
identity,
|
|
providerSessionId: "provider-old",
|
|
providerRecoveryPolicy: "allow_replacement_after_resume_failure",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const replacementSnapshot: PersistedNativeSession = {
|
|
...checkpoint,
|
|
sessionId: "driver-new",
|
|
providerSessionId: "provider-new",
|
|
providerRecoveryPolicy: "same_session_only",
|
|
};
|
|
const replacementSession: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-replacement" };
|
|
},
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-replacement" };
|
|
},
|
|
async snapshot() {
|
|
return structuredClone(replacementSnapshot);
|
|
},
|
|
async close() {},
|
|
};
|
|
const recoverSession = vi.fn(async () => ({
|
|
recovered: false as const,
|
|
reason: "provider reported the prior session missing",
|
|
}));
|
|
const openReplacementSession = vi.fn(async () => replacementSession);
|
|
const onContinuityBreak = vi.fn(async () => undefined);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "replacement-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("replacement seam must be used");
|
|
},
|
|
recoverSession,
|
|
openReplacementSession,
|
|
};
|
|
const events: PrpEvent[] = [];
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
events.push(structuredClone(event));
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-replacement",
|
|
controlPlaneInstanceId: "control-replacement",
|
|
onContinuityBreak,
|
|
}),
|
|
).resolves.toMatchObject({ providerSessionId: "provider-new" });
|
|
|
|
expect(recoverSession).toHaveBeenCalledOnce();
|
|
expect(openReplacementSession).toHaveBeenCalledOnce();
|
|
expect(onContinuityBreak).toHaveBeenCalledWith({
|
|
reason: "provider reported the prior session missing",
|
|
previousDriverSessionId: "driver-old",
|
|
previousProviderSessionId: "provider-old",
|
|
replacementDriverSessionId: "driver-new",
|
|
replacementProviderSessionId: "provider-new",
|
|
});
|
|
});
|
|
|
|
it("does not replace a failed provider session when recovery policy forbids it", async () => {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-failed-constrained",
|
|
identity,
|
|
providerSessionId: "provider-failed-constrained",
|
|
providerRecoveryPolicy: "same_session_only",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
semanticResult: null,
|
|
terminal: {
|
|
schema: "paperclip.prp.terminal.v1",
|
|
turnTerminalState: "failed",
|
|
runTerminalState: "failed",
|
|
reportedWorkDisposition: "yielded",
|
|
},
|
|
terminalTurns: [{ turnId: "turn-failed", fingerprint: "failed" }],
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const openRun = vi.fn(async () => undefined);
|
|
const openSession = vi.fn(async () => {
|
|
throw new Error("replacement is forbidden");
|
|
});
|
|
const recoverSession = vi.fn();
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "constrained-recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
recoverSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun,
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
async appendEvent() {
|
|
throw new Error("unexpected event");
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-constrained-recovery",
|
|
controlPlaneInstanceId: "control-constrained-recovery",
|
|
}),
|
|
).rejects.toMatchObject({
|
|
code: "native_provider_terminal_failed",
|
|
providerCode: "provider_checkpoint_failed_terminal",
|
|
recoverable: false,
|
|
});
|
|
|
|
expect(recoverSession).not.toHaveBeenCalled();
|
|
expect(openSession).not.toHaveBeenCalled();
|
|
expect(openRun).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
{ preparedMode: false, interrupted: false },
|
|
{ preparedMode: true, interrupted: false },
|
|
{ preparedMode: false, interrupted: true },
|
|
{ preparedMode: true, interrupted: true },
|
|
])("replaces a failed provider with full task context (prepared: $preparedMode, interrupted: $interrupted)", async ({ preparedMode, interrupted }) => {
|
|
const digest = "0".repeat(64);
|
|
const skill = {
|
|
key: "company/recovery-skill",
|
|
runtimeName: "recovery-skill",
|
|
versionId: "recovery-skill-v1",
|
|
bundle: {
|
|
schema: NATIVE_RUNTIME_ASSET_SCHEMA,
|
|
digest,
|
|
manifestDigest: digest,
|
|
rootPath: "/runtime/skills/recovery-skill",
|
|
fileCount: 1,
|
|
totalBytes: 1,
|
|
},
|
|
} as const;
|
|
const context = {
|
|
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
|
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
|
|
skills: [skill],
|
|
mcp: { assignmentSetId: "none", digest, bindingId: null },
|
|
} as const;
|
|
const legacyContext = { ...context, skills: [] as const };
|
|
const prepared = preparedInput();
|
|
const executionInput = preparedMode
|
|
? {
|
|
...prepared,
|
|
task: {
|
|
...prepared.task,
|
|
description: "Complete after recovery with $recovery-skill.",
|
|
prompt: "FULL_ASSIGNMENT_CONTEXT\nCURRENT_EVENT_CONTEXT",
|
|
},
|
|
runtimeContext: {
|
|
...context,
|
|
aggregateDigest: canonicalNativeRuntimeContextDigest(context),
|
|
},
|
|
continuationPrompt: "ONLY_NEW_COMMENT",
|
|
} as const
|
|
: {
|
|
...input,
|
|
task: { ...input.task, prompt: "FULL_ASSIGNMENT_CONTEXT\nCURRENT_EVENT_CONTEXT" },
|
|
schema: "paperclip.native-execution-input.v4" as const,
|
|
executionMode: "default" as const,
|
|
planningContext: null,
|
|
provider: { kind: "codex" as const, model: null, approvalPolicy: "never" as const },
|
|
runtimeContext: {
|
|
...legacyContext,
|
|
aggregateDigest: canonicalNativeRuntimeContextDigest(legacyContext),
|
|
},
|
|
continuationPrompt: "ONLY_NEW_COMMENT",
|
|
} as const;
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
driverKind: "codex_app_server",
|
|
sessionId: "driver-failed",
|
|
identity,
|
|
providerSessionId: "provider-failed",
|
|
providerRecoveryPolicy: "allow_replacement_after_resume_failure",
|
|
cursor: null,
|
|
activeTurnId: null,
|
|
semanticResult: null,
|
|
terminal: {
|
|
schema: "paperclip.prp.terminal.v1",
|
|
turnTerminalState: "failed",
|
|
runTerminalState: "failed",
|
|
reportedWorkDisposition: "yielded",
|
|
},
|
|
terminalTurns: [{ turnId: "turn-failed", fingerprint: interrupted ? JSON.stringify({
|
|
terminalState: "failed", result: null,
|
|
error: { code: "provider_turn_lost_on_restore", recoverable: true },
|
|
}) : "failed" }],
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const replacementSnapshot: PersistedNativeSession = {
|
|
...checkpoint,
|
|
sessionId: "driver-replacement",
|
|
providerSessionId: "provider-replacement",
|
|
terminal: null,
|
|
terminalTurns: [],
|
|
};
|
|
const startTurn = vi.fn(async () => ({ turnId: "turn-replacement" }));
|
|
const replacementSession: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield runnerEvent(1, "turn.completed");
|
|
},
|
|
startTurn,
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-replacement" };
|
|
},
|
|
async snapshot() {
|
|
return structuredClone(replacementSnapshot);
|
|
},
|
|
async close() {},
|
|
};
|
|
const recoverSession = vi.fn(async () => ({
|
|
recovered: false as const,
|
|
reason: "provider thread is unavailable",
|
|
}));
|
|
const openReplacementSession = vi.fn(async () => replacementSession);
|
|
const onContinuityBreak = vi.fn(async () => undefined);
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "replacement-backend",
|
|
version: "1",
|
|
runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" },
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("replacement seam must be used");
|
|
},
|
|
recoverSession,
|
|
openReplacementSession,
|
|
};
|
|
const events: PrpEvent[] = [];
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
events.push(structuredClone(event));
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const getFreshSessionHandoff = vi.fn(async () => "FRESH_HANDOFF: original goal, prior answers, and next step");
|
|
await expect(
|
|
executeNativeSession({
|
|
input: executionInput,
|
|
getFreshSessionHandoff,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-replacement",
|
|
controlPlaneInstanceId: "control-replacement",
|
|
onContinuityBreak,
|
|
}),
|
|
).resolves.toMatchObject({ providerSessionId: "provider-replacement" });
|
|
|
|
expect(getFreshSessionHandoff).toHaveBeenCalledOnce();
|
|
expect(recoverSession).toHaveBeenCalledTimes(interrupted ? 1 : 0);
|
|
expect(openReplacementSession).toHaveBeenCalledOnce();
|
|
const replacementEnvelope = JSON.parse(
|
|
startTurn.mock.calls[0]![0].message.text,
|
|
) as {
|
|
schema: string;
|
|
requestedSkills?: string[];
|
|
task: { prompt: string };
|
|
completionContract: unknown;
|
|
};
|
|
expect(replacementEnvelope).toMatchObject({
|
|
task: { prompt: "FRESH_HANDOFF: original goal, prior answers, and next step\n\nFULL_ASSIGNMENT_CONTEXT\nCURRENT_EVENT_CONTEXT" },
|
|
completionContract: executionInput.completionContract.contract,
|
|
});
|
|
expect(replacementEnvelope.schema).toBe(
|
|
preparedMode ? "paperclip.native-model-envelope.v3" : "paperclip.native-model-envelope.v2",
|
|
);
|
|
if (preparedMode) expect(replacementEnvelope.requestedSkills).toEqual(["recovery-skill"]);
|
|
else expect(replacementEnvelope).not.toHaveProperty("requestedSkills");
|
|
expect(JSON.stringify(replacementEnvelope)).not.toContain("ONLY_NEW_COMMENT");
|
|
expect(startTurn.mock.calls[0]![0]).not.toHaveProperty("continuation");
|
|
expect(onContinuityBreak).toHaveBeenCalledWith({
|
|
reason: interrupted ? "provider thread is unavailable" : "provider session ended with a failed terminal",
|
|
previousDriverSessionId: "driver-failed",
|
|
previousProviderSessionId: "provider-failed",
|
|
replacementDriverSessionId: "driver-replacement",
|
|
replacementProviderSessionId: "provider-replacement",
|
|
});
|
|
});
|
|
|
|
it.each([false, true])("only replays the original ACPX envelope for a proven effect-free initial turn (prepared: %s)", async (prepared) => {
|
|
const executionInput = prepared ? preparedInput() : input;
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
driverKind: "acpx_runtime",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "1",
|
|
activeTurnId: null,
|
|
terminalTurns: [
|
|
{ turnId: "turn-work", fingerprint: "terminal-fingerprint" },
|
|
],
|
|
dispositionOnlyRecoveryConsumed: true,
|
|
dispositionOnlyRecoveryTurnId: "turn-missing-disposition",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const recoveredSnapshot: PersistedNativeSession = {
|
|
...checkpoint,
|
|
dispositionOnlyRecoveryConsumed: false,
|
|
dispositionOnlyRecoveryTurnId: null,
|
|
};
|
|
const terminalEvent: PrpEvent = {
|
|
schema: "paperclip.prp.event.v1",
|
|
sourceEventId: "provider-recovery:2",
|
|
sourceSeq: 2,
|
|
sourceInstanceId: "provider-recovery",
|
|
sourceKind: "provider",
|
|
runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId,
|
|
turnId: "turn-continuation",
|
|
eventType: "turn.completed",
|
|
schemaVersion: 1,
|
|
priority: 0,
|
|
emittedAt: "2026-08-09T00:00:01.000Z",
|
|
payload: {},
|
|
};
|
|
const startTurn = vi.fn(async () => ({ turnId: "turn-continuation" }));
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield terminalEvent;
|
|
},
|
|
startTurn,
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-continuation" };
|
|
},
|
|
async snapshot() {
|
|
return structuredClone(recoveredSnapshot);
|
|
},
|
|
async close() {},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
preparedTaskConstraints: ["Retain the original workspace rule."],
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" },
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("must recover the provider session");
|
|
},
|
|
async recoverSession() {
|
|
return { recovered: true, session };
|
|
},
|
|
};
|
|
const bySource = new Map<string, PrpEvent[]>();
|
|
const replayedPages: PrpEvent[][] = [];
|
|
const replayEvents = vi.fn(
|
|
async (replay: Parameters<ControlPlanePort["replayEvents"]>[0]) => {
|
|
const list = bySource.get(replay.sourceInstanceId) ?? [];
|
|
const events = structuredClone(
|
|
list.filter((event) => event.sourceSeq > replay.afterSourceSeq),
|
|
);
|
|
replayedPages.push(events);
|
|
return {
|
|
events,
|
|
highestContiguousSourceSeq: highestContiguous(list),
|
|
};
|
|
},
|
|
);
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
const list = bySource.get(event.sourceInstanceId) ?? [];
|
|
list.push(structuredClone(event));
|
|
bySource.set(event.sourceInstanceId, list);
|
|
return {
|
|
cursor: list.length,
|
|
highestContiguousSourceSeq: highestContiguous(list),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
replayEvents,
|
|
async completeRun() {},
|
|
};
|
|
|
|
const submittedTurn = runnerEvent(1, "turn.submitted");
|
|
delete submittedTurn.turnId;
|
|
const effectFreeTurn = [
|
|
submittedTurn,
|
|
{
|
|
...runnerEvent(2, "turn.started", { status: "inProgress" }),
|
|
turnId: "turn-work",
|
|
},
|
|
{ ...runnerEvent(3, "turn.accepted"), turnId: "turn-work" },
|
|
{
|
|
...runnerEvent(4, "item.completed", {
|
|
kind: "usage",
|
|
usage: {
|
|
total: {
|
|
requests: 1,
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
activeSeconds: 0,
|
|
providerCostUsd: 0,
|
|
cacheReadTokens: 0,
|
|
cacheWriteTokens: 0,
|
|
},
|
|
runDelta: {
|
|
requests: 1,
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
activeSeconds: 0,
|
|
providerCostUsd: 0,
|
|
cacheReadTokens: 0,
|
|
cacheWriteTokens: 0,
|
|
},
|
|
},
|
|
}),
|
|
turnId: "turn-work",
|
|
},
|
|
{
|
|
...runnerEvent(5, "turn.completed", {
|
|
status: "completed",
|
|
error: null,
|
|
}),
|
|
turnId: "turn-work",
|
|
},
|
|
];
|
|
bySource.set("runner-recovery", effectFreeTurn);
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input: executionInput,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
turnId: "turn-continuation",
|
|
providerSessionId: "provider-recovery",
|
|
});
|
|
expect(startTurn).toHaveBeenCalledOnce();
|
|
expect(replayEvents).toHaveBeenCalledWith({
|
|
runId: identity.runId,
|
|
sourceInstanceId: "runner-recovery",
|
|
afterSourceSeq: 0,
|
|
limit: 1_000,
|
|
});
|
|
expect(
|
|
replayedPages.some(
|
|
(events) =>
|
|
events.length === effectFreeTurn.length &&
|
|
events.every(
|
|
(event, index) =>
|
|
event.sourceSeq === effectFreeTurn[index]!.sourceSeq,
|
|
),
|
|
),
|
|
).toBe(true);
|
|
const recoveryEnvelope = JSON.parse(
|
|
startTurn.mock.calls[0]![0].message.text,
|
|
) as { task: { prompt: string } };
|
|
expect(recoveryEnvelope.task.prompt).toBe(executionInput.task.prompt);
|
|
if (prepared) {
|
|
expect(recoveryEnvelope).toMatchObject({
|
|
schema: "paperclip.native-model-envelope.v3",
|
|
constraints: ["Retain the original workspace rule."],
|
|
completionContract: { criteria: [{ id: "objective", source: { id: identity.issueId, location: "task.prompt" } }] },
|
|
});
|
|
expect(recoveryEnvelope.task).not.toHaveProperty("description");
|
|
}
|
|
|
|
startTurn.mockClear();
|
|
bySource.set("runner-recovery", [
|
|
...effectFreeTurn.slice(0, 3),
|
|
{
|
|
...runnerEvent(4, "item.completed", {
|
|
kind: "agentMessage",
|
|
text: "Work may already have been performed.",
|
|
}),
|
|
turnId: "turn-work",
|
|
},
|
|
{
|
|
...runnerEvent(5, "turn.completed", {
|
|
status: "completed",
|
|
error: null,
|
|
}),
|
|
turnId: "turn-work",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input: executionInput,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
turnId: "turn-continuation",
|
|
providerSessionId: "provider-recovery",
|
|
});
|
|
const dispositionEnvelope = JSON.parse(
|
|
startTurn.mock.calls[0]![0].message.text,
|
|
) as { task: { prompt: string } };
|
|
expect(dispositionEnvelope.task.prompt).toContain(
|
|
"semantic-result recovery for a prior completed provider turn",
|
|
);
|
|
expect(dispositionEnvelope.task.prompt).toContain(
|
|
"Do not repeat implementation, tests, research, or the final answer",
|
|
);
|
|
expect(dispositionEnvelope.task.prompt).not.toContain(input.task.prompt);
|
|
|
|
expect(dispositionEnvelope).toHaveProperty("completionContract", input.completionContract.contract);
|
|
expect(dispositionEnvelope).not.toHaveProperty("constraints");
|
|
if (prepared) expect(dispositionEnvelope).toHaveProperty("requestedSkills", []);
|
|
|
|
checkpoint.dispositionOnlyRecoveryTurnId = undefined;
|
|
recoveredSnapshot.dispositionOnlyRecoveryTurnId = undefined;
|
|
startTurn.mockClear();
|
|
bySource.clear();
|
|
bySource.set("runner-recovery", [
|
|
{
|
|
...terminalEvent,
|
|
sourceEventId: "runner-recovery:stale-terminal",
|
|
turnId: "turn-stale-unbound",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
executeNativeSession({
|
|
input: executionInput,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
turnId: "turn-continuation",
|
|
providerSessionId: "provider-recovery",
|
|
});
|
|
expect(startTurn).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it.each(
|
|
[true, false].flatMap((dispositionRecovery) =>
|
|
(["turn.completed", "turn.interrupted"] as const).flatMap(
|
|
(terminalType) =>
|
|
[false, true].map((failInitialAppend) => ({
|
|
dispositionRecovery,
|
|
terminalType,
|
|
failInitialAppend,
|
|
})),
|
|
),
|
|
),
|
|
)(
|
|
"consumes adopted $terminalType without resending (disposition: $dispositionRecovery, failed first append: $failInitialAppend)",
|
|
async ({ dispositionRecovery, terminalType, failInitialAppend }) => {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "1",
|
|
activeTurnId: dispositionRecovery ? null : "turn-disposition",
|
|
terminalTurns: dispositionRecovery
|
|
? [{ turnId: "turn-work", fingerprint: "work-terminal" }]
|
|
: [],
|
|
dispositionOnlyRecoveryConsumed: false,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const recoveredSnapshot: PersistedNativeSession = {
|
|
...checkpoint,
|
|
cursor: "2",
|
|
activeTurnId: null,
|
|
terminalTurns: [
|
|
...checkpoint.terminalTurns!,
|
|
{ turnId: "turn-disposition", fingerprint: "disposition-terminal" },
|
|
],
|
|
dispositionOnlyRecoveryConsumed: dispositionRecovery,
|
|
};
|
|
const terminalEvent: PrpEvent = {
|
|
schema: "paperclip.prp.event.v1",
|
|
sourceEventId: "provider-recovery:2",
|
|
sourceSeq: 2,
|
|
sourceInstanceId: "provider-recovery",
|
|
sourceKind: "provider",
|
|
runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId,
|
|
turnId: "turn-disposition",
|
|
eventType: terminalType,
|
|
schemaVersion: 1,
|
|
priority: 0,
|
|
emittedAt: "2026-08-09T00:00:01.000Z",
|
|
payload: {},
|
|
};
|
|
const startTurn = vi.fn(async () => ({ turnId: "unexpected-turn" }));
|
|
const close = vi.fn(async () => undefined);
|
|
const completeRun = vi.fn(async () => undefined);
|
|
const appendFailure = new Error("adopted terminal append failed");
|
|
let failNextAppend = failInitialAppend;
|
|
let durableCheckpoint = structuredClone(checkpoint);
|
|
const recoveryCheckpoints: PersistedNativeSession[] = [];
|
|
let dispositionTerminalCommitted = false;
|
|
let prematureDispositionCheckpoint = false;
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield terminalEvent;
|
|
},
|
|
startTurn,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return structuredClone(recoveredSnapshot);
|
|
},
|
|
close,
|
|
};
|
|
const events: PrpEvent[] = [];
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("must recover the provider session");
|
|
},
|
|
async recoverSession(snapshot) {
|
|
recoveryCheckpoints.push(structuredClone(snapshot));
|
|
return { recovered: true, session: { ...session } };
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(durableCheckpoint);
|
|
},
|
|
async checkpointSession(snapshot) {
|
|
if (
|
|
snapshot.terminalTurns?.some(
|
|
(turn) => turn.turnId === "turn-disposition",
|
|
) &&
|
|
!dispositionTerminalCommitted
|
|
)
|
|
prematureDispositionCheckpoint = true;
|
|
durableCheckpoint = structuredClone(snapshot);
|
|
},
|
|
async appendEvent(event) {
|
|
if (event.eventType === terminalType && failNextAppend) {
|
|
failNextAppend = false;
|
|
throw appendFailure;
|
|
}
|
|
events.push(structuredClone(event));
|
|
if (
|
|
event.eventType === terminalType &&
|
|
event.turnId === "turn-disposition"
|
|
) {
|
|
dispositionTerminalCommitted = true;
|
|
}
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
return {
|
|
events: structuredClone(
|
|
events.filter(
|
|
(event) =>
|
|
event.sourceInstanceId === replay.sourceInstanceId &&
|
|
event.sourceSeq > replay.afterSourceSeq,
|
|
),
|
|
),
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
};
|
|
},
|
|
completeRun,
|
|
};
|
|
|
|
const execute = () =>
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
resolveMissingResult: async () => result,
|
|
});
|
|
if (failInitialAppend) {
|
|
await expect(execute()).rejects.toBe(appendFailure);
|
|
expect(events).toEqual([]);
|
|
expect(startTurn).not.toHaveBeenCalled();
|
|
expect(completeRun).not.toHaveBeenCalled();
|
|
expect(close).toHaveBeenCalledOnce();
|
|
expect(prematureDispositionCheckpoint).toBe(false);
|
|
expect(durableCheckpoint.activeTurnId).toBe(checkpoint.activeTurnId);
|
|
expect(durableCheckpoint.terminalTurns).toEqual(
|
|
checkpoint.terminalTurns,
|
|
);
|
|
expect(durableCheckpoint.identity).toEqual(checkpoint.identity);
|
|
}
|
|
await expect(execute()).resolves.toMatchObject({
|
|
result,
|
|
turnId: "turn-disposition",
|
|
terminal: {
|
|
turnTerminalState:
|
|
terminalType === "turn.completed" ? "completed" : "interrupted",
|
|
runTerminalState:
|
|
terminalType === "turn.completed" ? "succeeded" : "cancelled",
|
|
},
|
|
});
|
|
expect(recoveryCheckpoints).toHaveLength(failInitialAppend ? 2 : 1);
|
|
for (const recoveredCheckpoint of recoveryCheckpoints) {
|
|
expect(recoveredCheckpoint.activeTurnId).toBe(checkpoint.activeTurnId);
|
|
expect(recoveredCheckpoint.terminalTurns).toEqual(
|
|
checkpoint.terminalTurns,
|
|
);
|
|
expect(recoveredCheckpoint.identity).toEqual(checkpoint.identity);
|
|
}
|
|
expect(startTurn).not.toHaveBeenCalled();
|
|
expect(completeRun).toHaveBeenCalledOnce();
|
|
expect(prematureDispositionCheckpoint).toBe(false);
|
|
expect(events.map((event) => event.eventType)).toEqual([
|
|
terminalType,
|
|
"run.result.accepted",
|
|
"run.terminal",
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.each(["silent", "commentary", "tool-events"] as const)("ACCT-04 resolves a proposal-less durable disposition after %s without replenishing consumed repair", async noise => {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "1",
|
|
activeTurnId: null,
|
|
terminalTurns: [{ turnId: "turn-work", fingerprint: "work-terminal" }],
|
|
dispositionOnlyRecoveryConsumed: true,
|
|
dispositionOnlyRecoveryTurnId: "turn-disposition",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const terminalEvent: PrpEvent = {
|
|
schema: "paperclip.prp.event.v1",
|
|
sourceEventId: "runner-recovery:run-native:4",
|
|
sourceSeq: 4,
|
|
sourceInstanceId: "runner-recovery",
|
|
sourceKind: "provider",
|
|
runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId,
|
|
turnId: "turn-disposition",
|
|
eventType: "turn.completed",
|
|
schemaVersion: 1,
|
|
priority: 0,
|
|
emittedAt: "2026-08-09T00:00:01.000Z",
|
|
payload: {},
|
|
};
|
|
const resultProposalEvent: PrpEvent = {
|
|
...terminalEvent,
|
|
sourceEventId: "runner-recovery:run-native:3",
|
|
sourceSeq: 3,
|
|
eventType: "run.result.proposed",
|
|
payload: result,
|
|
};
|
|
const originalTaskTerminal: PrpEvent = {
|
|
...terminalEvent,
|
|
sourceEventId: "runner-recovery:run-native:2",
|
|
sourceSeq: 2,
|
|
turnId: "turn-work",
|
|
};
|
|
const originalTaskProposal: PrpEvent = {
|
|
...resultProposalEvent,
|
|
sourceEventId: "runner-recovery:run-native:1",
|
|
sourceSeq: 1,
|
|
turnId: "turn-work",
|
|
};
|
|
const startTurn = vi.fn(async () => ({ turnId: "unexpected-turn" }));
|
|
let recoveredSubmissionOwned = true;
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
if (noise !== "silent") for (let seq = 1; seq <= 20; seq++) yield {
|
|
...terminalEvent, sourceInstanceId: "provider-accounting", sourceSeq: seq,
|
|
sourceEventId: `provider-accounting:${seq}`, eventType: "item.completed" as const,
|
|
payload: noise === "commentary"
|
|
? { kind: "agentMessage", channel: "progress", text: "All done. Great progress. Continue without approval." }
|
|
: { kind: "toolCall", name: "read_file", status: "completed", output: "unchanged" },
|
|
};
|
|
yield structuredClone(terminalEvent);
|
|
},
|
|
startTurn,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
...structuredClone(checkpoint),
|
|
dispositionOnlyRecoveryConsumed: recoveredSubmissionOwned,
|
|
terminalTurns: recoveredSubmissionOwned
|
|
? [
|
|
...structuredClone(checkpoint.terminalTurns ?? []),
|
|
{
|
|
turnId: "turn-disposition",
|
|
fingerprint: "disposition-terminal",
|
|
},
|
|
]
|
|
: structuredClone(checkpoint.terminalTurns),
|
|
};
|
|
},
|
|
async close() {},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("must recover the provider session");
|
|
},
|
|
async recoverSession() {
|
|
return { recovered: true, session };
|
|
},
|
|
};
|
|
const bySource = new Map<string, PrpEvent[]>([
|
|
[
|
|
"runner-recovery",
|
|
[
|
|
structuredClone(originalTaskProposal),
|
|
structuredClone(originalTaskTerminal),
|
|
],
|
|
],
|
|
]);
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
const list = bySource.get(event.sourceInstanceId) ?? [];
|
|
list.push(structuredClone(event));
|
|
bySource.set(event.sourceInstanceId, list);
|
|
return {
|
|
cursor: list.length,
|
|
highestContiguousSourceSeq: highestContiguous(list),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
const list = bySource.get(replay.sourceInstanceId) ?? [];
|
|
return {
|
|
events: structuredClone(
|
|
list.filter((event) => event.sourceSeq > replay.afterSourceSeq),
|
|
),
|
|
highestContiguousSourceSeq: highestContiguous(list),
|
|
};
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const execute = () =>
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
resolveMissingResult: async ({ terminalEvent: replayed }) => {
|
|
expect(replayed).toEqual(terminalEvent);
|
|
return result;
|
|
},
|
|
});
|
|
await expect(execute()).resolves.toMatchObject({
|
|
result,
|
|
turnId: "turn-disposition",
|
|
});
|
|
bySource.set("runner-recovery", [
|
|
structuredClone(originalTaskProposal),
|
|
structuredClone(originalTaskTerminal),
|
|
structuredClone(resultProposalEvent),
|
|
structuredClone(terminalEvent),
|
|
]);
|
|
// Provider recovery may clear a legacy pre-acceptance marker when thread
|
|
// history has no matching turn. Durable replay remains authoritative and
|
|
// must still prevent a duplicate disposition submission.
|
|
recoveredSubmissionOwned = false;
|
|
|
|
await expect(execute()).resolves.toMatchObject({
|
|
result,
|
|
turnId: "turn-disposition",
|
|
});
|
|
expect(startTurn).not.toHaveBeenCalled();
|
|
expect(bySource.get("runner-recovery")).toEqual([
|
|
originalTaskProposal,
|
|
originalTaskTerminal,
|
|
resultProposalEvent,
|
|
terminalEvent,
|
|
]);
|
|
expect(
|
|
bySource.get("control-recovery")?.map((event) => event.eventType),
|
|
).toEqual(["run.result.accepted", "run.terminal"]);
|
|
});
|
|
|
|
it("resolves a checkpointed result-less disposition without resubmitting when its terminal event is missing", async () => {
|
|
const workProposal: PrpEvent = {
|
|
...runnerEvent(1, "run.result.proposed", result),
|
|
turnId: "turn-work",
|
|
};
|
|
const workTerminal: PrpEvent = {
|
|
...runnerEvent(2, "turn.completed"),
|
|
turnId: "turn-work",
|
|
};
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "3",
|
|
activeTurnId: null,
|
|
terminalTurns: [
|
|
{ turnId: "turn-work", fingerprint: "work-terminal" },
|
|
{ turnId: "turn-disposition", fingerprint: "disposition-terminal" },
|
|
],
|
|
dispositionOnlyRecoveryConsumed: true,
|
|
dispositionOnlyRecoveryTurnId: "turn-disposition",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const recoveredCheckpoint: PersistedNativeSession =
|
|
structuredClone(checkpoint);
|
|
const startTurn = vi.fn(async () => ({ turnId: "unexpected-turn" }));
|
|
const events = vi.fn(() =>
|
|
(async function* () {
|
|
throw new Error("checkpoint fallback must not consume provider events");
|
|
})(),
|
|
);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
events,
|
|
startTurn,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return structuredClone(recoveredCheckpoint);
|
|
},
|
|
async close() {},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
throw new Error("must recover the provider session");
|
|
},
|
|
async recoverSession() {
|
|
return { recovered: true, session };
|
|
},
|
|
};
|
|
const bySource = new Map<string, PrpEvent[]>([
|
|
["runner-recovery", [workProposal, workTerminal]],
|
|
]);
|
|
const completeRun = vi.fn(async () => undefined);
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
const list = bySource.get(event.sourceInstanceId) ?? [];
|
|
list.push(structuredClone(event));
|
|
bySource.set(event.sourceInstanceId, list);
|
|
return {
|
|
cursor: list.length,
|
|
highestContiguousSourceSeq: highestContiguous(list),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
const list = bySource.get(replay.sourceInstanceId) ?? [];
|
|
return {
|
|
events: structuredClone(
|
|
list.filter((event) => event.sourceSeq > replay.afterSourceSeq),
|
|
),
|
|
highestContiguousSourceSeq: highestContiguous(list),
|
|
};
|
|
},
|
|
completeRun,
|
|
};
|
|
|
|
const execute = () =>
|
|
executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
resolveMissingResult: async ({ turnId, terminalEvent }) => {
|
|
expect(turnId).toBe("turn-disposition");
|
|
expect(terminalEvent).toMatchObject({
|
|
sourceInstanceId: "control-recovery",
|
|
sourceKind: "control_plane",
|
|
runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId,
|
|
turnId: "turn-disposition",
|
|
eventType: "turn.completed",
|
|
payload: {
|
|
recovery: "checkpointed_resultless_disposition",
|
|
terminalFingerprint: "disposition-terminal",
|
|
},
|
|
});
|
|
return result;
|
|
},
|
|
});
|
|
await expect(execute()).resolves.toMatchObject({
|
|
result,
|
|
turnId: "turn-disposition",
|
|
});
|
|
expect(startTurn).not.toHaveBeenCalled();
|
|
expect(events).not.toHaveBeenCalled();
|
|
expect(completeRun).toHaveBeenCalledOnce();
|
|
expect(bySource.get("runner-recovery")).toEqual([
|
|
workProposal,
|
|
workTerminal,
|
|
]);
|
|
expect(
|
|
bySource.get("control-recovery")?.map((event) => event.eventType),
|
|
).toEqual(["run.result.accepted", "run.terminal"]);
|
|
|
|
recoveredCheckpoint.terminalTurns![1]!.fingerprint = "conflicting-terminal";
|
|
await expect(execute()).rejects.toThrow(
|
|
"native_disposition_recovery_checkpoint_conflict",
|
|
);
|
|
expect(startTurn).not.toHaveBeenCalled();
|
|
expect(events).not.toHaveBeenCalled();
|
|
expect(completeRun).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it.each([false, true])("confirms a redacted completion after a lost acknowledgement (conflict: %s)", async conflict => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const completion = { ...result, summary: JSON.stringify({ operationId: "call_api", data: "private-fixture-value" }) };
|
|
const history: PrpEvent[] = [];
|
|
const normalize = (payload: PrpEvent["payload"]): PrpEvent["payload"] =>
|
|
JSON.parse(JSON.stringify(payload).replaceAll("private-fixture-value", "***REDACTED***"));
|
|
let committed!: () => void;
|
|
const saved = new Promise<void>(resolve => { committed = resolve; });
|
|
let lostAcknowledgement = false;
|
|
const completeRun = vi.fn(async () => undefined);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() { return { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
|
|
async startTurn() { return { turnId: "turn-recovery" }; },
|
|
async *events() { yield runnerEvent(1, "turn.completed"); },
|
|
async result() { return { result: completion, terminal, turnId: "turn-recovery" }; },
|
|
async snapshot() { return { backendKind: "mock", sessionId: identity.sessionId, identity, activeTurnId: null }; },
|
|
async close() {},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {}, async checkpointSession() {}, normalizeReplayPayload: normalize, completeRun,
|
|
async appendEvent(event) {
|
|
const row = event as PrpEvent;
|
|
history.push({ ...row, payload: normalize(row.payload) });
|
|
if (row.eventType === "run.result.accepted" && !lostAcknowledgement) {
|
|
lostAcknowledgement = true;
|
|
if (conflict) history.at(-1)!.payload = { ...history.at(-1)!.payload, changed: true };
|
|
committed();
|
|
return await new Promise<never>(() => undefined);
|
|
}
|
|
return { cursor: history.length, highestContiguousSourceSeq: row.sourceSeq, disposition: "committed" };
|
|
},
|
|
async replayEvents(request) {
|
|
const events = history.filter(row => row.sourceInstanceId === request.sourceInstanceId && row.sourceSeq > request.afterSourceSeq);
|
|
return { events, highestContiguousSourceSeq: events.at(-1)?.sourceSeq ?? request.afterSourceSeq };
|
|
},
|
|
};
|
|
const execution = executeNativeSession({ input, backend: {
|
|
async descriptor() { return { kind: "mock", name: "redacted-completion", version: "1", capabilities: await session.capabilities() }; },
|
|
async openSession() { return session; },
|
|
}, controlPlane: port, runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery", timeoutMs: 20 });
|
|
const outcome = conflict ? expect(execution).rejects.toThrow("native_control_event_replay_conflict:1")
|
|
: expect(execution).resolves.toMatchObject({ result: completion });
|
|
await saved;
|
|
await vi.advanceTimersByTimeAsync(25);
|
|
await outcome;
|
|
expect(completeRun).toHaveBeenCalledTimes(conflict ? 0 : 1);
|
|
expect(JSON.stringify(history)).not.toContain("private-fixture-value");
|
|
expect(history.filter(event => event.eventType === "run.result.accepted")).toHaveLength(1);
|
|
} finally { vi.useRealTimers(); }
|
|
});
|
|
|
|
it("keeps a reconstructed semantic result on its matched terminal turn", async () => {
|
|
const semanticFingerprint = canonicalTestJson(result);
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock",
|
|
sessionId: "driver-recovery",
|
|
identity,
|
|
providerSessionId: "provider-recovery",
|
|
cursor: "4",
|
|
semanticResult: result,
|
|
terminal,
|
|
activeTurnId: null,
|
|
terminalTurns: [
|
|
{
|
|
turnId: "turn-with-result",
|
|
fingerprint: JSON.stringify({
|
|
status: "completed",
|
|
semanticResult: semanticFingerprint,
|
|
}),
|
|
},
|
|
{
|
|
turnId: "turn-later-failed",
|
|
fingerprint: JSON.stringify({ status: "failed" }),
|
|
},
|
|
],
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
const events = [
|
|
{
|
|
...controlEvent(1, "run.result.accepted", { result }),
|
|
turnId: "turn-with-result",
|
|
},
|
|
];
|
|
const checkpoints: PersistedNativeSession[] = [];
|
|
const completeRun = vi.fn(async () => undefined);
|
|
const startTurn = vi.fn(async () => ({ turnId: "unexpected-turn" }));
|
|
const openSession = vi.fn(async () => {
|
|
throw new Error(
|
|
"a recovered run must not open a second provider session",
|
|
);
|
|
});
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {},
|
|
startTurn,
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-recovery" };
|
|
},
|
|
async snapshot() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
async close() {},
|
|
};
|
|
const recoverSession = vi.fn(async () => ({ recovered: true, session }));
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "recovery-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: true,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
openSession,
|
|
recoverSession,
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async loadSessionCheckpoint() {
|
|
return structuredClone(checkpoint);
|
|
},
|
|
async checkpointSession(snapshot) {
|
|
checkpoints.push(structuredClone(snapshot));
|
|
},
|
|
async appendEvent(event) {
|
|
events.push(structuredClone(event as PrpEvent));
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
const replayed = events.filter(
|
|
(event) => event.sourceSeq > replay.afterSourceSeq,
|
|
);
|
|
return {
|
|
events: structuredClone(replayed),
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
};
|
|
},
|
|
completeRun,
|
|
};
|
|
|
|
const completed = await executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
|
|
expect(openSession).not.toHaveBeenCalled();
|
|
expect(recoverSession).toHaveBeenCalledOnce();
|
|
expect(startTurn).not.toHaveBeenCalled();
|
|
expect(events.map((event) => event.eventType)).toEqual([
|
|
"run.result.accepted",
|
|
"run.terminal",
|
|
]);
|
|
expect(events.map((event) => event.sourceSeq)).toEqual([1, 2]);
|
|
expect(events.map((event) => event.turnId)).toEqual([
|
|
"turn-with-result",
|
|
"turn-with-result",
|
|
]);
|
|
expect(completeRun).toHaveBeenCalledOnce();
|
|
expect(completeRun).toHaveBeenCalledWith(
|
|
expect.objectContaining({ turnId: "turn-with-result" }),
|
|
expect.anything(),
|
|
);
|
|
expect(completed).toMatchObject({
|
|
nativeEventCount: 1,
|
|
highestContiguousSourceSeq: 2,
|
|
});
|
|
expect(checkpoints.at(-1)).toMatchObject({
|
|
semanticResult: result,
|
|
terminal,
|
|
});
|
|
});
|
|
|
|
it("rejects visible provider final text without an accepted current-turn completion report", async () => {
|
|
const capabilities = { resume: false, typedEvents: true, steering: false, interruption: true, structuredResult: true };
|
|
const events: PrpEvent[] = [];
|
|
const completeRun = vi.fn(async () => undefined);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() { return capabilities; },
|
|
async *events() {
|
|
yield runnerEvent(1, "item.completed", { kind: "agentMessage", channel: "final", text: "Contacts retrieved: Ada Fixture. Verification code: CONTACTS_fixture." });
|
|
yield runnerEvent(2, "turn.completed");
|
|
},
|
|
async startTurn() { return { turnId: "turn-recovery" }; },
|
|
async result() { return null; },
|
|
async snapshot() { return { backendKind: "mock", sessionId: identity.sessionId, identity,
|
|
providerSessionId: "provider-recovery", cursor: "2", activeTurnId: "turn-recovery", pendingRuntimeRequests: [], lineage: [] }; },
|
|
async close() {},
|
|
};
|
|
await expect(executeNativeSession({ input,
|
|
backend: { async descriptor() { return { kind: "mock", name: "missing-current-completion", version: "1", capabilities }; }, async openSession() { return session; } },
|
|
controlPlane: {
|
|
async openRun() {}, async checkpointSession() {},
|
|
async appendEvent(event) { events.push(structuredClone(event as PrpEvent)); return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
|
async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; }, completeRun,
|
|
}, runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery", timeoutMs: 1000,
|
|
resolveMissingResult: async () => null,
|
|
})).rejects.toThrow("native_finalization_missing");
|
|
expect(completeRun).not.toHaveBeenCalled();
|
|
expect(events.some(event => event.eventType === "item.completed")).toBe(true);
|
|
expect(events.some(event => event.eventType === "run.result.accepted")).toBe(false);
|
|
});
|
|
|
|
it("accepts a control-plane governed wait when a completed turn omitted its semantic result", async () => {
|
|
const terminalEvent: PrpEvent = {
|
|
schema: "paperclip.prp.event.v1",
|
|
sourceEventId: "provider-recovery:1",
|
|
sourceSeq: 1,
|
|
sourceInstanceId: "provider-recovery",
|
|
sourceKind: "provider",
|
|
runId: identity.runId,
|
|
normalizedSessionId: identity.sessionId,
|
|
turnId: "turn-waiting",
|
|
eventType: "turn.completed",
|
|
schemaVersion: 1,
|
|
priority: 0,
|
|
emittedAt: "2026-08-09T00:00:00.000Z",
|
|
payload: {},
|
|
};
|
|
const yielded: PrpStructuredRunResult = {
|
|
schema: "paperclip.run_result.v1",
|
|
reportedWorkDisposition: "yielded",
|
|
summary: "Waiting for the requested response.",
|
|
completionClaim: {
|
|
contractRevision: "1",
|
|
objectiveSatisfied: false,
|
|
criteria: [
|
|
{
|
|
criterionId: "objective",
|
|
status: "unknown",
|
|
evidenceRefs: ["interaction:pending"],
|
|
},
|
|
],
|
|
remainingWork: [
|
|
{ description: "Resume after the response.", blocksCompletion: true },
|
|
],
|
|
},
|
|
evidence: [{ ref: "interaction:pending" }],
|
|
verification: [],
|
|
attentionRequests: [],
|
|
artifacts: [],
|
|
continuation: {
|
|
kind: "response_wake",
|
|
summary: "Resume from the answer.",
|
|
idempotencyKey: "interaction-response:pending",
|
|
},
|
|
};
|
|
const events: PrpEvent[] = [];
|
|
const completeRun = vi.fn(async () => undefined);
|
|
const resolveMissingResult = vi.fn(async () => yielded);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield terminalEvent;
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-waiting" };
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-waiting",
|
|
cursor: "1",
|
|
activeTurnId: "turn-waiting",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
async close() {},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "governed-wait-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
events.push(structuredClone(event as PrpEvent));
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
const replayed = events.filter(
|
|
(event) =>
|
|
event.sourceInstanceId === replay.sourceInstanceId &&
|
|
event.sourceSeq > replay.afterSourceSeq,
|
|
);
|
|
return {
|
|
events: structuredClone(replayed),
|
|
highestContiguousSourceSeq: highestContiguous(replayed),
|
|
};
|
|
},
|
|
completeRun,
|
|
};
|
|
|
|
const completed = await executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
resolveMissingResult,
|
|
});
|
|
|
|
expect(resolveMissingResult).toHaveBeenCalledWith({
|
|
turnId: "turn-waiting",
|
|
terminalEvent,
|
|
});
|
|
expect(completed).toMatchObject({
|
|
result: yielded,
|
|
terminal: {
|
|
runTerminalState: "succeeded",
|
|
reportedWorkDisposition: "yielded",
|
|
},
|
|
turnId: "turn-waiting",
|
|
});
|
|
expect(completeRun).toHaveBeenCalledWith(
|
|
expect.objectContaining({ result: yielded }),
|
|
{ signal: expect.any(AbortSignal) },
|
|
);
|
|
expect(events.map((event) => event.eventType)).toEqual([
|
|
"turn.completed",
|
|
"run.result.accepted",
|
|
"run.terminal",
|
|
]);
|
|
});
|
|
|
|
it.each(["end", "throw", "timeout"])("keeps a governed wait unfinalized when complete usage is followed by %s without a provider terminal", async fault => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const trigger = { ...runnerEvent(1, "item.completed", { kind: "dynamicToolCall" }), turnId: "turn-waiting" };
|
|
const usage = { ...runnerEvent(2, "item.completed", { kind: "usage", usage: { runDelta: { inputTokens: 7, outputTokens: 3 }, runDeltaComplete: true } }), turnId: trigger.turnId };
|
|
const history: PrpEvent[] = [];
|
|
let checkpoint: PersistedNativeSession | undefined;
|
|
let releaseStream!: () => void;
|
|
const streamClosed = new Promise<void>(resolve => { releaseStream = resolve; });
|
|
const complete = vi.fn(async () => undefined);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() { return { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
|
|
async startTurn() { return { turnId: trigger.turnId }; },
|
|
cancel() { return { cleanup: Promise.resolve() }; },
|
|
async snapshot() { return { backendKind: "mock", sessionId: identity.sessionId, identity, activeTurnId: trigger.turnId }; },
|
|
async result() { return null; }, async close() { releaseStream(); },
|
|
async *events() {
|
|
yield trigger; yield usage;
|
|
if (fault === "throw") throw new Error("provider stream lost");
|
|
if (fault === "timeout") await streamClosed;
|
|
},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() { return { kind: "mock", name: "missing-stop", version: "1", capabilities: await session.capabilities() }; },
|
|
async openSession() { return session; },
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {}, completeRun: complete,
|
|
async checkpointSession(value) { checkpoint = structuredClone(value); },
|
|
async appendEvent(event) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
|
async replayEvents(request) { return { events: history.filter(row => row.sourceInstanceId === request.sourceInstanceId && row.sourceSeq > request.afterSourceSeq), highestContiguousSourceSeq: 0 }; },
|
|
};
|
|
const outcome = expect(executeNativeSession({ input, backend, controlPlane: port, timeoutMs: 1_000,
|
|
runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery", resolveGovernedWait: () => yieldedResult }))
|
|
.rejects.toMatchObject({ providerCode: "governed_wait_provider_terminal_unproven" });
|
|
void outcome.catch(() => undefined);
|
|
await vi.advanceTimersByTimeAsync(600);
|
|
await outcome;
|
|
expect(checkpoint?.governedWait).toEqual({ sourceEvent: trigger, result: yieldedResult });
|
|
expect(complete).not.toHaveBeenCalled();
|
|
expect(history).toEqual([trigger, usage]);
|
|
} finally { vi.useRealTimers(); }
|
|
});
|
|
|
|
it.each(["interrupt_acknowledgement", "runtime_request.cancelled", "runtime_request.expired"])("retains %s in contiguous governed-wait history for recovery", async cancellationKind => {
|
|
const trigger = { ...runnerEvent(1, "item.completed", { kind: "dynamicToolCall" }), turnId: "turn-waiting" };
|
|
const acknowledgement: PrpEvent = { ...runnerEvent(2, cancellationKind === "interrupt_acknowledgement" ? "item.completed" : cancellationKind as "runtime_request.cancelled" | "runtime_request.expired",
|
|
cancellationKind === "interrupt_acknowledgement" ? { kind: cancellationKind } : { requestId: "request-1" }), turnId: trigger.turnId };
|
|
const usage = { ...runnerEvent(3, "item.completed", { kind: "usage", usage: { runDelta: { inputTokens: 7, outputTokens: 3 }, runDeltaComplete: true } }), turnId: trigger.turnId };
|
|
const stopped = { ...runnerEvent(4, "turn.interrupted", { providerTerminalObserved: true }), turnId: trigger.turnId };
|
|
const history: PrpEvent[] = [];
|
|
let checkpoint: PersistedNativeSession | undefined;
|
|
const start = vi.fn(async () => ({ turnId: trigger.turnId }));
|
|
const cancel = vi.fn(() => ({ cleanup: Promise.resolve() }));
|
|
const complete = vi.fn(async () => undefined);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() { return { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
|
|
startTurn: start, cancel,
|
|
async snapshot() { return { backendKind: "mock", sessionId: identity.sessionId, identity, activeTurnId: trigger.turnId }; },
|
|
async result() { return null; }, async close() {},
|
|
async *events() { yield trigger; yield acknowledgement; yield usage; yield stopped; },
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() { return { kind: "mock", name: "cancel-history", version: "1", capabilities: await session.capabilities() }; },
|
|
async openSession() { return session; }, async recoverSession() { return { recovered: true, session }; },
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {}, completeRun: complete,
|
|
async checkpointSession(value) { checkpoint = structuredClone(value); },
|
|
async appendEvent(event) {
|
|
const duplicate = history.some(row => row.sourceEventId === event.sourceEventId);
|
|
if (!duplicate) history.push(event);
|
|
return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: duplicate ? "duplicate" : "committed" };
|
|
},
|
|
async replayEvents(request) {
|
|
const events = history.filter(row => row.sourceInstanceId === request.sourceInstanceId && row.sourceSeq > request.afterSourceSeq);
|
|
return { events, highestContiguousSourceSeq: events.at(-1)?.sourceSeq ?? request.afterSourceSeq };
|
|
},
|
|
};
|
|
const options = { input, backend, controlPlane: port, runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery" };
|
|
await expect(executeNativeSession({ ...options, resolveGovernedWait: () => yieldedResult }))
|
|
.resolves.toMatchObject({ result: yieldedResult });
|
|
expect(history.filter(event => event.sourceInstanceId === trigger.sourceInstanceId)).toEqual([trigger, acknowledgement, usage, stopped]);
|
|
await expect(executeNativeSession({ ...options, persistedSession: checkpoint })).resolves.toMatchObject({ result: yieldedResult });
|
|
expect(start).toHaveBeenCalledOnce();
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it.each(["trigger", "usage", "terminal"])("recovers a journaled governed wait after its answer is saved (last committed: %s)", async lastCommitted => {
|
|
const terminalCommitted = lastCommitted === "terminal";
|
|
const trigger = { ...runnerEvent(1, "item.completed", { kind: "dynamicToolCall", item: { id: "ask-1", name: "request_human_input" } }), turnId: "turn-waiting" };
|
|
const usage = { ...runnerEvent(2, "item.completed", { kind: "usage", usage: { runDelta: { inputTokens: 7, outputTokens: 3 }, runDeltaComplete: true } }), turnId: trigger.turnId };
|
|
const stopped = { ...runnerEvent(3, "turn.interrupted", { providerTerminalObserved: true }), turnId: trigger.turnId };
|
|
const history: PrpEvent[] = [];
|
|
let checkpoint: PersistedNativeSession | undefined;
|
|
let crashed = false;
|
|
let interrupted = false;
|
|
const revoke = vi.fn();
|
|
const cancel = vi.fn(() => { interrupted = true; return { cleanup: Promise.resolve() }; });
|
|
const start = vi.fn(async () => ({ turnId: trigger.turnId }));
|
|
const complete = vi.fn(async () => undefined);
|
|
const snapshot = (): PersistedNativeSession => ({ backendKind: "mock", sessionId: identity.sessionId, identity,
|
|
providerSessionId: "same-provider", activeTurnId: trigger.turnId, cursor: String(history.at(-1)?.sourceSeq ?? 0) });
|
|
const makeSession = (recovering: boolean): NativeSession => ({ identity: () => identity,
|
|
async capabilities() { return { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
|
|
startTurn: start, revokeTurnPublication: revoke, cancel,
|
|
async snapshot() { return snapshot(); }, async result() { return null; }, async close() {},
|
|
async *events() {
|
|
if (!recovering) { yield trigger; return; }
|
|
// The restart resumes settlement only; it must not submit a model turn.
|
|
expect(interrupted).toBe(true);
|
|
yield usage;
|
|
yield stopped;
|
|
},
|
|
});
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() { return { kind: "mock", name: "durable-wait", version: "1", capabilities: await makeSession(false).capabilities() }; },
|
|
async openSession() { return makeSession(false); },
|
|
async recoverSession() { return { recovered: true, session: makeSession(true) }; },
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {}, completeRun: complete,
|
|
async checkpointSession(value) {
|
|
checkpoint = structuredClone(value);
|
|
if (value.governedWait && !crashed) {
|
|
expect(revoke).toHaveBeenCalledOnce();
|
|
expect(cancel).not.toHaveBeenCalled();
|
|
crashed = true;
|
|
throw new Error("controller lost after governed checkpoint");
|
|
}
|
|
},
|
|
async appendEvent(event) {
|
|
if (!history.some(row => row.sourceEventId === event.sourceEventId)) history.push(event);
|
|
return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" };
|
|
},
|
|
async replayEvents(request) {
|
|
const events = history.filter(row => row.sourceInstanceId === request.sourceInstanceId && row.sourceSeq > request.afterSourceSeq);
|
|
return { events, highestContiguousSourceSeq: events.at(-1)?.sourceSeq ?? request.afterSourceSeq };
|
|
},
|
|
};
|
|
const options = { input, backend, controlPlane: port, runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery" };
|
|
await expect(executeNativeSession({ ...options, resolveGovernedWait: () => yieldedResult }))
|
|
.rejects.toThrow("controller lost after governed checkpoint");
|
|
expect(checkpoint?.governedWait).toEqual({ sourceEvent: trigger, result: yieldedResult });
|
|
expect(complete).not.toHaveBeenCalled();
|
|
// The failed controller's ordinary cleanup also invokes cancel. Its
|
|
// checkpoint assertion above proves interruption followed persistence.
|
|
cancel.mockClear(); interrupted = false;
|
|
if (lastCommitted !== "trigger") history.push(usage);
|
|
if (terminalCommitted) history.push(stopped);
|
|
const answerAlreadySaved = vi.fn(() => null);
|
|
await expect(executeNativeSession({ ...options, persistedSession: checkpoint, resolveGovernedWait: answerAlreadySaved }))
|
|
.resolves.toMatchObject({ result: yieldedResult, turnId: trigger.turnId, terminal: { runTerminalState: "succeeded" } });
|
|
expect(start).toHaveBeenCalledOnce();
|
|
expect(cancel).toHaveBeenCalledTimes(terminalCommitted ? 0 : 1);
|
|
expect(answerAlreadySaved).not.toHaveBeenCalled();
|
|
expect(complete).toHaveBeenCalledOnce();
|
|
expect(history.filter(event => event.payload.kind === "usage")).toEqual([usage]);
|
|
});
|
|
|
|
it.each(["missing-trigger", "changed-trigger", "unproven-terminal", "different-turn"])("rejects unsafe governed-wait recovery: %s", async fault => {
|
|
const trigger = { ...runnerEvent(1, "item.completed", { kind: "dynamicToolCall" }), turnId: "turn-waiting" };
|
|
const checkpoint: PersistedNativeSession = { backendKind: "mock", sessionId: identity.sessionId, identity,
|
|
activeTurnId: trigger.turnId, governedWait: { sourceEvent: trigger, result: yieldedResult } };
|
|
const history = fault === "missing-trigger" ? [] : [fault === "changed-trigger" ? { ...trigger, payload: {} } : trigger];
|
|
if (fault === "unproven-terminal") history.push({ ...trigger, sourceSeq: 2, sourceEventId: "unproven", eventType: "turn.failed", payload: { providerTerminalObserved: false } });
|
|
if (fault === "different-turn") history.push({ ...trigger, sourceSeq: 2, sourceEventId: "new-turn", turnId: "different", eventType: "turn.started" });
|
|
const start = vi.fn();
|
|
const complete = vi.fn();
|
|
const session: NativeSession = { identity: () => identity, startTurn: start, async result() { return null; },
|
|
async capabilities() { return { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
|
|
async snapshot() { return checkpoint; }, async *events() {}, async close() {} };
|
|
const backend: NativeSessionBackend = { async descriptor() { return { kind: "mock", name: "unsafe-wait", version: "1", capabilities: await session.capabilities() }; },
|
|
async openSession() { return session; }, async recoverSession() { return { recovered: true, session }; } };
|
|
const port: ControlPlanePort = { async openRun() {}, async checkpointSession() {}, async appendEvent() { throw new Error("unexpected append"); },
|
|
completeRun: complete, async replayEvents(request) { return { events: history.filter(row => row.sourceSeq > request.afterSourceSeq), highestContiguousSourceSeq: history.at(-1)?.sourceSeq ?? 0 }; } };
|
|
await expect(executeNativeSession({ input, backend, controlPlane: port, persistedSession: checkpoint,
|
|
runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery" })).rejects.toThrow(/governed_wait|failed terminal/);
|
|
expect(start).not.toHaveBeenCalled(); expect(complete).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([0, 6_000, 12_000])("retains actual shutdown usage after %s ms before finalizing a governed wait", async (receiptDelayMs) => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const yielded: PrpStructuredRunResult = {
|
|
schema: "paperclip.run_result.v1",
|
|
reportedWorkDisposition: "yielded",
|
|
summary: "Waiting for the requested response.",
|
|
completionClaim: {
|
|
contractRevision: "1",
|
|
objectiveSatisfied: false,
|
|
criteria: [
|
|
{
|
|
criterionId: "objective",
|
|
status: "unknown",
|
|
evidenceRefs: ["interaction:pending"],
|
|
},
|
|
],
|
|
remainingWork: [
|
|
{ description: "Resume after the response.", blocksCompletion: true },
|
|
],
|
|
},
|
|
evidence: [{ ref: "interaction:pending" }],
|
|
verification: [],
|
|
attentionRequests: [],
|
|
artifacts: [],
|
|
continuation: {
|
|
kind: "response_wake",
|
|
summary: "Resume from the answer.",
|
|
idempotencyKey: "interaction-response:pending",
|
|
},
|
|
};
|
|
const itemCompleted: PrpEvent = {
|
|
...controlEvent(1, "item.completed", {
|
|
kind: "dynamicToolCall",
|
|
item: { id: "ask-1", name: "ask_user_questions" },
|
|
}),
|
|
sourceEventId: "provider-recovery:1",
|
|
sourceInstanceId: "provider-recovery",
|
|
sourceKind: "provider",
|
|
turnId: "turn-waiting",
|
|
};
|
|
const turnInterrupted: PrpEvent = {
|
|
...controlEvent(3, "turn.interrupted", { reason: "governed_wait" }),
|
|
sourceEventId: "provider-recovery:3",
|
|
sourceInstanceId: "provider-recovery",
|
|
sourceKind: "provider",
|
|
turnId: "turn-waiting",
|
|
};
|
|
let releaseCancelled!: () => void;
|
|
const cancelled = new Promise<void>((resolve) => {
|
|
releaseCancelled = resolve;
|
|
});
|
|
const cancel = vi.fn(() => {
|
|
releaseCancelled();
|
|
return { cleanup: Promise.resolve() };
|
|
});
|
|
const events: PrpEvent[] = [];
|
|
const completeRun = vi.fn(async () => undefined);
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield itemCompleted;
|
|
await cancelled;
|
|
await new Promise<void>(resolve => setTimeout(resolve, receiptDelayMs));
|
|
yield { ...itemCompleted, sourceSeq: 2, sourceEventId: "provider-recovery:2",
|
|
payload: { kind: "usage", usage: { runDelta: { inputTokens: 7, outputTokens: 3 }, runDeltaComplete: true } } };
|
|
yield turnInterrupted;
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-waiting" };
|
|
},
|
|
cancel,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-waiting",
|
|
cursor: "2",
|
|
activeTurnId: "turn-waiting",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
async close() {},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "governed-wait-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
events.push(structuredClone(event as PrpEvent));
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(events),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
const replayed = events.filter(
|
|
(event) =>
|
|
event.sourceInstanceId === replay.sourceInstanceId &&
|
|
event.sourceSeq > replay.afterSourceSeq,
|
|
);
|
|
return {
|
|
events: structuredClone(replayed),
|
|
highestContiguousSourceSeq: highestContiguous(replayed),
|
|
};
|
|
},
|
|
completeRun,
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
resolveGovernedWait: ({ event }) =>
|
|
event.eventType === "item.completed" ? yielded : null,
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(receiptDelayMs + 1);
|
|
const completed = await execution;
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
expect(events.find(event => event.payload.kind === "usage")?.payload.usage).toMatchObject({ runDelta: { inputTokens: 7, outputTokens: 3 } });
|
|
expect(completed).toMatchObject({
|
|
result: yielded,
|
|
terminal: {
|
|
turnTerminalState: "completed",
|
|
runTerminalState: "succeeded",
|
|
reportedWorkDisposition: "yielded",
|
|
},
|
|
});
|
|
expect(events.map((event) => event.eventType)).toEqual([
|
|
"item.completed",
|
|
"item.completed",
|
|
"turn.interrupted",
|
|
"run.result.accepted",
|
|
"run.terminal",
|
|
]);
|
|
} finally { vi.useRealTimers(); }
|
|
});
|
|
|
|
it("hands a committed structured input to the durable wait after its live window", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
const questionSet = {
|
|
schema: "paperclip.question_set.v1" as const,
|
|
questions: [
|
|
{
|
|
id: "region",
|
|
prompt: "Which region?",
|
|
required: true,
|
|
answerMode: "single_select" as const,
|
|
options: [
|
|
{ id: "us", label: "US" },
|
|
{ id: "eu", label: "Europe" },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
const request = {
|
|
schema: "paperclip.runtime_request.v2",
|
|
requestKind: "runtime",
|
|
requestId: "input-1",
|
|
type: "input",
|
|
status: "pending",
|
|
prompt: "Which region?",
|
|
input: questionSet,
|
|
origin: { adapter: "mock" },
|
|
turnId: "turn-waiting",
|
|
itemId: "input-1",
|
|
};
|
|
const created = {
|
|
...runnerEvent(1, "runtime_request.created", { request }),
|
|
turnId: "turn-waiting",
|
|
};
|
|
const expired = {
|
|
...runnerEvent(2, "runtime_request.expired", {
|
|
requestId: "input-1",
|
|
requestKind: "runtime",
|
|
turnId: "turn-waiting",
|
|
itemId: "input-1",
|
|
reason: "durable_handoff",
|
|
replayAllowed: false,
|
|
requestType: "input",
|
|
request,
|
|
}),
|
|
turnId: "turn-waiting",
|
|
};
|
|
const interrupted = {
|
|
...runnerEvent(3, "turn.interrupted", { reason: "governed_wait" }),
|
|
turnId: "turn-waiting",
|
|
};
|
|
let releaseHandoff!: () => void;
|
|
const handedOff = new Promise<void>((resolve) => {
|
|
releaseHandoff = resolve;
|
|
});
|
|
let releaseCancelled!: () => void;
|
|
const cancelled = new Promise<void>((resolve) => {
|
|
releaseCancelled = resolve;
|
|
});
|
|
let releaseCreated!: () => void;
|
|
const createdCommitted = new Promise<void>((resolve) => {
|
|
releaseCreated = resolve;
|
|
});
|
|
const handoffRuntimeRequest = vi.fn(() => {
|
|
releaseHandoff();
|
|
return { result: "handed_off" as const, cleanup: Promise.resolve() };
|
|
});
|
|
const cancel = vi.fn(() => {
|
|
releaseCancelled();
|
|
return { cleanup: Promise.resolve() };
|
|
});
|
|
const events: PrpEvent[] = [];
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
runtimeRequestHandoff: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield created;
|
|
await handedOff;
|
|
yield expired;
|
|
await cancelled;
|
|
yield interrupted;
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-waiting" };
|
|
},
|
|
handoffRuntimeRequest,
|
|
cancel,
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-waiting",
|
|
cursor: "3",
|
|
activeTurnId: "turn-waiting",
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
async close() {},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "runtime-input-wait-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
runtimeRequestHandoff: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
events.push(structuredClone(event as PrpEvent));
|
|
if (event.eventType === "runtime_request.created") releaseCreated();
|
|
const sourceEvents = events.filter(
|
|
(candidate) =>
|
|
candidate.sourceInstanceId === event.sourceInstanceId,
|
|
);
|
|
return {
|
|
cursor: events.length,
|
|
highestContiguousSourceSeq: highestContiguous(sourceEvents),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
const replayed = events.filter(
|
|
(event) =>
|
|
event.sourceInstanceId === replay.sourceInstanceId &&
|
|
event.sourceSeq > replay.afterSourceSeq,
|
|
);
|
|
return {
|
|
events: structuredClone(replayed),
|
|
highestContiguousSourceSeq: highestContiguous(replayed),
|
|
};
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
runtimeInputLiveWindowMs: 120,
|
|
resolveGovernedWait: ({ event }) =>
|
|
event.eventType === "runtime_request.expired" ? yieldedResult : null,
|
|
});
|
|
await createdCommitted;
|
|
expect(handoffRuntimeRequest).not.toHaveBeenCalled();
|
|
await vi.advanceTimersByTimeAsync(119);
|
|
expect(handoffRuntimeRequest).not.toHaveBeenCalled();
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
|
|
await expect(execution).resolves.toMatchObject({ result: yieldedResult });
|
|
expect(handoffRuntimeRequest).toHaveBeenCalledWith({
|
|
requestId: "input-1",
|
|
turnId: "turn-waiting",
|
|
reason: "durable_handoff",
|
|
signal: expect.any(AbortSignal),
|
|
});
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
expect(events.map((event) => event.eventType)).toContain(
|
|
"runtime_request.expired",
|
|
);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("aborts and bounds a durable handoff that never settles", async () => {
|
|
const request = {
|
|
schema: "paperclip.runtime_request.v2",
|
|
requestKind: "runtime",
|
|
requestId: "input-stalled",
|
|
type: "input",
|
|
status: "pending",
|
|
prompt: "Which region?",
|
|
input: {
|
|
schema: "paperclip.question_set.v1",
|
|
questions: [
|
|
{
|
|
id: "region",
|
|
prompt: "Which region?",
|
|
required: true,
|
|
answerMode: "text",
|
|
},
|
|
],
|
|
},
|
|
origin: { adapter: "mock" },
|
|
turnId: "turn-stalled",
|
|
itemId: "input-stalled",
|
|
};
|
|
const created = {
|
|
...runnerEvent(1, "runtime_request.created", { request }),
|
|
turnId: "turn-stalled",
|
|
};
|
|
let releaseEvents = () => {};
|
|
const eventsReleased = new Promise<void>((resolve) => {
|
|
releaseEvents = resolve;
|
|
});
|
|
let markHandoffStarted = () => {};
|
|
const handoffStarted = new Promise<void>((resolve) => {
|
|
markHandoffStarted = resolve;
|
|
});
|
|
let handoffSignal: AbortSignal | undefined;
|
|
let releaseHandoff = () => {};
|
|
const close = vi.fn(async () => releaseEvents());
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
runtimeRequestHandoff: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield created;
|
|
await eventsReleased;
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-stalled" };
|
|
},
|
|
handoffRuntimeRequest(input) {
|
|
handoffSignal = input.signal;
|
|
markHandoffStarted();
|
|
return {
|
|
result: "handed_off",
|
|
cleanup: new Promise<void>((resolve) => {
|
|
releaseHandoff = resolve;
|
|
}),
|
|
};
|
|
},
|
|
cancel() {
|
|
releaseEvents();
|
|
return { cleanup: Promise.resolve() };
|
|
},
|
|
async result() {
|
|
return null;
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-stalled",
|
|
activeTurnId: "turn-stalled",
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "stalled-handoff-backend",
|
|
version: "1",
|
|
capabilities: await session.capabilities(),
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
runtimeInputLiveWindowMs: 1,
|
|
timeoutMs: 25,
|
|
keepSessionOpen: true,
|
|
});
|
|
await handoffStarted;
|
|
await vi.waitFor(() => expect(close).toHaveBeenCalledOnce());
|
|
expect(handoffSignal?.aborted).toBe(true);
|
|
await expect(execution).rejects.toThrow("native session timed out");
|
|
releaseHandoff();
|
|
expect(close).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("preserves terminal success while iterator teardown remains pending", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
let releaseTeardown = () => {};
|
|
const teardownStarted = vi.fn();
|
|
const close = vi.fn(async () => undefined);
|
|
const readResult = vi.fn(async () => ({
|
|
result,
|
|
terminal,
|
|
turnId: "turn-terminal",
|
|
}));
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
try {
|
|
yield {
|
|
...runnerEvent(1, "turn.completed"),
|
|
turnId: "turn-terminal",
|
|
};
|
|
} finally {
|
|
teardownStarted();
|
|
await new Promise<void>((resolve) => {
|
|
releaseTeardown = resolve;
|
|
});
|
|
}
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-terminal" };
|
|
},
|
|
result: readResult,
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-terminal",
|
|
cursor: "1",
|
|
activeTurnId: null,
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "slow-teardown-backend",
|
|
version: "1",
|
|
capabilities: await session.capabilities(),
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const completeRun = vi.fn(async () => undefined);
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent() {
|
|
return {
|
|
cursor: 1,
|
|
highestContiguousSourceSeq: 1,
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
completeRun,
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
});
|
|
await vi.waitFor(() => expect(teardownStarted).toHaveBeenCalledOnce());
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
await expect(execution).resolves.toMatchObject({ result });
|
|
expect(readResult).toHaveBeenCalledOnce();
|
|
expect(completeRun).toHaveBeenCalledOnce();
|
|
expect(close).toHaveBeenCalledOnce();
|
|
releaseTeardown();
|
|
await Promise.resolve();
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("preserves terminal success while quarantining stalled handoff cleanup", async () => {
|
|
const request = {
|
|
schema: "paperclip.runtime_request.v2",
|
|
requestKind: "runtime",
|
|
requestId: "input-terminal",
|
|
type: "input",
|
|
status: "pending",
|
|
prompt: "Which region?",
|
|
input: {
|
|
schema: "paperclip.question_set.v1",
|
|
questions: [
|
|
{
|
|
id: "region",
|
|
prompt: "Which region?",
|
|
required: true,
|
|
answerMode: "text",
|
|
},
|
|
],
|
|
},
|
|
origin: { adapter: "mock" },
|
|
turnId: "turn-terminal",
|
|
itemId: "input-terminal",
|
|
};
|
|
let markHandoffStarted = () => {};
|
|
const handoffStarted = new Promise<void>((resolve) => {
|
|
markHandoffStarted = resolve;
|
|
});
|
|
let releaseHandoff = () => {};
|
|
let handoffSignal: AbortSignal | undefined;
|
|
const close = vi.fn(async () => undefined);
|
|
const onSession = vi.fn();
|
|
const completeRun = vi.fn(async () => undefined);
|
|
const readResult = vi.fn(async () => ({
|
|
result,
|
|
terminal,
|
|
turnId: "turn-terminal",
|
|
}));
|
|
const providerEvents = [
|
|
{
|
|
...runnerEvent(1, "runtime_request.created", { request }),
|
|
turnId: "turn-terminal",
|
|
},
|
|
{ ...runnerEvent(2, "turn.completed"), turnId: "turn-terminal" },
|
|
];
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
runtimeRequestHandoff: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield providerEvents[0]!;
|
|
await handoffStarted;
|
|
yield providerEvents[1]!;
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-terminal" };
|
|
},
|
|
handoffRuntimeRequest(input) {
|
|
handoffSignal = input.signal;
|
|
markHandoffStarted();
|
|
return {
|
|
result: "handed_off",
|
|
cleanup: new Promise<void>((resolve) => {
|
|
releaseHandoff = resolve;
|
|
}),
|
|
};
|
|
},
|
|
result: readResult,
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-terminal",
|
|
cursor: "2",
|
|
activeTurnId: null,
|
|
};
|
|
},
|
|
close,
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "terminal-handoff-backend",
|
|
version: "1",
|
|
capabilities: await session.capabilities(),
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const appended: PrpEvent[] = [];
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
appended.push(structuredClone(event as PrpEvent));
|
|
return {
|
|
cursor: appended.length,
|
|
highestContiguousSourceSeq: highestContiguous(appended),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents() {
|
|
return { events: [], highestContiguousSourceSeq: 0 };
|
|
},
|
|
completeRun,
|
|
};
|
|
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
runtimeInputLiveWindowMs: 1,
|
|
keepSessionOpen: true,
|
|
onSession,
|
|
});
|
|
await handoffStarted;
|
|
await vi.waitFor(() => expect(handoffSignal?.aborted).toBe(true));
|
|
await expect(execution).resolves.toMatchObject({ result });
|
|
expect(readResult).toHaveBeenCalledOnce();
|
|
expect(completeRun).toHaveBeenCalledOnce();
|
|
expect(close).toHaveBeenCalledOnce();
|
|
expect(onSession).toHaveBeenLastCalledWith(null);
|
|
releaseHandoff();
|
|
await Promise.resolve();
|
|
expect(close).toHaveBeenCalledOnce();
|
|
expect(completeRun).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("keeps a settling structured input in the original turn while its append crosses expiry", async () => {
|
|
const request = {
|
|
schema: "paperclip.runtime_request.v2",
|
|
requestKind: "runtime",
|
|
requestId: "input-live",
|
|
type: "input",
|
|
status: "pending",
|
|
prompt: "Which region?",
|
|
input: {
|
|
schema: "paperclip.question_set.v1",
|
|
questions: [
|
|
{
|
|
id: "region",
|
|
prompt: "Which region?",
|
|
required: true,
|
|
answerMode: "text",
|
|
},
|
|
],
|
|
},
|
|
origin: { adapter: "mock" },
|
|
turnId: "turn-live",
|
|
itemId: "input-live",
|
|
};
|
|
const providerEvents = [
|
|
{
|
|
...runnerEvent(1, "runtime_request.created", { request }),
|
|
turnId: "turn-live",
|
|
},
|
|
{
|
|
...runnerEvent(2, "runtime_request.resolved", {
|
|
requestId: "input-live",
|
|
requestKind: "user_input",
|
|
turnId: "turn-live",
|
|
itemId: "input-live",
|
|
action: "submit",
|
|
requestType: "input",
|
|
}),
|
|
turnId: "turn-live",
|
|
},
|
|
{ ...runnerEvent(3, "turn.completed"), turnId: "turn-live" },
|
|
];
|
|
const appended: PrpEvent[] = [];
|
|
let markSettlementAppendStarted!: () => void;
|
|
const settlementAppendStarted = new Promise<void>((resolve) => {
|
|
markSettlementAppendStarted = resolve;
|
|
});
|
|
let releaseSettlementAppend!: () => void;
|
|
const settlementAppendReleased = new Promise<void>((resolve) => {
|
|
releaseSettlementAppend = resolve;
|
|
});
|
|
const handoffRuntimeRequest = vi.fn(() => ({
|
|
result: "handed_off" as const,
|
|
cleanup: Promise.resolve(),
|
|
}));
|
|
const session: NativeSession = {
|
|
identity: () => identity,
|
|
async capabilities() {
|
|
return {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
runtimeRequestHandoff: true,
|
|
};
|
|
},
|
|
async *events() {
|
|
yield* providerEvents;
|
|
},
|
|
async startTurn() {
|
|
return { turnId: "turn-live" };
|
|
},
|
|
handoffRuntimeRequest,
|
|
async result() {
|
|
return { result, terminal, turnId: "turn-live" };
|
|
},
|
|
async snapshot() {
|
|
return {
|
|
backendKind: "mock",
|
|
sessionId: identity.sessionId,
|
|
identity,
|
|
providerSessionId: "provider-live",
|
|
cursor: "3",
|
|
activeTurnId: null,
|
|
pendingRuntimeRequests: [],
|
|
lineage: [],
|
|
};
|
|
},
|
|
async close() {},
|
|
};
|
|
const backend: NativeSessionBackend = {
|
|
async descriptor() {
|
|
return {
|
|
kind: "mock",
|
|
name: "runtime-input-live-backend",
|
|
version: "1",
|
|
capabilities: {
|
|
resume: false,
|
|
typedEvents: true,
|
|
steering: false,
|
|
interruption: true,
|
|
structuredResult: true,
|
|
runtimeRequestHandoff: true,
|
|
},
|
|
};
|
|
},
|
|
async openSession() {
|
|
return session;
|
|
},
|
|
};
|
|
const port: ControlPlanePort = {
|
|
async openRun() {},
|
|
async checkpointSession() {},
|
|
async appendEvent(event) {
|
|
if (event.eventType === "runtime_request.resolved") {
|
|
markSettlementAppendStarted();
|
|
await settlementAppendReleased;
|
|
}
|
|
appended.push(structuredClone(event as PrpEvent));
|
|
const sourceEvents = appended.filter(
|
|
(candidate) => candidate.sourceInstanceId === event.sourceInstanceId,
|
|
);
|
|
return {
|
|
cursor: appended.length,
|
|
highestContiguousSourceSeq: highestContiguous(sourceEvents),
|
|
disposition: "committed",
|
|
};
|
|
},
|
|
async replayEvents(replay) {
|
|
const replayed = appended.filter(
|
|
(event) =>
|
|
event.sourceInstanceId === replay.sourceInstanceId &&
|
|
event.sourceSeq > replay.afterSourceSeq,
|
|
);
|
|
return {
|
|
events: structuredClone(replayed),
|
|
highestContiguousSourceSeq: highestContiguous(replayed),
|
|
};
|
|
},
|
|
async completeRun() {},
|
|
};
|
|
|
|
const originalSetTimeout = globalThis.setTimeout;
|
|
let queuedHandoffCallback: (() => void) | null = null;
|
|
const timeoutSpy = vi.spyOn(globalThis, "setTimeout").mockImplementation(((
|
|
callback,
|
|
delay,
|
|
...args
|
|
) => {
|
|
if (delay === 123_456) {
|
|
queuedHandoffCallback = () => callback(...args);
|
|
const handle = originalSetTimeout(() => undefined, 60_000);
|
|
handle.unref?.();
|
|
return handle;
|
|
}
|
|
return originalSetTimeout(callback, delay, ...args);
|
|
}) as typeof setTimeout);
|
|
try {
|
|
const execution = executeNativeSession({
|
|
input,
|
|
backend,
|
|
controlPlane: port,
|
|
runnerInstanceId: "runner-recovery",
|
|
controlPlaneInstanceId: "control-recovery",
|
|
runtimeInputLiveWindowMs: 123_456,
|
|
});
|
|
await settlementAppendStarted;
|
|
expect(queuedHandoffCallback).not.toBeNull();
|
|
queuedHandoffCallback?.();
|
|
await Promise.resolve();
|
|
expect(handoffRuntimeRequest).not.toHaveBeenCalled();
|
|
releaseSettlementAppend();
|
|
await expect(execution).resolves.toMatchObject({ result });
|
|
queuedHandoffCallback?.();
|
|
await Promise.resolve();
|
|
expect(handoffRuntimeRequest).not.toHaveBeenCalled();
|
|
expect(
|
|
appended.some((event) => event.eventType === "runtime_request.expired"),
|
|
).toBe(false);
|
|
} finally {
|
|
releaseSettlementAppend();
|
|
timeoutSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
it("fails preconfigured recovery without replacing a missing provider session", async () => {
|
|
const checkpoint: PersistedNativeSession = {
|
|
backendKind: "mock", sessionId: "driver-preconfigured", identity,
|
|
providerSessionId: "provider-missing", providerRecoveryPolicy: "allow_replacement_after_resume_failure",
|
|
cursor: "0", activeTurnId: null, pendingRuntimeRequests: [], lineage: [],
|
|
};
|
|
const openSession = vi.fn();
|
|
const openReplacementSession = vi.fn();
|
|
const backend: NativeSessionBackend = {
|
|
descriptor: async () => ({ kind: "mock", name: "preconfigured", version: "1",
|
|
runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" },
|
|
capabilities: { resume: true, typedEvents: true, steering: false, interruption: true } }),
|
|
openSession, openReplacementSession,
|
|
recoverSession: async () => ({ recovered: false, reason: "provider history is missing" }),
|
|
};
|
|
const port: ControlPlanePort = {
|
|
openRun: vi.fn(), appendEvent: vi.fn(), completeRun: vi.fn(),
|
|
replayEvents: async () => ({ events: [], highestContiguousSourceSeq: 0 }),
|
|
};
|
|
await expect(executeNativeSession({
|
|
input: { ...preparedInput(), schema: "paperclip.native-execution-input.v7", homeMode: "preconfigured" },
|
|
backend, controlPlane: port, runnerInstanceId: "runner-preconfigured", persistedSession: checkpoint,
|
|
})).rejects.toThrow("native_session_recovery_failed: provider history is missing");
|
|
expect(openSession).not.toHaveBeenCalled();
|
|
expect(openReplacementSession).not.toHaveBeenCalled();
|
|
});
|