diff --git a/server/drivers/claude.test.ts b/server/drivers/claude.test.ts index 6b4a6f7bc2..ba9b10dc9e 100644 --- a/server/drivers/claude.test.ts +++ b/server/drivers/claude.test.ts @@ -1434,6 +1434,19 @@ describe("ClaudeDriver turns (fake CLI)", () => { }); }); + it("withholds --autocompact from a CLI above the floor whose --help does not list it", async () => { + // 2.1.129 clears the 2.1.122 floor yet rejects the flag ("unknown option") + const dump = join(scratch, "no-autocompact-cli.json"); + await create(undefined, { FAKE_CLAUDE_DUMP: dump, FAKE_CLAUDE_VERSION: "2.1.129", FAKE_CLAUDE_AUTOCOMPACT: "0" }); + await instance.snapshot(); + await instance.adapter.sendTurn({ threadId: "t-no-autocompact", text: "hi" }); + await recorder.until((e) => e.type === "turn.completed"); + + const seen = JSON.parse(readFileSync(dump, "utf8")); + expect(seen.argv).not.toContain("--autocompact"); + expect(seen.argv).toContain("--strict-mcp-config"); + }); + it("keeps only the isolation flag a very old CLI accepts", async () => { // 1.0.100: --strict-mcp-config exists (1.0.60), --setting-sources does // not yet (1.0.122) diff --git a/server/drivers/claude.ts b/server/drivers/claude.ts index ec9feeceb8..b2cb3311ef 100644 --- a/server/drivers/claude.ts +++ b/server/drivers/claude.ts @@ -1182,6 +1182,12 @@ export const ClaudeDriver: ProviderDriver = { // other context controls and an unknown flag would reject that request. let cliVersion: ClaudeCliVersion | null = null; let cliVersionChecked = false; + // Whether `claude --help` lists --autocompact, read once per CLI version + // by snapshot(). The flag is not in every build above its version floor + // (2.1.129 rejects it), so the listing wins over the floor; null until + // probed, or when the probe fails. + let cliHasAutocompact: boolean | null = null; + let cliHelpVersion: string | null = null; const readCliVersion = (env: NodeJS.ProcessEnv): Promise => new Promise((resolve) => { execCli(config.cli, ["--version"], { timeout: 8000, env }, (err, stdout) => @@ -1439,7 +1445,7 @@ export const ClaudeDriver: ProviderDriver = { if (claudeCliSupports(cliVersion, "--setting-sources")) args.push("--setting-sources", "project"); } const compactWindow = autoCompactWindow(turnEnvironment); - if (compactWindow && claudeCliSupports(cliVersion, "--autocompact")) { + if (compactWindow && (cliHasAutocompact ?? claudeCliSupports(cliVersion, "--autocompact"))) { args.push("--autocompact", compactWindow); } // An old pair conversation can still carry its first assignment in @@ -2431,6 +2437,13 @@ export const ClaudeDriver: ProviderDriver = { if (!version) return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` }; cliVersion = parseClaudeCliVersion(version); cliVersionChecked = true; + if (version !== cliHelpVersion) { + const help = await new Promise((resolve) => { + execCli(config.cli, ["--help"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout)); + }); + cliHasAutocompact = help === null ? null : /^\s*--autocompact\b/m.test(help); + cliHelpVersion = version; + } const update = claudeCliUpdate(version, config.cli); const warning = claudeInheritWarning(env); if (config.requireApiKey) { diff --git a/server/testing/fake-claude-cli.ts b/server/testing/fake-claude-cli.ts index 0008bee95a..6864125bf3 100755 --- a/server/testing/fake-claude-cli.ts +++ b/server/testing/fake-claude-cli.ts @@ -237,6 +237,18 @@ if (argv[0] === "--version") { process.exit(0); } +if (argv[0] === "--help") { + // Lists --autocompact in the option column like the real CLI, unless the + // fake stands in for a build without it: FAKE_CLAUDE_AUTOCOMPACT=0, or a + // version below the 2.1.122 floor. + const [maj = 0, min = 0, pat = 0] = (process.env.FAKE_CLAUDE_VERSION ?? "2.1.232").split(".").map(Number); + const has = process.env.FAKE_CLAUDE_AUTOCOMPACT !== "0" && (maj > 2 || (maj === 2 && (min > 1 || (min === 1 && pat >= 122)))); + process.stdout.write( + `Usage: claude [options]\n\nOptions:\n --model Model\n${has ? " --autocompact Compaction window\n" : ""} -h, --help Display help\n`, + ); + process.exit(0); +} + if (argv[0] === "update") { if (process.env.FAKE_CLAUDE_UPDATE === "fail") { process.stderr.write("fake-claude: simulated update failure\n");