Skip to content

fix: /history 只列真人轮次(按 source.kind 过滤合成注入)+ 冷路径优先模型面 readSurface - #6

Closed
PerryLink wants to merge 1 commit into
pan17:mainfrom
PerryLink:fix/history-human-turns
Closed

PerryLink wants to merge 1 commit into
pan17:mainfrom
PerryLink:fix/history-human-turns

Conversation

@PerryLink

Copy link
Copy Markdown
Contributor

症状

微信 /history 会把压缩检查点、上下文插件注入、goal 续跑等合成 user 消息当成人话列出来(重启后走持久路径时更明显)。这些正文往往是被替换掉的整段历史,读起来像用户自己发的。

根因

  • 宿主给每条 user 消息标注来源:data.source.kind = user(真人)/ plugin(上下文注入、压缩检查点,如 plugin: "compact")/ goal(目标续跑轮次)/ session-reference(召回)/ agent-message(子代理中继)。GUI 侧栏的规则就是只列 kind === "user":packages/session/session-turn-outline/src/projection.ts:110。
  • 本仓 src/dsh/ops.ts 的 getSessionHistory()(:611)只按事件类型过滤(user/message + assistant/message),不看 source.kind —— 内存路径(:621)与持久路径(:650)都一样。
  • 持久路径还优先读 readSession()(完整原始日志)(:642),而原始日志包含全部非 surface 注入;宿主有 readSurface()(当前模型面)时本可少踩这些事件。

改法

  • src/dsh/ops.ts:抽出 historyEntries() 与 isHumanUserMessage() 两条路径共用;user/message 仅在 source.kind === "user"(或缺省)时列出 —— 没有 source 的老载荷照旧可见,不误伤真消息(data.message.source 的嵌套形状也一并兼容)。
  • SessionQuery 增加 readSurface?;持久回退顺序改为 readSurface()(模型面,一次折叠)→ readSession()(原始日志)→ listEvents()(元数据兜底,保持原行为)。
  • 新增 tests/history-provenance.test.ts(5 例):内存路径隐藏 plugin / goal 注入;无 source 的老载荷仍可见;冷路径优先 readSurface 且同样过滤(断言 readSession 未被调用);无 surface 时回落 readSession。

验证(本仓原生门禁,Node 22)

  • npm run build 通过;npm test 426/426 全绿(34 文件,相对 base +1 文件 +5 例)。
  • 反向验证:仅把 src/ 还原成改动前、保留新测试 → tests/history-provenance.test.ts 3 failed / 2 passed(失败的正是注入过滤与 readSurface 优先那三例),证明测试确实复现问题。

边界说明(避免误读)

  • V3 的系统提示词走独立 system/message 事件类型,本就不在 user/message 里,本次不涉及。
  • listEvents() 兜底路径仍取不到文本(宿主该接口自 0.1.2 起不带 data),行为与改动前一致,未扩大范围。
  • 过滤规则与 GUI 侧栏同源(source.kind === "user"),不引入本仓独有的判定。

base = main @ 12e0c90(#5 合并之后)。

The host attributes synthesized user-role messages through `source.kind` — `plugin`
for context injections and compaction checkpoints, `goal` for goal rounds,
`session-reference` for recalls, `agent-message` for relays — and the GUI sidebar
lists only `kind === "user"` (session-turn-outline). `/history` filtered by event
type alone, so compaction checkpoints and injected context were listed as if the
user had typed them. The persisted fallback also read the complete raw log first,
which carries every non-surface injection.

- ops.ts: `historyEntries()` + `isHumanUserMessage()` shared by both paths; legacy
  payloads without a `source` stay visible, so nothing real is hidden.
- ops.ts: the persisted fallback now prefers `sessionQuery.readSurface()` (current
  model surface) and only then `readSession()` (complete raw log).
- history-provenance.test.ts: live and cold cases — injected plugin/goal messages
  hidden, legacy payloads still visible, `readSurface` preferred over `readSession`.

Verified with the repo's own gates on Node 22: `npm run build` clean, `npm test`
426/426 passing.
@pan17

pan17 commented Sep 10, 2026

Copy link
Copy Markdown
Owner

感谢排查和补测试。症状是对的:/history 不该把压缩检查点、插件注入、goal 续跑当成真人说话。

这条没合,因为冷路径优先 readSurface() 会让压缩前的真人轮次在重启后从 /history 消失(热路径读 raw log,冷路径读模型面,两边对不上)。过滤规则也更适合分类,而不是删掉。

已在 v0.9.6 用另一条实现落地:

  • 合成 user/message 标成「系统」,默认 /history 仍只看你和助手
  • /history all 把系统消息一起列出
  • 冷路径继续 readSession() 完整原始日志
  • 每条改成独立一块(角色单独一行,正文保留换行)

因此关闭本 PR。

@pan17 pan17 closed this Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants