Skip to content

feat(acp): honor Claude auto-compaction window - #237

Open
MoozLee wants to merge 1 commit into
xintaofei:mainfrom
MoozLee:feat/cc-agent-custom-compaction
Open

feat(acp): honor Claude auto-compaction window#237
MoozLee wants to merge 1 commit into
xintaofei:mainfrom
MoozLee:feat/cc-agent-custom-compaction

Conversation

@MoozLee

@MoozLee MoozLee commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Forward CLAUDE_CODE_AUTO_COMPACT_WINDOW into Claude Code ACP session metadata as options.settings.autoCompactWindow.
  • Preserve existing Claude Code user SDK options from ~/.claude/settings.json while merging the configured auto-compaction window.
  • Add the Claude 1M context beta when the configured auto-compaction window exceeds the default 200K window.
  • Update the status bar context-window display to prefer the configured Claude Code auto-compaction limit, so the shown denominator and percentage match the effective compaction threshold.

Test plan

  • cargo test --manifest-path src-tauri/Cargo.toml --lib build_new_session_request
  • cargo test --manifest-path src-tauri/Cargo.toml --lib build_load_session_request
  • cargo check --manifest-path src-tauri/Cargo.toml
  • cargo check --manifest-path src-tauri/Cargo.toml --bin codeg-server --no-default-features
  • pnpm eslint src/components/layout/status-bar-tokens.tsx

摘要

  • CLAUDE_CODE_AUTO_COMPACT_WINDOW 写入 Claude Code ACP 会话 metadata,即 options.settings.autoCompactWindow
  • 合并自定义压缩窗口时,保留 ~/.claude/settings.json 中已有的 Claude Code 用户 SDK options。
  • 当配置的自动压缩窗口超过默认 200K 时,自动追加 Claude 1M context beta。
  • 更新状态栏上下文窗口展示逻辑,优先使用 Claude Code 配置的自动压缩窗口,保证展示的分母和百分比与实际压缩阈值一致。

测试计划

  • cargo test --manifest-path src-tauri/Cargo.toml --lib build_new_session_request
  • cargo test --manifest-path src-tauri/Cargo.toml --lib build_load_session_request
  • cargo check --manifest-path src-tauri/Cargo.toml
  • cargo check --manifest-path src-tauri/Cargo.toml --bin codeg-server --no-default-features
  • pnpm eslint src/components/layout/status-bar-tokens.tsx

Forward the configured Claude Code auto-compaction limit into ACP session metadata and show the effective limit in the status bar.
@xintaofei

Copy link
Copy Markdown
Owner

Thanks for this — and sorry for the slow review. 🙏

I dug into it properly (including disassembling the bundled Claude engine to check the actual precedence rules), and I want to say up front: you found the right lever. That part is genuinely good work:

  • _meta.claudeCode.options really is the adapter's official pass-through into SDK options (claude-agent-acp dist/acp-agent.js: const userProvidedOptions = sessionMeta?.claudeCode?.options → spread into query({ options })).

  • The 100_000..=1_000_000 bound isn't arbitrary — it matches the engine's own zod schema exactly: autoCompactWindow: z.number().int().min(1e5).max(1e6).

  • Most importantly, betas: ["context-1m-2025-08-07"] is the thing that actually makes this work. The engine computes the window as:

    // detected window
    if (betas?.includes(ONE_M_HEADER) && modelSupports1M(model)) return 1_000_000;
    ...
    return 200_000;
    // effective threshold
    return { window: Math.min(detected, configured), ... }

    Without the beta, a configured 1M gets clamped straight back to 200K. A lot of people miss that; you didn't.

So the direction is right. But I don't think it's ready to land yet — here's what I found.


Blocking

1. It no longer merges, and the frontend file is gone.

The PR is from early June and main has moved a fair bit. git merge-tree main <pr> conflicts in both files, and src/components/layout/status-bar-tokens.tsx doesn't exist anymore — "move context usage and connection status below the composer" renamed and rewrote it as src/components/chat/composer-context-usage.tsx. The frontend half needs to be re-authored against that file, not just rebased.

2. The session/resume path is missed.

