From 22e244dc9216b240f332795112e7bb35734a3080 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 21 Aug 2026 09:52:55 -0500 Subject: [PATCH] fix(runner): allow projectless local tasks --- server/src/services/heartbeat.ts | 12 ++++++++---- .../src/services/native-runtime/runtime-mode.test.ts | 10 ++++++++++ server/src/services/native-runtime/runtime-mode.ts | 5 ++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index a1509c6fdc0..381b3b0e80e 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -15845,9 +15845,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) let nativeExecution: ReturnType | null = null; let nativeRunnerInstanceId: string | null = null; if (nativeRuntimeResolution.kind === "native") { - if (!issueRef || !persistedExecutionWorkspace) { - throw new Error("native_runtime_ineligible: issue and realized workspace are required"); + if (!issueRef) { + throw new Error("native_runtime_ineligible: issue is required"); } + const nativeExecutionWorkspaceId = persistedExecutionWorkspace?.id ?? run.id; const persistedContract = run.completionContractId ? await db.select().from(completionContracts) .where(and( @@ -15876,7 +15877,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) || nativeExecution.binding.runId !== run.id || nativeExecution.binding.issueId !== issueRef.id || nativeExecution.binding.agentId !== agent.id - || nativeExecution.binding.executionWorkspaceId !== persistedExecutionWorkspace.id + || nativeExecution.binding.executionWorkspaceId !== nativeExecutionWorkspaceId || nativeExecution.completionContract.id !== completionContract.row.id || nativeExecution.completionContract.sha256 !== completionContract.row.canonicalSha256 ) throw new Error("native_execution_input_persisted_binding_mismatch"); @@ -15896,7 +15897,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) issue: issueRef, agentId: agent.id, workspace: { - id: persistedExecutionWorkspace.id, + // Projectless paperclip_runner tasks still have a resolved local cwd. Bind that + // transient workspace to the run id so the native input remains durable and replayable + // without fabricating a project-scoped execution_workspaces row. + id: nativeExecutionWorkspaceId, cwd: executionWorkspace.cwd, repoUrl: executionWorkspace.repoUrl, repoRef: executionWorkspace.repoRef, diff --git a/server/src/services/native-runtime/runtime-mode.test.ts b/server/src/services/native-runtime/runtime-mode.test.ts index eba550bf9ad..bc2fb9ddebc 100644 --- a/server/src/services/native-runtime/runtime-mode.test.ts +++ b/server/src/services/native-runtime/runtime-mode.test.ts @@ -82,4 +82,14 @@ describe("resolveNativeRuntimeMode", () => { expect(() => resolveNativeRuntimeMode({ ...eligible, target: { kind: "remote" } })) .toThrow(NativeRuntimeEligibilityError); }); + + it("allows paperclip_runner to use a transient local workspace for projectless issues", () => { + expect(resolveNativeRuntimeMode({ + ...eligible, + workspaceId: null, + agent: { ...eligible.agent, adapterType: "paperclip_runner" }, + runtimeConfig: {}, + adapterConfig: { provider: "codex" }, + })).toEqual(expect.objectContaining({ kind: "native" })); + }); }); diff --git a/server/src/services/native-runtime/runtime-mode.ts b/server/src/services/native-runtime/runtime-mode.ts index 0765639a9e6..7a8d5060af0 100644 --- a/server/src/services/native-runtime/runtime-mode.ts +++ b/server/src/services/native-runtime/runtime-mode.ts @@ -83,7 +83,10 @@ export function resolveNativeRuntimeMode(input: { if (!input.issue || input.issue.workMode !== "standard") { throw new NativeRuntimeEligibilityError("run must be bound to a standard issue"); } - if (!input.workspaceId || input.target?.kind && input.target.kind !== "local") { + if ( + (!input.workspaceId && !runnerAdapterSelected) + || input.target?.kind && input.target.kind !== "local" + ) { throw new NativeRuntimeEligibilityError("a realized local workspace is required"); } const rollout = resolveNativeMigrationStatus({