diff --git a/extensions/acpx/npm-shrinkwrap.json b/extensions/acpx/npm-shrinkwrap.json index 1573d0a2474..ee2c04ebba8 100644 --- a/extensions/acpx/npm-shrinkwrap.json +++ b/extensions/acpx/npm-shrinkwrap.json @@ -10,7 +10,7 @@ "dependencies": { "@agentclientprotocol/claude-agent-acp": "0.39.0", "@zed-industries/codex-acp": "0.15.0", - "acpx": "0.10.0", + "acpx": "0.11.1", "zod": "4.4.3" } }, @@ -196,9 +196,9 @@ } }, "node_modules/@clack/core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.1.tgz", - "integrity": "sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.3.1.tgz", + "integrity": "sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==", "license": "MIT", "dependencies": { "fast-wrap-ansi": "^0.2.0", @@ -209,12 +209,12 @@ } }, "node_modules/@clack/prompts": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.5.1.tgz", - "integrity": "sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.4.0.tgz", + "integrity": "sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==", "license": "MIT", "dependencies": { - "@clack/core": "1.4.1", + "@clack/core": "1.3.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" @@ -831,15 +831,15 @@ } }, "node_modules/acpx": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/acpx/-/acpx-0.10.0.tgz", - "integrity": "sha512-hd48XV03gG3sd409T1lDrOKJTTz1ap4g0wrndXjxQ590tN85pBYlvfNLyerybvGRrtUGsZjNdt99r1jpIt6ukA==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/acpx/-/acpx-0.11.1.tgz", + "integrity": "sha512-AfjHUYnuxRpvb1JLjjIZILEbCskIENCTP9Mgc5w0BiaPLmsL2hTg5gbM7sG1EssOkceNLQwgeAuVtbukLz0KFw==", "license": "MIT", "dependencies": { - "@agentclientprotocol/sdk": "^0.22.1", - "commander": "^14.0.3", - "skillflag": "^0.1.4", - "tsx": "^4.22.0", + "@agentclientprotocol/sdk": "^0.28.1", + "commander": "^15.0.0", + "skillflag": "^0.2.0", + "tsx": "^4.22.4", "zod": "^4.4.3" }, "bin": { @@ -849,6 +849,15 @@ "node": ">=22.13.0" } }, + "node_modules/acpx/node_modules/@agentclientprotocol/sdk": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.28.1.tgz", + "integrity": "sha512-Z2Frs6YtPhnZZ+XwFXyQkRDXY0fn8FjCalEs0W4yUhQnY4TztmNq0/RnfzWdFN3vqT3h0jTz5klzYbZHGxCDyQ==", + "license": "Apache-2.0", + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -1050,12 +1059,12 @@ } }, "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22.12.0" } }, "node_modules/content-disposition": { @@ -2052,9 +2061,9 @@ "license": "MIT" }, "node_modules/skillflag": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/skillflag/-/skillflag-0.1.4.tgz", - "integrity": "sha512-egFg+XCF5sloOWdtzxZivTX7n4UDj5pxQoY33wbT8h+YSDjMQJ76MZUg2rXQIBXmIDtlZhLgirS1g/3R5/qaHA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/skillflag/-/skillflag-0.2.0.tgz", + "integrity": "sha512-7ZmEpBeEoPLc+hqZ/StAnCO/hulgEPANzPyZgOM/CZ5zc3b0ApSp3URavY5POM/OKyi5d9+UC/Q21OoiYC2kJw==", "license": "MIT", "dependencies": { "@clack/prompts": "^1.0.1", diff --git a/extensions/acpx/package.json b/extensions/acpx/package.json index d80ff4bb36d..5e36e47169b 100644 --- a/extensions/acpx/package.json +++ b/extensions/acpx/package.json @@ -10,7 +10,7 @@ "dependencies": { "@agentclientprotocol/claude-agent-acp": "0.39.0", "@zed-industries/codex-acp": "0.15.0", - "acpx": "0.10.0", + "acpx": "0.11.1", "zod": "4.4.3" }, "devDependencies": { diff --git a/extensions/acpx/src/runtime.test.ts b/extensions/acpx/src/runtime.test.ts index 97c4e059aa0..2e3bdd6ff7f 100644 --- a/extensions/acpx/src/runtime.test.ts +++ b/extensions/acpx/src/runtime.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { RequestedModelUnsupportedError } from "acpx/runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { AcpRuntimeError, @@ -708,6 +709,100 @@ describe("AcpxRuntime fresh reset wrapper", () => { }); }); + it("retries without a model when ACPX reports missing model capability", async () => { + const baseStore: TestSessionStore = { + load: vi.fn(async () => undefined), + save: vi.fn(async () => {}), + }; + const { runtime, delegate } = makeRuntime(baseStore, { + agentRegistry: { + resolve: (agentName: string) => (agentName === "opencode" ? "opencode acp" : agentName), + list: () => ["opencode"], + }, + }); + const ensure = vi + .spyOn(delegate, "ensureSession") + .mockRejectedValueOnce( + new RequestedModelUnsupportedError( + "Cannot apply --model: the ACP agent did not advertise model support", + "missing-capability", + ), + ) + .mockResolvedValueOnce({ + sessionKey: "agent:opencode:acp:test", + backend: "acpx", + runtimeSessionName: "opencode", + }); + + await runtime.ensureSession({ + sessionKey: "agent:opencode:acp:test", + agent: "opencode", + mode: "persistent", + model: "openrouter/owl-alpha", + }); + + expect(ensure).toHaveBeenCalledTimes(2); + expect(readFirstEnsureSessionInput(ensure)).toMatchObject({ + model: "openrouter/owl-alpha", + sessionOptions: { model: "openrouter/owl-alpha" }, + }); + const [, secondCall] = ensure.mock.calls; + expect(secondCall?.[0]).not.toHaveProperty("sessionOptions"); + expect((secondCall?.[0] as { model?: string } | undefined)?.model).toBeUndefined(); + }); + + it("does not retry when ACPX rejects an explicitly unsupported model id", async () => { + const baseStore: TestSessionStore = { + load: vi.fn(async () => undefined), + save: vi.fn(async () => {}), + }; + const { runtime, delegate } = makeRuntime(baseStore, { + agentRegistry: { + resolve: (agentName: string) => (agentName === "opencode" ? "opencode acp" : agentName), + list: () => ["opencode"], + }, + }); + const ensure = vi + .spyOn(delegate, "ensureSession") + .mockRejectedValueOnce( + new RequestedModelUnsupportedError( + "Cannot apply --model: the ACP agent did not advertise that model", + "unadvertised-model", + ), + ); + + await expect( + runtime.ensureSession({ + sessionKey: "agent:opencode:acp:test", + agent: "opencode", + mode: "persistent", + model: "unknown/model", + }), + ).rejects.toThrow("did not advertise that model"); + expect(ensure).toHaveBeenCalledTimes(1); + }); + + it("does not retry an unrelated error with similar wording", async () => { + const baseStore: TestSessionStore = { + load: vi.fn(async () => undefined), + save: vi.fn(async () => {}), + }; + const { runtime, delegate } = makeRuntime(baseStore); + const ensure = vi + .spyOn(delegate, "ensureSession") + .mockRejectedValueOnce(new Error("the ACP agent did not advertise model support")); + + await expect( + runtime.ensureSession({ + sessionKey: "agent:main:acp:test", + agent: "main", + mode: "persistent", + model: "openrouter/owl-alpha", + }), + ).rejects.toThrow("did not advertise model support"); + expect(ensure).toHaveBeenCalledTimes(1); + }); + it("injects Codex ACP startup config into the scoped registry", () => { expect(testing.isCodexAcpCommand(CODEX_ACP_COMMAND)).toBe(true); expect(testing.isCodexAcpCommand(CODEX_ACP_WRAPPER_COMMAND)).toBe(true); diff --git a/extensions/acpx/src/runtime.ts b/extensions/acpx/src/runtime.ts index d8fd3b7e0b4..2032c1b20b5 100644 --- a/extensions/acpx/src/runtime.ts +++ b/extensions/acpx/src/runtime.ts @@ -13,6 +13,7 @@ import { createFileSessionStore, decodeAcpxRuntimeHandleState, encodeAcpxRuntimeHandleState, + isRequestedModelUnsupportedError, type AcpAgentRegistry, type AcpRuntimeDoctorReport, type AcpRuntimeEvent, @@ -586,6 +587,26 @@ function withAcpxSessionOptions(input: OpenClawRuntimeEnsureInput): AcpxDelegate } as AcpxDelegateEnsureInput; } +function isAcpModelCapabilityMissingError(error: unknown): boolean { + return isRequestedModelUnsupportedError(error) && error.reason === "missing-capability"; +} + +// ACPX owns the distinction between missing model capability and an invalid model id. +// Retry only the former so explicit model mistakes remain visible to the caller. +async function ensureDelegateSessionWithModelFallback( + delegate: BaseAcpxRuntime, + input: OpenClawRuntimeEnsureInput, +): Promise { + try { + return await delegate.ensureSession(withAcpxSessionOptions(input)); + } catch (error) { + if (!input.model || !isAcpModelCapabilityMissingError(error)) { + throw error; + } + return await delegate.ensureSession(withAcpxSessionOptions({ ...input, model: undefined })); + } +} + function quoteShellArg(value: string): string { if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) { return value; @@ -989,7 +1010,7 @@ export class AcpxRuntime implements AcpRuntime { this.withCodexWrapperDiagnostics({ command: stableLaunchCommand, fallbackCode: "ACP_SESSION_INIT_FAILED", - run: () => delegate.ensureSession(withAcpxSessionOptions(ensureInput)), + run: () => ensureDelegateSessionWithModelFallback(delegate, ensureInput), }), }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3020f48a8d..967b11570a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -313,8 +313,8 @@ importers: specifier: 0.15.0 version: 0.15.0 acpx: - specifier: 0.10.0 - version: 0.10.0 + specifier: 0.11.1 + version: 0.11.1 zod: specifier: 4.4.3 version: 4.4.3 @@ -1995,6 +1995,11 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 + '@agentclientprotocol/sdk@0.28.1': + resolution: {integrity: sha512-Z2Frs6YtPhnZZ+XwFXyQkRDXY0fn8FjCalEs0W4yUhQnY4TztmNq0/RnfzWdFN3vqT3h0jTz5klzYbZHGxCDyQ==} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.156': resolution: {integrity: sha512-IkjcS9dqAUlD4Nb62L9AZtmAXCa+FV4ul8lIlyXXUprh3nlecbKsWOXVd/GORrzAhMmynJaX4+iV1JiutFKXUA==} cpu: [arm64] @@ -4606,8 +4611,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acpx@0.10.0: - resolution: {integrity: sha512-hd48XV03gG3sd409T1lDrOKJTTz1ap4g0wrndXjxQ590tN85pBYlvfNLyerybvGRrtUGsZjNdt99r1jpIt6ukA==} + acpx@0.11.1: + resolution: {integrity: sha512-AfjHUYnuxRpvb1JLjjIZILEbCskIENCTP9Mgc5w0BiaPLmsL2hTg5gbM7sG1EssOkceNLQwgeAuVtbukLz0KFw==} engines: {node: '>=22.13.0'} hasBin: true @@ -5012,6 +5017,10 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + commander@5.1.0: resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} engines: {node: '>= 6'} @@ -7151,8 +7160,8 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - skillflag@0.1.4: - resolution: {integrity: sha512-egFg+XCF5sloOWdtzxZivTX7n4UDj5pxQoY33wbT8h+YSDjMQJ76MZUg2rXQIBXmIDtlZhLgirS1g/3R5/qaHA==} + skillflag@0.2.0: + resolution: {integrity: sha512-7ZmEpBeEoPLc+hqZ/StAnCO/hulgEPANzPyZgOM/CZ5zc3b0ApSp3URavY5POM/OKyi5d9+UC/Q21OoiYC2kJw==} engines: {node: '>=18'} hasBin: true @@ -7884,6 +7893,10 @@ snapshots: dependencies: zod: 4.4.3 + '@agentclientprotocol/sdk@0.28.1(zod@4.4.3)': + dependencies: + zod: 4.4.3 + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.156': optional: true @@ -10643,11 +10656,11 @@ snapshots: acorn@8.16.0: {} - acpx@0.10.0: + acpx@0.11.1: dependencies: - '@agentclientprotocol/sdk': 0.22.1(zod@4.4.3) - commander: 14.0.3 - skillflag: 0.1.4 + '@agentclientprotocol/sdk': 0.28.1(zod@4.4.3) + commander: 15.0.0 + skillflag: 0.2.0 tsx: 4.22.4 zod: 4.4.3 transitivePeerDependencies: @@ -11054,6 +11067,8 @@ snapshots: commander@14.0.3: {} + commander@15.0.0: {} + commander@5.1.0: {} constantinople@4.0.1: @@ -13754,9 +13769,9 @@ snapshots: sisteransi@1.0.5: {} - skillflag@0.1.4: + skillflag@0.2.0: dependencies: - '@clack/prompts': 1.5.1 + '@clack/prompts': 1.4.0 tar-stream: 3.2.0 transitivePeerDependencies: - bare-abort-controller