From 5dc63f24fbeebe0e74545a46961f72a9d3881c9e Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 6 Nov 2026 14:22:15 -0600 Subject: [PATCH] fix(native): keep the remote bootstrap retry for a closed listener A remote Runner has no process evidence on the controller host, so startup recovery cannot adopt a claimed remote startup that the drain suspended. Keep the existing same-run bootstrap retry for remote targets and detach only local Runner executions. Co-Authored-By: Claude Opus 5.5 --- doc/execution-semantics.md | 8 +++++--- .../services/native-runtime/native-session-executor.ts | 10 ++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/doc/execution-semantics.md b/doc/execution-semantics.md index 5bfe2a89fd4..9e95cdc5490 100644 --- a/doc/execution-semantics.md +++ b/doc/execution-semantics.md @@ -3292,11 +3292,13 @@ A graceful shutdown can also suspend a restart-recovery claim whose execution has not yet reached provider dispatch. That execution has lost its authority: it leaves the run running, its budget hold held and its workspace unmarked, and the next controller adopts the same run. It does not settle the run as failed. -Shutdown closes the runtime API listener before it drains runs. A native +Shutdown closes the runtime API listener before it drains runs. A local Runner execution that then needs the listener (for example, to give a new runner its callback origin) detaches the same way instead of failing. It keeps its claim, -the drain suspends it, and the next controller adopts the run. A listener that -never started is still a startup failure. +the drain suspends it, and the next controller adopts the run. A remote Runner +execution has no process evidence on the controller host, so it keeps the +same-run bootstrap retry. A listener that never started is still a startup +failure. Remote Runner processes use their authenticated connection lease as the bound on disconnected lifetime. The controller's shorter connection-recovery budget limits its own attempt; it must not terminate the remote owner while another diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 2ac1d64ab47..da1fe1eca97 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -10707,11 +10707,13 @@ async function executePaperclipNativeSessionWithinScope( error = identityRedactor.redact(error); } // A closed runtime API listener means this process is shutting down; it - // closes HTTP admission before it drains runs. Keep the claim for the - // drain to suspend, so the next controller adopts the run without spending - // a failure or a same-run retry. + // closes HTTP admission before it drains runs. For a local Runner, keep the + // claim for the drain to suspend: the next controller proves the Runner + // process identity and adopts the run without spending a failure or a + // same-run retry. A remote Runner has no process evidence on this host, so + // it keeps the bootstrap retry below. if (nativeRunsDetachingForRestart.has(input.execution.binding.runId) - || error instanceof LocalRuntimeApiListenerClosedError) { + || (error instanceof LocalRuntimeApiListenerClosedError && input.runnerExecutionTarget?.kind !== "remote")) { await leaseRenewal.stop().catch(() => undefined); liveQuestions.close(); activeNativeSessions.delete(input.execution.binding.runId);