Preserve plugin session continuity through warm execution and checkpoint recovery (#17820)

This commit is contained in:
Nicky Leach authored and GitHub committed 2026-10-26 20:25:10 -07:00
1 parent dcfc2acfa8
commit ea727dc3c2
23 files changed
+433 -82

No files matched your search

@@ -2062,29 +2062,34 @@ function savePluginRunnerCheckpoint(root: string, state: Record<string, unknown>
renameSync(temporary, path);
}
function pluginRunnerdAuthorityLifecycle(
root: string,
identity: RunnerdDurableIdentity,
): "absent" | "suspended" | "not_suspended" | "indeterminate" {
try {
const state = record(JSON.parse(readBoundedNativeFile(resolve(root, "plugin-runner-state.json"),
NATIVE_RUNNER_STATE_MAX_BYTES, "runner_state_too_large").toString("utf8")));
if (state.schema !== RUNNERD_STATE_SCHEMA || !RUNNERD_STATE_LIFECYCLES.has(String(state.lifecycle))
|| !Object.entries(identity).every(([key, value]) => state[key] === value)) return "indeterminate";
return state.lifecycle === "suspended" ? "suspended" : "not_suspended";
} catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT" ? "absent" : "indeterminate"; }
}
function runnerdAuthorityLifecycle(
root: string,
identity: RunnerdDurableIdentity,
): "absent" | "suspended" | "not_suspended" | "indeterminate" {
const runnerRoot = resolve(root, "runner");
if (!existsSync(runnerRoot)) {
try {
const state = record(JSON.parse(readBoundedNativeFile(resolve(root, "plugin-runner-state.json"),
NATIVE_RUNNER_STATE_MAX_BYTES, "runner_state_too_large").toString("utf8")));
if (state.schema !== RUNNERD_STATE_SCHEMA || !RUNNERD_STATE_LIFECYCLES.has(String(state.lifecycle))
|| !Object.entries(identity).every(([key, value]) => state[key] === value)) return "indeterminate";
return state.lifecycle === "suspended" ? "suspended" : "not_suspended";
} catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT" ? "absent" : "indeterminate"; }
}
if (!existsSync(runnerRoot)) return pluginRunnerdAuthorityLifecycle(root, identity);
if (!isSafeNativeStateDirectory(runnerRoot)) return "indeterminate";
const statePath = resolve(runnerRoot, "runner-state.json");
if (!existsSync(statePath)) {
try {
// Older remote transports created this controller-side placeholder even
// though runner state was owned by the sandbox. It carries no authority,
// so a verified failover backup may be consulted. Any non-empty direct
// so a verified plugin receipt or failover backup may be consulted. Any non-empty direct
// directory remains indeterminate and therefore blocks fallback.
return readdirSync(runnerRoot).length === 0 ? "absent" : "indeterminate";
return readdirSync(runnerRoot).length === 0 ? pluginRunnerdAuthorityLifecycle(root, identity) : "indeterminate";
} catch {
return "indeterminate";
}
@@ -4894,6 +4899,72 @@ async function verifyWarmTransitionRestart(input: {
return verified;
}
async function recoverPluginRunnerCheckpoint(input: {
db: Db;
execution: NativeExecutionInput;
runnerExecutionTarget?: AdapterExecutionTarget | null;
}, scoped: string): Promise<void> {
const target = input.runnerExecutionTarget;
if (target?.kind !== "remote" || target.transport !== "sandbox" || !target.runnerTask || !target.leaseId ||
target.sandboxLeaseAcquisition?.outcome === "resumed") return;
let root = scoped;
const quarantined = !existsSync(scoped);
const deny = (): never => { throw new Error("runner_state_identity_mismatch: plugin_checkpoint_recovery_unproven"); };
if (quarantined) {
const quarantine = resolve(runnerdStateBase(), "quarantine");
if (!existsSync(quarantine)) return;
if (!isSafeNativeStateDirectory(quarantine)) return deny();
const candidates = readdirSync(quarantine).filter(name => name.startsWith(`${basename(scoped)}.identity_indeterminate.`));
if (candidates.length === 0) return;
if (candidates.length !== 1) return deny();
root = resolve(quarantine, candidates[0]!);
}
if (!existsSync(root)) return;
if (!isSafeNativeStateDirectory(root)) return deny();
if (hasRetainedWarmTransitionEvidence(root)) {
if (quarantined) return deny();
return;
}
const identity = readRunnerdDurableIdentity(root);
if (!durableIdentityMatchesSession(identity, input.execution) || identity.runId === input.execution.binding.runId) {
if (quarantined) return deny();
return;
}
const direct = runnerdAuthorityLifecycle(root, identity);
const verifiedPluginReceipt = direct === "suspended" &&
!existsSync(resolve(root, "runner", "runner-state.json")) && pluginRunnerdAuthorityLifecycle(root, identity) === "suspended";
// Never hide contradictory direct authority behind a remote checkpoint.
if (direct !== "absent" && !(quarantined && direct === "suspended") && !verifiedPluginReceipt) {
if (quarantined) return deny();
return;
}
await assertCleanupActivationCommitted(input.db, root, input.execution);
const prior = await verifyPriorRunnerdStateForSessionScope({ ...input, root, identity, allowVerifiedBackup: true });
if (prior !== "terminal_state_indeterminate" && !((quarantined || verifiedPluginReceipt) && prior === "verified")) {
if (quarantined) return deny();
return;
}
const control = record(JSON.parse(readBoundedNativeFile(resolve(root, "control-plane", "control-plane-state.json"),
NATIVE_CONTROL_PLANE_STATE_MAX_BYTES, "runner_durable_identity_too_large").toString("utf8")));
const terminal = record(Array.isArray(control.committedEvents) ? control.committedEvents.at(-1) : null);
if (!Array.isArray(control.commands) || control.commands.some(command => record(command).status !== "completed") ||
terminal.eventType !== "run.terminal" || record(terminal.envelope).runId !== identity.runId ||
record(terminal.envelope).normalizedSessionId !== identity.normalizedSessionId) return deny();
const resumeFrom = await resolvePluginTaskResume(input.db, { companyId: input.execution.binding.companyId,
leaseId: target.leaseId, priorIdentity: identity, inspectCheckpoint: true });
const checkpoint = environmentTaskResultSchema.parse(await target.runnerTask({ kind: "read_state", identity, resumeFrom }));
if (checkpoint.kind !== "state" || checkpoint.state.lifecycle !== "suspended" ||
!Array.isArray(checkpoint.state.outbox) || checkpoint.state.outbox.length !== 0 ||
checkpoint.state.pendingTerminalDelivery != null || checkpoint.state.pendingProviderCleanup != null ||
checkpoint.state.warmTransition != null) return deny();
savePluginRunnerCheckpoint(root, checkpoint.state, identity);
if (await verifyPriorRunnerdStateForSessionScope({ ...input, root, identity, allowVerifiedBackup: true }) !== "verified") return deny();
if (quarantined) {
if (existsSync(scoped)) return deny();
renameSync(root, scoped);
}
}
async function migrateRunnerdStateRootForExecution(input: {
db: Db;
execution: NativeExecutionInput;
@@ -4913,6 +4984,9 @@ async function migrateRunnerdStateRootForExecution(input: {
issueId: input.execution.binding.issueId,
stateKey: basename(scoped),
});
if (!input.restartRecovery && !input.retainedWarmOwnerRunId) {
await recoverPluginRunnerCheckpoint(input, scoped);
}
if (
input.allowLocalRecovery &&
!input.retainedWarmOwnerRunId &&
@@ -8000,10 +8074,7 @@ async function executePaperclipNativeSessionWithinScope(
// Inspection can replace a stopped retained task before deciding between adoption and checkpoint recovery.
await taskTarget.runnerTask({kind:"describe"});
if (taskTarget.sandboxLeaseAcquisition?.outcome !== "resumed") {
const resumeFrom=await resolvePluginTaskResume(input.db,{companyId:input.execution.binding.companyId,leaseId:taskTarget.leaseId,priorIdentity:identity});
const checkpoint=environmentTaskResultSchema.parse(await taskTarget.runnerTask({kind:"read_state",identity,resumeFrom}));
if(checkpoint.kind!=="state") throw new Error("Plugin Runner checkpoint unavailable");
savePluginRunnerCheckpoint(root,checkpoint.state,identity);
await recoverPluginRunnerCheckpoint(input, root);
}
}
if (durableIdentityMatchesSession(identity, input.execution) && identity.runId !== input.execution.binding.runId &&
@@ -11802,8 +11873,8 @@ async function createRunnerdBackendWithinSessionClaim(
const remoteTarget = target.kind === "remote" ? target : null;
const pluginTask = remoteTarget?.transport === "sandbox" ? remoteTarget.runnerTask : undefined;
if (pluginTask && input.runnerIngressAuthorized !== true) throw new Error("runner_ingress_unavailable");
if (pluginTask && (input.execution.schema === "paperclip.native-execution-input.v6" || input.execution.schema === "paperclip.native-execution-input.v8" || (input.execution.session.lifecyclePolicy.mode === "warm" && input.execution.provider.kind !== "codex"))) {
throw new Error("Plugin warm Runner tasks require a Codex workspace session");
if (pluginTask && (input.execution.schema === "paperclip.native-execution-input.v6" || input.execution.schema === "paperclip.native-execution-input.v8")) {
throw new Error("Plugin Runner tasks require a workspace session");
}
const remoteCommandRunner = remoteTarget
? remoteTarget.transport === "ssh"
@@ -11992,7 +12063,7 @@ async function createRunnerdBackendWithinSessionClaim(
.where(and(eq(environmentLeases.id, remoteTarget.leaseId!), eq(environmentLeases.companyId, input.execution.binding.companyId)));
}, onDispatched: input.onTaskDispatched,
};
if (input.execution.session.lifecyclePolicy.mode === "warm" && (!credentials.accountIdentity || description.warmTaskVersion !== 1)) throw new Error("Plugin Codex warm task capability or account unavailable");
if (input.execution.session.lifecyclePolicy.mode === "warm" && (!credentials.accountIdentity || description.warmTaskVersion !== 1)) throw new Error("Plugin warm task capability or account unavailable");
const owner = retainedOwner ?? createPluginRunnerTaskOwner(ownerInput);
if (retainedOwner) retainedOwner.rebind(ownerInput);
if (description.workspaceFileReadVersion === 1) {