Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # Claude Code 缓存 Bug 验证报告
- ## 验证环境
- - 当前系统 Claude Code: v2.1.84 (ELF standalone, 228MB, Bun v1.3.11 commit 759ce802)
- - 对比版本: v2.1.68 (237MB), v2.1.69 (235MB)
- - 分析方法: 二进制静态分析 (strings/objdump/readelf), JS bundle 提取对比, 模拟哈希计算
- ---
- ## Bug 1: Sentinel Replacement (cch=00000)
- ### analyze.md 的声明
- > standalone 二进制中存在 native-layer (Zig) 字符串替换,在 HTTP 请求发送前扫描 JSON body,
- > 将第一个 `cch=00000` 替换为基于 body 哈希的 5 位 hex。当对话内容包含该 sentinel 时,
- > 替换命中 messages[] 而非 system[],导致缓存失效。
- ### 验证结果: 部分验证 / 核心声明无法从静态分析确认
- #### 已确认的部分
- 1. **`cch=00000` sentinel 确实存在** — 在三个版本(v2.1.68/69/84)中均出现 3 次,全部位于 `.bun` JS bundle 内:
- ```
- L = " cch=00000;" // 硬编码在 attribution header builder 中
- ```
- 2. **Attribution header 的实际用途是 system prompt block** — 并非 HTTP header,而是作为 system[0] 的文本内容注入:
- ```javascript
- // 函数 M0$() 构建如下字符串:
- `x-anthropic-billing-header: cc_version=${VERSION}.${hash}; cc_entrypoint=${entrypoint}; cch=00000;`
- // 该字符串被 push 到 system prompt blocks,带有 cacheScope: null
- ```
- 3. **cc_version 哈希计算机制已确认** — 使用 SHA-256 从第一条 user message 的第 4、7、20 位字符计算:
- ```javascript
- function sGA(H, $) {
- let L = [4, 7, 20].map((_) => H[_] || "0").join("");
- let D = `59cf53e54c78${L}${$}`; // salt + chars + version
- return crypto.createHash("sha256").update(D).digest("hex").slice(0, 3);
- }
- ```
- 4. **billing header 使用 `cacheScope: null`** — 意味着该 block 本身不设缓存断点,但其内容变化仍会破坏后续 block 的前缀缓存。
- #### 无法确认的部分
- 5. **native-layer (Zig) 字符串替换:未找到证据**
- - 搜索整个 native code section (0x00000000 - 0x06568000),`cch=` 模式出现 **0 次**
- - 二进制符号表中无 HTTP body scanning 或 sentinel replacement 相关函数
- - `wyhash` 存在于 native code(标准 Bun 功能),但无证据表明用于 sentinel 替换
- - 嵌入的 Bun v1.3.11 (759ce802) 可能是定制 fork,但静态分析无法证实 HTTP 层是否被修改
- 6. **"5-char hex derived from hashing the body" 的声明与 JS 代码不一致**
- - JS 代码中的哈希是 3 字符 (`hex.slice(0,3)`),用于 `cc_version` 后缀
- - `cch=00000` 的 `00000` 是 5 个零,若被替换应是 5 字符 hex
- - 两者机制不同,可能确实存在 native 层额外替换 `00000` 的逻辑,但无法通过静态分析验证
- ### Bug 1 结论
- JS 层的 attribution header 和 billing hash 机制完全可验证。但 **native-layer 替换** 这一核心声明在二进制静态分析中找不到直接证据。验证此声明需要运行时流量捕获 (MITM proxy) 或 Ghidra 深度反编译。
- ---
- ## Bug 2: --resume 每次都破坏缓存 (since v2.1.69)
- ### analyze.md 的声明
- > v2.1.69 引入 deferred_tools_delta,导致 fresh session 和 resume session 的 messages[0] 内容不同。
- > 三个独立的缓存破坏因素: messages[0] 大小差异、system[0] billing hash 变化、cache_control 断点位置变化。
- ### 验证结果: 强烈确认 ✓
- #### 版本对比数据
- | 指标 | v2.1.68 | v2.1.69 | v2.1.84 |
- |------|---------|---------|---------|
- | `deferred_tools_delta` 出现次数 | **0** | **14** | **16** |
- | `cch=00000` 出现次数 | 3 | 3 | 3 |
- | `[4,7,20]` 哈希计算 | 2 | 2 | 2 |
- | `cacheScope:null` | 10 | 10 | — |
- | `cacheScope:"org"` | 8 | 8 | — |
- #### 确认的机制
- 1. **`deferred_tools_delta` 在 v2.1.69 中引入** — v2.1.68 中完全不存在该字符串(grep count = 0),v2.1.69 中出现 14 次。这完全吻合 analyze.md 的声明。
- 2. **`deferred_tools_delta` 的实际功能** — 列出通过 ToolSearch 可用的 deferred tools:
- ```javascript
- case "deferred_tools_delta": {
- if (H.addedLines.length > 0)
- A.push(`The following deferred tools are now available via ToolSearch:\n${H.addedLines.join('\n')}`);
- // ...
- return qM([AA({content: A.join('\n\n'), isMeta: true})]);
- }
- ```
- 3. **messages[0] 内容差异已验证** — 通过哈希计算模拟:
- - Fresh session: 第一条 user message 包含 system-reminders (~13KB),chars at [4,7,20] = 例如 `t`, `-`, `n`
- - Resume session: 第一条 user message 仅包含 AU$ context (~352B),chars at [4,7,20] = 例如 `s`, `e`, `t`
- - 计算结果: Fresh hash = `360`, Resume hash = `153` — **完全不同**
- 4. **cc_version 差异导致 system[0] 内容变化**:
- - Fresh: `cc_version=2.1.84.360`
- - Resume: `cc_version=2.1.84.153`
- - system[0] (billing header) 内容改变 → 后续所有 block 的缓存前缀失效
- 5. **cache_control 断点位置移动** — 在 fresh session 中,`cache_control` 设置在 messages[0] 上;resume 时,由于 messages[0] 内容不同,断点实际上无法匹配。
- #### 根因分析
- 在 fresh session 中,所有 system-reminder 附件(包括 deferred_tools_delta、MCP instructions、skills list)在第一次 API 调用前被注入到 messages[0](第一条 user message)中。
- 在 resume session 中,对话历史从磁盘加载,messages[0] 保持原始内容(仅有 AU$ context)。新计算的 deferred_tools_delta 被追加到最新的 message position(messages[N]),而不是 messages[0]。
- 这导致:
- - messages[0] 的文本内容不同 → cc_version billing hash 不同 → system[0] 内容不同
- - 整个缓存前缀失效,~500k tokens 全部重建
- ### Bug 2 结论
- **完全确认**。analyze.md 的描述准确无误:
- - `deferred_tools_delta` 确实在 v2.1.69 引入
- - resume 确实导致 messages[0] 内容差异
- - 差异确实通过 billing hash ([4,7,20] 位置) 传播到 system[0]
- - 这确实导致整个缓存前缀失效
- ---
- ## 总结
- | Bug | 状态 | 置信度 |
- |-----|------|--------|
- | Bug 1 (sentinel replacement) | JS 层机制确认;native 层替换无静态证据 | 60% |
- | Bug 2 (--resume cache break) | **完全确认** | **95%** |
- ---
- ## 补充分析: 直接 API 调用 vs Claude Code SDK 是否受影响
- ### 直接使用 Anthropic API SDK (Python/JS)
- **不受影响。**
- 直接使用 `anthropic` Python 或 JS SDK 调用 Claude 模型的用户完全不受此 bug 影响。原因:
- - SDK 用户自行构建 `messages[]` 和 `system[]` 数组
- - 不存在 `deferred_tools_delta` 附件注入
- - 不存在 `cch=00000` sentinel 或 billing hash 机制
- - 缓存策略由用户通过 `cache_control` 参数完全控制
- ### 使用 Claude Code Agent SDK (@anthropic-ai/claude-code)
- **受影响。** 代码流分析显示:
- 1. **共享代码路径**: `DQ_` 函数负责计算所有 turn 的附件(包括 `deferred_tools_delta`),**无 `isNonInteractive` 条件判断**:
- ```javascript
- // 附件计算函数 - CLI 和 SDK 共用
- async function DQ_(H, $, A, L, D, f) {
- if (CLAUDE_CODE_DISABLE_ATTACHMENTS || CLAUDE_CODE_SIMPLE) return [];
- // ... 没有 isNonInteractive 检查 ...
- gK("deferred_tools_delta", () => Promise.resolve(CV$(...))),
- // ...
- }
- ```
- 2. **billing hash 也在共享路径**: `ixD(Z)` → `M0$(k)` 链条在 CLI 和 SDK 中都执行:
- ```javascript
- // system prompt 构建 - 同时传入 isNonInteractiveSession
- $ = G9([M0$(k), K0$({isNonInteractive: f.isNonInteractiveSession, ...}), ...])
- ```
- `isNonInteractive` 仅影响 identity 字符串选择("You are Claude Code..." vs "You are a Claude agent..."),**不跳过 billing hash 计算**。
- 3. **`resumeSession` 在 SDK 中存在** (23 处引用),意味着 SDK 用户使用 resume 功能时同样会触发 Bug 2。
- ### API Key vs OAuth 登录
- 在 Claude Code 中配置 API key(而非 OAuth 登录)**同样受影响**。代码分析确认:
- - `vL()` 对 API key 和 OAuth 用户都返回 `"firstParty"`(都请求 api.anthropic.com)
- - `rX6()` (控制 attribution header 生成) 不区分认证方式
- - `DQ_()` (附件计算,包括 `deferred_tools_delta`) 不区分认证方式
- - `VUH()` (`deferred_tools_delta` 的特性开关) 是服务端 feature flag,非认证相关
- 因此,**无论 API key 还是 OAuth,在 Claude Code 中运行的代码路径完全相同**。Bug 2 对 API key 用户同样生效。
- 实际上,对 API key 用户影响**更大**:因为 API key 按 token 计费,缓存失效直接增加费用(`cache_creation` \$0.30/MTok vs `cache_read` \$0.03/MTok,10x 差异)。
- ### 结论
- | 使用方式 | Bug 1 影响 | Bug 2 影响 |
- |----------|-----------|-----------|
- | 直接使用 Anthropic SDK (不经过 Claude Code) | ❌ 不受影响 | ❌ 不受影响 |
- | Claude Code CLI (OAuth 登录) | ⚠️ 可能受影响 | ✅ 受影响 |
- | Claude Code CLI (API key) | ⚠️ 可能受影响 | ✅ 受影响,且费用影响更直接 |
- | Claude Code Agent SDK | ⚠️ 可能受影响 | ✅ 受影响 (使用 resume 时) |
- ---
- ### 进一步验证建议
- - **Bug 1**: 使用 mitmproxy 捕获 standalone 二进制 vs npm 版本的实际 HTTP 请求,对比 `cch=` 字段是否被替换
- - **Bug 2**: 无需进一步验证,代码证据充分
Add Comment
Please, Sign In to add comment