fix(runner): issue the remote bootstrap ticket at dispatch and retry unauthenticated starts (#21073)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on a remote sandbox start `runnerd` in the sandbox, and
Paperclip connects to it through provider ingress and authenticates it
with a one-use bootstrap ticket
> - The ticket lives for 60 seconds, but Paperclip issued it before the
remote launcher staged its files in the sandbox
> - Under load, staging took 60 to 81 seconds, so `runnerd` presented an
expired ticket and every connection was closed. The run failed with
`runner_ingress_auth_failed`, and the task stopped behind a manual Retry
> - This pull request issues a fresh ticket right before dispatch,
retries these failures automatically when the controller checkpoint
proves no runner ever authenticated, and adds spans that attribute the
slow startup window
> - The benefit is that a slow but healthy start authenticates, a failed
start recovers on its own, and the remaining startup delay becomes
measurable

## Linked Issues or Issue Description

**What happened?**

On a hosted instance, 9 runs of one Codex agent on Daytona failed in
about 70 minutes with `native_session_interrupted` and the message
`runner_ingress_auth_failed: Runner ingress did not complete PRP
authentication before the deadline.` In the same window only 6 runs
succeeded company-wide. The run log showed Paperclip reconnecting for 60
seconds while `runnerd` reported that its peer closed each WebSocket. In
294 remote runs with timing spans, the time between the bootstrap ticket
issue (end of `runner.transport.connect`) and the process dispatch
(start of `runner.process.launch`) decides the outcome without
exception:

- 60 s or more: 23 of 23 runs failed authentication.
- Under 60 s: 271 of 271 runs authenticated, in 0 to 1 s.

The tasks did not recover by themselves, because the failure was
classified as ambiguous state.

**Expected behavior**

A runner that starts slowly but correctly authenticates. If ingress
authentication still fails before any runner authenticated, Paperclip
retries the start automatically. The time between the start request and
the runner connection is visible in run spans.

**Steps to reproduce**

1. Run a Codex agent on a Daytona sandbox environment that uses provider
ingress.
2. Make remote staging (`runner.runtime.stage`, inside the remote
launcher) take 60 seconds or more. Many concurrent runs of the same
agent do this on staging.
3. See that `runner.prp.authenticate` fails after 60 seconds and the run
ends with `runner_ingress_auth_failed`.
4. The regression test `runnerd-bootstrap-ticket-refresh.test.ts`
reproduces this locally with a 75-second staging delay.

**Paperclip version or commit**

`4e519bd4` (deployed), reproduced on `origin/master` at `4228bcf89c`.

**Deployment mode**

Hosted (cloud), controller on Railway, sandboxes on Daytona.

**Agent adapter(s) involved**

`paperclip_runner` with the Codex app-server provider (`gpt-6.1-sol`),
remote sandbox target, provider-ingress transport.

## What Changed

- `RunnerProcessLaunchSpec` and `spawnRunner` accept
`refreshBootstrapTicket`. A restart does not inherit it.
- The runnerd Codex transport passes a refresh that issues a new
bootstrap ticket and flushes the controller checkpoint. It does not do
this for a warm-transition launch.
- The remote sandbox launcher calls the refresh after `ensureArtifact`
and immediately before the launch RPC, and puts the new ticket in the
launch environment.
- `inspectUnauthenticatedBootstrapCheckpoint` proves from a controller
checkpoint that no runner authenticated: no connections, no leases, no
used tickets, no events, and only pending commands.
- `nativeSessionFailureSourceCode` classifies the outbound ingress
startup failures as `runner_ingress_startup_failed`.
`nativeProviderRecoveryEvidence` treats it as pre-session
(`bootstrap_retry`) only when that proof holds for the exact run
identity and there is no provider session or provider event. Restart
recovery and failed-run retry accept the new code with the same rules as
similar startup codes.
- Remote startup now records `runner.control_plane.open`,
`runner.launch.prepare`, `runner.checkpoint.register` and
`stage.asset.codex_skills.verify`. The 6 to 11 minute gap between
`native.process_start_requested` and `runner.transport.connect` had no
spans before.

## Verification

- Red/green:
`packages/paperclip-runner/src/live/runnerd-bootstrap-ticket-refresh.test.ts`
starts a real `runnerd` with a 75-second staging delay. Without the fix
it fails with `runner never connected`. With the fix it opens the
session.
- Red/green: `native-session-executor.test.ts` "launches runnerd with a
bootstrap ticket issued after remote staging". Without the fix the
launch RPC gets the stale ticket.
- Red/green: `native-controller-recovery.integration.test.ts` "retries a
fresh remote start whose launched runner never authenticated through
provider ingress" (embedded Postgres). Without the fix the failure is
`native_session_interrupted` and `ambiguous_state`.
- `durable-prp-control-plane.test.ts`: the new checkpoint proof, its
negative cases, and the restart rule for the refresh.
- `native-run-trace.test.ts`: remote startup spans are recorded without
duplicate launcher spans.
- Focused suites pass: runner package 5 files and 235 tests; server 9
files and 1,607 tests, including `native-session-executor`,
`native-runner-execution-driver`, `remote-launch-assets`,
`native-restart-recovery`, `runner-prp-outbound` and the recovery
integration tests.
- `tsc --noEmit` passes for `packages/paperclip-runner` and `server`.

## Risks

- A launch now issues two bootstrap tickets. The first one is never sent
to `runnerd` and expires in 60 seconds. Bootstrap counters already allow
more than one issued credential.
- The new retry path needs a checkpoint proof that no runner
authenticated, so a run that did provider work cannot be retried this
way. The existing attempt budget still bounds the retries.
- This change does not remove the 6 to 11 minute controller-side delay
before `runner.transport.connect`. The new spans make it attributable
for a follow-up change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, browser, code search) and extended reasoning.

