Skip to content

feat(sessions): 系统提示词入会话日志 + 离线重放(H3) - #357

Merged
oratis merged 2 commits into
mainfrom
claude/h3-session-log-truth-source
Aug 14, 2026
Merged

feat(sessions): 系统提示词入会话日志 + 离线重放(H3)#357
oratis merged 2 commits into
mainfrom
claude/h3-session-log-truth-source

Conversation

@oratis

@oratis oratis commented Aug 13, 2026

Copy link
Copy Markdown
Owner

计划见 #356docs/PLAN_HARNESS_ALIGNMENT_v1.0.md §4(H3)。本 PR 是其 Step 1。

问题

会话 JSONL 一直记录 tool_use / tool_result(它们是 StoredMessage 的 content block),这点比预想的好。真正缺的是系统提示词

  • 它由 soul + memory + skills + mood + KB 摘要动态构建(prompt.ts:66);
  • 而且会话中途会热重载 —— soul_patch / memory 写入后下一轮就换了一份(agent.tshotReload),只发一个 system_prompt_rebuilt 事件,不落盘;
  • 结果:拿一条历史会话,重建不出当时模型看到的 persona

ROADMAP 1.0 判定标准第 4 条要求 Reve 产出可复现的 drift / coherence 指标,而 drift 的定义就是"她的自我描述随时间怎么变"。日志里没有当时的自我描述,这个指标只能在线测一次 —— 无法离线复算、无法换个定义重算、无法做消融。

改动

会话格式 v1 → v2,新增 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.ts CLI 包装

失败处理:持久化异常只写一行日志,绝不拖垮活轮次。

诚实边界

写进了 replay.ts 的模块头注释,也在这里说清:

  • systemPrompt逐字精确的(CLI 与 web 都持久化了含 suffix 的最终串);
  • messages 是会话正典全量。web 表层实际只发历史的有界后缀,窗口边界尚未记录 —— 这是 H3 Step 2。做 token 级精确重放的调用方需自行处理;做"她被告知自己是谁"分析的不受影响。

向后兼容

v1 会话读取完全正常(所有条目读取器本来就跳过未知 type)。重放 v1 会话时 promptsRecorded=false 且每轮 systemPromptnull,脚本会**明说"未记录"**而不是打印空 persona —— 避免下游指标把"没记"当成"没有人格"。

验证

npx tsx scripts/replay-session.ts --list
npx tsx scripts/replay-session.ts <session-id>
  • store 6 例(版本、去重、往返、恢复不重复、不污染消息分页/反思)
  • replay 9 例(轮次切分、中途换 persona、工具轮、prompt 时间线、v1 降级、断尾行容错、空文件报错)
  • 全量 1566 通过 / 0 失败,typecheck 干净
  • 合成 v2 会话跑通脚本三种输出模式

🤖 Generated with Claude Code

会话 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>
@oratis

oratis commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

Adversarial review — findings + fixes applied

The 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 config.env), the PII that is in it (USER.md, soul, MEMORY.md) is already on disk, and .jsonl logs are never uploaded (the web API filters type:"prompt"). No HIGH found. Fixes pushed on b2bf8d3; typecheck clean, sessions tests green.

MED — fixed

Session logs were world/group-readable (0644 in a 0755 dir). H3 concentrates all sensitive user context — full persona + USER.md + MEMORY.md + KB — into one file, and if a user ever writes a credential into MEMORY.md/USER.md it's now duplicated into every session log. Every other user-data file in the tree is 0600 (config.env, mail secrets, devices, OTP, accounts); session logs weren't. → create() now makes the dir 0700 and chmods the file 0600 after the header write (append's mode only applies on create). Regression test added. Same fix covers the multi-tenant cloud path (users/<uid>/sessions).

LOW — fixed

  • replay --turn 3 <id> read "3" as the session id — the positional-id search didn't skip --turn's value. Now excluded.
  • A v2 session whose prompt-persist failed was labelled "predates format v2." promptsRecorded is inferred purely from prompt-entry presence and ignored header.version, so a v2 file that lost turn-1's persist (disk full / EPERM) told the drift-analysis user it was pre-v2. Now branched on header.version: pre-v2 → "predates H3"; v2 with no entries → "persistence likely failed."

Noted, not changed

  • Log size scales with prompt-input mutation count: any kb_write/soul_*/memory call flips the whole-prompt fingerprint, so the next turn re-serializes the entire multi-KB prompt (dedup only helps the unchanged case). Acceptable for now — worth content-addressing prompts in a follow-up if logs grow.
  • Replay's per-turn messages is the full canonical transcript, not the bounded/compacted window actually sent — disclosed in the PR; fine for drift analysis, don't build a token-exact cost audit on it.

@oratis
oratis merged commit bb957ad into main Aug 14, 2026
1 check passed
@oratis
oratis deleted the claude/h3-session-log-truth-source branch August 14, 2026 05:37
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.

1 participant