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 修复
This commit is contained in:
unraid
2026-04-22 16:07:42 +08:00
parent 711927f01b
commit 95fece4b51
316 changed files with 39611 additions and 14298 deletions

View File

@@ -0,0 +1,350 @@
# 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
```