diff --git a/doc/plans/2026-10-06-default-agent-runner.md b/doc/plans/2026-10-06-default-agent-runner.md index 5b57e886582..085cbe6ae83 100644 --- a/doc/plans/2026-10-06-default-agent-runner.md +++ b/doc/plans/2026-10-06-default-agent-runner.md @@ -2,6 +2,43 @@ Updated 2026-10-12. +## Codex configuration review closure checkpoint + +Runtime prerequisite [#16028](https://github.com/paperclipai/paperclip/pull/16028) +advances to `d16eccf109b524778ceb896f79ccb3881bc61a9b`, with 57 files. Its small +shared builder correction preserves saved or selected Codex reasoning effort +without adding Codex settings to other providers. All 52 existing builder tests +pass in the actual prerequisite checkout. The earlier `cae179` fresh 5/5 review +is retained historical evidence; this new source requires fresh checks/review. + +Codex behavior source is `b63b779fe141ffe8db6d200580335de84ee78651`, on +`codex/codex-runner-default`; [#15683](https://github.com/paperclipai/paperclip/pull/15683) +retains 99 changed files. Review reproduced eight selection failures and six +import/builder failures before correction. All six actual ACP aliases now follow +Legacy precedence before native compatibility checks. Unsupported settings name +the effective source field and offer explicit Legacy; Legacy itself is unchanged. +Native imports retain hidden runtime settings and account bindings through +ordinary edits. Explicit clears/removals and intentional harness changes remain +effective. Existing controls pass: 100 selection, 91 UI, and 52 builder; counts +can overlap. Canonical server/UI/adapter no-emit, token gates, and independent +source-matching reviews pass. No new suite, schema, or API was introduced. + +The [local test-drive](http://127.0.0.1:3125/COD/agents/all) restarted on this exact +behavior source and serves `2026.1005.0+403.git.b63b779fe1`. Startup recovery is +ready, active runs are zero, and all four complete legacy/native execution +configurations and two historical native run records match before/after. +The deprecated flag remains false. Prefix-specific receipts are +`/private/tmp/pr2-b63b-preservation-{before,after}.json`. The staged Mac daemon +passed preflight with `sourceBuild=false` and `needsBuild=false`; startup used +JavaScript preparation only. Provider actions, Docker builds, and Cargo builds +are zero. This checkpoint's subsequent commit changes only this dated record. + +Current-head full CI/review and managed onboarding/task/follow-up remain required. +No merge or production deployment has occurred. Keep expensive final previews +deferred until the prerequisite merges and Codex is reconciled with actual master. +The authorized Claude owner is coordinating its dependent PRs and existing +bounded evidence; root retains normal sequential merge ownership. + ## Metadata review closure checkpoint Runtime prerequisite source is `cae179c21412048d08aeb94e0fe8b0f7844119de`, 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 e0f950212ec..ed6156cddd7 100644 --- a/packages/adapters/codex-local/src/ui/build-config.test.ts +++ b/packages/adapters/codex-local/src/ui/build-config.test.ts @@ -118,6 +118,11 @@ describe("buildCodexLocalConfig", () => { }); describe("buildPaperclipRunnerConfig", () => { + it.each(["", "max"])("preserves or overrides saved native Codex reasoning effort (%s)", thinkingEffort => { + const values = makeValues({ thinkingEffort, adapterSchemaValues: { provider: "codex", modelReasoningEffort: "high" } }); + expect(buildPaperclipRunnerConfig(values).modelReasoningEffort).toBe(thinkingEffort || "high"); + expect(buildPaperclipRunnerConfig({ ...values, adapterSchemaValues: { provider: "acpx", acpxAgent: "claude", modelReasoningEffort: "high" } })).not.toHaveProperty("modelReasoningEffort"); + }); it.each([undefined, false, true])("preserves Dot attachment consent in create/import forms: %s", value => { const config = buildPaperclipRunnerConfig(makeValues({ adapterType: "paperclip_runner", adapterSchemaValues: { provider: "openai_dot", dotAttachmentAccess: value } })); expect(config.dotAttachmentAccess).toBe(value === true); @@ -142,6 +147,7 @@ describe("buildPaperclipRunnerConfig", () => { codexPermissionMode: "never", lifecycleMode: "per_turn", model: "gpt-5.4", + modelReasoningEffort: "high", timeoutSec: 0, graceSec: 15, }); @@ -149,7 +155,6 @@ describe("buildPaperclipRunnerConfig", () => { "engine", "agentCommand", "stateDir", - "modelReasoningEffort", "search", "fastMode", "dangerouslyBypassApprovalsAndSandbox", diff --git a/packages/adapters/codex-local/src/ui/build-config.ts b/packages/adapters/codex-local/src/ui/build-config.ts index 455e573a2ba..6a79f7a4a85 100644 --- a/packages/adapters/codex-local/src/ui/build-config.ts +++ b/packages/adapters/codex-local/src/ui/build-config.ts @@ -241,7 +241,8 @@ export function buildPaperclipRunnerConfig(v: CreateConfigValues): Record ({ readSandboxPlatformQualification: sta vi.mock("./environment-config.js", () => ({ resolveEnvironmentDriverConfigForRuntime: state.resolveEnvironment })); vi.mock("@paperclipai/adapter-utils/ssh", () => ({ runSshCommand: state.ssh })); import { AGENT_RUNNER_SSH_PLATFORM_COMMAND, agentRunnerAvailability, resolveAgentRunnerTargetForCompany, resolveNewAgentRunner, resolveNewAgentRunnerForCompany } from "./agent-runner-selection.js"; +import { buildCodexAcpConfig } from "@paperclipai/adapter-codex-local/server"; import { localAgentRunnerTarget } from "../lib/agent-runner-platform.js"; const target = { driver: "local", platform: "linux", architecture: "x64" }; const qualifiedTargets = [target, { driver: "local", platform: "linux", architecture: "arm64" }, { driver: "local", platform: "darwin", architecture: "arm64" }, { driver: "local", platform: "darwin", architecture: "x64" }]; @@ -117,6 +118,71 @@ describe("server-owned Codex runner selection", () => { try { resolveNewAgentRunner({ adapterType: "codex_local", target, adapterConfig: { [field]: true } }); } catch (error) { expect(error).toMatchObject({ status: 422, details: { fields: [field] } }); } }); + it.each([ + ["agentCommand", "acpAgentCommand", "custom-codex-acp"], + ["stateDir", "acpStateDir", "/custom/acp-state"], + ["mode", "acpMode", "oneshot"], + ["permissionMode", "acpPermissionMode", "deny-all"], + ["nonInteractivePermissions", "acpNonInteractivePermissions", "fail"], + ["warmHandleIdleMs", "acpWarmHandleIdleMs", 30_000], + ])("rejects effective Legacy ACP alias %s/%s before automatic native translation", (canonical, alias, value) => { + const adapterConfig = { engine: "acp", [alias]: value }; + expect(buildCodexAcpConfig(adapterConfig)).toHaveProperty(canonical, value); + for (const runner of ["auto", "paperclip"] as const) { + expect(() => resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target, runner })) + .toThrow(expect.objectContaining({ status: 422, details: { code: "agent_runner_config_incompatible", fields: [alias] } })); + } + expect(resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target, runner: "legacy" })) + .toEqual({ adapterType: "codex_local", adapterConfig }); + }); + + it.each([ + ["agentCommand", "acpAgentCommand", "canonical-command", "alias-command"], + ["stateDir", "acpStateDir", "/canonical-state", "/alias-state"], + ["permissionMode", "acpPermissionMode", "approve-all", "deny-all"], + ])("checks the effective canonical ACP %s ahead of its alias", (canonical, alias, value, aliasedValue) => { + const adapterConfig = { engine: "acp", [canonical]: value, [alias]: aliasedValue }; + expect(buildCodexAcpConfig(adapterConfig)).toHaveProperty(canonical, value); + expect(() => resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target })) + .toThrow(expect.objectContaining({ status: 422, details: { code: "agent_runner_config_incompatible", fields: [canonical] } })); + }); + + it("uses Legacy ACP canonical precedence and removes compatible or shadowed aliases idempotently", () => { + const adapterConfig = { engine: "acp", mode: "persistent", acpMode: "oneshot", + nonInteractivePermissions: "deny", acpNonInteractivePermissions: "fail", + warmHandleIdleMs: 0, acpWarmHandleIdleMs: 30_000 }; + expect(buildCodexAcpConfig(adapterConfig)).toMatchObject({ mode: "persistent", nonInteractivePermissions: "deny", warmHandleIdleMs: 0 }); + const resolved = resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target }); + expect(resolved).toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { codexPermissionMode: "never" } }); + for (const field of Object.keys(adapterConfig)) expect(resolved.adapterConfig).not.toHaveProperty(field); + expect(resolveNewAgentRunner(resolved)).toEqual(resolved); + expect(resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target, runner: "legacy" })) + .toEqual({ adapterType: "codex_local", adapterConfig }); + }); + + it("accepts equivalent ACP default aliases without persisting legacy-only controls", () => { + const adapterConfig = { engine: "acp", acpMode: " persistent ", acpNonInteractivePermissions: " deny ", acpWarmHandleIdleMs: 0 }; + expect(buildCodexAcpConfig(adapterConfig)).toMatchObject({ mode: "persistent", nonInteractivePermissions: "deny", warmHandleIdleMs: 0 }); + const resolved = resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target }); + expect(resolved.adapterType).toBe("paperclip_runner"); + for (const field of Object.keys(adapterConfig)) expect(resolved.adapterConfig).not.toHaveProperty(field); + expect(resolveNewAgentRunner(resolved)).toEqual(resolved); + }); + + it("uses nullish ACP idle precedence instead of treating a canonical zero as absent", () => { + const adapterConfig = { engine: "acp", warmHandleIdleMs: null, acpWarmHandleIdleMs: 30_000 }; + expect(buildCodexAcpConfig(adapterConfig)).toHaveProperty("warmHandleIdleMs", 30_000); + expect(() => resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target })) + .toThrow(expect.objectContaining({ status: 422, details: { code: "agent_runner_config_incompatible", fields: ["acpWarmHandleIdleMs"] } })); + }); + + it("uses a nonempty ACP alias when its canonical string is blank", () => { + const adapterConfig = { engine: "acp", permissionMode: " ", acpPermissionMode: " deny-all " }; + expect(buildCodexAcpConfig(adapterConfig)).toHaveProperty("permissionMode", "deny-all"); + expect(() => resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target })) + .toThrow(expect.objectContaining({ status: 422, details: { code: "agent_runner_config_incompatible", fields: ["acpPermissionMode"] } })); + }); + it("preserves explicit historical native profiles, including managed configurations", () => { for (const config of [{ provider: "acpx", acpxAgent: "claude", model: "claude-sonnet-5" }, { provider: "claude_managed", managedProfileId: "profile", managedAgentsRetentionAcknowledged: true }, { provider: "aws_agentcore", agentCoreProfileId: "profile" }]) expect(resolveNewAgentRunner({ adapterType: "paperclip_runner", adapterConfig: config })).toEqual({ adapterType: "paperclip_runner", adapterConfig: config }); }); diff --git a/server/src/services/agent-runner-selection.ts b/server/src/services/agent-runner-selection.ts index b1958eb086b..6842cf4c164 100644 --- a/server/src/services/agent-runner-selection.ts +++ b/server/src/services/agent-runner-selection.ts @@ -120,6 +120,32 @@ export function resolveNewAgentRunner(input: AgentRunnerSelection): { adapterTyp } return { adapterType: "paperclip_runner", adapterConfig: config }; } + const aliasSources = new Map(); + // Legacy ACP uses the first nonempty canonical/alias string, and nullish + // precedence for its idle timeout. Validate that effective choice, not an + // unchecked prefixed field, while retaining its name in actionable errors. + for (const [canonical, alias] of [ + ["agentCommand", "acpAgentCommand"], ["stateDir", "acpStateDir"], + ["mode", "acpMode"], ["permissionMode", "acpPermissionMode"], + ["nonInteractivePermissions", "acpNonInteractivePermissions"], + ]) { + if (config[alias] === undefined) continue; + const canonicalValue = typeof config[canonical] === "string" ? config[canonical].trim() : ""; + const aliasValue = typeof config[alias] === "string" ? config[alias].trim() : ""; + if (canonicalValue || aliasValue) { + config[canonical] = canonicalValue || aliasValue; + if (!canonicalValue) aliasSources.set(canonical, alias); + } else if (config[alias] !== null && typeof config[alias] !== "string") incompatible([alias]); + else if (typeof config[canonical] === "string") delete config[canonical]; + delete config[alias]; + } + if (config.acpWarmHandleIdleMs !== undefined) { + if (config.warmHandleIdleMs == null && config.acpWarmHandleIdleMs != null) { + config.warmHandleIdleMs = config.acpWarmHandleIdleMs; + aliasSources.set("warmHandleIdleMs", "acpWarmHandleIdleMs"); + } + delete config.acpWarmHandleIdleMs; + } const unsupported = ["agentCommand", "stateDir", "warmHandleIdleMs", "extraArgs", "args", "search", "fastMode", "variant", "filesystemScope", "filesystemExtraPaths", "filesystemSandboxCommand", "networkScope", "networkAllowlist", "permissionMode", "maxTurns", "disableWebSearch", "outputInactivityTimeoutMs", "terminalResultCleanupGraceMs"] .filter(key => config[key] !== undefined && config[key] !== false && config[key] !== "" && config[key] !== 0 && !(Array.isArray(config[key]) && config[key].length === 0)); if (config.command && config.command !== "codex") unsupported.push("command"); @@ -128,7 +154,7 @@ export function resolveNewAgentRunner(input: AgentRunnerSelection): { adapterTyp if (config.nonInteractivePermissions && config.nonInteractivePermissions !== "deny") unsupported.push("nonInteractivePermissions"); if (config.dangerouslyBypassApprovalsAndSandbox === false) unsupported.push("dangerouslyBypassApprovalsAndSandbox"); if (config.dangerouslyBypassSandbox === false) unsupported.push("dangerouslyBypassSandbox"); - if (unsupported.length) incompatible(unsupported); + if (unsupported.length) incompatible(unsupported.map(field => aliasSources.get(field) ?? field)); const effort = config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort; for (const key of ["engine", "command", "extraArgs", "args", "agentCommand", "mode", "nonInteractivePermissions", "stateDir", "warmHandleIdleMs", "dangerouslyBypassApprovalsAndSandbox", "dangerouslyBypassSandbox", "search", "fastMode", "reasoningEffort", "effort"]) delete config[key]; if (config.model !== undefined && typeof config.model !== "string") incompatible(["model"]); diff --git a/ui/src/adapters/codex-local/config-fields.test.tsx b/ui/src/adapters/codex-local/config-fields.test.tsx index addc2b47b00..c63d3f725a4 100644 --- a/ui/src/adapters/codex-local/config-fields.test.tsx +++ b/ui/src/adapters/codex-local/config-fields.test.tsx @@ -12,6 +12,7 @@ import { CodexLocalConfigFields } from "./config-fields"; import { buildCodexRunnerConfig } from "./config-fields"; import { defaultCreateValues } from "@/components/agent-config-defaults"; import { resolveCodexLocalCliPolicy } from "@paperclipai/adapter-codex-local"; +import { buildPaperclipRunnerConfig } from "@paperclipai/adapter-codex-local/ui"; beforeAll(() => { Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); }); @@ -54,6 +55,11 @@ async function renderRunner(config: Record, expand?: string, in } describe("Paperclip Runner Codex configuration", () => { + it.each(["", "max"])("preserves or overrides saved native Codex reasoning effort (%s)", thinkingEffort => { + const values = { ...defaultCreateValues, thinkingEffort, adapterSchemaValues: { provider: "codex", modelReasoningEffort: "high" } }; + expect(buildPaperclipRunnerConfig(values).modelReasoningEffort).toBe(thinkingEffort || "high"); + expect(buildPaperclipRunnerConfig({ ...values, adapterSchemaValues: { provider: "acpx", acpxAgent: "claude", modelReasoningEffort: "high" } })).not.toHaveProperty("modelReasoningEffort"); + }); it.each(["", "--sandbox,read-only"])("shows actual automatic Legacy policy and preserves the explicit toggle (%s)", async (extraArgs) => { const container = document.createElement("div"); document.body.appendChild(container); diff --git a/ui/src/pages/CompanyImport.test.tsx b/ui/src/pages/CompanyImport.test.tsx index 67629da1986..69ebded9a50 100644 --- a/ui/src/pages/CompanyImport.test.tsx +++ b/ui/src/pages/CompanyImport.test.tsx @@ -1360,6 +1360,76 @@ describe("CompanyImport", () => { expect(lastImportMeta().adapterOverrides?.coder?.adapterConfig).not.toHaveProperty("codexLegacySandboxBypass"); }); + it.each(["model-clear", "effort-clear", "env-remove", "legacy-model", "changed-harness"] as const)("preserves native Codex source configuration during an ordinary import edit (%s)", async edit => { + mockOptionalCompany.mockReturnValue({ selectedCompanyId: "company-1" }); + mockAdaptersApi.list.mockResolvedValue([ + { type: "claude_local", disabled: false }, + { type: "codex_local", disabled: false, supportedRunners: ["paperclip", "legacy"], defaultRunner: "paperclip" }, + { type: "paperclip_runner", disabled: false }, + ]); + const preview = buildPreviewResult(); + const sourceConfig = { + provider: "codex", model: "gpt-5.6-sol", modelReasoningEffort: "high", reasoningEffort: "low", + env: { CODEX_HOME: { type: "secret_ref", secretId: "saved-account", version: "latest" }, + OPENAI_API_KEY: { type: "user_secret_ref", key: "OPENAI_API_KEY", version: "latest", required: true } }, + timeoutSec: 120, graceSec: 30, cwd: "/saved/workspace", instructionsFilePath: "/saved/AGENTS.md", + workspaceStrategy: { type: "git_worktree", baseRef: "main", branchTemplate: "qa/{runId}", worktreeParentDir: "/saved/worktrees" }, + workspaceRuntime: { services: [{ name: "app", command: "pnpm dev" }] }, + lifecycleMode: "warm", idleTimeoutMs: 45000, + }; + preview.manifest.agents[0]!.adapterType = "paperclip_runner"; + preview.manifest.agents[0]!.runner = "paperclip"; + preview.manifest.agents[0]!.adapterConfig = sourceConfig; + mockCompaniesApi.importPreview.mockResolvedValue(preview); + await renderPage(); + await enterGithubUrl(); + await clickButton(text => text === "Preview import"); + if (edit === "changed-harness") { + const harness = findAdapterSelects()[0]!; + await act(async () => { + Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value")!.set!.call(harness, "claude_local"); + harness.dispatchEvent(new Event("change", { bubbles: true })); + }); + } + await clickButton(text => text.includes("configure adapter")); + await clickButton(text => text.includes("Advanced")); + const expected: Record = structuredClone(sourceConfig); + if (edit === "legacy-model") { + await act(async () => container.querySelector('[aria-label="Runner"]')!.click()); + await act(async () => document.querySelector('[role="option"][data-value="legacy"]')!.click()); + } + if (edit === "model-clear" || edit === "legacy-model" || edit === "changed-harness") { + await act(async () => container.querySelector('[aria-label="Model"]')!.click()); + await act(async () => [...document.querySelectorAll('[role="option"]')].find(option => option.textContent?.trim().startsWith("Default"))!.click()); + delete expected.model; + } else if (edit === "effort-clear") { + expect(container.querySelector('[aria-label="Thinking effort"]')?.textContent).toContain("High"); + await act(async () => container.querySelector('[aria-label="Thinking effort"]')!.click()); + await act(async () => document.querySelector('[role="option"][data-value=""]')!.click()); + delete expected.modelReasoningEffort; + delete expected.reasoningEffort; + } else { + const remove = container.querySelector('[aria-label="Remove CODEX_HOME"]'); + expect(remove).toBeTruthy(); + await act(async () => remove!.click()); + await clickButton(text => text === "Save"); + expected.env = { OPENAI_API_KEY: sourceConfig.env.OPENAI_API_KEY }; + } + await clickButton(text => text.startsWith("Import 3 file")); + await settle(); + if (edit === "changed-harness") { + expect(lastImportMeta().adapterOverrides?.coder?.adapterType).toBe("claude_local"); + const config = lastImportMeta().adapterOverrides?.coder?.adapterConfig; + for (const field of ["provider", "env", "workspaceStrategy", "workspaceRuntime", "lifecycleMode", "idleTimeoutMs", "modelReasoningEffort"]) expect(config).not.toHaveProperty(field); + expect(config?.timeoutSec).toBe(0); + return; + } + expect(lastImportMeta().adapterOverrides?.coder).toEqual({ + adapterType: "paperclip_runner", runner: edit === "legacy-model" ? "legacy" : "paperclip", adapterConfig: expected, + }); + expect(preview.manifest.agents[0]!.adapterConfig).toEqual(sourceConfig); + }); + it("does not reinterpret an unknown native provider as Codex when its configuration is edited", async () => { mockAdaptersApi.list.mockResolvedValue([{ type: "paperclip_runner", disabled: false }]); const preview = buildPreviewResult(); diff --git a/ui/src/pages/CompanyImport.tsx b/ui/src/pages/CompanyImport.tsx index 8c3a031deb5..e982e0db5f0 100644 --- a/ui/src/pages/CompanyImport.tsx +++ b/ui/src/pages/CompanyImport.tsx @@ -26,8 +26,8 @@ import { getAgentOrderStorageKey, writeAgentOrder } from "../lib/agent-order"; import { MarkdownBody } from "../components/MarkdownBody"; import { Button } from "@/components/ui/button"; import { EmptyState } from "../components/EmptyState"; -import { AgentConfigForm } from "../components/AgentConfigForm"; -import { cn } from "../lib/utils"; +import { AgentConfigForm, overlayValuesEqual } from "../components/AgentConfigForm"; +import { asObject, cn } from "../lib/utils"; import { ArrowRight, Check, @@ -585,7 +585,7 @@ function importAdapterValues( ...(runner !== undefined ? { runner } : nativeHarness ? { runner: agentRunner("paperclip_runner") } : {}), ...(sourceConfig && choice === "codex_local" ? { model: typeof sourceConfig.model === "string" ? sourceConfig.model : "", - thinkingEffort: String(sourceConfig.reasoningEffort ?? sourceConfig.modelReasoningEffort ?? ""), + thinkingEffort: String(sourceConfig.modelReasoningEffort ?? sourceConfig.reasoningEffort ?? ""), ...(typeof savedBypass === "boolean" ? { codexLegacySandboxBypass: savedBypass } : {}), // Runner-only imports preserve server-translated policy; display that policy too. dangerouslyBypassSandbox: typeof savedBypass === "boolean" ? savedBypass @@ -594,6 +594,18 @@ function importAdapterValues( : resolveCodexLocalCliPolicy(sourceConfig).bypass, } : {}), ...(sourceConfig && choice === "claude_local" ? { model: typeof sourceConfig.model === "string" ? sourceConfig.model : "", thinkingEffort: String(sourceConfig.modelReasoningEffort ?? sourceConfig.reasoningEffort ?? sourceConfig.effort ?? "") } : {}), + ...(nativeHarness && sourceConfig ? { + cwd: typeof sourceConfig.cwd === "string" ? sourceConfig.cwd : "", + instructionsFilePath: typeof sourceConfig.instructionsFilePath === "string" ? sourceConfig.instructionsFilePath : "", + envBindings: { ...asObject(sourceConfig.env) }, + workspaceStrategyType: String(asObject(sourceConfig.workspaceStrategy).type ?? "project_primary"), + workspaceBaseRef: String(asObject(sourceConfig.workspaceStrategy).baseRef ?? ""), + workspaceBranchTemplate: String(asObject(sourceConfig.workspaceStrategy).branchTemplate ?? ""), + worktreeParentDir: String(asObject(sourceConfig.workspaceStrategy).worktreeParentDir ?? ""), + runtimeServicesJson: sourceConfig.workspaceRuntime ? JSON.stringify(sourceConfig.workspaceRuntime) : "", + ...(sourceConfig.lifecycleMode === "warm" || sourceConfig.lifecycleMode === "per_turn" ? { paperclipRunnerLifecycleMode: sourceConfig.lifecycleMode } : {}), + ...(typeof sourceConfig.idleTimeoutMs === "number" ? { paperclipRunnerIdleTimeoutMs: sourceConfig.idleTimeoutMs } : {}), + } : {}), ...(sourceAdapterType === "paperclip_runner" && choice !== "openai_dot" ? { adapterSchemaValues: { ...sourceConfig } } : {}), ...(choice === "openai_dot" ? { @@ -1738,7 +1750,36 @@ export function CompanyImport() { if (selectedType === "openai_dot" && configVals.adapterSchemaValues?.allowUnmeteredProvider !== true) { throw new Error("OpenAI Dot requires acknowledgement of external billing. Configure the Dot adapter and enable Allow externally billed provider."); } - override.adapterConfig = getUIAdapter(retainsNativeHarness ? "paperclip_runner" : configVals.adapterType).buildAdapterConfig(configVals); + const build = getUIAdapter(retainsNativeHarness && selectedType === "claude_local" ? "paperclip_runner" : configVals.adapterType).buildAdapterConfig; + const edited = build(configVals); + if (agent.adapterType === "paperclip_runner" && selectedType === agent.adapterChoice + && ["codex_local", "claude_local"].includes(selectedType) && configVals.adapterType === selectedType + && agentHarnessType("paperclip_runner", configVals.adapterSchemaValues) === selectedType) { + // Project only real form changes onto the saved native configuration. + // Form defaults must not replace bindings or runtime fields; explicit + // clears must not be filled back in from the source either. + const original = importAdapterValues(selectedType, agent.adapterConfig, agent.adapterType, configVals.runner); + const baseline = build(original); + const retained = { ...agent.adapterConfig }; + const applyChanges = (before: Record, after: Record) => { + for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) { + if (overlayValuesEqual(before[key], after[key])) continue; + if (after[key] === undefined) delete retained[key]; + else retained[key] = after[key]; + } + }; + applyChanges(original.adapterSchemaValues ?? {}, configVals.adapterSchemaValues ?? {}); + applyChanges(baseline, edited); + if (original.model !== configVals.model && !configVals.model) delete retained.model; + if (original.thinkingEffort !== configVals.thinkingEffort) { + delete retained.reasoningEffort; + delete retained.effort; + if (!configVals.thinkingEffort) delete retained.modelReasoningEffort; + } + override.adapterConfig = retained; + } else { + override.adapterConfig = edited; + } } overrides[agent.slug] = override; }