From 23d834fc0a63d7aaa7bd426122b89cf690f68623 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 12 Oct 2026 13:12:58 -0500 Subject: [PATCH] fix(claude): honor Auto effort and implicit native harness settings Co-Authored-By: Paperclip --- .../adapters/codex-local/src/ui/build-config.test.ts | 4 ++++ packages/adapters/codex-local/src/ui/build-config.ts | 10 +++++++++- .../services/heartbeat-runner-provider-config.test.ts | 4 ++-- server/src/services/heartbeat/runtime-selection.ts | 4 ++-- server/src/services/native-runtime/provider-profile.ts | 7 ++++--- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/adapters/codex-local/src/ui/build-config.test.ts b/packages/adapters/codex-local/src/ui/build-config.test.ts index 82db065ae3c..e0f950212ec 100644 --- a/packages/adapters/codex-local/src/ui/build-config.test.ts +++ b/packages/adapters/codex-local/src/ui/build-config.test.ts @@ -220,6 +220,10 @@ describe("buildPaperclipRunnerConfig", () => { }); }); + it.each(["modelReasoningEffort", "reasoningEffort", "effort"])("keeps explicit Auto Claude effort cleared over saved %s", key => { + const config = buildPaperclipRunnerConfig(makeValues({ thinkingEffort: "", adapterSchemaValues: { provider: "acpx", acpxAgent: "claude", [key]: "high" } })); + for (const effortKey of ["modelReasoningEffort", "reasoningEffort", "effort"]) expect(config).not.toHaveProperty(effortKey); + }); it.each(["claude-opus-5", "my-custom-model"])("preserves the selected ACPX Claude model %s", (model) => { expect(buildPaperclipRunnerConfig(makeValues({ model, adapterSchemaValues: { provider: "acpx" } }))) .toMatchObject({ provider: "acpx", acpxAgent: "claude", model }); diff --git a/packages/adapters/codex-local/src/ui/build-config.ts b/packages/adapters/codex-local/src/ui/build-config.ts index ee1972f9d50..455e573a2ba 100644 --- a/packages/adapters/codex-local/src/ui/build-config.ts +++ b/packages/adapters/codex-local/src/ui/build-config.ts @@ -76,7 +76,9 @@ export function buildCodexLocalConfig(v: CreateConfigValues): Record { const config = buildCodexLocalConfig(v); const schemaValues = normalizeLegacyRunnerProvider({ ...(v.adapterSchemaValues ?? {}) }); - const configuredEffort = v.thinkingEffort || schemaValues.modelReasoningEffort || schemaValues.reasoningEffort || schemaValues.effort; + const configuredEffort = typeof v.thinkingEffort === "string" + ? v.thinkingEffort.trim() + : schemaValues.modelReasoningEffort || schemaValues.reasoningEffort || schemaValues.effort; for (const unsupportedKey of [ "engine", "agentCommand", @@ -228,6 +230,12 @@ export function buildPaperclipRunnerConfig(v: CreateConfigValues): Record { .toMatchObject({ provider: "codex", model: "gpt-6-astra", modelReasoningEffort: "high" }); }); - it("carries Claude task effort while retaining provider and permissions", () => { - const base = { provider: "acpx", acpxAgent: "claude", model: "claude-opus-5-5", modelReasoningEffort: "medium", acpxPermissionMode: "deny-all" }; + it.each([true, false])("carries Claude task effort while retaining provider and permissions (explicit agent: %s)", explicitAgent => { + const base = { provider: "acpx", ...(explicitAgent ? { acpxAgent: "claude" } : {}), model: "claude-opus-5-5", modelReasoningEffort: "medium", acpxPermissionMode: "deny-all" }; const projected = projectPaperclipRunnerTaskConfig("acpx_runtime", base, { effort: "xhigh", provider: "codex", acpxAgent: "pi", acpxPermissionMode: "approve-all" }); expect(projected).toEqual({ ...base, modelReasoningEffort: "xhigh" }); expect(resolvePaperclipRunnerNativeProviderInput({ backend: "acpx_runtime", adapterConfig: projected })).toMatchObject({ provider: "acpx", acpxAgent: "claude", claudeEffort: "xhigh", acpxPermissionMode: "deny-all" }); diff --git a/server/src/services/heartbeat/runtime-selection.ts b/server/src/services/heartbeat/runtime-selection.ts index 5e5b41b940c..1ea725be1f7 100644 --- a/server/src/services/heartbeat/runtime-selection.ts +++ b/server/src/services/heartbeat/runtime-selection.ts @@ -18,7 +18,7 @@ import type { agentInstructionWorkingCopyService } from "../agent-instruction-wo import { createHash, randomUUID } from "node:crypto"; import { and, eq, inArray, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; -import { CHAT_PROVIDERS } from "@paperclipai/shared"; +import { agentHarnessType, CHAT_PROVIDERS } from "@paperclipai/shared"; import { type agents, completionContracts, @@ -696,7 +696,7 @@ export async function selectHeartbeatRuntime(db: Db, input: HeartbeatRuntimeSele }); getNativeFreshSessionHandoff = nativeReviewRequest ? undefined : getFreshSessionHandoff; const nativeProviderConfig = nativeRuntimeResolution.profile.backend === "codex_app_server" - || (nativeRuntimeResolution.profile.backend === "acpx_runtime" && parseObject(agent.adapterConfig).provider === "acpx" && parseObject(agent.adapterConfig).acpxAgent === "claude") + || (nativeRuntimeResolution.profile.backend === "acpx_runtime" && agentHarnessType(agent.adapterType, parseObject(agent.adapterConfig)) === "claude_local") || nativeRuntimeResolution.profile.backend === "opencode_server" ? projectPaperclipRunnerTaskConfig( nativeRuntimeResolution.profile.backend, diff --git a/server/src/services/native-runtime/provider-profile.ts b/server/src/services/native-runtime/provider-profile.ts index a1eaf65db08..750cb8802cf 100644 --- a/server/src/services/native-runtime/provider-profile.ts +++ b/server/src/services/native-runtime/provider-profile.ts @@ -1,3 +1,4 @@ +import { agentHarnessType } from "@paperclipai/shared"; import { resolveClaudeEffort, resolveQualifiedAcpxProfile } from "../../vendor/paperclip-runner/index.js"; import { isPaperclipRunnerProvider, @@ -179,7 +180,7 @@ export function projectPaperclipRunnerTaskConfig( // The server resolves a connection's model namespace after task overrides. // Carry that model into durable input without allowing routing to change the harness. const model = optionalString(managedModel) ?? optionalString(task.model); - const supportsEffort = backend === "codex_app_server" || (backend === "acpx_runtime" && base.acpxAgent === "claude"); + const supportsEffort = backend === "codex_app_server" || (backend === "acpx_runtime" && agentHarnessType("paperclip_runner", base) === "claude_local"); const effortKey = supportsEffort ? ["modelReasoningEffort", "reasoningEffort", "effort"].find((key) => key in task) : undefined; @@ -389,7 +390,7 @@ export function resolvePaperclipRunnerProviderProfile( const bindingId = validatePaperclipRunnerDotConfig(config)!; return { provider: "openai_dot", backend: "openai_dot_mcp", model: null, dotBindingId: bindingId }; } - if (candidate === "acpx" && config.acpxAgent === "claude") { + if (candidate === "acpx" && agentHarnessType("paperclip_runner", config) === "claude_local") { const unsupported = claudeRunnerUnsupportedSettings(config); if (unsupported.length) throw new PaperclipRunnerProviderProfileError("agent_runner_config_incompatible", `Paperclip Runner cannot honor these settings: ${unsupported.join(", ")}. Remove them or select Legacy runner in Advanced.`); @@ -403,7 +404,7 @@ export function resolvePaperclipRunnerProviderProfile( error instanceof Error ? error.message : "Invalid Cursor session mode", ); } - try { if (candidate === "acpx" && config.acpxAgent === "claude") resolveClaudeEffort("claude", config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort); } catch (error) { + try { if (candidate === "acpx" && agentHarnessType("paperclip_runner", config) === "claude_local") resolveClaudeEffort("claude", config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort); } catch (error) { throw new PaperclipRunnerProviderProfileError("paperclip_runner_claude_effort_invalid", error instanceof Error ? error.message : "Invalid Claude effort"); } try { resolvePaperclipRunnerPiThinkingLevel(candidate, config.acpxAgent, config.piThinkingLevel); } catch (error) {