main now has three request builders, not two: build_new_session_request, build_load_session_request, and build_resume_session_request. claude-agent-acp 0.64 advertises resume: {} in agentCapabilities, and run_connection prefers session/resume whenever it's advertised (falling back to session/load only on error). So after a rebase, a brand-new session would carry the window but a reconnected one silently wouldn't — and reconnect is the more common path.

3. The settings.autoCompactWindow half is shadowed by the very env var it reads from.

The engine resolves the window like this:

function resolveAutoCompactWindow(model, settingsValue) {
  const detected = detectWindow(model, betas);
  if (process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW) {
    ...
    return { window: Math.min(detected, c), configured: c, source: "env" };   // ← returns here
  }
  if (settingsValue !== undefined)
    return { window: Math.min(detected, settingsValue), configured: settingsValue, source: "settings" };
  ...
}

The env var is checked first and short-circuits; the engine's own /config UI even says "CLAUDE_CODE_AUTO_COMPACT_WINDOW is set and takes precedence. Unset it to change this setting here."

And that env var is already reaching the engine without this PR: codeg puts it in runtime_envmerge_agent_env → the adapter's process env, and the adapter passes env: { ...process.env, ... } to the SDK, which hands it to the CLI. Since the PR derives the setting from that same env var, the two are always both present and the env one always wins. (Strictly, the only way the settings value could become active is if something separately unset the env var via options.env — which nothing here does.)

Net effect: only the betas injection actually changes behavior. The settings write, read_claude_code_user_options, merge_json_objects and the whole options-merge (~100 lines + 2 tests) is machinery around a value that never gets read.

4. read_claude_code_user_options is built on a key that doesn't exist.

