Files
claude-code/docs/bugs/cached-microcompact-issues.md
unraid 95fece4b51 feat: 整合功能恢复与技能学习闭环(含 ECC v2.1 parity + Opus 4.7 接入 + prompt 工程优化)
主要变更:
- Skill Learning 闭环系统 (9/9 AC)
- Opus 4.7 模型层接入 + adaptive thinking
- Prompt 工程优化 (64 审计测试)
- Agent Teams 简化门控 (默认启用)
- Windows Terminal 后端修复 (EncodedCommand/WT_SESSION)
- TF-IDF 技能搜索精准化 (字段加权/CJK 优化)
- Autonomy 系统 (/autonomy 命令)
- ACP 协议完整实现
- mock.module 泄漏修复 (CI 全绿)
- 152+ lint/type 修复
2026-04-22 16:07:42 +08:00

351 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Bug: cachedMicrocompact 缓存编辑实现存在 5 个问题
## 背景
分支 `chore/lint-cleanup``src/services/compact/cachedMicrocompact.ts` 从全 stubno-op改为真实实现。该模块负责 Cached Microcompact缓存编辑功能在对话过程中通过 API 的 `cache_edits` 机制删除旧的 tool result避免重新发送完整 prompt 前缀,从而节省 token 和成本。
当前因问题 3 和问题 4 的阻断,这些 Bug 在运行时不会触发。但一旦启用 feature flag问题 1 会立即暴露。
---
## 问题 1`deletedRefs` 从未被填充(关键 Bug
### 严重级别CRITICAL
### 问题描述
`getToolResultsToDelete()` 返回待删除的 tool ID 列表,但**既不在函数内部,也不在调用方 `cachedMicrocompactPath()` 中**将这些 ID 添加到 `state.deletedRefs`
### 涉及文件
| 文件 | 行号 | 角色 |
|------|------|------|
| `src/services/compact/cachedMicrocompact.ts` | 87-93 | `getToolResultsToDelete` — 返回待删除 ID但不更新 `deletedRefs` |
| `src/services/compact/microCompact.ts` | 332-339 | `cachedMicrocompactPath` — 调用 `getToolResultsToDelete` 后不更新 `deletedRefs` |
| `src/services/compact/__tests__/cachedMicrocompact.test.ts` | 78-92 | 测试用例**手动**填充 `deletedRefs`,掩盖了生产代码中的缺失 |
### 当前代码
`cachedMicrocompact.ts:87-93`
```typescript
export function getToolResultsToDelete(state: CachedMCState): string[] {
const { triggerThreshold, keepRecent } = getCachedMCConfig()
const active = state.toolOrder.filter(id => !state.deletedRefs.has(id))
if (active.length <= triggerThreshold) return []
const toDelete = active.slice(0, active.length - keepRecent)
return toDelete
// ← 缺失:没有将 toDelete 添加到 state.deletedRefs
}
```
`microCompact.ts:332-339`(调用方):
```typescript
const toolsToDelete = mod.getToolResultsToDelete(state)
if (toolsToDelete.length > 0) {
const cacheEdits = mod.createCacheEditsBlock(state, toolsToDelete)
if (cacheEdits) {
pendingCacheEdits = cacheEdits
}
// ← 缺失:没有将 toolsToDelete 标记为已删除
}
```
### 后果
1. **重复删除**:每次 API 调用都会重复返回相同的 tool ID 进行删除
2. **统计失真**`activeToolCount` 计算为 `state.toolOrder.length - state.deletedRefs.size`,但 `deletedRefs.size` 永远为 0
3. **API 浪费**:重复的 `cache_edits` 请求增加请求体大小
### 测试文件如何掩盖此问题
`__tests__/cachedMicrocompact.test.ts:78-92`
```typescript
test('already deleted tools are not suggested again', () => {
// ... 注册 12 个 tool
const first = getToolResultsToDelete(state)
// 测试手动模拟删除——生产代码中没有等价操作
for (const id of first) {
state.deletedRefs.add(id) // ← 只在测试中手动做了
}
const second = getToolResultsToDelete(state)
// 验证不会重复建议——但前提是 deletedRefs 被正确填充
})
```
### 修复方案
**方案 A推荐在 `getToolResultsToDelete` 内部标记**
`cachedMicrocompact.ts`
```typescript
export function getToolResultsToDelete(state: CachedMCState): string[] {
const { triggerThreshold, keepRecent } = getCachedMCConfig()
const active = state.toolOrder.filter(id => !state.deletedRefs.has(id))
if (active.length <= triggerThreshold) return []
const toDelete = active.slice(0, active.length - keepRecent)
// 标记为已删除,防止下次重复返回
for (const id of toDelete) {
state.deletedRefs.add(id)
}
return toDelete
}
```
**方案 B在调用方标记**
`microCompact.ts``cachedMicrocompactPath` 中:
```typescript
const toolsToDelete = mod.getToolResultsToDelete(state)
if (toolsToDelete.length > 0) {
// 标记已删除
for (const id of toolsToDelete) {
state.deletedRefs.add(id)
}
const cacheEdits = mod.createCacheEditsBlock(state, toolsToDelete)
// ...
}
```
**推荐方案 A**:将副作用收敛在模块内部,调用方不需要关心内部状态管理。
### 测试修复
现有测试的手动 `deletedRefs.add` 应该被删除,改为验证 `getToolResultsToDelete` 自动填充:
```typescript
test('already deleted tools are not suggested again', () => {
for (let i = 0; i < 12; i++) {
registerToolResult(state, `tool-${i}`)
}
const first = getToolResultsToDelete(state)
// 不需要手动 add — getToolResultsToDelete 应该已经标记了
expect(first.length).toBeGreaterThan(0)
for (const id of first) {
expect(state.deletedRefs.has(id)).toBe(true)
}
const second = getToolResultsToDelete(state)
for (const id of first) {
expect(second).not.toContain(id)
}
})
```
---
## 问题 2两个同名 `getCachedMCConfig` 导出,签名冲突
### 严重级别MEDIUM
### 问题描述
两个不同文件导出同名函数 `getCachedMCConfig`,但类型签名和用途完全不同:
| 文件 | 返回类型 | 用途 | 调用方 |
|------|----------|------|--------|
| `cachedMCConfig.ts`stub | `{ enabled?, systemPromptSuggestSummaries?, supportedModels?, [key: string]: unknown }``{}` | 系统 prompt 配置 | `prompts.ts:70` |
| `cachedMicrocompact.ts`(新实现) | `{ triggerThreshold: 10, keepRecent: 5 }` | 微压缩阈值配置 | `claude.ts:1212``microCompact.ts:311` |
### 后果
1. **命名混淆**:同一个名字在不同上下文意味完全不同的东西
2. **`claude.ts:1226` 读取不存在的字段**
```typescript
const config = getCachedMCConfig() // 从 cachedMicrocompact.ts 导入
logForDebugging(
`... supportedModels=${jsonStringify((config as Record<string, unknown>).supportedModels)}`
// ^^^^^^^^^^^^^^^^ 新实现中不存在此字段,永远输出 undefined
)
```
### 修复方案
将 `cachedMicrocompact.ts` 中的函数重命名为 `getCachedMicrocompactConfig`,或将 `cachedMCConfig.ts` 的重命名为 `getCachedMCFeatureConfig`,消除歧义。同步更新所有调用方。
---
## 问题 3`CACHE_EDITING_BETA_HEADER` 为空字符串——当前分支已修复(三层防御)
### 严重级别:~~HIGH~~ → **已修复INFO**
### 原始问题
`src/constants/betas.ts:50`
```typescript
export const CACHE_EDITING_BETA_HEADER: string = '';
```
上游origin/main的代码中`cacheEditingHeaderLatched` 为 `true` 时会无条件 push 空字符串到 betas 数组,导致 API 请求中出现无效的 `anthropic-beta` header如 `"a,b,"` 或 `"a,,b"`),触发 API 400 错误。
### 当前分支的三层修复
当前分支已包含完整的三层防御,通过 `git diff origin/main HEAD -- src/services/api/claude.ts` 可以确认:
**第 1 层:`cachedMCEnabled` 入口增加 `headerAvailable` 检查**
`claude.ts:1218-1223`(本分支新增):
```typescript
// cachedMC requires a non-empty beta header; the CACHE_EDITING_BETA_HEADER
// constant is '' in this fork (upstream hasn't published the real value).
// Without it, cache_reference and cache_edits in the request body cause
// API 400: "tool_result.cache_reference: Extra inputs are not permitted".
const headerAvailable = !!cacheEditingBetaHeader
cachedMCEnabled = featureEnabled && modelSupported && headerAvailable
```
上游原始代码为:`cachedMCEnabled = featureEnabled && modelSupported`(无 header 检查)。
**第 2 层latch push 增加 truthy 检查**
`claude.ts:1731-1732`(本分支新增 `cacheEditingBetaHeader &&`
```typescript
if (
cacheEditingHeaderLatched &&
cacheEditingBetaHeader && // ← 本分支新增:空字符串不 push
getAPIProvider() === 'firstParty' &&
options.querySource === 'repl_main_thread' &&
!betasParams.includes(cacheEditingBetaHeader)
) {
betasParams.push(cacheEditingBetaHeader)
}
```
上游原始代码缺少 `cacheEditingBetaHeader &&` 这行,导致 latch 生效时空字符串被 push。
**第 3 层:最终过滤(兜底防御)**
`claude.ts:1749-1753`(本分支新增):
```typescript
// Filter out any empty-string beta headers before sending.
// Constants like CACHE_EDITING_BETA_HEADER or AFK_MODE_BETA_HEADER
// can be '' when their feature gate is off; an empty string in the
// betas array produces an invalid anthropic-beta header (400 error).
const filteredBetas = betasParams.filter(Boolean)
lastRequestBetas = filteredBetas
```
上游原始代码直接 `lastRequestBetas = betasParams`,无过滤。
### 测试覆盖
`src/services/api/__tests__/betaHeaders.test.ts` 包含完整的验证:
| 测试 | 验证点 |
|------|--------|
| `known potentially-empty constants are identified` | 确认 `CACHE_EDITING_BETA_HEADER === ''`Boolean 检查为 false |
| `truthy check correctly gates empty beta headers` | 模拟 truthy 检查阻止空 header push |
| `simulates full header pipeline with all fixes` | 模拟三层防御完整管道,验证空 header 不泄漏 |
| `simulates the bug scenario WITHOUT fix` | 重现修复前 bug空字符串被 push → `toString()` 产生无效逗号 |
| `useBetas flag correctly handles empty-after-filter` | 验证全部 betas 为空时 filter 后不发送 |
### 当前状态
**此问题已完全修复,无需额外操作。** 当 Anthropic 公开 cache editing 的 beta header 值后,只需更新 `betas.ts:50` 的常量值即可,三层防御逻辑无需改动。
---
## 问题 4Feature Flag 未注册(当前为死代码)
### 严重级别INFO
### 问题描述
`CACHED_MICROCOMPACT` 不在 `build.ts` 或 `scripts/defines.ts` 的 feature 列表中。
当前 build 默认 features19 个):
```
BUDDY, TRANSCRIPT_CLASSIFIER, BRIDGE_MODE, AGENT_TRIGGERS_REMOTE,
CHICAGO_MCP, VOICE_MODE, SHOT_STATS, PROMPT_CACHE_BREAK_DETECTION,
TOKEN_BUDGET, AGENT_TRIGGERS, ULTRATHINK, BUILTIN_EXPLORE_PLAN_AGENTS,
LODESTONE, EXTRACT_MEMORIES, VERIFICATION_AGENT, KAIROS_BRIEF,
AWAY_SUMMARY, ULTRAPLAN, DAEMON
```
`CACHED_MICROCOMPACT` 不在其中。`feature('CACHED_MICROCOMPACT')` 在构建和 dev 模式下都返回 `false`。
### 后果
`cachedMicrocompact.ts` 的所有真实实现是不可达代码。`cachedMicrocompactPath` 永远不会被执行。
### 修复方案
这是设计选择而非 Bug。当问题 1 和问题 3 修复后,可以将 `CACHED_MICROCOMPACT` 添加到 build defines 的 P1 或 P2 列表中启用。
---
## 问题 5`isModelSupportedForCacheEditing` 正则过于宽泛
### 严重级别LOW
### 问题描述
`cachedMicrocompact.ts:34`
```typescript
export function isModelSupportedForCacheEditing(model: string): boolean {
return /claude-[a-z]+-4[-\d]/.test(model)
}
```
该正则匹配任何 Claude 4.x 模型,包括 `claude-haiku-4-5`。但 cache editing 是 API 层面的特殊功能,可能只有 Opus/Sonnet 支持Haiku 未必支持。
### 后果
如果 Haiku 不支持 cache editing在 Haiku 模型下启用此功能会导致 API 错误。
### 修复方案
根据 API 文档精确限定支持的模型:
```typescript
export function isModelSupportedForCacheEditing(model: string): boolean {
return /claude-(opus|sonnet)-4[-\d]/.test(model)
}
```
或者在上游明确支持的模型列表可用后,改为白名单匹配。
---
## 修复优先级
| 优先级 | 问题 | 状态 | 原因 |
|--------|------|------|------|
| P0 | 问题 1`deletedRefs` 未填充 | **待修复** | 启用后立即导致重复删除的逻辑 Bug |
| ~~P1~~ | ~~问题 3beta header 为空~~ | **已修复** ✓ | 当前分支已包含三层防御 + 测试覆盖 |
| P2 | 问题 2同名函数冲突 | **待修复** | 增加维护混淆风险 |
| P3 | 问题 4feature flag 未注册 | **设计选择** | 问题 1 修复后可按需启用 |
| P3 | 问题 5正则过宽 | **待确认** | 低风险,待 API 文档确认 |
## 验证步骤
### 问题 1 修复后验证
```bash
# 运行现有测试(应该在修复 getToolResultsToDelete 后仍然通过)
bun test src/services/compact/__tests__/cachedMicrocompact.test.ts
# 新增测试验证getToolResultsToDelete 自动填充 deletedRefs
# 1. 注册 12 个 tool
# 2. 调用 getToolResultsToDelete → 返回 7 个
# 3. 验证 state.deletedRefs.size === 7
# 4. 再次调用 getToolResultsToDelete → 返回 0因为 active 只剩 5 个,低于阈值 10
```
### 问题 3 修复后验证
```bash
# 设置环境变量启用缓存编辑
FEATURE_CACHED_MICROCOMPACT=1 CLAUDE_CACHED_MICROCOMPACT=1 bun run dev
# 观察 debug 日志中的 Cached MC gate 输出
# 确认 headerAvailable=true需要 beta header 有值)
# 确认 cachedMCEnabled=true
```
### 全流程验证
```bash
# 完整测试
bun test src/services/compact/__tests__/cachedMicrocompact.test.ts
bun run typecheck
bun run test:all
```