diff --git a/packages/adapters/acpx-local/src/server/execute.ts b/packages/adapters/acpx-local/src/server/execute.ts index 211eb4f97fe..7d9080c6535 100644 --- a/packages/adapters/acpx-local/src/server/execute.ts +++ b/packages/adapters/acpx-local/src/server/execute.ts @@ -832,7 +832,10 @@ async function applySessionConfigOptions(input: { const options = sessionConfigOptions(input.prepared); if (options.length === 0) return; if (!input.runtime.setConfigOption) { - throw new Error("ACPX runtime does not expose session config controls."); + const message = + "ACPX runtime does not expose session config controls; upgrade ACPX or remove configured model, effort, and fast mode overrides."; + await input.onLog("stderr", `[paperclip] ${message}\n`); + throw new Error(message); } for (const option of options) { await input.runtime.setConfigOption({ diff --git a/packages/adapters/acpx-local/src/ui/build-config.ts b/packages/adapters/acpx-local/src/ui/build-config.ts index 3df73ecc934..729d16c12ca 100644 --- a/packages/adapters/acpx-local/src/ui/build-config.ts +++ b/packages/adapters/acpx-local/src/ui/build-config.ts @@ -5,6 +5,7 @@ import { DEFAULT_ACPX_LOCAL_NON_INTERACTIVE_PERMISSIONS, DEFAULT_ACPX_LOCAL_PERMISSION_MODE, DEFAULT_ACPX_LOCAL_TIMEOUT_SEC, + DEFAULT_ACPX_LOCAL_WARM_HANDLE_IDLE_MS, } from "../index.js"; function parseCommaArgs(value: string): string[] { @@ -88,7 +89,7 @@ export function buildAcpxLocalConfig(v: CreateConfigValues): Record { } }); + it("logs a clear error when configured session options need unsupported runtime controls", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-acpx-missing-config-controls-")); + try { + const runtime = new FakeRuntime({} as AcpRuntimeOptions); + Object.defineProperty(runtime, "setConfigOption", { value: undefined }); + const logs: LogEntry[] = []; + const execute = createAcpxLocalExecutor({ + createRuntime: () => runtime, + }); + const result = await execute(buildContext(root, { + config: { + agent: "codex", + cwd: root, + stateDir: path.join(root, "state"), + promptTemplate: "Do the assigned work.", + model: "gpt-5.4", + }, + onLog: async (stream, chunk) => logs.push({ stream, chunk }), + })); + + expect(result.exitCode).toBe(1); + expect(result.errorMessage).toContain("does not expose session config controls"); + expect(logs).toEqual(expect.arrayContaining([ + expect.objectContaining({ + stream: "stderr", + chunk: expect.stringContaining("upgrade ACPX or remove configured model"), + }), + ])); + expect(runtime.closeInputs).toEqual([ + expect.objectContaining({ + reason: "paperclip config cleanup", + discardPersistentState: false, + }), + ]); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + it("reuses a compatible warm session and starts fresh when cwd changes", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-acpx-reuse-")); const other = path.join(root, "other");