问题
Message ID alias capacity exceeded.
Cannot allocate more than m9999 aliases in this session.@tarquinen/opencode-dcp 在我的 OpenCode session 里频繁崩溃。每次报这个错,就只能开新 session,用一段时间又崩。
第一步:判断泄漏
拿到这个错误信息后,我的第一反应是:肯定是哪里有问题, 9999 是一个很大的数字,我没有在这个 session 里对话 9999 条消息。如果它满了,说明可能存在泄漏。而且按这个速度,大概率每次消息往返都在漏。
顺着这个思路,我让 AI 从源码开始找。
第二步:定位泄漏
AI 搜索 assignMessageRefs 的调用位置,在 lib/hooks.ts 的 createChatMessageTransformHandler 管线中找到了问题顺序:
assignMessageRefs → prune → injectMessageIdsassignMessageRefs 跑在 prune 之前,意味着所有消息先被分配 alias,然后 prune 删掉一部分消息——但已分配的 alias 留在 state.messageIds 里,再也没人清理。
泄漏路径确认。每次 assistant 回复进入管线,都产生一批孤儿 alias,只增不减。
第三步:验证移动安全性
确定了根本原因是顺序问题后,我的第一个追问是:移后会不会破坏上下游?
AI 检查了 assignMessageRefs 和 prune 之间的三个函数:
| 函数 | 依赖 state.messageIds? |
证据 |
|---|---|---|
syncCompressionBlocks |
否 | 操作的是 state.prune.messages.blocksById(block ID ↔ 消息 ID 映射),不涉及 alias ref |
syncToolCache |
否 | 遍历 output.messages 扫描 tool_use/tool_result 类型的 part,只关心 tool call ID(msg_call_xxx) |
buildToolIdList |
否 | 从 syncToolCache 的结果建索引,不碰 alias |
三家都不依赖。移动安全。
第四步:验证覆盖完整性
我的第二个追问:会不会有消息因此拿不到 alias?
这个追问把 AI 带进了 prune 内部的 filterCompressedRanges。它做两件事:删除已被压缩的旧消息、创建一个新的合成摘要消息(createSyntheticUserMessage)。
在旧顺序下,合成消息创建在 assignMessageRefs 之后——它没有 alias。这其实是旧顺序的另一个 bug。
在新顺序下:
- 删旧消息 → 从未分配 alias → 零泄漏
assignMessageRefs跑在prune之后 → 合成消息也能拿到 alias
双重改善。这个追问没有发现风险,反而发现了一个被忽略的覆盖缺漏。
修复
- assignMessageRefs(state, output.messages)
syncCompressionBlocks(state, logger, output.messages)
syncToolCache(state, config, logger, output.messages)
buildToolIdList(state, output.messages)
prune(state, logger, config, output.messages)
+ assignMessageRefs(state, output.messages)验证
我要的不是"看起来没问题"——我要一个可复现的测试。RED→GREEN:构造有多条消息且经过完整管线的场景。旧顺序下 nextRef 明显增长(alias 被白白分配),新顺序下 alias 只分配给幸存消息。AI 写测试前这个断言对旧代码 FAIL、对新代码 PASS。
全部 87 个已有测试通过。
merge
总结
这次调试的关键推进来自几个判断和追问,这也是人的价值:
- "应该是泄漏"——把方向从症状修引向根因
- "确认依赖关系"——防止了"看起来没问题"式盲猜
- "会不会有东西没被 assign"——发现了旧顺序下合成消息缺 alias 的双重问题
- "写 RED→GREEN 测试"——让修复可验证、可追溯
PR 被合并。改动:一行。