Repository navigation
Description
环境
| 项 | 值 |
|---|---|
| DSH | 0.1.6-alpha.1(macOS,Web profile) |
| @modusensus/dsh-mneme | 0.8.2(npm 最新 0.8.2;main 分支同为该问题存在) |
症状
autoSummarize 送进模型的蒸馏转录里,所有工具结果都是空壳:只有 [工具结果] 成功 这一行,看不到任何输出内容。顺带,失败的工具也被标成「成功」。
更严重的是:子会话(subagent)的交付结果完全进不了蒸馏。用 subagent 工具派发任务后,子代理的最终报告在蒸馏输入里一个字都没有。
这不是"信息少一点"的问题——它意味着:凡是靠子代理做的调研/实现,其结论都不会被沉淀成记忆。
实测数据
对两个真实长会话(macOS,~/.dsh/sessions/**/session.v3.jsonl.zstd)按 collectMessages 的规则逐条重放统计:
| 会话 | 行数 | mneme 采集到的转录(截断前) | 助手正文 | 用户消息 | 工具结果文本(当前恒为空) |
|---|---|---|---|---|---|
session-aafea559-… |
22,427 | 1,939,923 字符 | 822,286 | 10,348 | 3,984,954 字符(3,553 条) |
session-72a2d2f1-… |
17,495 | 1,289,277 字符 | 256,964 | 13,918 | 3,666,103 字符(3,364 条) |
被丢弃的工具结果按工具分布(会话 aafea559):
bash 3,162,456 字符 / 2587 条 / 均 1,222
read 453,750 字符 / 174 条 / 均 2,607
skill 112,548 字符 / 19 条 / 均 5,923
web_fetch 81,780 字符 / 13 条 / 均 6,290
edit 63,014 字符 / 364 条 / 均 173
job_output 45,682 字符 / 51 条 / 均 895
即:被丢弃的工具结果是「用户 + 助手正文」的 4.8 倍(会话 aafea559)/ 13.5 倍(会话 72a2d2f1)。
根因(三处独立缺陷)
1. tool/result / tool/code-dispatch 读了不存在的字段
src/summarize.js:
case "tool/result": {
const out = typeof data.output === "string" ? data.output : data.output ? JSON.stringify(data.output) : "";
const status = data.ok === false ? STR.statusFail[language] : STR.statusOk[language];
lines.push(STR.transcriptToolResult[language](status, trim(out, 500)));
break;
}
case "tool/code-dispatch": {
const out = typeof data.output === "string" ? data.output : data.output ? JSON.stringify(data.output) : "";
const status = data.ok === false ? STR.statusFail[language] : STR.statusOk[language];但 DSH 的 tool/result 事件 payload 既没有 output,也没有 ok。产出点 @deepseek-ai/dsh-agent-loop/lib/index.js:697:
function appendToolResult(session, turn, step, block, result, callSeq) {
const message = createToolResultMessage({
callId: block.id,
content: result.content,
isError: result.isError
});
session.append("tool/result", {
turn,
step,
message,
...result.error?.info ? { error: result.error.info } : {},
...result.meta !== void 0 ? { meta: result.meta } : {}
}, { ... });
}会话格式的规范形状(@deepseek-ai/dsh-session-format-v0-to-v1/lib/index.js:217):
"tool/result": disposition([
"turn",
"step",
"message"
], ["error", "meta"], ["meta"]),所以 data.output 恒为 undefined → out 恒为 "";data.ok 也恒为 undefined → status 恒为「成功」。失败的工具被记成成功。
正文与错误标记的真实位置是:
data.message.content[0] // { type: "tool-result", toolCallId, content: [...], isError }
data.message.content[0].content // [{ type: "text", text: "…" }, …]
data.message.content[0].isError // 真正的失败判据(createToolResultMessage 见 @deepseek-ai/dsh-llm/lib/index.js:94,它经 createUserMessage 构造,所以全部内容都在 data.message 下。)
2. switch 未覆盖 agent/inbox/spliced —— 子会话交付结果全丢
collectMessages 的 switch 只处理 user/message / assistant/message / tool/call / tool/result / tool/code-dispatch,其余走 default: break(src/summarize.js:143)。全库 grep agent/inbox 命中数 0。
而子会话的结果不走 tool/result。subagent 工具的返回只是一个句柄,实测每条都是 53 字符:
"started subagent d85a8ddc-e50c-47a0-b171-64f571281074"
真正的交付报告走 agent/inbox/spliced 事件(data.inserted[]):
Background subagent 69a39984-d35d-4d2b-9246-ca7c15c87810 finished and will do no further work unless you send it more.
Its closing message:
已交付完成,结果也已发给父 agent。## 文件清单(绝对路径,全部新建)…
实测规模(会话 aafea559):
| 项 | 值 |
|---|---|
agent/inbox/spliced 条数 |
1,146 |
| 其中文本合计 | 679,055 字符 |
按 source.kind 拆 |
agent-message 293,065 / goal 182,274 / subagent-settled 181,654 / user 10,594 / agent-instructions 5,879 / plugin 5,589 |
其中 subagent-settled + agent-message(合计 47.5 万字符)就是子会话的交付结果,当前 100% 被丢弃。
3. user/message 的 kind 过滤把注入类消息一并挡掉
src/summarize.js:110:
case "user/message": {
if (kind !== undefined && kind !== "user") break;该过滤本身是合理的(避免把工具结果当作对话正文重复采集),但副作用是注入类消息(goal / agent-message / subagent-settled / agent-instructions / skill-catalog)也以 user 角色出现,于是被一并跳过。实测被跳过 569 条 / 1,092,323 字符。
⚠️ 实现时请注意去重:这些注入消息与agent/inbox/spliced的data.inserted[]是同一批内容的两种视图(实测按前 80 字比对,569 条中 528 条重合),不能简单叠加采集,否则内容会被重复计入。
为什么长期没被发现
与 #129 / #136 同类:字段路径漂移 + 症状静默。
trim(out, 500)说明作者本意是要采集工具结果的,只是路径不对 → 收了个空壳,而空壳在转录里看起来"格式正常"- 短会话里"取会话开头"的取样方向问题会掩盖它(与本项目 [Bug] 蒸馏取样方向反了:maxChars 上限用 slice(0, n) 取【会话开头】,长会话/复制来的会话会每轮反复提炼同一段 #210 同源现象)
- 一旦失败被标成「成功」,连失败线索也没了
建议修复方向
不建议简单放开——aafea559 里 bash 输出占 3,162,456 / 3,984,954 ≈ 79%,原样采集会让蒸馏上下文被命令输出淹没,信噪比反而下降。建议分档限量:
| 来源 | 建议 |
|---|---|
子会话交付(agent/inbox/spliced 的 subagent-settled / agent-message) |
高价值,可放宽(如 2,000–4,000 字符),这是当前损失最大的部分 |
普通工具结果(bash / read / grep) |
保留少量首尾(如 300–500 字符)作为线索即可 |
| 失败工具 | 至少输出 status=fail + error,别静默标成功 |
采集 agent/inbox/spliced |
必须与 user/message 去重(见上文 |
字段取值建议改为:
const tr = data.message?.content?.[0];
const out = (tr?.content ?? []).filter(b => b?.type === "text").map(b => b.text).join("\n");
const status = tr?.isError === true ? STR.statusFail[language] : STR.statusOk[language];复现方法
- 在一个会话里用
subagent派发一个会产生长报告的任务,等它settled - 触发一次
autoSummarize - 检查送进模型的蒸馏文本:所有工具结果行为
[工具结果] 成功(无正文),且子会话的 closing message 完全不出现 - 也可离线复现:解压
~/.dsh/sessions/**/session.v3.jsonl.zstd,按collectMessages规则重放,统计data.output的取值(恒为undefined)与agent/inbox/spliced的文本量
相关
- [hot-memory] 短期上下文里 A 恒为空:textOf 只读 data.content,漏了 assistant 的 data.message.content #129 / fix(inject): read assistant body from data.message.content (Issue #129) #136 —— 同一个 bug 类:
inject.js的textOf只读data.content,漏了data.message.content,已修;但summarize.js的tool/result/tool/code-dispatch分支读的还是错的路径,本次一并报出 - [Test] 全工具矩阵的「DTO 键集 ⊆ output schema」系统性断言——防 #184 类漂移 #195 —— 「DTO 键集 ⊆ output schema」系统性断言(防 [Bug] memory_get 输出 schema 漏声明 scope 来源列:命中任何被标注的行必然报 invalid output(0.8.1) #184 类漂移),方向一致,但未覆盖本文这三处
- [Bug] 蒸馏取样方向反了:maxChars 上限用 slice(0, n) 取【会话开头】,长会话/复制来的会话会每轮反复提炼同一段 #210 —— 蒸馏取样方向(
slice(0, n)取的是会话开头)
Activity
lxl1782586616-alt commentedon Sep 16, 2026
补一条与 #78 的关联观察,供定位参考。
#78(v0.7.14,修「私有 reasoning 不入蒸馏」)为该行为补了回归测试,但没有覆盖工具结果——而本 issue 的根因
data.output/data.ok就在同一段collectMessages里。同一份转录采集逻辑,一处有测试、一处没有。这也是它能长期隐身的原因之一:测试覆盖缺口 + 症状静默(
[工具结果] 成功这一行格式完全正常,看不出正文丢了)叠加,让「工具结果会被采集」成了一个看起来成立、实际从未成立的假设。同理可作为回归测试的锚点:
trim(out, 500)明确表达了「要采集」的意图,测试只要断言「工具结果文本非空」即可守住这条路径。modusensus commentedon Sep 17, 2026
已对照源码核实,三处缺陷都属实:
tool/result/tool/code-dispatch读的data.output/data.ok在宿主事件 payload 中不存在,真实内容在data.message.content、失败判据是isError——当前所有工具结果都是空壳,且失败恒被标成「成功」;agent/inbox/spliced,子会话交付结果当前完全进不了蒸馏;user/message的 kind 过滤把注入类消息一并挡掉的副作用也确认存在。你补充的 #78 关联观察同样成立:同一段采集逻辑,一处有回归测试、一处没有,正是覆盖缺口叠加症状静默的典型。分档限量表也合理——尤其是 bash 类输出只留首尾线索,不让命令输出淹没蒸馏上下文。
欢迎提 PR,几点期望:
agent/inbox/spliced的data.inserted[]是同一批内容的两种视图,采集必须去重,这条请有专门用例;isError),别再静默标成功——一并补回归测试;collectMessages,建议 [Bug] 蒸馏取样方向反了:maxChars 上限用 slice(0, n) 取【会话开头】,长会话/复制来的会话会每轮反复提炼同一段 #210(取样方向 / 增量提炼)先合,本 PR 基于其上做;验收观察点:蒸馏转录中工具结果带正文且失败可见;子代理交付进入蒸馏且限量合理;同内容不重复计入。
z2Ace0107 commentedon Sep 18, 2026
感谢合并 #226。我想继续负责并认领 #211,按 Issue 描述推进工具结果、代码派发结果和子会话消息的蒸馏补全,请确认这个安排是否合适。
另外,基于 #226 的 review,我发现一项独立的边界补漏:非空 JSON 数组在过滤后全部无效时,当前会被视为成功并推进 seq 游标。我已在本地准备最小修复和回归测试:空数组仍视为合法成功;全无效数组保留窗口并重试;混合数组继续保存有效条目。这个补漏应并入 #211,还是单独开一个 PR?
你在 #226 中提到的另一个小尾巴,即
lastDistilledSeq目前是进程内存态、重启后会重新蒸馏历史窗口,是否建议另开一个 Issue/PR 单独处理?我倾向于不把游标持久化混入 #211,除非你建议合并处理。modusensus commentedon Sep 18, 2026
先道谢——#226 的增量蒸馏质量很高,review 里那几处边界你也跟得很紧。认领确认:#211 归你,范围按 issue 描述推进(
tool/result与tool/code-dispatch读取真实字段、失败状态透传、agent/inbox/spliced三类子会话交付的采集),照惯例 PR 见。两个拆分问题,我们的意见:
数组过滤边界补漏——建议并入 [Bug] 蒸馏从不采集工具结果与子会话交付:tool/result 与 tool/code-dispatch 读了不存在的 data.output,且 switch 未覆盖 agent/inbox/spliced #211 同一个 PR。它与主缺陷同在蒸馏采集的「窗口推进正确性」域,验收线都是「不丢、不重、不假成功」,拆开反而要两套上下文。但请在 PR 描述里单列一节并配独立回归测试,方便 review 对账。三条口径都同意:空数组=合法成功;全无效=保留窗口重试;混合=保存有效条目。
lastDistilledSeq持久化——单开 issue/PR,不混入 [Bug] 蒸馏从不采集工具结果与子会话交付:tool/result 与 tool/code-dispatch 读了不存在的 data.output,且 switch 未覆盖 agent/inbox/spliced #211。同意你的判断:游标持久化是 schema 变更,存量库要配幂等迁移,防御等级不一样,混进来会放大 review 面。我们随后开跟踪 issue,把重启重蒸馏的触发条件与游标语义写清楚,届时优先由你认领。节奏不赶:#226 刚合入 main,建议动手前先把 collectMessages 的现状对一遍,避免和刚落地的改动打架。