Files
PaperClipAI/server/src/services/native-runtime/native-controller-migration.integration.test.ts
T
DottaandPaperclip 61b2455319 fix(runtime): preserve failed sources and durable checkpoint authority (#19038)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A volume-free web server must preserve retained native session data.
> - An old failed follow-up can keep its own snapshot while its
controller file still belongs to the preceding successful turn.
> - The Runner correctly refuses to import that file as the failed
turn's execution authority.
> - That refusal also blocks migration when no warm supervisor is
present.
> - This pull request preserves the file after it proves the older run's
lineage, and uses existing typed database authority when present.
> - Failed history and files survive, while normal execution authority
stays unchanged.

## Linked Issues or Issue Description

Refs #19024, which preserves the related older-supervisor case. The
duplicate search found no PR for this older-controller case.

**What happened?**

Offline native-controller migration stops with
`native_runner_checkpoint_binding_mismatch` for a failed retained
follow-up. The local controller still contains the preceding successful
run. A per-turn source can have no supervisor file, so the
older-supervisor archive path does not apply. Read-only checks matched
the database snapshots and controller-file hashes between the original
source and the isolated rehearsal copy.

**Expected behavior**

Preserve the available file as evidence through the existing encrypted
archive. Require a valid older controller, a completed successful prior
run, and the same company, agent, task, native session, Runner and
provider thread. Keep successful sources and active owners outside this
archival fallback. When a valid typed database checkpoint already
exists, derive only its missing recovery metadata and ignore obsolete
local projections.

**Steps to reproduce**

Create a successful native turn and a later failed follow-up in the same
session. Retain the successful controller file. Keep the failed snapshot
and admitted input in the database. Leave the per-turn source without a
supervisor. Run offline migration. The new positive regression fails
with the binding mismatch before this change.

## What Changed

- Share the scoped older-successful-run proof with the existing
supervisor preservation path.
- Validate the older controller through the Runner parser and artifact
checks before archive publication.
- Archive this failed legacy source without changing its snapshot,
status, ownership, queued work or session pointer.
- Preserve the normal refusal for foreign, changed, active, successful,
invalid and already-durable cases.
- Permit evidence archival for a stale observed coordinator only with
exact host-owned stop receipts for every remote lease, no current task
ownership, no result and no retry deadline. Recheck the proof before
publication.
- Use execution start for lifecycle ordering. A queued follow-up can
predate its prior turn completion, and a remote stop can predate a later
recovery timeout.
- Resolve destruction receipts through the existing reusable-lease
handoff verifier. Reject stopped tips, broken chains and competing tips.
- Prefer a valid typed database checkpoint over a stale or absent local
warm supervisor. Add only missing recovery metadata after current
admitted-input and checkpoint binding checks.
- Document the evidence and execution-authority boundary.

## Verification

- Positive regression: red before the owner change and green afterward.
- `pnpm exec vitest run
server/src/services/native-runtime/native-controller-migration.integration.test.ts
server/src/services/native-runtime/native-controller-recovery.integration.test.ts
server/src/services/native-runtime/native-harness-backup-storage.integration.test.ts`:
81 checks passed across migration, recovery and durable storage.
- The checks cover storage failure, repeated migration, exact file
restoration after old-home deletion, unchanged history and ownership,
and refusal to treat the archive as executable recovery authority.
- A second regression reproduced the archive fence refusal for an
observed failed source with a valid stopped-remote receipt. Negative
controls cover missing, foreign, old and revoked receipts, live leases,
pending results, retry deadlines and task ownership. Revoking the
receipt during upload prevents publication.
- Two additional regressions cover a follow-up queued before the prior
turn finished and a remote stop before a later logical failure. Both
fail with the earlier timestamp comparisons and pass with
execution-start ordering. Genuine execution overlap and old receipts
stay blocked.
- Two historical source snapshots and controller-file hashes matched in
a read-only live-versus-copy check. The first candidate reached the
archive fence and exposed the observed-coordinator case. That failure is
retained. A fresh candidate replay is required for the expanded proof.
It does not qualify the full packaged migration or hosted Railway
handoff.
- Existing typed-authority checks cover stale and missing supervisors,
repeat migration, byte-identical checkpoint rows, unchanged
snapshots/owners/history, old-home deletion and ordinary queued-turn
recovery. Foreign Runner, session and snapshot controls remain blocked.
- Handoff checks cover an exact destroyed successor, a stopped
successor, a broken proof and a competing tip.
- Isolated replay of the two retained typed-checkpoint sources imported
only derived recovery metadata. Checks confirmed unchanged checkpoint
rows (12 and 9 parts), history, snapshots, finalization owners and
task-session pointers. The replay used three source-module overlays with
a blocked provider network; it does not qualify the released bundle.
- Final-head CI attempt 1 passed typecheck, build and the migration
suites but failed one unrelated budget-concurrency assertion in
`decision-models.test.ts` (one provider call observed instead of two).
The original log is retained. The isolated local comparison cannot
classify the timing failure because this checkout has stale linked
budget schema exports. The one retry of that failed CI job passed, and
final-head CI attempt 2 is green. The original failure remains retained;
its cause is not established.
- Full local typecheck, repository tests and build are not claimed. The
server typecheck reports pre-existing linked declaration mismatches
outside the changed files; the full clean CI typecheck has passed. This
checkout uses linked generated declarations; clean CI is required before
merge. Prior head d71dc913 had passing full CI and a 5/5 Greptile
review. Head 8ff3a22109 adds the handoff and typed-authority fixes. Its
fresh full CI is green and Greptile is 5/5, with no unresolved review
threads. Earlier head7729 hit unrelated 5-second CLI ZIP and 15-second
setup-token test timeouts; those failures remain retained.

## Risks

This path preserves evidence for a source that has already failed. It
does not make that source resumable. The Runner must accept the older
controller under its own identity and artifact binding. Scoped
successful history must match its session and provider thread. The
archive owner still requires stopped writers, a finished failed run and
no live lease. An observed coordinator also needs exact remote-stop
receipts after execution started, no accepted result or retry deadline,
and no current task ownership. The coordinator stays unchanged. Existing
durable authority stays authoritative.

## Model Used

OpenAI Codex, exact model ID `gpt-6.1-sol`, with reasoning, tool use and
code execution. The host does not expose context-window size.

## 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 described the issue in-PR following the bug template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
ticket id or instance-derived details
- [x] I have run the relevant tests locally; verification limits are
listed above
- [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 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>
2026-11-04 15:37:54 -06:00

476 lines
41 KiB
TypeScript

import { createHash, randomUUID } from "node:crypto";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { eq } from "drizzle-orm";
import { afterEach, expect, it, vi } from "vitest";
import { agentTaskSessions, environmentLeases, environments, heartbeatRuns, issues, nativeHarnessBackups, nativeRunFinalizations, nativeRunnerCheckpoints } from "@paperclipai/db";
import { captureLegacyDurablePrpCheckpoint, DurablePrpControlPlane, parseNativeExecutionInput } from "../../vendor/paperclip-runner/index.js";
import { describeEmbeddedPostgres, nativeRestartFixture } from "../../__tests__/helpers/native-restart.js";
import { nativeToolContractFingerprintForTarget } from "./native-session-resume.js";
import { nativeControllerMigrationDescriptor } from "./native-session-executor.js";
import { hasLocalNativeControllerObligations, migrateNativeControllers } from "./native-controller-migration.js";
import { insertMigratedNativeRunnerCheckpoint, loadNativeRunnerCheckpoint } from "./native-runner-checkpoint.js";
import { createNativeControllerRecovery, resolveNativeControllerRecovery } from "./native-controller-recovery.js";
import { resolveRemoteProviderPackAuthority } from "./remote-provider-pack-authority.js";
import { canonicalProviderPackJson } from "./remote-provider-pack-verification.js";
import { createReusableLeaseHandoff } from "../environment-lease-handoff.js";
import { remoteTerminationReceipt } from "../remote-execution-termination.js";
import { nativeSha256 } from "./canonical.js";
import { createLocalEncryptedCipher } from "../../secrets/local-encrypted-provider.js";
import { createLocalDiskStorageProvider } from "../../storage/local-disk-provider.js";
import { createStorageService } from "../../storage/service.js";
import { legacyNativeControllerArchive } from "./native-controller-archive.js";
import { restoreDurableNativeHarnessBackup } from "./native-harness-backup-storage.js";
describeEmbeddedPostgres.each(["import", "archive", "success", "batch", "durable_failed",
"stale_confirmed", "stale_foreign", "stale_changed", "stale_queued", "stale_overlap", "stale_success", "stale_busy", "stale_durable", "stale_durable_absent",
"stale_durable_runner", "stale_durable_session", "stale_durable_snapshot",
"controller_confirmed", "controller_foreign", "controller_thread", "controller_runner", "controller_success", "controller_busy", "controller_invalid", "controller_durable",
"controller_observed_stopped", "controller_observed_stop_before_finish", "controller_observed_missing", "controller_observed_foreign",
"controller_observed_live", "controller_observed_result", "controller_observed_retry", "controller_observed_task", "controller_observed_old_receipt", "controller_observed_revoked",
"controller_observed_handoff", "controller_observed_handoff_stopped", "controller_observed_handoff_broken", "controller_observed_handoff_fork"])("offline native controller migration (%s)", scenario => {
const environment = nativeRestartFixture();
afterEach(() => vi.unstubAllEnvs());
async function fixture(commandCount = 1, warm = false) {
vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY", "73".repeat(32));
const f = await environment.seedRun(randomUUID());
const { companyId, agentId, paperclipHome, runtimeRoot } = environment.current;
const binding = { companyId, agentId, issueId: f.issueId, runId: f.runId, executionWorkspaceId: f.native.environmentLeaseId };
const execution = parseNativeExecutionInput({
schema: "paperclip.native-execution-input.v1", provider: { kind: "codex", model: null }, binding,
task: { identifier: "MIG-1", title: "Retained work", description: null, prompt: "Continue the work.", workMode: "standard" },
workspace: { cwd: "/workspace", repoUrl: null, repoRef: null, branchName: null },
session: { normalizedSessionId: f.native.normalizedSessionId, driverKind: "codex_app_server", protocolVersion: 1,
lifecyclePolicy: warm ? { mode: "warm", idleTimeoutMs: 60_000 } : { mode: "per_turn", idleTimeoutMs: null } },
completionContract: { id: randomUUID(), sha256: "fixture", schemaVersion: "paperclip.completion-contract.v1",
contract: { revision: "1", objective: "Continue", criteria: [{ id: "objective", requirement: "Save work." }] } },
credentialBindings: [], interactionResponses: [],
});
const fingerprint = nativeToolContractFingerprintForTarget("remote", "codex");
const paths = nativeControllerMigrationDescriptor(execution, fingerprint);
const identity = { runId: f.runId, runnerInstanceId: f.native.runnerInstanceId, normalizedSessionId: f.native.normalizedSessionId,
environmentLeaseId: f.native.environmentLeaseId, turnId: f.native.turnId, itemId: f.native.itemId };
const core = new DurablePrpControlPlane({ stateDirectory: join(paths.root, "control-plane"), identity,
expectedRunnerVersion: "migration-fixture", expectedRunnerDigest: `sha256:${"a".repeat(64)}` });
core.issueBootstrapTicket();
for (let index = 0; index < commandCount; index++) core.queueCommand("session.snapshot",
{ diagnostic: "x".repeat(commandCount > 1 ? 6000 : 0) }, `retained-command-${index}`);
await core.stop();
const file = join(paths.root, "control-plane/control-plane-state.json");
const state = JSON.parse(await readFile(file, "utf8"));
state.tickets = {};
const source = JSON.stringify(state); await writeFile(file, source, { mode: 0o600 });
const packBinding = { companyId, agentId, workspaceId: binding.executionWorkspaceId,
sessionId: f.native.normalizedSessionId, runnerInstanceId: f.native.runnerInstanceId,
environmentLeaseId: f.native.environmentLeaseId, remoteCwd: "/remote/workspace", providerDigest: nativeSha256(execution.provider) };
const payload = {
runnerSourceRevision: "a".repeat(40), target: { platform: "linux", architecture: "x64" },
pins: { opencode: "1.18.34" }, artifacts: {
nodeCommand: { path: "node", sha256: `sha256:${"1".repeat(64)}` },
opencodeCommand: { path: "opencode", sha256: `sha256:${"2".repeat(64)}` },
},
};
const pack = resolveRemoteProviderPackAuthority({ stateDirectory: paths.root, reattach: false, binding: packBinding,
current: () => ({ schema: "paperclip-runner/remote-provider-pack/v1", payload,
digest: `sha256:${createHash("sha256").update(canonicalProviderPackJson(payload)).digest("hex")}` }),
runnerArtifact: () => ({ version: "migration-fixture", digest: `sha256:${"a".repeat(64)}` }) });
pack.verified();
const snapshot = { backendKind: "mock" as const, sessionId: f.native.normalizedSessionId,
identity: { companyId, agentId, issueId: f.issueId, runId: f.runId, sessionId: f.native.normalizedSessionId } };
const profile = { ...f.preparedProfile, nativeExecutionInput: execution, nativeToolContractFingerprint: fingerprint, sessionCheckpoint: snapshot,
nativeWorkspaceSync: { schema: "paperclip.native-workspace-sync/v1", state: "prepared", descriptorSha256: "a".repeat(64),
baselineSha256: "b".repeat(64), finalHostSha256: null, workspaceId: binding.executionWorkspaceId, leaseId: randomUUID(),
providerLeaseId: randomUUID(), remoteCwd: "/remote/workspace", resourceDisposition: "destroy" } };
await f.db.update(heartbeatRuns).set({ status: "failed", finishedAt: new Date(), runnerProfileJson: profile }).where(eq(heartbeatRuns.id, f.runId));
await f.db.update(nativeRunFinalizations).set({ phase: "terminal_failure", leaseOwner: null }).where(eq(nativeRunFinalizations.runId, f.runId));
const queuedId = randomUUID();
await f.db.insert(heartbeatRuns).values({ id: queuedId, companyId, agentId, issueId: f.issueId, status: "queued" });
await f.db.update(issues).set({ executionRunId: queuedId }).where(eq(issues.id, f.issueId));
const [session] = await f.db.insert(agentTaskSessions).values({ companyId, agentId, adapterType: "paperclip_runner",
taskKey: f.issueId, lastRunId: f.runId }).returning();
return { ...f, companyId, agentId, paperclipHome, runtimeRoot, paths, file, source, profile, session, queuedId, identity, snapshot };
}
if (scenario === "import") it("imports an exact settled authority once, preserves queued work, and resumes from an empty home", async () => {
const f = await fixture();
const { companyId, agentId, paperclipHome, runtimeRoot, profile, file, source, session, queuedId, identity, snapshot } = f;
expect(await hasLocalNativeControllerObligations(f.db)).toBe(true);
await f.db.update(heartbeatRuns).set({ runnerProfileJson: { ...profile,
nativeWorkspaceSync: { ...profile.nativeWorkspaceSync, workspaceId: randomUUID() } } }).where(eq(heartbeatRuns.id, f.runId));
await expect(migrateNativeControllers(f.db, { writersStopped: true, signal: AbortSignal.timeout(30_000) }))
.rejects.toThrow("native_controller_migration_artifact_binding");
await f.db.update(heartbeatRuns).set({ runnerProfileJson: profile }).where(eq(heartbeatRuns.id, f.runId));
expect(profile.nativeWorkspaceSync.leaseId).not.toBe(identity.environmentLeaseId);
expect(profile.nativeWorkspaceSync.workspaceId).toBe(identity.environmentLeaseId);
const foreignProtocol = JSON.parse(source);
foreignProtocol.identity.environmentLeaseId = randomUUID();
await writeFile(file, JSON.stringify(foreignProtocol), { mode: 0o600 });
await expect(migrateNativeControllers(f.db, { writersStopped: true, signal: AbortSignal.timeout(30_000) }))
.rejects.toThrow("saved provider pack authority is invalid or does not match this execution");
expect(await f.db.select().from(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId))).toHaveLength(0);
await writeFile(file, source, { mode: 0o600 });
await f.db.update(nativeRunFinalizations).set({ leaseOwner: "unsettled-owner" }).where(eq(nativeRunFinalizations.runId, f.runId));
await expect(migrateNativeControllers(f.db, { writersStopped: true, signal: AbortSignal.timeout(30_000) })).rejects.toThrow("normal_ownership_recovery_required");
expect(await f.db.select().from(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId))).toHaveLength(0);
await f.db.update(nativeRunFinalizations).set({ leaseOwner: "expired-owner", leaseExpiresAt: new Date(0),
phase: "retryable_failure", recoveryState: "resuming_session" }).where(eq(nativeRunFinalizations.runId, f.runId));
const [preservedOwner] = await f.db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId));
expect(await migrateNativeControllers(f.db, { writersStopped: true, signal: AbortSignal.timeout(30_000) }))
.toMatchObject({ imported: 1, sourceAlreadyAbsent: 0 });
expect(await migrateNativeControllers(f.db, { writersStopped: true, signal: AbortSignal.timeout(30_000) })).toMatchObject({ unchanged: 1, imported: 0 });
expect(await hasLocalNativeControllerObligations(f.db)).toBe(false);
expect((await f.db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId)))[0]).toEqual(preservedOwner);
expect(await readFile(file, "utf8")).toBe(source);
expect((await f.db.select().from(agentTaskSessions).where(eq(agentTaskSessions.id, session.id)))[0].lastRunId).toBe(f.runId);
expect((await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, queuedId)))[0].status).toBe("queued");
await rm(paperclipHome, { recursive: true, force: true });
await f.db.update(heartbeatRuns).set({ nativeIssueId: f.issueId, nativeSessionId: f.native.normalizedSessionId,
status: "running", runtimeMode: "native" }).where(eq(heartbeatRuns.id, queuedId));
const recovered = await resolveNativeControllerRecovery(f.db, { companyId, agentId, issueId: f.issueId,
normalizedSessionId: f.native.normalizedSessionId, currentRunId: queuedId });
expect(recovered.kind).toBe("recoverable");
if (recovered.kind !== "recoverable") throw new Error(JSON.stringify(recovered));
const replacement = join(runtimeRoot, "replacement"); await mkdir(replacement, { mode: 0o700 });
recovered.checkpoint.restoreSessionDirectory(replacement);
expect(JSON.parse(await readFile(join(replacement, "control-plane/control-plane-state.json"), "utf8")).identity).toEqual(identity);
expect(recovered.snapshot).toEqual(snapshot);
});
if (scenario === "batch") it("imports a large checkpoint with bounded writes and rolls back every batch on a late conflict", async () => {
const f = await fixture(160);
const binding = { companyId: f.companyId, issueId: f.issueId, runId: f.runId };
const checkpoint = captureLegacyDurablePrpCheckpoint(f.source, { runId: f.runId,
runnerInstanceId: f.native.runnerInstanceId, normalizedSessionId: f.native.normalizedSessionId },
{ version: "migration-fixture", digest: `sha256:${"a".repeat(64)}` });
const last = [...checkpoint.records].at(-1)!;
await f.db.transaction(tx => insertMigratedNativeRunnerCheckpoint(tx, binding, {
...checkpoint, records: new Map([last]),
}));
const before = await f.db.select().from(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId));
const parameters: number[] = [];
const payloadBytes: number[] = [];
const previousDebug = f.db.$client.options.debug;
f.db.$client.options.debug = (_connection, query, values) => {
if (query.startsWith('insert into "native_runner_checkpoints"')) {
parameters.push(values.length);
payloadBytes.push(values.reduce((sum, value) => sum + Buffer.byteLength(JSON.stringify(value)), 0));
}
};
try {
await expect(f.db.transaction(tx => insertMigratedNativeRunnerCheckpoint(tx, binding, checkpoint)))
.rejects.toThrow();
expect(await f.db.select().from(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId))).toEqual(before);
expect(parameters.length).toBeGreaterThan(1);
expect(Math.max(...parameters)).toBeLessThanOrEqual(320);
expect(Math.max(...payloadBytes)).toBeLessThanOrEqual(512 * 1024);
await f.db.delete(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId));
expect(await migrateNativeControllers(f.db, { writersStopped: true, signal: AbortSignal.timeout(30_000) }))
.toMatchObject({ imported: 1 });
const rows = await f.db.select().from(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId));
const cipher = createLocalEncryptedCipher("read");
for (const row of rows) expect(JSON.parse(cipher.decrypt(row.material))).toEqual({
...binding, part: row.part, contents: checkpoint.records.get(row.part),
});
const restored = await loadNativeRunnerCheckpoint(f.db, binding);
expect(restored?.records).toEqual(checkpoint.records);
expect(await readFile(f.file, "utf8")).toBe(f.source);
} finally { f.db.$client.options.debug = previousDebug; }
}, 60_000);
if (scenario === "durable_failed") it("preserves a failed durable checkpoint without inventing a missing session snapshot", async () => {
const f = await fixture();
const binding = { companyId: f.companyId, issueId: f.issueId, runId: f.runId };
const checkpoint = captureLegacyDurablePrpCheckpoint(f.source, { runId: f.runId,
runnerInstanceId: f.native.runnerInstanceId, normalizedSessionId: f.native.normalizedSessionId },
{ version: "migration-fixture", digest: `sha256:${"a".repeat(64)}` });
await f.db.transaction(tx => insertMigratedNativeRunnerCheckpoint(tx, binding, checkpoint));
await f.db.update(heartbeatRuns).set({ processPid: 12345, runnerProfileJson: { ...f.profile, sessionCheckpoint: null } })
.where(eq(heartbeatRuns.id, f.runId));
const options = { writersStopped: true as const, signal: AbortSignal.timeout(30_000) };
await f.db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, f.runId));
await expect(migrateNativeControllers(f.db, options)).rejects.toThrow("stale_supervisor_snapshot");
await f.db.update(heartbeatRuns).set({ status: "failed" }).where(eq(heartbeatRuns.id, f.runId));
const [run] = await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const [owner] = await f.db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId));
const rows = await f.db.select().from(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId));
const foreign = JSON.parse(f.source);
foreign.identity.normalizedSessionId = randomUUID();
const foreignCheckpoint = captureLegacyDurablePrpCheckpoint(JSON.stringify(foreign), { runId: f.runId,
runnerInstanceId: f.native.runnerInstanceId, normalizedSessionId: foreign.identity.normalizedSessionId },
{ version: "migration-fixture", digest: `sha256:${"a".repeat(64)}` });
await f.db.delete(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId));
await f.db.transaction(tx => insertMigratedNativeRunnerCheckpoint(tx, binding, foreignCheckpoint));
expect(await hasLocalNativeControllerObligations(f.db)).toBe(true);
await expect(migrateNativeControllers(f.db, options)).rejects.toThrow("stale_supervisor_snapshot");
await f.db.delete(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId));
await f.db.insert(nativeRunnerCheckpoints).values(rows);
expect(await migrateNativeControllers(f.db, options)).toMatchObject({ preservedFailed: 1, imported: 0 });
expect(await hasLocalNativeControllerObligations(f.db)).toBe(false);
expect(await migrateNativeControllers(f.db, options)).toMatchObject({ preservedFailed: 1, imported: 0 });
expect(await f.db.select().from(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId))).toEqual(rows);
expect((await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId)))[0]).toEqual(run);
expect((await f.db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId)))[0]).toEqual(owner);
expect((await f.db.select().from(agentTaskSessions).where(eq(agentTaskSessions.id, f.session.id)))[0].lastRunId).toBe(f.runId);
expect((await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.queuedId)))[0].status).toBe("queued");
await rm(f.paperclipHome, { recursive: true, force: true });
expect((await loadNativeRunnerCheckpoint(f.db, binding))?.records).toEqual(checkpoint.records);
const recovery = await resolveNativeControllerRecovery(f.db, { companyId: f.companyId, agentId: f.agentId,
issueId: f.issueId, normalizedSessionId: f.native.normalizedSessionId, currentRunId: f.queuedId });
expect(recovery.kind).not.toBe("recoverable");
});
if (scenario === "archive") it("preserves an already-failed legacy controller without inventing executable recovery authority", async () => {
const f = await fixture();
await rm(join(f.paths.root, "provider-pack-authority.json"));
await f.db.update(nativeRunFinalizations).set({ recoveryState: "blocked", failureCode: "native_provider_terminal_failed" })
.where(eq(nativeRunFinalizations.runId, f.runId));
const [originalRun] = await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const [originalOwner] = await f.db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId));
const storage = createStorageService(createLocalDiskStorageProvider(join(f.runtimeRoot, "durable-objects")));
const options = { storage, writersStopped: true as const, signal: AbortSignal.timeout(30_000) };
const put = vi.spyOn(storage, "putFile").mockRejectedValue(new Error("storage unavailable"));
await expect(migrateNativeControllers(f.db, options)).rejects.toThrow("storage unavailable");
put.mockRestore();
expect(await hasLocalNativeControllerObligations(f.db, storage)).toBe(true);
expect(await migrateNativeControllers(f.db, options)).toMatchObject({ archivedFailed: 1, imported: 0 });
expect(await migrateNativeControllers(f.db, options)).toMatchObject({ archivedFailed: 1, imported: 0 });
expect(await hasLocalNativeControllerObligations(f.db, storage)).toBe(false);
expect(await f.db.select().from(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId))).toHaveLength(0);
expect((await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId)))[0]).toEqual(originalRun);
expect((await f.db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId)))[0]).toEqual(originalOwner);
expect((await f.db.select().from(issues).where(eq(issues.id, f.issueId)))[0].executionRunId).toBe(f.queuedId);
expect((await f.db.select().from(agentTaskSessions).where(eq(agentTaskSessions.id, f.session.id)))[0].lastRunId).toBe(f.runId);
await rm(f.paperclipHome, { recursive: true, force: true });
const descriptor = legacyNativeControllerArchive({ companyId: f.companyId, issueId: f.issueId, runId: f.runId, controller: f.source });
const replacement = join(f.runtimeRoot, "archived-inspection");
expect(await restoreDurableNativeHarnessBackup({ db: f.db, storage, companyId: f.companyId,
scopeDigest: descriptor.scopeDigest, backupRoot: replacement })).toBe(true);
expect(await readFile(join(replacement, "current", "control-plane-state.json"), "utf8")).toBe(f.source);
const recovery = await resolveNativeControllerRecovery(f.db, { companyId: f.companyId, agentId: f.agentId,
issueId: f.issueId, normalizedSessionId: f.native.normalizedSessionId, currentRunId: f.queuedId });
expect(recovery.kind).not.toBe("recoverable");
});
if (scenario.startsWith("stale_")) it("archives a failed source only when its older supervisor matches scoped history", async () => {
const f = await fixture(1, true), priorId = randomUUID();
const priorSnapshot = { ...f.snapshot, identity: { ...f.snapshot.identity, runId: priorId } };
if (scenario === "stale_foreign") priorSnapshot.identity.agentId = randomUUID();
const priorProfile = { ...f.profile, nativeExecutionInput: { ...f.profile.nativeExecutionInput,
binding: { ...f.profile.nativeExecutionInput.binding, runId: priorId } }, sessionCheckpoint: priorSnapshot };
await f.db.insert(heartbeatRuns).values({ id: priorId, companyId: f.companyId, agentId: f.agentId,
nativeIssueId: f.issueId, nativeSessionId: f.native.normalizedSessionId, runnerInstanceId: f.native.runnerInstanceId,
runtimeMode: "native", status: "succeeded", createdAt: new Date(Date.now() - 60_000),
finishedAt: new Date(Date.now() - 50_000), runnerProfileJson: priorProfile });
if (scenario === "stale_queued" || scenario === "stale_overlap") {
const now = Date.now();
await f.db.update(heartbeatRuns).set({ createdAt: new Date(now - 45_000),
startedAt: new Date(now - (scenario === "stale_queued" ? 30_000 : 43_000)) }).where(eq(heartbeatRuns.id, f.runId));
await f.db.update(heartbeatRuns).set({ finishedAt: new Date(now - 40_000) }).where(eq(heartbeatRuns.id, priorId));
}
const supervisor = JSON.stringify({ schema: "paperclip.native-session-supervisor.v1",
configDigest: f.paths.configDigest, snapshot: priorSnapshot });
await mkdir(dirname(f.paths.supervisorPath), { recursive: true });
await writeFile(f.paths.supervisorPath, supervisor, { mode: 0o600 });
if (scenario === "stale_changed") await f.db.update(heartbeatRuns).set({ runnerProfileJson: {
...priorProfile, sessionCheckpoint: { ...priorSnapshot, sessionId: "different-provider-thread" },
} }).where(eq(heartbeatRuns.id, priorId));
if (scenario === "stale_success") await f.db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, f.runId));
if (scenario === "stale_busy") await f.db.update(nativeRunFinalizations).set({ leaseOwner: "live-owner",
leaseExpiresAt: new Date(Date.now() + 60_000) }).where(eq(nativeRunFinalizations.runId, f.runId));
if (scenario.startsWith("stale_durable")) {
const state = JSON.parse(f.source);
if (scenario === "stale_durable_runner") state.identity.runnerInstanceId = randomUUID();
if (scenario === "stale_durable_session") state.identity.normalizedSessionId = randomUUID();
const checkpoint = captureLegacyDurablePrpCheckpoint(JSON.stringify(state), { runId: f.runId,
runnerInstanceId: state.identity.runnerInstanceId, normalizedSessionId: state.identity.normalizedSessionId },
{ version: "migration-fixture", digest: `sha256:${"a".repeat(64)}` });
await f.db.transaction(tx => insertMigratedNativeRunnerCheckpoint(tx,
{ companyId: f.companyId, issueId: f.issueId, runId: f.runId }, checkpoint));
if (scenario === "stale_durable_absent") await rm(f.paths.supervisorPath);
if (scenario === "stale_durable_snapshot") await f.db.update(heartbeatRuns).set({ runnerProfileJson: {
...f.profile, sessionCheckpoint: { ...f.snapshot, identity: { ...f.snapshot.identity, agentId: randomUUID() } },
} }).where(eq(heartbeatRuns.id, f.runId));
}
const [originalRun] = await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const [originalOwner] = await f.db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId));
const storage = createStorageService(createLocalDiskStorageProvider(join(f.runtimeRoot, "durable-objects")));
const options = { storage, writersStopped: true as const, signal: AbortSignal.timeout(30_000) };
if (scenario === "stale_durable" || scenario === "stale_durable_absent") {
const binding = { companyId: f.companyId, issueId: f.issueId, runId: f.runId };
const before = await f.db.select().from(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId));
expect(await migrateNativeControllers(f.db, options)).toMatchObject({ imported: 1, archivedFailed: 0 });
expect(await migrateNativeControllers(f.db, options)).toMatchObject({ unchanged: 1, archivedFailed: 0 });
expect(await hasLocalNativeControllerObligations(f.db, storage)).toBe(false);
expect(await f.db.select().from(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId))).toEqual(before);
expect(await f.db.select().from(nativeHarnessBackups)).toHaveLength(0);
const [afterRun] = await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
expect(afterRun.runnerProfileJson?.sessionCheckpoint).toEqual(originalRun.runnerProfileJson?.sessionCheckpoint);
expect(afterRun).toEqual({ ...originalRun, runnerProfileJson: afterRun.runnerProfileJson, updatedAt: afterRun.updatedAt });
expect(afterRun.runnerProfileJson).toEqual({ ...originalRun.runnerProfileJson,
nativeControllerRecovery: expect.objectContaining({ snapshotSha256: nativeSha256(f.snapshot),
checkpoint: { runId: f.runId, runnerInstanceId: f.native.runnerInstanceId, normalizedSessionId: f.native.normalizedSessionId } }) });
expect((await f.db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId)))[0]).toEqual(originalOwner);
await rm(f.paperclipHome, { recursive: true, force: true });
expect((await loadNativeRunnerCheckpoint(f.db, binding))?.records.size).toBe(before.length);
await f.db.update(heartbeatRuns).set({ nativeIssueId: f.issueId, nativeSessionId: f.native.normalizedSessionId,
status: "running", runtimeMode: "native" }).where(eq(heartbeatRuns.id, f.queuedId));
expect((await resolveNativeControllerRecovery(f.db, { companyId: f.companyId, agentId: f.agentId,
issueId: f.issueId, normalizedSessionId: f.native.normalizedSessionId, currentRunId: f.queuedId })).kind).toBe("recoverable");
return;
}
if (!["stale_confirmed", "stale_queued"].includes(scenario)) {
await expect(migrateNativeControllers(f.db, options)).rejects.toThrow();
expect(await f.db.select().from(nativeHarnessBackups)).toHaveLength(0);
expect(await hasLocalNativeControllerObligations(f.db, storage)).toBe(true);
expect((await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId)))[0]).toEqual(originalRun);
expect((await f.db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId)))[0]).toEqual(originalOwner);
return;
}
const put = vi.spyOn(storage, "putFile").mockRejectedValue(new Error("storage unavailable"));
await expect(migrateNativeControllers(f.db, options)).rejects.toThrow("storage unavailable");
put.mockRestore();
expect(await migrateNativeControllers(f.db, options)).toMatchObject({ archivedFailed: 1, imported: 0 });
expect(await migrateNativeControllers(f.db, options)).toMatchObject({ archivedFailed: 1, imported: 0 });
expect(await hasLocalNativeControllerObligations(f.db, storage)).toBe(false);
expect(await f.db.select().from(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId))).toHaveLength(0);
expect((await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId)))[0]).toEqual(originalRun);
expect((await f.db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId)))[0]).toEqual(originalOwner);
expect((await f.db.select().from(agentTaskSessions).where(eq(agentTaskSessions.id, f.session.id)))[0].lastRunId).toBe(f.runId);
await rm(f.paperclipHome, { recursive: true, force: true });
const descriptor = legacyNativeControllerArchive({ companyId: f.companyId, issueId: f.issueId,
runId: f.runId, controller: f.source, supervisor });
const replacement = join(f.runtimeRoot, "archived-stale-supervisor");
expect(await restoreDurableNativeHarnessBackup({ db: f.db, storage, companyId: f.companyId,
scopeDigest: descriptor.scopeDigest, backupRoot: replacement })).toBe(true);
expect(await readFile(join(replacement, "current", "control-plane-state.json"), "utf8")).toBe(f.source);
expect(await readFile(join(replacement, "current", "supervisor.json"), "utf8")).toBe(supervisor);
expect((await resolveNativeControllerRecovery(f.db, { companyId: f.companyId, agentId: f.agentId,
issueId: f.issueId, normalizedSessionId: f.native.normalizedSessionId, currentRunId: f.queuedId })).kind).not.toBe("recoverable");
});
if (scenario.startsWith("controller_")) it("preserves failed legacy files only with proven older controller lineage", async () => {
const f = await fixture(), priorId = randomUUID();
const priorSnapshot = { ...f.snapshot, identity: { ...f.snapshot.identity, runId: priorId } };
if (scenario === "controller_foreign") priorSnapshot.identity.agentId = randomUUID();
const priorProfile = { ...f.profile, nativeExecutionInput: { ...f.profile.nativeExecutionInput,
binding: { ...f.profile.nativeExecutionInput.binding, runId: priorId } }, sessionCheckpoint: priorSnapshot };
await f.db.insert(heartbeatRuns).values({ id: priorId, companyId: f.companyId,
agentId: f.agentId,
nativeIssueId: f.issueId, nativeSessionId: f.native.normalizedSessionId,
runnerInstanceId: scenario === "controller_runner" ? randomUUID() : f.native.runnerInstanceId,
runtimeMode: "native", status: "succeeded", createdAt: new Date(Date.now() - 60_000),
finishedAt: new Date(Date.now() - 50_000), runnerProfileJson: scenario === "controller_thread"
? { ...priorProfile, sessionCheckpoint: { ...priorSnapshot, sessionId: "foreign-provider-thread" } } : priorProfile });
const state = JSON.parse(f.source);
state.identity = { ...state.identity, runId: priorId, turnId: `turn-${priorId}`, itemId: `item-${priorId}` };
if (scenario === "controller_invalid") state.schema = "unrecognized";
const source = JSON.stringify(state);
await writeFile(f.file, source, { mode: 0o600 });
if (scenario === "controller_success") await f.db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, f.runId));
if (scenario === "controller_busy") await f.db.update(nativeRunFinalizations).set({ leaseOwner: "live-owner",
leaseExpiresAt: new Date(Date.now() + 60_000) }).where(eq(nativeRunFinalizations.runId, f.runId));
if (scenario === "controller_durable") {
const checkpoint = captureLegacyDurablePrpCheckpoint(f.source, f.identity,
{ version: "migration-fixture", digest: `sha256:${"a".repeat(64)}` });
await f.db.transaction(tx => insertMigratedNativeRunnerCheckpoint(tx,
{ companyId: f.companyId, issueId: f.issueId, runId: f.runId }, checkpoint));
await f.db.update(heartbeatRuns).set({ runnerProfileJson: { ...f.profile, nativeControllerRecovery:
createNativeControllerRecovery(f.snapshot, { configDigest: f.paths.configDigest, processConfigDigest: null },
{ runId: f.runId, runnerInstanceId: f.native.runnerInstanceId, normalizedSessionId: f.native.normalizedSessionId }) } })
.where(eq(heartbeatRuns.id, f.runId));
}
if (scenario.startsWith("controller_observed_")) {
await f.db.update(nativeRunFinalizations).set({ phase: scenario === "controller_observed_result" ? "result_accepted" : "observed",
leaseOwner: "retired-controller", leaseExpiresAt: new Date(Date.now() + (scenario === "controller_observed_live" ? 60_000 : -60_000)),
nextAttemptAt: scenario === "controller_observed_retry" ? new Date(Date.now() + 60_000) : null,
}).where(eq(nativeRunFinalizations.runId, f.runId));
if (scenario === "controller_observed_task") await f.db.update(issues).set({ executionRunId: f.runId }).where(eq(issues.id, f.issueId));
if (scenario !== "controller_observed_missing") {
const lease = { id: randomUUID(), companyId: f.companyId, heartbeatRunId: f.runId, issueId: f.issueId,
provider: "daytona", providerLeaseId: randomUUID(), status: "released", cleanupStatus: "success", releasedAt: new Date() };
const receipt = remoteTerminationReceipt(lease, { providerLeaseId: lease.providerLeaseId, state: "stopped" })!;
if (scenario === "controller_observed_stop_before_finish") {
const now = Date.now();
await f.db.update(heartbeatRuns).set({ createdAt: new Date(now - 40_000), startedAt: new Date(now - 30_000) })
.where(eq(heartbeatRuns.id, f.runId));
receipt.confirmedAt = new Date(now - 10_000).toISOString();
lease.releasedAt = new Date(now - 10_000);
}
if (scenario === "controller_observed_foreign") receipt.runId = randomUUID();
if (scenario === "controller_observed_old_receipt") receipt.confirmedAt = new Date(0).toISOString();
await f.db.insert(environmentLeases).values({ ...lease, metadata: { remoteExecutionTermination: receipt } });
if (scenario.startsWith("controller_observed_handoff")) {
const environmentId = randomUUID(), successorRunId = randomUUID(), handoffAt = new Date(Date.now() - 1000);
await f.db.insert(environments).values({ id: environmentId, name: environmentId, driver: "sandbox" });
await f.db.insert(heartbeatRuns).values({ id: successorRunId, companyId: f.companyId, agentId: f.agentId,
nativeIssueId: f.issueId, nativeSessionId: f.native.normalizedSessionId, runtimeMode: "native", status: "succeeded", finishedAt: new Date() });
const [previous] = await f.db.update(environmentLeases).set({ environmentId, leasePolicy: "reuse_by_environment",
status: "expired", acquiredAt: new Date(handoffAt.getTime() - 1000), releasedAt: handoffAt,
metadata: { agentId: f.agentId } }).where(eq(environmentLeases.id, lease.id)).returning();
const successor = { ...previous, id: randomUUID(), heartbeatRunId: successorRunId, acquiredAt: handoffAt,
status: "released", releasedAt: new Date(), metadata: { agentId: f.agentId } };
const handoff = createReusableLeaseHandoff(previous, successor);
const termination = remoteTerminationReceipt(successor, { providerLeaseId: successor.providerLeaseId,
state: scenario === "controller_observed_handoff_stopped" ? "stopped" : "destroyed" });
await f.db.update(environmentLeases).set({ metadata: { agentId: f.agentId, reusableLeaseSuccessor: handoff } })
.where(eq(environmentLeases.id, previous.id));
await f.db.insert(environmentLeases).values({ ...successor, metadata: { agentId: f.agentId,
reusableLeasePredecessor: scenario === "controller_observed_handoff_broken" ? { ...handoff, handoffId: randomUUID() } : handoff,
remoteExecutionTermination: termination } });
if (scenario === "controller_observed_handoff_fork") await f.db.insert(environmentLeases).values({ ...previous,
id: randomUUID(), heartbeatRunId: f.queuedId, status: "active", releasedAt: null, metadata: { agentId: f.agentId } });
}
}
}
const [run] = await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const [owner] = await f.db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId));
const storage = createStorageService(createLocalDiskStorageProvider(join(f.runtimeRoot, "durable-objects")));
const options = { storage, writersStopped: true as const, signal: AbortSignal.timeout(30_000) };
if (scenario === "controller_observed_revoked") {
const put = storage.putFile.bind(storage);
vi.spyOn(storage, "putFile").mockImplementation(async args => {
const result = await put(args);
await f.db.update(environmentLeases).set({ metadata: {} }).where(eq(environmentLeases.heartbeatRunId, f.runId));
return result;
});
}
if (scenario === "controller_durable") {
expect(await migrateNativeControllers(f.db, options)).toMatchObject({ unchanged: 1, archivedFailed: 0 });
} else if (!["controller_confirmed", "controller_observed_stopped", "controller_observed_stop_before_finish", "controller_observed_handoff"].includes(scenario)) {
await expect(migrateNativeControllers(f.db, options)).rejects.toThrow();
const archives = await f.db.select().from(nativeHarnessBackups);
if (scenario === "controller_observed_revoked") {
expect(storage.putFile).toHaveBeenCalledOnce();
expect(archives).toHaveLength(1);
expect(archives[0].status).toBe("pending");
}
else expect(archives).toHaveLength(0);
} else {
const put = vi.spyOn(storage, "putFile").mockRejectedValue(new Error("storage unavailable"));
await expect(migrateNativeControllers(f.db, options)).rejects.toThrow("storage unavailable");
put.mockRestore();
expect(await migrateNativeControllers(f.db, options)).toMatchObject({ archivedFailed: 1, imported: 0 });
expect(await migrateNativeControllers(f.db, options)).toMatchObject({ archivedFailed: 1, imported: 0 });
expect(await hasLocalNativeControllerObligations(f.db, storage)).toBe(false);
expect(await f.db.select().from(nativeRunnerCheckpoints).where(eq(nativeRunnerCheckpoints.runId, f.runId))).toHaveLength(0);
await rm(f.paperclipHome, { recursive: true, force: true });
const descriptor = legacyNativeControllerArchive({ companyId: f.companyId, issueId: f.issueId, runId: f.runId, controller: source });
const replacement = join(f.runtimeRoot, "archived-older-controller");
expect(await restoreDurableNativeHarnessBackup({ db: f.db, storage, companyId: f.companyId,
scopeDigest: descriptor.scopeDigest, backupRoot: replacement })).toBe(true);
expect(await readFile(join(replacement, "current", "control-plane-state.json"), "utf8")).toBe(source);
expect((await resolveNativeControllerRecovery(f.db, { companyId: f.companyId, agentId: f.agentId,
issueId: f.issueId, normalizedSessionId: f.native.normalizedSessionId, currentRunId: f.queuedId })).kind).not.toBe("recoverable");
}
expect((await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId)))[0]).toEqual(run);
expect((await f.db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId)))[0]).toEqual(owner);
expect((await f.db.select().from(agentTaskSessions).where(eq(agentTaskSessions.id, f.session.id)))[0].lastRunId).toBe(f.runId);
expect((await f.db.select().from(issues).where(eq(issues.id, f.issueId)))[0].executionRunId)
.toBe(scenario === "controller_observed_task" ? f.runId : f.queuedId);
});
if (scenario === "success") it("does not substitute a failure archive for a successful retained session", async () => {
const f = await fixture();
await rm(join(f.paths.root, "provider-pack-authority.json"));
await f.db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, f.runId));
const storage = createStorageService(createLocalDiskStorageProvider(join(f.runtimeRoot, "durable-objects")));
await expect(migrateNativeControllers(f.db, { storage, writersStopped: true, signal: AbortSignal.timeout(30_000) }))
.rejects.toThrow("native_runner_checkpoint_artifact_unproven");
expect(await f.db.select().from(nativeHarnessBackups)).toHaveLength(0);
expect(await hasLocalNativeControllerObligations(f.db, storage)).toBe(true);
});
});