Files

7.7 KiB
Raw Permalink Blame History

tags, create time
tags create time
OOM
内存溢出
缓存治理
2026-06-09 22:30

Skill Search / Skill Learning 溢出 Bug 修复报告

概述

审计发现 EXPERIMENTAL_SKILL_SEARCH 和 SKILL_LEARNING 子系统存在 5 个无界模块级状态,与 autonomy OOM 属于同类 bug。修复方案采用 FIFO/LRU 淘汰 + 运行时双层门控,默认关闭实验特性直到运维人员显式启用。

正文

问题背景

  • 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 子系统存在同类无界模块级状态。用户明确要求进行 肯定也有同类溢出 审计。

问题分析

autonomy OOM 来自无界模块级状态(运行记录、调度队列、心跳时间戳)随进程生命周期不断增长。skill search + skill learning 子系统在 5 个模块级 Map/Set 上表现出相同类型的 bug,其中只有一个在 scripts/defines.ts 中有文档记录("projectContext cache 无淘汰机制(非 GB 级主因)")。

这些 bug 之前是潜伏的,原因如下:

  • EXPERIMENTAL_SKILL_SEARCH / SKILL_LEARNING 在 DEFAULT_BUILD_FEATURES 中默认启用,但测试通过是因为它们只走短路径
  • 这些无界缓存不是按每次工具调用增长,而是按不同查询 / 不同 cwd / 不同 skill 名称 / 不同 gap 信号 / 不同 promotion 增长,相对于会话长度是亚线性的,但永远单调递增
  • 长时间运行的守护进程式进程(KAIROS 会话、多日 worktree)会观察到增长

模块级状态审计

文件:行 符号 修复前上限 修复前淘汰
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 不适用——注册表模式,有限

Warning

8 个模块级可变状态中有 5 个无界。所有 5 个都在本次 PR 中修复。

严重性分析

每个条目的成本很小(key 字符串 + 小对象),所以正常工作站上几天内 OOM 不太可能。但以下场景值得关注:

  • 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),但长期运行的沙盒可能推高它;防御性上限成本很低。

修复方案

文件 变更
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 注释标注,说明构建标志现在仅用于编译命令;运行时激活由运维驱动

修复用 FIFO/LRU 淘汰在合理大小(200-1000 条)上限制所有 5 个。没有数据损坏风险:达到上限时的行为是良性的(重新发出重复信号、重新 Haiku 查询、重新解析 cwd 上下文)。

为什么默认关闭(而不是从构建中移除)

除了无界缓存问题外,还有三个原因:

  1. 隐式成本:intentNormalize 在缓存未命中时调用 Haiku。默认开启意味着输入中文的每个会话都要付费一次 API 调用,即使运维人员从未要求 skill search。
  2. 磁盘副作用:SKILL_LEARNING 附加观察者,将观察结果持久化到 ~/.claude 存储。存储量应该是 opt-in,而不是后台行为。
  3. 实验状态:标志名称就是 EXPERIMENTAL_*。默认启用实验子系统与命名约定矛盾。

[!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 中看到切换开关,但子系统关闭直到他们手动切换。

范围外(已提交 follow-up)

  • CI 测试失败(prefetch.test.ts、skillLearningSmoke.test.ts)出现在本分支 CI 中。两个测试都显式通过环境变量启用了特性,所以默认禁用不是原因。它们是实验代码路径中的既有功能问题,值得单独 flow 处理。
  • 持久化层上限(观察文件、instinct 注册表):observationStore.ts 已有 30 天清除和 1MB 归档阈值;skillGapStore.ts 使用有限状态生命周期。磁盘侧状态有适当限制;OOM 类问题严格在进程内状态。

验证

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

新上限是可观察行为:在持续负载下,Map/Set 大小在配置的最大值处趋于平稳,而不是单调增长。

关联笔记