mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(native): restart an interrupted remote bootstrap on a resumed reusable lease (t06) (#20103)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A native run on a remote sandbox must survive a control-plane restart; a bootstrap interrupted before its first turn is restarted on the same run > - With a reusable (warm) sandbox lease, the restart always resumes the same sandbox. The interrupted runner's state is still on disk there: never suspended, and with no provider turn > - The resumed check accepted only a suspended checkpoint or no state at all, so it failed closed with `resumed_not_suspended`, and the task needed a Retry (restart chaos t06) > - The pre-phase had already proven no turn was sent and had stopped the stale runner, but that proof never reached the resumed check > - This pull request carries that proof forward, for this run only, and starts the bootstrap again on the resumed sandbox > - The benefit is that a warm task interrupted during startup continues on its own after the restart ## Linked Issues or Issue Description **What happened?** In restart-chaos wave w-20261108T192459-s9501 (Daytona warm, claude C1), run 9bf86b59's runner reached transport-ready at 19:37:42 UTC. The control plane was then down (another task's K4, then this task's K1). Restart recovery classified the run `bootstrap_incomplete` and restarted it on the same run at 19:41:51. The reusable lease resumed the same sandbox. `inspectRemoteHarnessState` found this run's runner state with `lifecycle` other than `suspended`, and the run failed at 19:41:59 with `runner_ingress_unavailable: runnerd process failed: runner_harness_state_mismatch: resumed_not_suspended`. The task needed a manual Retry. Per-turn and non-reusable leases do not resume, so they take another branch and were unaffected. **Expected behavior** A same-run restart of a bootstrap that provably sent no turn starts the bootstrap again on the resumed sandbox, with no Retry. It must never take over a live provider session or another run's warm session. **Steps to reproduce** 1. Run a task on a reusable (warm) Daytona lease. 2. Kill the control plane after the runner connected and before the first turn, so the run is `bootstrap_incomplete`. 3. Before this change, the restart fails with `resumed_not_suspended` on the resumed lease. **Paperclip version or commit** master atd007afbf86(the wave ran onc5a172a183). ## What Changed - `native-session-executor.ts`: - **Pre-phase:** the `bootstrap_incomplete` remote-sandbox pre-phase clears any earlier proof for the run, then records a per-run proof (`recordRemoteBootstrapRestartProof`). It records one only for a confirmed stop: - `absent_confirmed`: verification reported `process_not_running`; - `stopped` or `killed`: the exit was confirmed after the signal. An uncertain probe or a runner that outlives SIGKILL records nothing (`stopStaleRemoteRunnerBeforeBootstrap` now distinguishes these outcomes). - **Inspection:** `inspectRemoteHarnessState` also returns the `runId` of a not-suspended runner state (`notSuspendedRunId`). Its other fields are unchanged. - **Resumed check:** on a `bootstrap_incomplete` restart, a not-suspended state with no backup is restarted only when all of these hold: - the on-disk state is this run's own; - the proof is present; - the interrupted session folder moved aside by rename, kept as evidence and never deleted. The runtime root, the sessions parent and the folder must each be a real, non-symlink directory. The proof is consumed only after the move succeeds. It then claims a fresh session through the existing `claimUntouchedRemoteSession`, which still requires an unconnected controller with matching identity. - Every other case keeps `runner_harness_state_mismatch: resumed_not_suspended`. ## Verification - Red/green: a new resumed-lease scenario, `interrupted_bootstrap`, in "bootstraps only an untouched provider session in a $outcome workspace lease". It fails without the change and passes with it. It checks that the old session folder is kept beside the new claim and that the claim runs exactly once. - Counter-cases that still fail closed, with nothing moved aside: `interrupted_bootstrap_other_run` (a previous warm run's state on disk), `interrupted_bootstrap_unproven` (no pre-phase proof) and `interrupted_bootstrap_symlink_parent` (a symlinked sessions parent). The existing warm "no-proof" case keeps `resumed_not_suspended`. - Stale-runner stop: "does not confirm absence when the process probe is uncertain", "reports a runner that outlives SIGKILL as not stopped", and "records a bootstrap restart proof only for a confirmed stop" (one case per outcome). - `pnpm exec vitest run src/services/native-runtime/native-session-executor.test.ts src/services/native-runtime/native-runner-restart-recovery.integration.test.ts` (server): 2 files, 1440 tests passed (14 skipped). - `PAPERCLIP_TYPECHECK_LOW_MEMORY=1 pnpm typecheck` (server): passed. ## Risks - **Taking over a live session:** this is the risk to watch. It is bounded by every gate together: the `bootstrap_incomplete` claim, the pre-phase proof from the restored controller record (no turn sent) with the stale runner stopped, this run's own on-disk state, no backup, the folder move, and the existing unconnected-controller claim. A failure at any step keeps the current fail-closed error. - **Upgrade safety:** the old runner's session folder is kept by rename, never deleted. The change applies only to state a restart left behind, so old-saved state takes the same gated path. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use (shell, file edits, test execution, a read-only research subagent). ## 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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c4ba3974a7
commit
776f4cb602
2 files changed
+129
-8
No files matched your search
@@ -401,6 +401,10 @@ import {
|
||||
createGovernedWaitEventObservation,
|
||||
createRemoteRunnerProcessLauncher,
|
||||
createRunnerdBackend,
|
||||
noteRemoteBootstrapRestartProven,
|
||||
recordRemoteBootstrapRestartProof,
|
||||
hasRemoteBootstrapRestartProof,
|
||||
takeRemoteBootstrapRestartProof,
|
||||
unidentifiedLocalRunnerProvenAbsent,
|
||||
executePaperclipNativeSession,
|
||||
detachNativeSessionsForRestart,
|
||||
@@ -627,10 +631,23 @@ describe("remote controller restart adoption", () => {
|
||||
});
|
||||
it("escalates to SIGKILL when the runner ignores SIGTERM", async () => {
|
||||
const { execute, target } = fixture();
|
||||
const live = execute.getMockImplementation()!;
|
||||
let killed = false;
|
||||
execute.mockImplementation(async request => {
|
||||
if (killed && request.args[1]!.includes("expected_pid")) return { stdout: "", stderr: "", exitCode: 45, timedOut: false };
|
||||
if (request.args.at(-1) === "KILL") killed = true;
|
||||
return live(request);
|
||||
});
|
||||
await expect(stopStaleRemoteRunnerBeforeBootstrap({ execution, runnerExecutionTarget: target as never, identity },
|
||||
{ waitMs: 20, pollMs: 1 })).resolves.toBe("killed");
|
||||
expect(signalled(execute)).toEqual(["TERM", "KILL"]);
|
||||
});
|
||||
it("reports a runner that outlives SIGKILL as not stopped", async () => {
|
||||
const { execute, target } = fixture();
|
||||
await expect(stopStaleRemoteRunnerBeforeBootstrap({ execution, runnerExecutionTarget: target as never, identity },
|
||||
{ waitMs: 20, pollMs: 1 })).resolves.toBe("stop_failed");
|
||||
expect(signalled(execute)).toEqual(["TERM", "KILL"]);
|
||||
});
|
||||
it("signals nothing when the sandbox runner belongs to another identity", async () => {
|
||||
const { execute, target } = fixture({ runnerInstanceId: "different" });
|
||||
await expect(stopStaleRemoteRunnerBeforeBootstrap({ execution, runnerExecutionTarget: target as never, identity },
|
||||
@@ -643,7 +660,21 @@ describe("remote controller restart adoption", () => {
|
||||
execute.mockImplementation(async request => request.args[1]!.includes("expected_pid")
|
||||
? { stdout: "", stderr: "", exitCode: 45, timedOut: false } : live(request));
|
||||
await expect(stopStaleRemoteRunnerBeforeBootstrap({ execution, runnerExecutionTarget: target as never, identity },
|
||||
{ waitMs: 20, pollMs: 1 })).resolves.toBe("no_prior_runner");
|
||||
{ waitMs: 20, pollMs: 1 })).resolves.toBe("absent_confirmed");
|
||||
expect(signalled(execute)).toEqual([]);
|
||||
});
|
||||
it.each([["absent_confirmed", true], ["stopped", true], ["killed", true], ["stop_failed", false], ["no_prior_runner", false]] as const)(
|
||||
"records a bootstrap restart proof only for a confirmed stop: %s", (stopped, proven) => {
|
||||
noteRemoteBootstrapRestartProven("run", "stopped");
|
||||
expect(recordRemoteBootstrapRestartProof("run", stopped)).toBe(proven);
|
||||
expect(hasRemoteBootstrapRestartProof("run")).toBe(proven);
|
||||
takeRemoteBootstrapRestartProof("run");
|
||||
});
|
||||
it("does not confirm absence when the process probe is uncertain", async () => {
|
||||
const { execute, target } = fixture();
|
||||
const verify = async () => { throw new RemoteRunnerRecoveryError({ stage: "process_verification", reason: "probe_timeout" } as never); };
|
||||
await expect(stopStaleRemoteRunnerBeforeBootstrap({ execution, runnerExecutionTarget: target as never, identity },
|
||||
{ waitMs: 20, pollMs: 1, verify: verify as never })).resolves.toBe("no_prior_runner");
|
||||
expect(signalled(execute)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -19570,6 +19601,8 @@ describe("runnerd provider runtime wiring", () => {
|
||||
["fresh", "external_checkpoint", "external_pending_turn", "external_wrong_identity", "external_unregistered", "existing_state", "symlink_parent", "wrong_identity", "wrong_runner", "wrong_lease", "missing_control_plane", "connected", "committed_event", "committed_command", "pending_turn", "remote_probe_failed", "backup_present"]
|
||||
.map((scenario) => ({ outcome, scenario, homeMode: "isolated" }))),
|
||||
...["resumed", "replacement"].map(outcome => ({ outcome, scenario: "fresh", homeMode: "preconfigured" })),
|
||||
...["interrupted_bootstrap", "interrupted_bootstrap_other_run", "interrupted_bootstrap_unproven", "interrupted_bootstrap_symlink_parent"]
|
||||
.map(scenario => ({ outcome: "resumed", scenario, homeMode: "isolated" })),
|
||||
])(
|
||||
"bootstraps only an untouched provider session in a $outcome workspace lease: $scenario ($homeMode)", async ({ outcome, scenario, homeMode }) => {
|
||||
const remoteCwd = join(isolatedStateDirectory, "remote");
|
||||
@@ -19577,7 +19610,12 @@ describe("runnerd provider runtime wiring", () => {
|
||||
await mkdir(runtimeRoot, { recursive: true });
|
||||
const sessionRoot = join(runtimeRoot, "sessions", createHash("sha256").update(execution.session.normalizedSessionId!).digest("hex"));
|
||||
if (scenario === "existing_state") await mkdir(sessionRoot, { recursive: true });
|
||||
if (scenario === "symlink_parent") await symlink(isolatedStateDirectory, join(runtimeRoot, "sessions"));
|
||||
const symlinkTarget = scenario === "interrupted_bootstrap_symlink_parent" ? join(isolatedStateDirectory, "elsewhere") : isolatedStateDirectory;
|
||||
if (scenario === "symlink_parent" || scenario === "interrupted_bootstrap_symlink_parent") {
|
||||
await mkdir(symlinkTarget, { recursive: true });
|
||||
await symlink(symlinkTarget, join(runtimeRoot, "sessions"));
|
||||
}
|
||||
if (scenario.startsWith("interrupted_bootstrap")) await mkdir(sessionRoot, { recursive: true });
|
||||
const syncIn = vi.fn(async () => undefined);
|
||||
const remoteExecute = vi.fn(async (command: { command: string; args?: string[] }) => {
|
||||
if (command.args?.[2] === "paperclip-runner-claim-unstarted-session") {
|
||||
@@ -19604,6 +19642,16 @@ describe("runnerd provider runtime wiring", () => {
|
||||
if (command.args?.[2] === "paperclip-runner-launch") {
|
||||
throw new Error("fixture_stop_after_launch_staging");
|
||||
}
|
||||
if (command.args?.[2] === "paperclip-runner-bootstrap-restart") {
|
||||
try { execFileSync("sh", command.args, { stdio: "pipe" }); return { exitCode: 0, timedOut: false, stdout: "", stderr: "" }; }
|
||||
catch { return { exitCode: 1, timedOut: false, stdout: "", stderr: "" }; }
|
||||
}
|
||||
if (command.args?.[1]?.includes("base64") && scenario.startsWith("interrupted_bootstrap")) return {
|
||||
exitCode: 0, timedOut: false, stderr: "", stdout: Buffer.from(JSON.stringify({
|
||||
runnerInstanceId: "runner-new-in-retained-workspace", normalizedSessionId: execution.session.normalizedSessionId,
|
||||
runId: scenario === "interrupted_bootstrap_other_run" ? "previous-warm-run" : execution.binding.runId,
|
||||
lifecycle: "ready", outbox: [], pendingTerminalDelivery: null })).toString("base64"),
|
||||
};
|
||||
if (command.args?.[1]?.includes("base64")) return {
|
||||
exitCode: 1, timedOut: false, stdout: "", stderr: "",
|
||||
};
|
||||
@@ -19621,6 +19669,9 @@ describe("runnerd provider runtime wiring", () => {
|
||||
const backend = await createRunnerdBackend({
|
||||
db: leaseDb(executionWithContext), execution: executionWithContext, runnerInstanceId: "runner-new-in-retained-workspace",
|
||||
...(scenario.startsWith("external_") ? { runnerRemoteBinaryPath: controllerArtifact, runnerRemoteCodexPath: controllerArtifact } : {}),
|
||||
...(scenario.startsWith("interrupted_bootstrap") ? { restartRecovery: { kind: "bootstrap_incomplete" as const,
|
||||
runId: execution.binding.runId, leaseOwner: "controller", controllerGeneration: 3, providerAttempt: 1,
|
||||
restartKind: "hard" as const, recoveryRequestId: null } } : {}),
|
||||
trace: createNativeRunTrace({ runId: execution.binding.runId, onEvent: onTraceEvent }),
|
||||
runnerIngressAuthorized: true,
|
||||
runnerExecutionTarget: {
|
||||
@@ -19690,7 +19741,16 @@ describe("runnerd provider runtime wiring", () => {
|
||||
await mkdir(join(options.stateDirectory!, "failover-backups", "current"), { recursive: true });
|
||||
await writeFile(join(options.stateDirectory!, "failover-backups", "current", "manifest.json"), "{}");
|
||||
}
|
||||
if (scenario === "fresh" || scenario === "external_checkpoint") {
|
||||
if (["interrupted_bootstrap", "interrupted_bootstrap_other_run", "interrupted_bootstrap_symlink_parent"].includes(scenario)) {
|
||||
noteRemoteBootstrapRestartProven(execution.binding.runId, "stopped");
|
||||
}
|
||||
if (scenario === "interrupted_bootstrap") {
|
||||
await expect(options.prepareExternalRunnerState()).resolves.toBeUndefined();
|
||||
const siblings = await readdir(dirname(sessionRoot));
|
||||
expect(siblings.filter(name => name.startsWith(`${basename(sessionRoot)}.bootstrap-restart-`))).toHaveLength(1);
|
||||
expect((await lstat(sessionRoot)).isDirectory()).toBe(true);
|
||||
expect(remoteExecute.mock.calls.filter(([command]) => command.args?.[2] === "paperclip-runner-claim-unstarted-session")).toHaveLength(1);
|
||||
} else if (scenario === "fresh" || scenario === "external_checkpoint") {
|
||||
await expect(options.prepareExternalRunnerState()).resolves.toBeUndefined();
|
||||
expect(onTraceEvent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
@@ -19748,6 +19808,13 @@ describe("runnerd provider runtime wiring", () => {
|
||||
);
|
||||
expect(remoteExecute.mock.calls.some(([command]) => command.args?.[1]?.includes("install -d"))).toBe(false);
|
||||
expect(syncIn).not.toHaveBeenCalled();
|
||||
if (scenario === "interrupted_bootstrap_symlink_parent") {
|
||||
expect((await readdir(symlinkTarget)).some(name => name.includes(".bootstrap-restart-"))).toBe(false);
|
||||
expect((await lstat(sessionRoot)).isDirectory()).toBe(true);
|
||||
} else {
|
||||
expect(remoteExecute.mock.calls.some(([command]) => command.args?.[2] === "paperclip-runner-bootstrap-restart")).toBe(false);
|
||||
}
|
||||
takeRemoteBootstrapRestartProof(execution.binding.runId);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5748,7 +5748,11 @@ async function migrateRunnerdStateRootForExecution(input: {
|
||||
// holds no provider work (RC-29d).
|
||||
if (runnerdStateProvesIncompleteBootstrap(scoped, { localProcesses })
|
||||
|| (remoteSandbox && runnerdStateProvesNoRemoteProviderTurn(scoped))) {
|
||||
if (remoteSandbox) await stopStaleRemoteRunnerBeforeBootstrap({ ...input, identity });
|
||||
if (remoteSandbox) {
|
||||
takeRemoteBootstrapRestartProof(input.execution.binding.runId);
|
||||
recordRemoteBootstrapRestartProof(input.execution.binding.runId,
|
||||
await stopStaleRemoteRunnerBeforeBootstrap({ ...input, identity }));
|
||||
}
|
||||
quarantineRunnerdStateRoot(scoped, "identity_indeterminate");
|
||||
return;
|
||||
}
|
||||
@@ -6287,6 +6291,28 @@ const STALE_REMOTE_RUNNER_STOP_WAIT_MS = 10_000;
|
||||
* runner can be verified there is nothing to stop, and a remaining conflict is
|
||||
* still caught by the bounded same-run bootstrap retry.
|
||||
*/
|
||||
const provenRemoteBootstrapRestarts = new Map<string, "absent_confirmed" | "stopped" | "killed">();
|
||||
|
||||
export function noteRemoteBootstrapRestartProven(runId: string, stopped: "absent_confirmed" | "stopped" | "killed"): void {
|
||||
provenRemoteBootstrapRestarts.set(runId, stopped);
|
||||
}
|
||||
|
||||
export function recordRemoteBootstrapRestartProof(runId: string,
|
||||
stopped: Awaited<ReturnType<typeof stopStaleRemoteRunnerBeforeBootstrap>>): boolean {
|
||||
provenRemoteBootstrapRestarts.delete(runId);
|
||||
if (stopped !== "absent_confirmed" && stopped !== "stopped" && stopped !== "killed") return false;
|
||||
provenRemoteBootstrapRestarts.set(runId, stopped);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function hasRemoteBootstrapRestartProof(runId: string): boolean {
|
||||
return provenRemoteBootstrapRestarts.has(runId);
|
||||
}
|
||||
|
||||
export function takeRemoteBootstrapRestartProof(runId: string): boolean {
|
||||
return provenRemoteBootstrapRestarts.delete(runId);
|
||||
}
|
||||
|
||||
export async function stopStaleRemoteRunnerBeforeBootstrap(input: {
|
||||
execution: NativeExecutionInput;
|
||||
runnerExecutionTarget?: AdapterExecutionTarget | null;
|
||||
@@ -6296,7 +6322,7 @@ export async function stopStaleRemoteRunnerBeforeBootstrap(input: {
|
||||
verify?: typeof verifyRemoteRunnerReattachment;
|
||||
waitMs?: number;
|
||||
pollMs?: number;
|
||||
} = {}): Promise<"no_prior_runner" | "stopped" | "killed" | "stop_failed"> {
|
||||
} = {}): Promise<"no_prior_runner" | "absent_confirmed" | "stopped" | "killed" | "stop_failed"> {
|
||||
const target = input.runnerExecutionTarget;
|
||||
if (target?.kind !== "remote" || target.transport !== "sandbox" || !target.runner) return "no_prior_runner";
|
||||
const providerLeaseId = target.sandboxLeaseAcquisition?.providerLeaseId;
|
||||
@@ -6309,9 +6335,10 @@ export async function stopStaleRemoteRunnerBeforeBootstrap(input: {
|
||||
claim: { runId, remote: { providerLeaseId, remoteCwd: target.remoteCwd } },
|
||||
target, identity: input.identity, runId, normalizedSessionId: nativeSessionKey(input.execution),
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// No runner with this run's exact identity and generation is alive here.
|
||||
return "no_prior_runner";
|
||||
return error instanceof RemoteRunnerRecoveryError && error.remoteRunnerRecovery.reason === "process_not_running"
|
||||
? "absent_confirmed" : "no_prior_runner";
|
||||
}
|
||||
try {
|
||||
let outcome: "stopped" | "killed" = "stopped";
|
||||
@@ -6323,6 +6350,11 @@ export async function stopStaleRemoteRunnerBeforeBootstrap(input: {
|
||||
if (await runner.isAlive()) {
|
||||
await runner.signal("SIGKILL");
|
||||
outcome = "killed";
|
||||
const killDeadline = Date.now() + (options.waitMs ?? STALE_REMOTE_RUNNER_STOP_WAIT_MS);
|
||||
while (Date.now() < killDeadline && await runner.isAlive()) {
|
||||
await new Promise(done => setTimeout(done, options.pollMs ?? 500));
|
||||
}
|
||||
if (await runner.isAlive()) return "stop_failed";
|
||||
}
|
||||
}
|
||||
await input.onLog?.("stdout", `[paperclip-runner] Stopped the interrupted runner ${runner.pid} before restarting the bootstrap.\n`);
|
||||
@@ -15073,6 +15105,7 @@ async function createRunnerdBackendWithinSessionClaim(
|
||||
runnerState: Record<string, unknown> | null;
|
||||
providerSessionIdentity: Record<string, unknown> | null;
|
||||
incompleteReason: "unavailable" | "not_suspended" | null;
|
||||
notSuspendedRunId?: string | null;
|
||||
}> => {
|
||||
if (!remoteCommandRunner || !remoteStateDirectory) {
|
||||
return {
|
||||
@@ -15140,6 +15173,7 @@ async function createRunnerdBackendWithinSessionClaim(
|
||||
runnerState: null,
|
||||
providerSessionIdentity: null,
|
||||
incompleteReason: "not_suspended",
|
||||
notSuspendedRunId: typeof runnerState.runId === "string" ? runnerState.runId : null,
|
||||
};
|
||||
}
|
||||
let providerState: Record<string, unknown>;
|
||||
@@ -15206,6 +15240,20 @@ async function createRunnerdBackendWithinSessionClaim(
|
||||
return probe.exitCode === 0 && !probe.timedOut;
|
||||
};
|
||||
|
||||
const moveInterruptedBootstrapSessionAside = async (): Promise<boolean> => {
|
||||
if (!remoteCommandRunner || !remoteRuntimeRoot || !remoteSessionRoot) return false;
|
||||
const moved = await remoteCommandRunner.execute({
|
||||
command: "sh",
|
||||
args: ["-c",
|
||||
'set -eu; for directory in "$1" "$2" "$3"; do test -d "$directory" && test ! -L "$directory" || exit 1; done; mv -- "$3" "$4"',
|
||||
"paperclip-runner-bootstrap-restart", remoteRuntimeRoot, posix.dirname(remoteSessionRoot), remoteSessionRoot,
|
||||
`${remoteSessionRoot}.bootstrap-restart-${Date.now()}-${randomUUID()}`],
|
||||
bypassSession: true,
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
return moved.exitCode === 0 && !moved.timedOut;
|
||||
};
|
||||
|
||||
const recordInPlaceHarnessReuse = async (
|
||||
providerSessionIdentity: Record<string, unknown>,
|
||||
startedAtMs = Date.now(),
|
||||
@@ -15481,7 +15529,13 @@ async function createRunnerdBackendWithinSessionClaim(
|
||||
!state.runnerState ||
|
||||
!state.providerSessionIdentity
|
||||
) {
|
||||
if (state.incompleteReason !== "unavailable" || backupAvailable ||
|
||||
const provenRestart = state.incompleteReason === "not_suspended" && !backupAvailable
|
||||
&& input.restartRecovery?.kind === "bootstrap_incomplete"
|
||||
&& state.notSuspendedRunId === input.execution.binding.runId
|
||||
&& hasRemoteBootstrapRestartProof(input.execution.binding.runId)
|
||||
&& await moveInterruptedBootstrapSessionAside()
|
||||
&& takeRemoteBootstrapRestartProof(input.execution.binding.runId);
|
||||
if ((state.incompleteReason !== "unavailable" && !provenRestart) || backupAvailable ||
|
||||
!(await claimUntouchedRemoteSession())) {
|
||||
throw new Error(`runner_harness_state_mismatch: resumed_${state.incompleteReason}`);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user