~/.claude/settings.json has no _meta.claudeCode.options in its schema (it's env / permissions / model / hooks / statusLine / enabledPlugins…), and nothing in codeg or Claude Code writes it. So "preserve existing user SDK options" is inert unless someone hand-writes an undocumented key.

It's also unnecessary on its own terms: SDK settings is --settings-equivalent — a highest-precedence additional layer — and the adapter already sets settingSources: ["user", "project", "local"], so user settings were never at risk of being clobbered.

Two things it does cost:

  • If that key ever were populated, it's an unfiltered whole-object pass-through into SDK options. ...userProvidedOptions is spread after systemPrompt and settingSources, so those become overridable, and options.resume is read by the adapter to decide session reuse.
  • Supplying settings at all makes the adapter drop its CLAUDE_MODEL_CONFIG-derived modelOverrides / availableModels:
    ...(!userProvidedOptions?.settings && modelConfig && { settings: { ...modelOverrides, ...availableModels } })

5. Production code reads CODEG_TEST_CLAUDE_SETTINGS_PATH out of user-editable env.

read_claude_code_user_options takes its path from runtime_env, which is the per-agent env_json the user edits in Settings. Every other CODEG_TEST_* in the codebase lives inside #[cfg(test)]. Worth passing the home dir / path in as a parameter instead, or pinning HOME with temp_env in the test the way the existing dirs::home_dir tests do.

6. The status bar can end up showing a number that isn't true.

The engine's effective threshold is min(detected, configured) and it renders · capped to <N> by model when they differ. The PR does:

const contextMax = configuredAutoCompactWindow ?? liveContextMax ?? sessionStats?. ?? null

so the configured value unconditionally beats the window the agent actually reports. When the model doesn't support 1M, or a gateway falls back to 200K, the bar reads x / 1M with a percentage about a fifth of reality — exactly when an accurate number matters most.

To be clear, the motivation is right — with a 1M model and a 300K configured window, today's bar shows /1M while compaction fires at 300K, and your change fixes that. I'd just take the min: min(configured, liveMax), falling back to configured when liveMax is absent.

7. The beta header goes out unconditionally.

autoCompactWindow > 200_000 triggers appending context-1m-2025-08-07, but that beta is a no-op for models that don't support 1M, and some third-party / self-hosted Anthropic-compatible gateways reject unknown beta headers outright — and custom ANTHROPIC_BASE_URL is a big part of this project's usage. There's no opt-out here; the engine's escape hatch (CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1) is something a user would have to find on their own. Deriving "send a beta header" from "compaction window size" is also a surprising, undocumented coupling.


Non-blocking notes

  • The 100k–1M rule + parsing is written twice (Rust + TS) with no shared source of truth, and the two read different things — frontend reads agents[].env, backend reads runtime_env after several layering steps. They agree today; they'll drift.
  • ClaudeAutoCompactWindowConfig wraps a single Option<i64> — returning Option<i64> is equivalent. merge_json_objects is a general recursive merge with exactly one fixed-patch caller.
  • fs::read_to_string is blocking I/O on the async session-creation path.
  • The tests use std::env::temp_dir() + a trailing remove_dir_all (leaks on panic); tempfile is already a dependency.
  • Verification is thinner than the repo gate in CLAUDE.md — no cargo clippy --all-targets --features test-utils -- -D warnings, no pnpm test, no pnpm build, and no frontend test (there's a status-bar-update.test.tsx next door as precedent).
  • No UI or docs — it's a raw env var typed into the agent env editor, and out-of-range values are dropped with no feedback.

What I'd suggest

I'd love to land the core of this. The smallest version that works:

  1. Rebase onto current main.
  2. Keep only the beta injection; drop the settings merge and the ~/.claude/settings.json read — the env var already does the rest.
  3. Wire all three request builders, including build_resume_session_request.
  4. Re-do the frontend on composer-context-usage.tsx, using min(configured, liveMax).
  5. Give the beta an opt-out (or gate it on the model), so custom gateways aren't broken by a compaction setting.
  6. Run the full check gate.

Happy to take it in pieces if that's easier — even just steps 1–3 would be a good standalone PR. Thanks again for digging into this corner; it's a real gap and you clearly did the homework on it. 🙌


中文摘要

结论:方向和核心杠杆都抓对了betas: ["context-1m-2025-08-07"] 确实是唯一能把检测窗口抬到 1M 的开关,没有它 Math.min(detected, configured) 会把 1M 钳回 200K;100_000..=1_000_000 也和引擎自带的 zod schema 完全一致)。但目前还不能合。

阻塞项:

  1. 已冲突且前端文件不存在了 —— status-bar-tokens.tsx 已被重构为 src/components/chat/composer-context-usage.tsx,前端这半边需要重写。
  2. 漏了 session/resume —— main 现在有三个 builder,adapter 广告了 resume 能力且 codeg 优先走它,rebase 后"重连老会话"会静默丢配置。
  3. settings.autoCompactWindow 被同一个 env 盖住 —— 引擎里 CLAUDE_CODE_AUTO_COMPACT_WINDOW 严格优先并直接 return,而这个 env 本来就已经通过 runtime_env → merge_agent_env → adapter 进程 env → SDK 到了引擎。真正改变行为的只有 betas
  4. read_claude_code_user_options 的前提不成立 —— ~/.claude/settings.json 里没有 _meta.claudeCode.options 这个键,也没人写它;而且 SDK 的 settings 等价 --settings(叠加层,优先级最高),adapter 又已设 settingSources: ["user","project","local"],本来就不会覆盖用户设置。副作用:传了 settings 会让 adapter 丢弃 CLAUDE_MODEL_CONFIG 派生的 modelOverrides/availableModels
  5. 生产代码读 CODEG_TEST_CLAUDE_SETTINGS_PATH —— 而且是从用户可编辑的 env_json 里读。
  6. 状态栏会显示不真实的分母 —— 引擎实际阈值是 min(detected, configured),PR 让 configured 无条件压过 agent 上报值;模型不支持 1M 或网关回退到 200K 时会显示 x / 1M、百分比只有真实值的约 1/5。改成取 min 即可(反方向的动机是对的,这个体验问题确实存在)。
  7. beta 头无条件下发 —— 对不支持 1M 的模型无意义,对部分第三方/自建网关会直接 4xx,且没有关闭开关。

建议的最小形态:rebase → 只保留 beta 注入 → 三个 builder 全接上(含 resume)→ 前端在 composer-context-usage.tsx 上用 min(configured, liveMax) 重做 → beta 加开关 → 跑全量检查。拆成多个 PR 也完全没问题,先做 1–3 就很有价值。辛苦了!🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants