merge: preserve Codex settings and intentional import removals

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 17:02:09 -05:00
commit ec6a2aa545
8 files changed
+259 -7

No files matched your search

@@ -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`,
@@ -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",
@@ -241,7 +241,8 @@ export function buildPaperclipRunnerConfig(v: CreateConfigValues): Record<string
...schemaValues,
provider,
...(provider === "codex"
? { model: resolvePaperclipRunnerModel("codex", config.model) }
? { model: resolvePaperclipRunnerModel("codex", config.model),
...(configuredEffort ? { modelReasoningEffort: configuredEffort } : {}) }
: {}),
codexPermissionMode: "never",
opencodePermissionMode: resolvePaperclipRunnerPermissionMode(
@@ -9,6 +9,7 @@ vi.mock("./environment-probe.js", () => ({ 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 });
});
+27 -1
View File
@@ -120,6 +120,32 @@ export function resolveNewAgentRunner(input: AgentRunnerSelection): { adapterTyp
}
return { adapterType: "paperclip_runner", adapterConfig: config };
}
const aliasSources = new Map<string, string>();
// 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"]);
@@ -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<string, unknown>, 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);
+70
View File
@@ -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<string, unknown> = structuredClone(sourceConfig);
if (edit === "legacy-model") {
await act(async () => container.querySelector<HTMLButtonElement>('[aria-label="Runner"]')!.click());
await act(async () => document.querySelector<HTMLButtonElement>('[role="option"][data-value="legacy"]')!.click());
}
if (edit === "model-clear" || edit === "legacy-model" || edit === "changed-harness") {
await act(async () => container.querySelector<HTMLButtonElement>('[aria-label="Model"]')!.click());
await act(async () => [...document.querySelectorAll<HTMLButtonElement>('[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<HTMLButtonElement>('[aria-label="Thinking effort"]')!.click());
await act(async () => document.querySelector<HTMLButtonElement>('[role="option"][data-value=""]')!.click());
delete expected.modelReasoningEffort;
delete expected.reasoningEffort;
} else {
const remove = container.querySelector<HTMLButtonElement>('[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();
+45 -4
View File
@@ -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<string, unknown>, after: Record<string, unknown>) => {
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;
}