From 56f8365a19c33825897fe6b9c23e0eb2e5958303 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 15 Oct 2026 07:47:31 -0500 Subject: [PATCH] fix(runner): preserve prepared turns for v7 native input Co-Authored-By: Paperclip --- .../src/backends/codex-native-backend.ts | 5 +- .../backends/native-backend-factory.test.ts | 57 ++++++++++++++++--- .../src/backends/opencode-native-backend.ts | 5 +- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/packages/paperclip-runner/src/backends/codex-native-backend.ts b/packages/paperclip-runner/src/backends/codex-native-backend.ts index a8d89cf4300..72766ec8761 100644 --- a/packages/paperclip-runner/src/backends/codex-native-backend.ts +++ b/packages/paperclip-runner/src/backends/codex-native-backend.ts @@ -1,6 +1,6 @@ import { createCodexTaskEnvelope } from "../contracts/codex.js"; import { ACPX_CAPABILITY_PROFILES } from "../drivers/acpx/capability-profiles.js"; -import { NATIVE_EXECUTION_INPUT_SCHEMA } from "../contracts/native-execution.js"; +import { NATIVE_EXECUTION_INPUT_SCHEMA, NATIVE_EXECUTION_INPUT_SCHEMA_V7 } from "../contracts/native-execution.js"; import type { NativeExecutionInput } from "../contracts/native-execution.js"; import type { PersistedHarnessSession } from "../contracts/harness-driver.js"; import type { @@ -115,7 +115,8 @@ function createTransportBackedNativeSessionBackend( } const driverIdentity = transportDriverIdentity(input); const isCodex = input.provider.kind === "codex"; - const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA; + const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA + || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7; const supportsCollaborativePlanning = isCodex || input.provider.kind === "opencode" || diff --git a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts index d7f0c1c9d95..90b4eb77a68 100644 --- a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts +++ b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts @@ -4,13 +4,13 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { parseNativeExecutionInput, type NativeExecutionInput } from "../contracts/native-execution.js"; +import { buildNativeModelEnvelope, parseNativeExecutionInput, type NativeExecutionInput } from "../contracts/native-execution.js"; import { NATIVE_RUNTIME_ASSET_SCHEMA, PAPERCLIP_EXECUTION_PROMPT, PAPERCLIP_EXECUTION_PROMPT_REVISION, nativeRuntimePromptDigest, canonicalNativeRuntimeContextDigest, composeNativeSystemInstructions } from "../contracts/runtime-context.js"; const contextRoots: string[] = []; afterEach(() => { for (const root of contextRoots.splice(0)) rmSync(root, { recursive: true, force: true }); }); -function preparedExecution(base: NativeExecutionInput): NativeExecutionInput { +function preparedExecution(base: NativeExecutionInput, version: "v5" | "v7" = "v5"): NativeExecutionInput { const root = mkdtempSync(join(tmpdir(), "paperclip-prepared-backend-")); contextRoots.push(root); writeFileSync(join(root, "AGENTS.md"), "Keep assigned instructions."); @@ -21,7 +21,7 @@ function preparedExecution(base: NativeExecutionInput): NativeExecutionInput { skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null }, }; return parseNativeExecutionInput({ - ...base, schema: "paperclip.native-execution-input.v5", executionMode: "default", planningContext: null, + ...base, schema: `paperclip.native-execution-input.${version}`, executionMode: "default", planningContext: null, runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) }, completionContract: { ...base.completionContract, contract: { ...base.completionContract.contract, criteria: [{ id: "objective", requirement: "Complete the task." }] } }, }); @@ -31,7 +31,7 @@ import { WORKSPACE, } from "../drivers/codex/codex-app-server-driver.test-support.js"; import { createNativeSessionBackend } from "../index.js"; -import { createCodexNativeSessionBackend } from "./codex-native-backend.js"; +import { createCodexNativeSessionBackend, createRunnerdNativeSessionBackend } from "./codex-native-backend.js"; import { createOpenCodeNativeSessionBackend } from "./opencode-native-backend.js"; function execution( @@ -597,8 +597,8 @@ describe("native backend factory", () => { ).toThrow("Codex native backend requires provider kind codex"); }); - it("selects prepared transport only for v5 native input and retains prepared constraints", async () => { - const input = preparedExecution(execution()); + it.each(["v5", "v7"] as const)("selects prepared transport for %s native input and retains prepared constraints", async version => { + const input = preparedExecution(execution(), version); const transport = new FakeCodexTransport(); const backend = createCodexNativeSessionBackend(input, { transportFactory: () => transport, @@ -626,8 +626,49 @@ describe("native backend factory", () => { ); }); - it("projects v5 OpenCode constraints to the prepared runtime boundary", () => { - const input = preparedExecution(opencodeExecution()); + it("allows a completion-only turn after v7 Claude work without wrapping prepared input", async () => { + const { qualificationModel: _model, reportedModelId: _reported, permissionPolicy: _policy, ...profile } = resolveQualifiedAcpxProfile("claude", "claude-opus-5-5"); + const input = preparedExecution({ ...acpxExecution("claude"), provider: { + kind: "acpx", agent: "claude", model: "claude-opus-5-5", permissionMode: "approve-all", profile, + } }, "v7"); + if (input.schema !== "paperclip.native-execution-input.v7" || input.provider.kind !== "acpx") throw new Error("invalid fixture"); + input.provider.claudeEffort = "high"; + const transport = new FakeCodexTransport("thread-1", "provider-session-1", { + kind: "acpx", normalizedSessionId: "session", acpxRecordId: "fixture-record", + backendSessionId: "fixture-backend", agentSessionId: "fixture-agent", + profileDigest: `sha256:${"a".repeat(64)}`, workspaceDigest: `sha256:${"b".repeat(64)}`, + requestedModel: input.provider.model, effectiveModel: input.provider.model, permissionMode: "approve-all", + providerLifetimeFenceCandidates: [60001, 60002, 60003], + }); + const backend = createRunnerdNativeSessionBackend(input, { + transportFactory: () => transport, + environment: { PAPERCLIP_WORKSPACE_CWD: WORKSPACE }, + }); + const session = await backend.openSession({ + identity: { companyId: "company", agentId: "agent", runId: "run", sessionId: "session" }, + workingDirectory: WORKSPACE, + }); + const first = JSON.stringify(buildNativeModelEnvelope(input)); + const completion = "Call paperclip_finish with the result of the work already completed."; + try { + for (const [index, text] of [first, completion].entries()) { + transport.turnStartResponse = Promise.resolve({ turn: { id: `turn-${index + 1}`, status: "inProgress", items: [] } }); + const turn = await session.startTurn({ message: { role: "user", text } }); + transport.push("turn/completed", { threadId: "thread-1", turn: { id: turn.turnId, status: "completed", items: [] } }); + for await (const event of session.events()) { + if (["turn.completed", "turn.failed", "turn.cancelled"].includes(event.eventType)) break; + } + } + const turns = transport.calls.filter(call => call.method === "turn/start"); + expect(turns.map(call => call.params.input)).toEqual([first, completion].map(text => [{ type: "text", text, text_elements: [] }])); + expect(backend.preparedTaskConstraints).toEqual(expect.arrayContaining([expect.stringContaining("Obtain one accepted result")])); + } finally { + await session.close({ reason: "test-completed" }); + } + }); + + it.each(["v5", "v7"] as const)("projects %s OpenCode constraints to the prepared runtime boundary", version => { + const input = preparedExecution(opencodeExecution(), version); const backend = createOpenCodeNativeSessionBackend(input, { runtimeDirectory: "/tmp/paperclip-opencode-prepared-factory", }); diff --git a/packages/paperclip-runner/src/backends/opencode-native-backend.ts b/packages/paperclip-runner/src/backends/opencode-native-backend.ts index 329cd7fafb2..02906c3d95c 100644 --- a/packages/paperclip-runner/src/backends/opencode-native-backend.ts +++ b/packages/paperclip-runner/src/backends/opencode-native-backend.ts @@ -1,5 +1,5 @@ import { createCodexTaskEnvelope } from "../contracts/codex.js"; -import { NATIVE_EXECUTION_INPUT_SCHEMA } from "../contracts/native-execution.js"; +import { NATIVE_EXECUTION_INPUT_SCHEMA, NATIVE_EXECUTION_INPUT_SCHEMA_V7 } from "../contracts/native-execution.js"; import type { NativeExecutionInput } from "../contracts/native-execution.js"; import type { NativeSessionBackend } from "../contracts/native-session-backend.js"; import { OpenCodeServerDriver, type OpenCodeServerDriverOptions } from "../drivers/opencode/opencode-server-driver.js"; @@ -13,7 +13,8 @@ export function createOpenCodeNativeSessionBackend( if (input.provider.kind !== "opencode" || !input.provider.model) { throw new Error("OpenCode native backend requires a persisted OpenCode provider/model selection"); } - const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA; + const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA + || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7; const constraints = nativeTaskConstraints(input); return new HarnessDriverBackend(new OpenCodeServerDriver({