fix(anthropic): preserve resumed CLI caches across Git changes (#140566)

* fix(anthropic): preserve resumed CLI caches across Git changes

* test(anthropic): expect cache policy with context windows

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
This commit is contained in:
Vito Cappello 2026-09-06 21:46:33 -04:00 • committed by GitHub
parent 3c6c2a4697
commit 940c359a36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 57 additions and 30 deletions

View file

@ -143,6 +143,13 @@ claude update
The bundled `claude-cli` backend prefers Claude Code's native skill resolver. When the current skills snapshot has at least one selected skill with a materialized path, OpenClaw passes a temporary Claude Code plugin via `--plugin-dir` and omits the duplicate OpenClaw skills catalog from the appended system prompt. Without a materialized plugin skill, OpenClaw keeps the prompt catalog as a fallback. Skill env/API key overrides still apply to the child process environment for the run.
OpenClaw disables Claude Code's built-in Git workflow instructions and startup
Git-status snapshot with `CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS=1`. Claude Code
rebuilds that snapshot when a process resumes, so workspace edits or commits
would otherwise invalidate cached conversation history. Git tools and workspace
instructions remain available. This does not prevent cache misses after prompt
changes, compaction, model or thinking changes, or cache expiry.
OpenClaw always launches Claude Code with its default permission mode.
OpenClaw's permission responses and `PreToolUse` hook keep native tools under
host control, including when user or enterprise settings would otherwise

View file

@ -272,23 +272,25 @@ export function buildAnthropicCliBackend(
}
: undefined;
const env = {
// Claude rebuilds the startup Git snapshot on process resume, rewriting
// the conversation prefix after workspace edits or commits. OpenClaw
// supplies workspace instructions; Git state can be read with tools.
CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS: "1",
...resolveClaudeCliAutoCompactEnv(context.contextTokenBudget),
...(context.contextWindow === "200k" ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: "1" } : {}),
...resolveClaudeCliThinkingEnv(context.thinkingLevel, context.modelId),
...authInput?.env,
};
return Object.keys(env).length > 0 || isolatedCompletion || cliExecution
? {
env,
// The paired side-question argv projection disables settings, memory,
// hooks, session persistence, and tools before process launch.
...(isolatedCompletion ? { isolatedCompletionEnforced: true as const } : {}),
...(authInput?.clearEnv ? { clearEnv: authInput.clearEnv } : {}),
...(authInput?.secretInput ? { secretInput: authInput.secretInput } : {}),
...(authInput?.cleanup ? { cleanup: authInput.cleanup } : {}),
...cliExecution,
}
: undefined;
return {
env,
// The paired side-question argv projection disables settings, memory,
// hooks, session persistence, and tools before process launch.
...(isolatedCompletion ? { isolatedCompletionEnforced: true as const } : {}),
...(authInput?.clearEnv ? { clearEnv: authInput.clearEnv } : {}),
...(authInput?.secretInput ? { secretInput: authInput.secretInput } : {}),
...(authInput?.cleanup ? { cleanup: authInput.cleanup } : {}),
...cliExecution,
};
};
const supportProbe = options.ensureDynamicSystemPromptSectionsSupport?.();
return supportProbe ? supportProbe.then(prepare) : prepare();

View file

