feat(sessions): 系统提示词入会话日志 + 离线重放(H3) - #357
Conversation
会话 JSONL 一直记录 tool_use/tool_result,但**系统提示词一个字节都不进日志**,
而它由 soul + memory + skills + mood + KB 动态构建、且会话中途还会热重载
(agent.ts 的 hotReload)。后果:拿一条历史会话,重建不出当时模型看到的 persona。
这不是洁癖问题。ROADMAP 1.0 判定标准第 4 条要求 Reve 产出可复现的 drift /
coherence 指标,而 drift 的定义就是"她的自我描述随时间怎么变"——日志里没有当时
的自我描述,这个指标只能在线测一次,无法离线复算、无法换指标重算、无法做消融。
改动:
- 会话格式升到 v2,新增 `prompt` 条目 {ts, fingerprint, text, reason}。
fingerprint 是正文的内容哈希;连续相同不重复写,所以"第 N 条时生效的提示词"
= 最近的前一条 prompt 条目。长对话若从不自我修改,只花一条。
- `runAgent` 新增 `onPromptPersist`,在**每次 provider 调用之前**触发——即提示词
真正对模型可见的那一刻,与 dsh 的"模型可见 ⟺ 已记录"同一位置。写在调用前,
所以中途崩溃的轮次也留得下"当时问的是什么"。持久化失败只记日志,绝不拖垮活轮次。
- 三个持有 SessionStore 的表层全部接上:REPL、web、渠道。子代理与 managed agent
无会话,按设计不接。
- `src/sessions/replay.ts` — 离线重建每一轮的 (systemPrompt, messages)。
- `scripts/replay-session.ts` — CLI 包装,--list / --json / --turn N。
诚实边界(写进模块头注释):systemPrompt 是**逐字精确**的;messages 是会话正典
全量,而 web 表层实际只发历史的有界后缀,窗口边界尚未记录(= H3 Step 2)。
做 token 级精确重放的调用方需自行处理;做"她被告知自己是谁"分析的不受影响。
向后兼容:v1 会话读取正常(所有条目读取器都跳过未知 type),重放时
`promptsRecorded=false` 且每轮 systemPrompt 为 null——脚本会明说"未记录"而不是
打印空 persona,避免下游指标把"没记"当成"没有人格"。
测试:store 6 例(版本、去重、往返、恢复不重复、不污染消息分页)
replay 9 例(轮次切分、中途换 persona、工具轮、v1 降级、断尾行容错)
全量 1566 通过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…abel Adversarial-review fixes for the prompt-into-session-log PR: - store.ts: the log now carries the full system prompt (soul, USER.md, MEMORY.md, KB) but was written 0644 in a 0755 dir — out of step with the 0600/0700 discipline for every other user-data file. Create the dir 0700 and chmod the file 0600 after the header write (append's mode only applies on create). Regression test added. - replay-session.ts: `replay --turn 3 <id>` read "3" as the session id (the positional search didn't skip --turn's value); now excluded. - replay-session.ts: a v2 session whose prompt-persist failed was labelled "predates format v2". Branch the message on header.version so a genuine v2 file with no prompt entries reads as "persistence failed", not "pre-v2". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial review — findings + fixes appliedThe replay engine is genuinely solid and well-tested — turn reconstruction, tool_use/result pairing, content-dedup, resume, v1 back-compat, and torn-tail tolerance all hold up, and replay is read-only (no side-effect re-execution). The "persisting the prompt leaks secrets" worry is largely not borne out: no provider key/token is ever interpolated into the system prompt (they live only in MED — fixedSession logs were world/group-readable (0644 in a 0755 dir). H3 concentrates all sensitive user context — full persona + LOW — fixed
Noted, not changed
|
问题
会话 JSONL 一直记录
tool_use/tool_result(它们是StoredMessage的 content block),这点比预想的好。真正缺的是系统提示词:prompt.ts:66);soul_patch/memory写入后下一轮就换了一份(agent.ts的hotReload),只发一个system_prompt_rebuilt事件,不落盘;ROADMAP 1.0 判定标准第 4 条要求 Reve 产出可复现的 drift / coherence 指标,而 drift 的定义就是"她的自我描述随时间怎么变"。日志里没有当时的自我描述,这个指标只能在线测一次 —— 无法离线复算、无法换个定义重算、无法做消融。
改动
prompt条目{ts, fingerprint, text, reason}fingerprint是正文内容哈希,连续相同不重复写 → "第 N 条时生效的提示词" = 最近的前一条 prompt 条目;从不自我修改的长对话只花一条runAgent新增onPromptPersist,在每次 provider 调用之前触发 —— 即提示词真正对模型可见的那一刻。写在调用前,所以中途崩溃的轮次也留得下"当时问的是什么"SessionStore的表层全接:REPL / web / 渠道。子代理与 managed agent 无会话,按设计不接src/sessions/replay.ts离线重建每轮(systemPrompt, messages);scripts/replay-session.tsCLI 包装失败处理:持久化异常只写一行日志,绝不拖垮活轮次。
诚实边界
写进了
replay.ts的模块头注释,也在这里说清:systemPrompt是逐字精确的(CLI 与 web 都持久化了含 suffix 的最终串);messages是会话正典全量。web 表层实际只发历史的有界后缀,窗口边界尚未记录 —— 这是 H3 Step 2。做 token 级精确重放的调用方需自行处理;做"她被告知自己是谁"分析的不受影响。向后兼容
v1 会话读取完全正常(所有条目读取器本来就跳过未知
type)。重放 v1 会话时promptsRecorded=false且每轮systemPrompt为null,脚本会**明说"未记录"**而不是打印空 persona —— 避免下游指标把"没记"当成"没有人格"。验证
🤖 Generated with Claude Code