Files

110 lines
7.7 KiB
Markdown
Raw Permalink Normal View History

2026-06-09 23:15:17 +08:00
---
tags:
- OOM
- 内存溢出
- 缓存治理
create time: 2026-06-09 22:30
---
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
# Skill Search / Skill Learning 溢出 Bug 修复报告
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
## 概述
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
审计发现 `EXPERIMENTAL_SKILL_SEARCH` 和 `SKILL_LEARNING` 子系统存在 5 个无界模块级状态,与 autonomy OOM 属于同类 bug。修复方案采用 FIFO/LRU 淘汰 + 运行时双层门控,默认关闭实验特性直到运维人员显式启用。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
## 正文
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
### 问题背景
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
- **Flow id**: `recurring-bug-skill-overflow`(与 `recurring-bug-loop-oom` 为姊妹审计)
- **分支**: `fix/loop-scheduled-autonomy-oom`(已合并到 OOM PR,使用相同的 audit-and-cap 模式)
- **触发**: 合并 autonomy OOM 修复后,发现相邻的 `EXPERIMENTAL_SKILL_SEARCH` 和 `SKILL_LEARNING` 子系统存在同类无界模块级状态。用户明确要求进行 `肯定也有同类溢出` 审计。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
### 问题分析
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
autonomy OOM 来自无界模块级状态(运行记录、调度队列、心跳时间戳)随进程生命周期不断增长。skill search + skill learning 子系统在 **5 个模块级 Map/Set** 上表现出相同类型的 bug,其中只有一个在 `scripts/defines.ts` 中有文档记录("projectContext cache 无淘汰机制(非 GB 级主因)")。
这些 bug 之前是潜伏的,原因如下:
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
- `EXPERIMENTAL_SKILL_SEARCH` / `SKILL_LEARNING` 在 `DEFAULT_BUILD_FEATURES` 中默认启用,但测试通过是因为它们只走短路径
- 这些无界缓存不是按每次工具调用增长,而是按**不同查询 / 不同 cwd / 不同 skill 名称 / 不同 gap 信号 / 不同 promotion** 增长,相对于会话长度是亚线性的,但永远单调递增
- 长时间运行的守护进程式进程(KAIROS 会话、多日 worktree)会观察到增长
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
### 模块级状态审计
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
| 文件:行 | 符号 | 修复前上限 | 修复前淘汰 |
|---|---|---|---|
| `intentNormalize.ts:52` | `cache: Map<query, keywords>` | 无 | 仅 `clearIntentNormalizeCache()` 测试用 |
| `prefetch.ts:17` | `discoveredThisSession: Set<skillName>` | 无 | 无 |
| `prefetch.ts:18` | `recordedGapSignals: Set<gapKey>` | 无 | 无 |
| `projectContext.ts:48` | `contextCache: Map<cwd, ProjectContext>` | 无 | 仅 `resetProjectContextCacheForTest()` 测试用 |
| `promotion.ts:26` | `sessionPromotedIds: Set<instinctId>` | 无 | 仅 `resetPromotionBookkeeping()` 测试用 |
| `runtimeObserver.ts:61` | `lastProcessedMessageIds: Set<msgKey>` | **MAX 1000** | FIFO trim 已有上限 |
| `toolEventObserver.ts:50` | `emittedTurns: Map<sid, Set<turn>>` | **MAP_MAX 50, SET_MAX 100** | LRU prune 已有上限 |
| `observerBackend.ts:21` | `registry: Map<name, Backend>` | 固定 N | 不适用——注册表模式,有限 |
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
> [!warning]
> 8 个模块级可变状态中有 **5 个无界**。所有 5 个都在本次 PR 中修复。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
### 严重性分析
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
每个条目的成本很小(key 字符串 + 小对象),所以正常工作站上几天内 OOM 不太可能。但以下场景值得关注:
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
- **`intentNormalize.cache`**:每个不同的中文查询 → Haiku 调用 → 缓存。浏览大型中文代码库或重放大量 transcript 的会话可以命中数千个不同查询;约 600 字节/条 x 10k = 约 6 MB。此外,**每次缓存未命中都是一次 Haiku API 调用**,默认启用意味着每个新会话在首次非 ASCII 查询时都要付费——这是意外成本。
- **`projectContext.contextCache`**:每个 `SkillLearningProjectContext` 携带 instinct + skill 列表。多 worktree 编排器(就是这个仓库!)会突破典型的"每会话 1 个 cwd"假设。
- **`prefetch` Sets**:在多话会话中,数千个 skill 发现名称会累积。
- **`sessionPromotedIds`**:实际风险最小(单会话通常个位数 promotion),但长期运行的沙盒可能推高它;防御性上限成本很低。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
### 修复方案
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
| 文件 | 变更 |
|---|---|
| `src/services/skillSearch/intentNormalize.ts` | `setCachedQueryIntent()` 辅助函数,`CACHE_MAX_ENTRIES=200` / `CACHE_TRIM_TO=150`,命中时 LRU touch |
| `src/services/skillSearch/prefetch.ts` | `addBoundedSessionEntry()` 辅助函数,`SESSION_TRACKING_MAX=1000` / `TRIM_TO=750`;`discoveredThisSession` 和 `recordedGapSignals` 通过它路由 |
| `src/services/skillLearning/projectContext.ts` | `setProjectContextCache()` 辅助函数,`PROJECT_CONTEXT_CACHE_MAX=32` / `TRIM_TO=24`,命中时 LRU touch |
| `src/services/skillLearning/promotion.ts` | `recordSessionPromoted()` 辅助函数,`SESSION_PROMOTED_IDS_MAX=256` / `TRIM_TO=192` |
| `src/services/skillSearch/featureCheck.ts` | 双层门控:构建标志必须开启 **且** `SKILL_SEARCH_ENABLED=1` 环境变量必须设置。环境变量未设置时默认关闭 |
| `src/services/skillLearning/featureCheck.ts` | 同样的双层模式(构建标志 + `SKILL_LEARNING_ENABLED=1` 或 `FEATURE_SKILL_LEARNING=1`) |
| `scripts/defines.ts` | 注释标注,说明构建标志现在仅用于编译命令;运行时激活由运维驱动 |
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
修复用 FIFO/LRU 淘汰在合理大小(200-1000 条)上限制所有 5 个。没有数据损坏风险:达到上限时的行为是良性的(重新发出重复信号、重新 Haiku 查询、重新解析 cwd 上下文)。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
### 为什么默认关闭(而不是从构建中移除)
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
除了无界缓存问题外,还有三个原因:
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
1. **隐式成本**:`intentNormalize` 在缓存未命中时调用 Haiku。默认开启意味着输入中文的每个会话都要付费一次 API 调用,即使运维人员从未要求 skill search。
2. **磁盘副作用**:`SKILL_LEARNING` 附加观察者,将观察结果持久化到 `~/.claude` 存储。存储量应该是 opt-in,而不是后台行为。
3. **实验状态**:标志名称就是 `EXPERIMENTAL_*`。默认启用实验子系统与命名约定矛盾。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
> [!info]
> 修复**不是**从 `DEFAULT_BUILD_FEATURES` 中移除标志——这样做也会从构建中剥离 `/skill-search` 和 `/skill-learning` slash 命令,让运维人员没有 UI 来 opt-in。相反,`featureCheck.ts` 中的激活逻辑改为双层门控:
>
> - **第 1 层(编译时)**:`feature('EXPERIMENTAL_SKILL_SEARCH')` / `feature('SKILL_LEARNING')` 必须开启。这些保留在 `DEFAULT_BUILD_FEATURES` 中,以便 slash 命令和观察者被编译进来。
> - **第 2 层(运行时)**:`SKILL_SEARCH_ENABLED=1` / `SKILL_LEARNING_ENABLED=1`(或 `FEATURE_SKILL_LEARNING=1`)环境变量必须设置。不设置的话,子系统存在但休眠——slash 命令存在,通过 `/skill-search` 或 `/skill-learning` 切换会翻转环境变量并激活热路径。
>
> 最终结果:运维人员在 UI 中看到切换开关,但子系统**关闭直到他们手动切换**。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
### 范围外(已提交 follow-up)
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
- **CI 测试失败**(`prefetch.test.ts`、`skillLearningSmoke.test.ts`)出现在本分支 CI 中。两个测试都**显式通过环境变量启用了特性**,所以默认禁用不是原因。它们是实验代码路径中的既有功能问题,值得单独 flow 处理。
- **持久化层上限**(观察文件、instinct 注册表):`observationStore.ts` 已有 30 天清除和 1MB 归档阈值;`skillGapStore.ts` 使用有限状态生命周期。磁盘侧状态有适当限制;OOM 类问题严格在进程内状态。
2026-06-08 23:08:57 +08:00
2026-06-09 23:15:17 +08:00
### 验证
2026-06-08 23:08:57 +08:00
```bash
bun run typecheck # 0 errors
bun test src/services/skillSearch/__tests__/intentNormalize.test.ts
bun test src/services/skillSearch/__tests__/prefetch.extractQuery.test.ts
bun test src/services/skillLearning/__tests__/projectContext.test.ts
bun test src/services/skillLearning/__tests__/promotion.test.ts
bun run lint
bun run build
```
2026-06-09 23:15:17 +08:00
新上限是可观察行为:在持续负载下,Map/Set 大小在配置的最大值处趋于平稳,而不是单调增长。
## 关联笔记
- [[sur-loop-scheduled-oom]]