## 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:
DottaandClaude Opus 5.5 authored and GitHub committed 2026-11-12 05:02:30 -06:00
1 parent f30005440b
commit 814fe5d20f
16 files changed
+356 -23

No files matched your search

@@ -2,6 +2,7 @@ import { execFile, spawn } from "node:child_process";
import { nativeProviderRecoveryEvidence, nativeSessionFailureSourceCode, restoreRemoteSuspendedRunnerCheckpoint,
verifyRemoteRunnerReattachment } from "./native-session-executor.js";
import { readRemoteRunnerRecoveryError } from "./remote-runner-recovery-diagnostics.js";
import { RunnerIngressConnectionError } from "../../realtime/runner-prp-outbound.js";
import { nativeToolContractFingerprintForTarget, rebindNativeSessionCheckpoint } from "./native-session-resume.js";
import { createHash, randomUUID } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
@@ -249,6 +250,55 @@ describe.skipIf(!recoveryPostgresSupport.supported)("native controller durable d
expect((await evidence("local_runtime_api_listener_unavailable")).recoveryMode).toBe("ambiguous_state");
});
it("retries a fresh remote start whose launched runner never authenticated through provider ingress", async () => {
const issueId = randomUUID(), runId = randomUUID(), runnerInstanceId = randomUUID();
const normalizedSessionId = randomUUID();
await db.insert(issues).values({ id: issueId, companyId, title: "Slow sandbox start", assigneeAgentId: agentId, status: "in_progress" });
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, status: "running", runtimeMode: "native",
nativeIssueId: issueId, nativeSessionId: normalizedSessionId, runnerInstanceId,
runnerProfileJson: { nativePreparationFencingVersion: 1 } });
const identity = { runnerInstanceId, environmentLeaseId: randomUUID(), runId, normalizedSessionId,
turnId: `turn-${runId}`, itemId: `item-${runId}` };
const owner = { companyId, issueId, runId, authorityRunId: runId, bootId: randomUUID(), generation: 1, leaseOwner: randomUUID() };
await db.insert(nativeRunFinalizations).values({ companyId, issueId, runId, phase: "observed", controllerBootId: owner.bootId,
controllerGeneration: 1, leaseOwner: owner.leaseOwner, leaseExpiresAt: new Date(Date.now() + 60_000) });
const core = new DurablePrpControlPlane({ stateDirectory: join(root, runId), identity,
expectedRunnerVersion: "discovery-fixture", expectedRunnerDigest: `sha256:${"a".repeat(64)}` });
core.setCheckpointPersistence(nativeRunnerCheckpointWriter(db, owner));
const failure = new RunnerIngressConnectionError("runner_ingress_auth_failed",
"Runner ingress did not complete PRP authentication before the deadline.");
const bootstrap = { binding: { companyId, issueId, runId }, identity, transientIngressFailure: false };
const evidence = () => nativeProviderRecoveryEvidence({ db, runId, executionTargetKind: "remote",
sourceFailureCode: nativeSessionFailureSourceCode(failure), bootstrap });
try {
const template = { provider: { provider: "codex", command: "fixture" }, authorizedTools: {} };
core.persistRunAttachTemplate(template);
core.queueCommand("run.prepare", template);
core.queueCommand("session.open", { reuse: "same_session" });
core.issueBootstrapTicket(60_000);
core.issueBootstrapTicket(60_000);
await core.flushCheckpoint();
expect(nativeSessionFailureSourceCode(failure)).toBe("runner_ingress_startup_failed");
expect(nativeSessionFailureSourceCode(new Error("runner_ingress_unavailable: runnerd exited unexpectedly with code 1")))
.toBe("native_runner_process_exited");
expect(await evidence()).toMatchObject({ recoveryMode: "bootstrap_retry", providerSessionEstablished: false,
providerEventsExist: false, checkpointExists: false });
expect(nativeSessionFailureSourceCode(new RunnerIngressConnectionError("runner_connection_lease_expired",
"The runner's connection lease expired before it was renewed.", { cause: failure }))).toBe("native_session_interrupted");
expect(nativeSessionFailureSourceCode(new RunnerIngressConnectionError("runner_ingress_auth_failed",
"Runner ingress connection failed."))).toBe("runner_ingress_startup_failed");
expect(await nativeProviderRecoveryEvidence({ db, runId, metadataOnly: true,
sourceFailureCode: "runner_ingress_startup_failed" })).toMatchObject({ recoveryMode: "bootstrap_retry",
providerSessionEstablished: false, providerEventsExist: false, checkpointExists: false });
await db.update(heartbeatRuns).set({ runnerInstanceId: randomUUID() }).where(eq(heartbeatRuns.id, runId));
expect((await evidence()).recoveryMode).toBe("ambiguous_state");
await db.update(heartbeatRuns).set({ runnerInstanceId }).where(eq(heartbeatRuns.id, runId));
await db.insert(heartbeatRunEvents).values({ companyId, runId, agentId, seq: 1, stream: "system", level: "info",
eventType: "session.resumed" });
expect((await evidence()).recoveryMode).toBe("ambiguous_state");
} finally { await core.stop(); }
});
it("uses the database clock when an import finds an expired owner", async () => {
const fixture = await seed();
const now = Date.now();
@@ -677,6 +677,28 @@ describe("local runner startup sub-steps", () => {
expect(names.indexOf("provider.session.load")).toBeLessThan(names.indexOf("task.provider_session.measured"));
});
it("attributes a remote runner's controller-side startup without duplicating the launcher's spans", async () => {
const events: AdapterRuntimeEvent[] = [];
const { traceContext } = createRecordingTraceContext();
const trace = createNativeRunTrace({ runId: "remote-startup-steps", startedAtMs: 1_000, traceContext,
onEvent: async (event) => { events.push(event); } });
const startup = trace.start("runner.session.startup", { parentName: "task.provider_session", startedAtMs: 1_000 });
recordNativeRunnerStartupStep(trace, { name: "runner.control_plane.open", mode: "bootstrap", startedAtMs: 1_000, endedAtMs: 1_050, outcome: "ok" }, "remote");
recordNativeRunnerStartupStep(trace, { name: "runner.runtime.stage", mode: "bootstrap", startedAtMs: 1_050, endedAtMs: 601_050, outcome: "ok" }, "remote");
recordNativeRunnerStartupStep(trace, { name: "runner.process.launch", mode: "bootstrap", startedAtMs: 601_050, endedAtMs: 601_100, outcome: "ok" }, "remote");
recordNativeRunnerStartupStep(trace, { name: "provider.session.load", mode: "bootstrap", startedAtMs: 601_100, endedAtMs: 601_200, outcome: "ok" }, "remote");
await trace.end(startup, { endedAtMs: 601_200 });
await trace.finish("ok");
const payloads = events.map((event) => event.payload as Record<string, unknown>);
expect(payloads.find((payload) => payload.span === "runner.control_plane.open"))
.toMatchObject({ parentSpan: "runner.session.startup", durationMs: 50, target: "remote" });
expect(payloads.find((payload) => payload.span === "runner.launch.prepare"))
.toMatchObject({ parentSpan: "runner.session.startup", startOffsetMs: 50, durationMs: 600_000, target: "remote" });
for (const owned of ["runner.runtime.stage", "runner.process.launch", "provider.session.load"]) {
expect(payloads.some((payload) => payload.span === owned)).toBe(false);
}
});
it("is a no-op without a trace", () => {
expect(() => recordNativeRunnerStartupStep(undefined, { name: "runner.process.launch", mode: "bootstrap", startedAtMs: 1, endedAtMs: 2, outcome: "ok" })).not.toThrow();
});
@@ -787,9 +787,9 @@ export async function recordNativeProviderPerformanceSpans(trace: NativeRunTrace
} catch { /* Diagnostics cannot replace the work's result. */ }
}
/** One local runner startup sub-step measured by the runner transport. */
/** One runner startup sub-step measured by the runner transport. */
export interface NativeRunnerStartupStep {
name: "runner.runtime.stage" | "runner.process.launch" | "provider.session.load";
name: "runner.control_plane.open" | "runner.runtime.stage" | "runner.process.launch" | "provider.session.load";
mode: "bootstrap" | "resume";
startedAtMs: number;
endedAtMs: number;
@@ -802,14 +802,22 @@ export interface NativeRunnerStartupStep {
* Telemetry. Detached: the run-log write never delays `turn.start`; the
* trace settles it at finish.
*/
export function recordNativeRunnerStartupStep(trace: NativeRunTrace | undefined, step: NativeRunnerStartupStep): void {
export function recordNativeRunnerStartupStep(
trace: NativeRunTrace | undefined,
step: NativeRunnerStartupStep,
target: "local" | "remote" = "local",
): void {
const name = target === "local" ? step.name
: step.name === "runner.runtime.stage" ? "runner.launch.prepare"
: step.name === "runner.control_plane.open" ? step.name : null;
if (!name) return;
trace?.defer(() => trace.record({
name: step.name,
name,
parentName: "runner.session.startup",
startedAtMs: step.startedAtMs,
endedAtMs: step.endedAtMs,
outcome: step.outcome,
attributes: { mode: step.mode, target: "local" },
attributes: { mode: step.mode, target },
}));
}
@@ -3,7 +3,7 @@ import { NATIVE_PREPARATION_FENCING_VERSION } from "./native-active-runs.js";
import { notifyLiveRunRecovery } from "../live-run-recovery-notifications.js";
import { and, eq, exists, gt, isNull, lte, inArray, notExists, or, sql } from "drizzle-orm";
import { checkpointDb, heartbeatRuns, issues, nativeRunFinalizations, nativeRunnerCheckpoints, type Db } from "@paperclipai/db";
import { inspectDurablePrpCheckpointHeader, inspectUnclaimedBootstrapCheckpoint, decodeDurablePrpCheckpoint, type DurablePrpCheckpoint, type DurablePrpControlPlane, type DurableRecoveryIdentity } from "../../vendor/paperclip-runner/index.js";
import { inspectDurablePrpCheckpointHeader, inspectUnauthenticatedBootstrapCheckpoint, inspectUnclaimedBootstrapCheckpoint, decodeDurablePrpCheckpoint, type DurablePrpCheckpoint, type DurablePrpControlPlane, type DurableRecoveryIdentity } from "../../vendor/paperclip-runner/index.js";
import { createLocalEncryptedCipher } from "../../secrets/local-encrypted-provider.js";
import { legacyControllerBootId } from "../legacy-controller-lease.js";
import { assertNativeSourceCheckpointConsumer, lockSourceRecoveryCoordinators } from "./native-source-recovery-ownership.js";
@@ -261,6 +261,24 @@ export async function loadNativeUnclaimedBootstrapCheckpoint(
} catch { return null; }
}
export async function nativeRunnerNeverAuthenticated(
db: Pick<Db, "select">, binding: Binding,
identity: Pick<DurableRecoveryIdentity, "runId" | "normalizedSessionId" | "runnerInstanceId">,
): Promise<boolean> {
try {
if (binding.runId !== identity.runId) return false;
const rows = await db.select().from(nativeRunnerCheckpoints).where(and(
eq(nativeRunnerCheckpoints.companyId, binding.companyId), eq(nativeRunnerCheckpoints.issueId, binding.issueId),
eq(nativeRunnerCheckpoints.runId, binding.runId))).limit(MAX_BOOTSTRAP_CHECKPOINT_COMMANDS + 3);
if (rows.length < 4 || rows.length > MAX_BOOTSTRAP_CHECKPOINT_COMMANDS + 2
|| rows.reduce((bytes, row) => bytes + Buffer.byteLength(JSON.stringify(row.material)), 0)
> 2 * NATIVE_CHECKPOINT_INSPECTION_TOTAL_MAX_BYTES) return false;
const cipher = createLocalEncryptedCipher("read");
const records = new Map(rows.map(row => [row.part, decryptPart(cipher, binding, row.part, row.material)] as const));
return inspectUnauthenticatedBootstrapCheckpoint(records, identity);
} catch { return false; }
}
const BOOTSTRAP_CHECKPOINT_PARTS = new Set(["header", "cursor"]);
const MAX_BOOTSTRAP_CHECKPOINT_COMMANDS = 8;
/** Session-open events are few; a longer journal is never a bootstrap. */
@@ -115,12 +115,14 @@ export function createNativeRunnerExecutionDriver(input: NativeRunnerExecutionDr
}
: undefined,
controlPlaneRegistration: async (authority, attachmentIdentity) => {
await input.beforeConnect?.(attachmentIdentity, authority);
if (input.controllerPersistence && input.binding.issueId) {
await nativeRunnerCheckpointRegistration({ ...input.controllerPersistence,
binding: { companyId: input.binding.companyId, issueId: input.binding.issueId, runId: input.binding.runId },
})(authority, attachmentIdentity);
}
await measureNativeRunnerSpan(input.trace, "runner.checkpoint.register", async () => {
await input.beforeConnect?.(attachmentIdentity, authority);
if (input.controllerPersistence && input.binding.issueId) {
await nativeRunnerCheckpointRegistration({ ...input.controllerPersistence,
binding: { companyId: input.binding.companyId, issueId: input.binding.issueId, runId: input.binding.runId },
})(authority, attachmentIdentity);
}
}, { parentName: "runner.session.startup" });
return measureNativeRunnerSpan(
input.trace,
"runner.transport.connect",
@@ -1442,6 +1442,25 @@ describe("remote runner process supervision", () => {
expect(blockedLog).toHaveBeenCalledWith("stderr", `[paperclip-runner] test bind fault failed on port ${port}\n`);
});
it("launches runnerd with a bootstrap ticket issued after remote staging", async () => {
const order: string[] = [];
const execute = vi.fn(async (request: { args: string[]; env?: Record<string, string> }) => {
order.push(`execute:${request.env?.PAPERCLIP_RUNNER_BOOTSTRAP_TICKET ?? "none"}`);
return { exitCode: 1, timedOut: false, stdout: "", stderr: "" };
});
const handle = createRemoteRunnerProcessLauncher({
target: { kind: "remote", transport: "sandbox", environmentId: "environment", leaseId: "lease", remoteCwd: "/workspace" },
runner: { execute } as never,
remoteBinary: "/runtime/runnerd", processIdentityPath: "/runtime/identity",
stateDirectory: "/runtime", diagnosticsDirectory: "/runtime/diagnostics", runnerInstanceId: "runner-remote",
ensureArtifact: async () => { order.push("stage"); },
})({ command: "/controller/runnerd", args: [], cwd: "/controller",
environment: { PAPERCLIP_RUNNER_BOOTSTRAP_TICKET: "issued-before-staging" },
refreshBootstrapTicket: async () => { order.push("refresh"); return "issued-at-dispatch"; } });
await expect(handle.completion).rejects.toThrow("runner_remote_process_launch_failed");
expect(order).toEqual(["stage", "refresh", "execute:issued-at-dispatch"]);
});
it("confirms no replacement process exists when staging fails before the launch RPC", async () => {
// A full sandbox disk rejects the runtime-context upload before dispatch.
const stagingError = new Error(
@@ -159,7 +159,7 @@ import {
import { lstat, readFile, readdir, readlink } from "node:fs/promises";
import { hostname, tmpdir, userInfo } from "node:os";
import { DatabaseSync } from "node:sqlite";
import { NATIVE_CHECKPOINT_HEADER_INSPECTION_MAX_BYTES, loadNativeRunnerCheckpoint, loadNativeRunnerCheckpointHeader, loadNativeUnclaimedBootstrapCheckpoint, nativeRunnerCheckpointPersistence, nativeRunnerCheckpointRegistration, restoreNativeRunnerCheckpoint } from "./native-runner-checkpoint.js";
import { NATIVE_CHECKPOINT_HEADER_INSPECTION_MAX_BYTES, loadNativeRunnerCheckpoint, loadNativeRunnerCheckpointHeader, loadNativeUnclaimedBootstrapCheckpoint, nativeRunnerCheckpointPersistence, nativeRunnerNeverAuthenticated, nativeRunnerCheckpointRegistration, restoreNativeRunnerCheckpoint } from "./native-runner-checkpoint.js";
import { REMOTE_CONTROLLER_LEASE_MS, REMOTE_RUNNER_RECONNECT_GRACE_MS, localRunnerProcessReconnectGraceMs } from "./native-runner-recovery-contract.js";
import type { NativeTaskSessionPublicationContext } from "./native-task-session-publication.js";
import { basename, dirname, join, posix, resolve } from "node:path";
@@ -8869,6 +8869,7 @@ export function nativeSessionFailureSourceCode(
| "runner_task_admission_rejected"
| "runner_ingress_access_rejected"
| "runner_ingress_acquisition_failed"
| "runner_ingress_startup_failed"
| "local_runtime_api_listener_unavailable"
| typeof NATIVE_PREDECESSOR_RETIREMENT_UNPROVEN_CODE
| "native_session_interrupted" {
@@ -9004,9 +9005,19 @@ export function nativeSessionFailureSourceCode(
if (/\blocal_runtime_api_listener_unavailable\b/.test(message)) {
return "local_runtime_api_listener_unavailable";
}
if (runnerIngressConnectionFailure(error)) return "runner_ingress_startup_failed";
return "native_session_interrupted";
}
function runnerIngressConnectionFailure(error: unknown, depth = 0): boolean {
if (!error || typeof error !== "object" || depth > 5) return false;
const value = error as Record<string, unknown>;
if (value.code === "runner_connection_lease_expired") return false;
if (typeof value.message === "string"
&& /^runner_ingress_(?:auth_failed|unavailable): (?:Runner ingress did not complete PRP authentication before the deadline|Runner ingress connection failed)\.$/.test(value.message)) return true;
return runnerIngressConnectionFailure(value.cause, depth + 1);
}
const NATIVE_PROVIDER_SIDE_EFFECT_PENDING_ACTION =
"The saved provider session has an unfinished provider turn, or a tool call whose result never reached the runner, so Paperclip did not replace the session or retry it. Reconcile the action: check whether it ran and record the outcome. The task then continues in a fresh provider session in the same workspace. The saved session and workspace are unchanged.";
@@ -9139,7 +9150,10 @@ export async function nativeProviderRecoveryEvidence(input: {
NATIVE_PROVIDER_STARTUP_REJECTED,
"provider_session_open_timeout",
"local_runtime_api_listener_unavailable",
]).has(input.sourceFailureCode);
]).has(input.sourceFailureCode) || (input.sourceFailureCode === "runner_ingress_startup_failed"
&& !checkpointExists && !providerEventsExist && !!run?.nativeIssueId && !!run.nativeSessionId && !!run.runnerInstanceId
&& await nativeRunnerNeverAuthenticated(input.db, { companyId: run.companyId, issueId: run.nativeIssueId, runId: run.id },
{ runId: run.id, normalizedSessionId: run.nativeSessionId, runnerInstanceId: run.runnerInstanceId }));
// A closed runtime API listener means this server stopped serving (a
// graceful drain) before the attempt could reach its remote runner. A
// follow-up may still carry its settled predecessor's provider session,
@@ -15290,6 +15304,11 @@ export function createRemoteRunnerProcessLauncher(input: {
}
}
if (pendingSignal) return cancelledBeforeDispatch();
const bootstrapTicket = spec.refreshBootstrapTicket ? await spec.refreshBootstrapTicket() : null;
if (pendingSignal) return cancelledBeforeDispatch();
const launchEnvironment = bootstrapTicket
? { ...spec.environment, PAPERCLIP_RUNNER_BOOTSTRAP_TICKET: bootstrapTicket }
: spec.environment;
const launchStartedAtMs = Date.now();
// The provider's onSpawn callback is optional and some sandbox command
// runners cannot report a remote pid until after the command has begun
@@ -15340,7 +15359,7 @@ export function createRemoteRunnerProcessLauncher(input: {
...remoteArgs,
],
cwd: input.target.remoteCwd,
env: processEnvironment(spec.environment),
env: processEnvironment(launchEnvironment),
timeoutMs: 20_000,
bypassSession: true,
onLog: input.onLog,
@@ -17820,7 +17839,8 @@ async function createRunnerdBackendWithinSessionClaim(
measureOperation: process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() ? measureRunnerOperation : undefined,
recordTimingReceipt: process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() ? recordRunnerTimingReceipt : undefined,
// Remote launchers record their own stage and launch spans.
...(remoteTarget ? {} : { onStartupStep: (step: Parameters<typeof recordNativeRunnerStartupStep>[1]) => recordNativeRunnerStartupStep(input.trace, step) }),
onStartupStep: (step: Parameters<typeof recordNativeRunnerStartupStep>[1]) =>
recordNativeRunnerStartupStep(input.trace, step, remoteTarget ? "remote" : "local"),
onSpawn: input.onSpawn,
...(input.execution.provider.kind === "codex" ? { disableCodexWebSearch: "webSearch" in input.execution.provider && input.execution.provider.webSearch === "disabled" } : {}),
homeMode: preconfiguredHome ? "preconfigured" : "isolated",
@@ -171,7 +171,8 @@ export async function stageRemoteLaunchAssets(input: LaunchAssetsInput): Promise
const directory = input.persistenceDirectories.find(entry => entry.name === "codex-home" && entry.location === "filesystem");
if (!directory?.targetPath) throw new Error("runner_harness_state_mismatch: bootstrap_target_unavailable");
const sourcePath = resolve(input.root, "codex-home", "skills");
await assertSelectedSkillsTree(sourcePath, input.runtimeContext?.source.skills.map(skill => skill.runtimeName) ?? []);
await measure("stage.asset.codex_skills.verify", () =>
assertSelectedSkillsTree(sourcePath, input.runtimeContext?.source.skills.map(skill => skill.runtimeName) ?? []));
await measure("stage.asset.codex_skills", async () => {
const cleared = await input.runner.execute({
command: "node",