Session 文件与回放边界
对应源码
| 主题 | 源码 |
|---|---|
| Session 内存对象与回放 | nanobot/session/manager.py::Session、Session.get_history |
| JSONL 加载、修复和原子保存 | nanobot/session/manager.py::SessionManager |
| Session Key 规范 | nanobot/session/keys.py |
| Turn 读取与保存调用点 | nanobot/agent/loop.py::AgentLoop._state_build、_save_turn、_state_save |
学习目标
读完后,你应当能够:
- 读懂 Session JSONL;
- 解释 metadata、messages 和
last_consolidated; - 说明 Session 为什么不会把磁盘内容原样全部回放;
- 理解原子保存和损坏恢复的基本策略。
前置知识
- 理解 JSONL;
- 已阅读一次 Turn 的 SAVE 阶段;
- 推荐补充 文件、JSON Lines 与流式处理。
1. Session 对象
定义:
nanobot/session/manager.py::Session
核心字段:
| 字段 | 含义 |
|---|---|
key | Session 唯一标识 |
messages | 已持久化消息 |
created_at | 创建时间 |
updated_at | 最近更新时间 |
metadata | Goal、checkpoint、UI 等 Session 级状态 |
last_consolidated | 已归档到 Memory history 的消息边界 |
last_consolidated 如果损坏或越界,会在 __post_init__ 中重置,避免错误 offset 隐藏全部历史。
2. 文件位置
默认位于:
<workspace>/sessions/*.jsonlSessionManager.safe_key() 将 Session key 转换为安全文件名。文件名不是理解归属的唯一依据,metadata 记录中仍保存原 key。
3. JSONL 结构
保存时第一行是 metadata record,后面每行是一条消息。示意:
{"_type":"metadata","key":"cli:direct","created_at":"...","updated_at":"...","metadata":{},"last_consolidated":0}
{"role":"user","content":"你好","timestamp":"..."}
{"role":"assistant","content":"你好!","timestamp":"..."}实际字段以当前 SessionManager.save() 为准。
这种格式的优点:
- 可逐行读取;
- 损坏时可尝试保留合法行;
- 列表页只读 metadata 和少量 preview;
- 不必一次解析一个巨大 JSON 数组。
4. add_message() 只负责追加
Session.add_message():
- 添加 role;
- 添加 content;
- 添加 timestamp;
- 合并附加 kwargs;
- 更新
updated_at。
它不负责:
- schema 完整性;
- Tool Call 边界;
- 上下文预算;
- 磁盘写入。
合法化和保存由上层负责。
5. get_history() 返回可回放视图
Session 可能保存很多消息,但 get_history() 会根据:
last_consolidated;max_messages;max_tokens;- user/tool 边界;
- tool call 配对;
- command/内部消息过滤;
- 时间戳选项;
- media/CLI/MCP breadcrumb
返回适合 ContextBuilder 的历史。
Session.messages
→ 排除已 consolidation 的前缀
→ 截取最近消息/token
→ 找到合法 user 起点
→ 修复 Provider replay
→ 可选添加 user timestamp
→ history6. 为什么只给 user 消息加时间
_annotate_message_time() 只为 user turn 添加时间前缀。
原因写在源码注释中:
- 如果每条 assistant 历史都带
[Message Time: ...]; - 模型可能通过 in-context imitation 在自己的回复中重复该格式;
- 这会把内部 metadata 泄露给用户。
用户时间已经足够为相邻 assistant 回复提供相对时间锚点。
这是一个典型的“上下文内容会反过来训练当前行为”的工程取舍。
7. 合法后缀
retain_recent_legal_suffix() 用于硬裁剪后保留可回放的最近片段。合法后缀不能:
- 从孤立 tool result 开始;
- 留下没有 tool result 的 assistant Tool Call;
- 破坏 user/assistant/tool 的协议边界。
因此“保留最后 N 条”不是简单切片。
8. 原子保存
SessionManager.save() 使用:
写入 .jsonl.tmp
→ flush
→ 可选 fsync 文件
→ os.replace
→ 可选 fsync 目录正常运行保存默认不一定每次 fsync;优雅关闭或明确要求 durable save 时可以启用。
这保证:
- 目标文件不会被普通写入过程截断;
- replace 在同一文件系统中是原子的;
- 临时文件写失败时清理。
Windows 上目录 fsync 可能因权限而跳过。
9. 损坏恢复
SessionManager 读取时会:
- 逐行解析;
- 跳过或记录损坏;
- 尝试恢复合法内容;
- 在只读列表/metadata 路径中避免因为一处损坏丢失所有条目。
但恢复不代表可以忽视磁盘损坏。调试时应保留原文件,不要先手工清空。
10. Session metadata 中可能有什么
常见内部状态:
goal_stateruntime_checkpointpending_user_turn_last_summary- workspace scope;
- WebUI title/显示状态;
- continuation 计数。
这些键多数是内部契约。扩展不应随意写入同名字段。
观察任务
使用隔离学习 workspace:
- 完成两轮 CLI 对话;
- 找到对应 Session JSONL;
- 对照
Session.messages和文件行; - 调用
get_history(max_messages=...)的相关测试; - 观察 user 时间戳何时添加;
- 不修改文件,记录 metadata 键。
源码与测试
nanobot/session/manager.pynanobot/session/keys.pytests/agent/test_session_manager_history.pytests/agent/test_session_atomic.pytests/agent/test_cursor_recovery.pytests/agent/test_session_delete.pytests/agent/test_max_messages_config.py
自检
- 为什么 Session JSONL 第一行单独保存 metadata?
- 为什么
get_history()不能只是messages[-120:]? - 为什么 assistant 时间戳可能污染未来回复?
os.replace解决什么问题,fsync 又解决什么问题?last_consolidated为什么必须验证边界?