@ -25,6 +25,7 @@ describe("Claude CLI context-window selection", () => {
env: {
CLAUDE_CODE_AUTO_COMPACT_WINDOW: "200000",
CLAUDE_CODE_DISABLE_1M_CONTEXT: "1",
CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS: "1",
},
});
expect(resolveModelId?.({ modelId, contextWindow: "1m" })).toBe(`${modelId}[1m]`);
@ -36,7 +37,12 @@ describe("Claude CLI context-window selection", () => {
contextWindow: "1m",
contextTokenBudget: 1_000_000,
}),
).toEqual({ env: { CLAUDE_CODE_AUTO_COMPACT_WINDOW: "1000000" } });
).toEqual({
env: {
CLAUDE_CODE_AUTO_COMPACT_WINDOW: "1000000",
CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS: "1",
},
});
expect(backend.config.clearEnv).toContain("CLAUDE_CODE_DISABLE_1M_CONTEXT");
expect(resolveModelId?.({ modelId: "claude-opus-4-8" })).toBe("claude-opus-4-8");
},

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { resolveClaudeCliThinkingEnv } from "./cli-shared.js";
import { resolveClaudeCliAutoCompactEnv, resolveClaudeCliThinkingEnv } from "./cli-shared.js";
describe("Claude CLI execution environment", () => {
it.each([
@ -17,3 +17,15 @@ describe("Claude CLI execution environment", () => {
},
);
});
describe("resolveClaudeCliAutoCompactEnv", () => {
it("maps the effective OpenClaw context budget into Claude Code compaction", () => {
expect(resolveClaudeCliAutoCompactEnv(100_000.9)).toEqual({
CLAUDE_CODE_AUTO_COMPACT_WINDOW: "100000",
});
});
it.each([undefined, 0, 0.5, Number.NaN])("rejects an invalid context budget: %s", (budget) => {
expect(resolveClaudeCliAutoCompactEnv(budget)).toBeUndefined();
});
});

View file

@ -5,7 +5,6 @@ import { buildAnthropicCliBackend } from "./cli-backend.js";
import {
CLAUDE_CLI_CLEAR_ENV,
normalizeClaudeBackendConfig,
resolveClaudeCliAutoCompactEnv,
resolveClaudeCliExecutionArgs,
supportsClaudeDynamicSystemPromptSections,
} from "./cli-shared.js";
@ -70,7 +69,10 @@ describe("Claude CLI adapter equivalence", () => {
isolatedCompletionSystemPrompt: string;
}) as { env?: Record<string, string>; isolatedCompletionEnforced?: true };
expect(prepared).toEqual({ env: {}, isolatedCompletionEnforced: true });
expect(prepared).toEqual({
env: { CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS: "1" },
isolatedCompletionEnforced: true,
});
});
it("builds Claude Code's native manual compaction command", () => {
@ -110,18 +112,6 @@ describe("Claude CLI adapter equivalence", () => {
});
});
describe("resolveClaudeCliAutoCompactEnv", () => {
it("maps the effective OpenClaw context budget into Claude Code compaction", () => {
expect(resolveClaudeCliAutoCompactEnv(100_000.9)).toEqual({
CLAUDE_CODE_AUTO_COMPACT_WINDOW: "100000",
});
});
it.each([undefined, 0, 0.5, Number.NaN])("rejects an invalid context budget: %s", (budget) => {
expect(resolveClaudeCliAutoCompactEnv(budget)).toBeUndefined();
});
});
function expectDefaultDisallowedTools(args: readonly string[] | undefined) {
const disallowedIndex = args?.indexOf("--disallowedTools") ?? -1;
expect(disallowedIndex).toBeGreaterThanOrEqual(0);
@ -918,7 +908,10 @@ describe("normalizeClaudeBackendConfig", () => {
contextTokenBudget: 100_000,
}),
).toEqual({
env: { CLAUDE_CODE_AUTO_COMPACT_WINDOW: "100000" },
env: {
CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS: "1",
CLAUDE_CODE_AUTO_COMPACT_WINDOW: "100000",
},
});
});
@ -948,6 +941,7 @@ describe("normalizeClaudeBackendConfig", () => {
}) as ClaudePreparedExecutionWithSecret;
expect(prepared.env).toEqual({
CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS: "1",
CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR: "3",
});
expect(prepared.env).not.toHaveProperty("CLAUDE_CODE_OAUTH_TOKEN");
@ -1058,6 +1052,7 @@ describe("normalizeClaudeBackendConfig", () => {
}) as ClaudePreparedExecutionWithSecret;
expect(prepared.env).toEqual({
CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS: "1",
CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR: "3",
});
expect(prepared.env).not.toHaveProperty("ANTHROPIC_API_KEY");

View file

@ -299,6 +299,7 @@ process.stdout.write(JSON.stringify({
descriptor: descriptor ?? null,
rawPresent: Object.hasOwn(process.env, "CLAUDE_CODE_OAUTH_TOKEN") || Object.hasOwn(process.env, "ANTHROPIC_API_KEY"),
scrubPresent: Object.hasOwn(process.env, "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB"),
gitInstructionsDisabled: process.env.CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS,
}) + "\\n");`);
const calls: Array<{ method: string; params: unknown }> = [];
const handleSystemRun = vi.fn(
@ -356,6 +357,7 @@ process.stdout.write(JSON.stringify({
});
expect(progress).toContain('"rawPresent":false');
expect(progress).toContain('"scrubPresent":false');
expect(progress).toContain('"gitInstructionsDisabled":"1"');
expect(calls).toContainEqual({
method: "node.invoke.result",
params: expect.objectContaining({
@ -520,6 +522,7 @@ process.stdout.write(JSON.stringify({
descriptor: process.env[${JSON.stringify(descriptorEnv)}],
rawPresent: Object.hasOwn(process.env, ${JSON.stringify(rawEnv)}),
scrubPresent: Object.hasOwn(process.env, "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB"),
gitInstructionsDisabled: process.env.CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS,
}) + "\\n");`);
const request = { argv: ["-p"], idleTimeoutMs: 1_000, timeoutMs: 5_000 };
const calls: Array<{ method: string; params: unknown }> = [];
@ -543,6 +546,7 @@ process.stdout.write(JSON.stringify({
expect(progress).toContain('"descriptor":"3"');
expect(progress).toContain('"rawPresent":false');
expect(progress).toContain('"scrubPresent":false');
expect(progress).toContain('"gitInstructionsDisabled":"1"');
expect(result).toMatchObject({ exitCode: 0, success: true });
},
);

View file

@ -151,7 +151,8 @@ export async function runClaudeCliNodeCommand(params: {
mode: "child",
argv,
cwd: params.cwd,
env: params.env,
// Apply the cache-stable Git policy locally without extending the node wire contract.
env: { ...(params.env ?? process.env), CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS: "1" },
exactEnv: true,
input:
skillSession?.rewriteReferences(params.request.stdin ?? "") ?? params.request.stdin ?? "",