pi 会话管理实战:JSONL 树形存储、/tree 分支导航与分支摘要机制
本篇指南围绕 pi(AI agent toolkit)coding agent 的会话(Session)体系展开:从会话的自动存储与命令行控制,到 /resume、/tree、/fork、/clone 等交互命令的完整用法,再到 JSONL 树形结构、分支切换与分支摘要的底层实现原理。读完后,你将能够熟练地续接、命名、分支和复用 pi 的历史会话,并理解其源码中 SessionManager 的组织方式与上下文重建逻辑。
会话存储:按工作目录组织的 JSONL 树
Pi 将每次对话保存为会话(session),让你可以续接工作、从较早的轮次分叉、回溯之前的路径。会话自动保存到 ~/.pi/agent/sessions/,并按启动 pi 时的工作目录(cwd)分子目录组织,每个会话是一个 JSONL 文件,内部采用树形(tree)结构而非线性列表。
从源码可以确认这一布局:config.ts 中 getSessionsDir() 返回 ~/.pi/agent/sessions 路径,而 session-manager.ts 的 forkFrom() 实现了文件命名规则——时间戳(冒号、点替换为连字符)加上下划线和会话 UUID,即 <timestamp>_<uuid>.jsonl。会话文件的第一行是 SessionHeader(不含 id/parentId,不属于树本身),后续每一行都是一个带 id 与 parentId 的树节点,例如:
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}
对于通过 /fork、/clone 或 newSession({ parentSession }) 创建的子会话,header 会额外携带 parentSession 字段指回源会话文件路径。当前仓库实现的会话版本为 3(CURRENT_SESSION_VERSION = 3),旧版本会话在加载时会自动迁移。各版本区别详见 Session Format 文档。
会话相关的 CLI 启动参数
以下参数用于控制 pi 启动时的会话行为,均可通过 pi --help 确认(参数解析见 args.ts):
pi -c # Continue most recent session
pi -r # Browse and select from past sessions
pi --no-session # Ephemeral mode; do not save
pi --name "my task" # Set session display name at startup
pi --session <path|id> # Use a specific session file or partial session ID
pi --fork <path|id> # Fork a session file or partial session ID into a new session
这些参数在源码中的落点各有不同:
-c/--continue:调用SessionManager.continueRecent(cwd),在默认会话目录(按当前 cwd 编码出的子目录)中找到最近修改的会话文件直接打开;若不存在则新建一个空会话。-r/--resume:启动时打开交互式会话选择器,功能与交互模式下的/resume一致。--no-session:走SessionManager.inMemory(cwd)路径,构造一个不持久化到磁盘的内存会话(persisted标记为 false),适合临时试错。--name/-n:在会话中追加一条session_info类型条目,设置人类可读的显示名。--session <path|id>:支持完整路径或部分会话 ID,通过SessionManager.open()打开指定会话文件。--fork <path|id>:将某个会话文件(或部分 ID)复制成一个新会话。源码中的forkFrom()会为新文件生成新 UUID、写入带parentSession的 header,再逐行拷贝源会话的所有非 header 条目。它甚至支持跨项目分叉——目标 cwd 与源不同,新会话存入目标项目的目录。
在交互模式下,随时用 /session 查看当前会话文件路径、会话 ID、消息数、token 消耗与费用。
会话命令一览
交互模式下的斜杠命令覆盖了会话的生命周期操作:
| 命令 | 说明 |
|---|---|
/resume |
浏览并选择历史会话 |
/new |
开始新会话 |
/name <name> |
设置当前会话显示名 |
/session |
显示会话信息 |
/tree |
在当前会话树中导航 |
/fork |
从某条历史用户消息创建新会话 |
/clone |
将当前活动分支复制为新会话 |
/compact [prompt] |
摘要较早的上下文,详见 Compaction 文档 |
/export [file] |
将会话导出为 HTML |
/share |
上传为私有 GitHub gist,获得可分享的 HTML 链接 |
恢复与删除会话
/resume 打开当前项目的交互式会话选择器,pi -r 则在启动时打开同一个选择器。选择器中的数据来自 SessionManager.list(cwd)——它扫描当前工作目录对应的会话子目录,聚合出每个会话的路径、ID、cwd、显示名、创建/修改时间、消息数与首条消息预览。
在选择器中可以:
- 直接输入文字进行模糊搜索
Ctrl+P切换路径显示Ctrl+S切换排序模式Ctrl+N过滤到已命名的会话Ctrl+R重命名会话Ctrl+D删除会话,随后按确认键
当系统安装了 trash CLI 时,pi 会优先用它删除会话而不是永久删除文件,降低误删风险。另外,直接删除 ~/.pi/agent/sessions/ 下对应的 .jsonl 文件也能移除会话。
命名会话
给会话取一个人类可读的名字,是长期管理多个工作线程的关键习惯:
/name Refactor auth module
也可以在启动时通过 --name / -n 一并设置:
pi --name "Refactor auth module"
pi --name "CI audit" -p "Review this build failure"
从存储角度看,命名操作会在会话中追加一条 session_info 类型条目({"type":"session_info","name":"Refactor auth module"});显示名取自最新一条 session_info 条目,即后续 /name 会覆盖旧名。在选择器中,已命名的会话会显示该名字而非首条消息文本,使 pi -r 中的检索更直观。
用 /tree 在树中分支
会话以树的形式存储:每个条目有 id 和 parentId,当前位置(leaf)就是树的某个活动叶子节点。/tree 让你在同一个会话文件内跳到任意历史节点并从中继续,无需新建文件。
一个典型的分支形态如下(某条助手回复后分出了两条不同的探索路径,A 是活动分支):
├─ user: "Hello, can you help..."
│ └─ assistant: "Of course! I can..."
│ ├─ user: "Let's try approach A..."
│ │ └─ assistant: "For approach A..."
│ │ └─ user: "That worked..." ← active
│ └─ user: "Actually, approach B..."
│ └─ assistant: "For approach B..."
在源码中,SessionManager.branch(entryId) 负责把 leaf 移动到某个更早的条目上,getTree() / getBranch(fromId) / getChildren(parentId) 提供完整的树查询能力,/tree 视图就是基于这些 API 渲染的。
树视图操作键
| 键 | 动作 |
|---|---|
| ↑/↓ | 在可见条目间导航 |
| ←/→ | 上/下翻页 |
| Ctrl+← / Ctrl+→ 或 Alt+← / Alt+→ | 折叠/展开,或在分支段之间跳转 |
| Shift+L | 为选中条目设置或清除标签(label) |
| Shift+T | 切换标签时间戳显示 |
| Enter | 选中条目 |
| Escape / Ctrl+C | 取消 |
| Ctrl+O | 循环切换过滤模式 |
过滤模式共五种:default、no-tools(隐藏工具调用)、user-only(只看用户消息)、labeled-only(只看打过标签的条目)、all。可以在 Settings 配置 中用 treeFilterMode 字段设定打开 /tree 时的默认模式(该字段在 settings-manager.ts 中定义为这五个字面量类型之一)。
选中条目的行为语义
不同条目类型被选中后,行为有明确区分:
选中用户消息或自定义消息时:
- leaf 移动到该消息的父条目(即把该消息"退回到"待发送状态);
- 该消息文本被放入编辑器;
- 你可以编辑后重新提交,从而创建一个新分支——这是"修改早前指令、换一种问法"的标准操作。
选中助手、工具、compaction 或其他非用户条目时:
- leaf 直接移动到该条目;
- 编辑器保持为空;
- 从这一点继续对话即可。
选中根用户消息时: leaf 重置到空对话(resetLeaf() 语义),原始 prompt 放入编辑器供你修改重发——相当于"带着原始需求重新开始"。
/tree、/fork 与 /clone 的取舍
三个命令都提供"从历史某点继续"的能力,但输出形态不同:
| 特性 | /tree |
/fork |
/clone |
|---|---|---|---|
| 输出 | 同一会话文件 | 新会话文件 | 新会话文件 |
| 视图 | 完整树 | 用户消息选择器 | 当前活动分支 |
| 典型用途 | 就地探索多种替代方案 | 从较早的 prompt 开一个新会话 | 继续当前工作前先复制一份 |
| 摘要 | 可选的分支摘要 | 无 | 无 |
经验法则是:想保留多种替代路径互相参照就用 /tree(所有分支共享一个文件,随时可以来回切换);想要相互独立、互不干扰的会话文件就用 /fork 或 /clone。/clone 对应源码中的 createBranchedSession(leafId),把当前活动分支抽取成独立文件;/fork 则提供一个用户消息选择器,让你指定从哪条历史 prompt 分叉出去。
分支摘要:离开分支前保留关键上下文
当你通过 /tree 从分支 A 切到分支 B 时,分支 A 中已积累的重要上下文(做过哪些尝试、读过哪些文件、得出什么结论)如果不加处理就会"丢失"。Pi 的解决方案是分支摘要(branch summary):切换分支时,pi 可以调用 LLM 对被放弃分支(从分叉点到 leaf 的那段路径)生成一份摘要,并把摘要作为 branch_summary 条目附加到新位置。这样你保留了离开路径的关键信息,又不用重放整个分支。
被提示时你可以三选一:
- 不生成摘要(no summary)
- 用默认 prompt 生成摘要
- 用自定义的关注点说明生成摘要
存储上,branch_summary 条目记录 fromId(从哪个条目分出的)与 summary 文本,可选携带 usage(生成摘要消耗的 LLM 用量,计入会话 token 与费用统计)、details(文件追踪数据,如 readFiles/modifiedFiles)和 fromHook 标记(扩展生成还是 pi 生成)。摘要生成时还会自动追踪该分支期间读取/修改过的文件并写入 details,这是摘要"有据可依"的来源。分支摘要的扩展钩子与内部实现细节见 Compaction 文档 与 Session Format 文档。
会话格式与 SessionManager API 概览
会话文件是 JSONL,每一行一个 JSON 对象且带 type 字段。除首行 header 外,所有条目都继承 SessionEntryBase(type、8 位十六进制 id、parentId、ISO timestamp),并由此连成树。条目类型包括:
message:对话消息,message字段是一个AgentMessage(用户消息、助手消息、工具结果、bash 执行记录、自定义消息等);model_change/thinking_level_change:记录会话中途切换模型或思考级别的时刻;compaction:上下文压缩检查点,携带summary、tokensBefore,新式压缩还会把压缩后保留的尾部消息物化进retainedTail,使检查点自包含;branch_summary:如上所述的分支切换摘要;custom/custom_message:扩展持久化状态(不参与 LLM 上下文)与扩展注入消息(参与上下文);label:用户对条目的书签标记;session_info:会话元数据,如/name设置的显示名。
上下文重建由 SessionManager 的两个方法完成,这也是树形存储能正确处理压缩与分支的关键:
buildContextEntries()从当前 leaf 走到根,收集活动路径上的条目,并遵守压缩语义(retainedTail存在时以压缩条目为自包含检查点,否则按firstKeptEntryId回溯保留区间);buildSessionContext()在此基础上产出最终发给 LLM 的消息列表,同时从完整路径中提取当前模型与思考级别设置。
完整的字段定义(各消息接口的 TypeScript 定义、header 示例、树结构图解、解析示例代码)以及 SessionManager 的完整 API——静态方法(create / open / continueRecent / inMemory / forkFrom / list / listAll)、追加方法(appendMessage / appendCompaction / appendSessionInfo 等)、树导航方法(branch / branchWithSummary / getTree / getBranch 等)——都记录在 Session Format 文档 中;如果你要写扩展、SDK 集成或外部解析器,该文档是必读参考。
小结
pi 的会话系统用"JSONL + id/parentId 树"这一极简存储实现了三件大事:会话内分叉(/tree 原地切换叶子节点并可选生成分支摘要)、会话间分叉(/fork / --fork 生成带 parentSession 血缘的新文件)、上下文经济(压缩检查点与分支摘要共同避免上下文重复)。配合 /resume 选择器、命名过滤与 trash 安全删除,多任务、多假设的 agent 工作流可以在一套文件系统约定下长期演进。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
