沿着前两篇的假设场景继续往下走:OpenClaw 已经读完会议纪要,整理出三个待办。你给它补上合适的文件权限,这回,清单终于成功保存到了 notes/todo.md。
然后,你关掉服务,过了一会儿再启动,接着问:
刚才第二项待办,能不能再拆细一点?
它如果接得上话,我们很容易觉得:“不错,它有记忆。”
可把重启的时间往前挪一点,问题就变了。假设文件刚刚写完,工具结果还没来得及记进会话,进程就退出了。重新启动以后,它究竟应该继续解释第二项,重新保存文件,还是先确认刚才发生了什么?
“记得我们聊过什么”和“知道一个动作有没有完成”,在聊天窗口里看起来很接近,落到工程实现里,却需要不同的证据。这一篇就从这次重启出发,看看 OpenClaw 留下了什么,又有哪些事情不能仅靠聊天记录判断。
本文沿用 OpenClaw v2026.5.22,以及对应 Pi 0.75.4 的内置执行路径。以下讨论的是这组源码里的会话存储实现,不把它推广到所有版本、所有执行器,也不把会话记录等同于独立的长期记忆检索系统。会话存储说明
先找到这段对话,再翻开记录
重新启动之后,OpenClaw 首先需要知道:你现在说的这句话,应该接到哪一段对话上。
第一篇讲过 sessionKey,它描述的是消息归属。存储层还要再回答一层:这个会话键当前对应哪个 sessionId,以及应该打开哪份记录文件。默认目录里有两个重要角色:sessions.json 保存会话条目;通常以 <sessionId>.jsonl 命名的文件保存对话记录。条目还可以携带具体的 sessionFile 路径。会话条目定义
把前者想成目录卡片、后者想成一本工作笔记,会比较好理解。卡片帮助系统找到笔记,还记着模型配置、更新时间、生命周期等信息;真正说过的话、模型发出的工具调用、工具返回的结果,则要到笔记里看。
卡片上的时间也不止一种。这个版本明确区分 sessionStartedAt、lastInteractionAt 和 updatedAt:会话何时开始、最后一次有效交互是什么时候、存储条目何时被修改,并不是同一个问题。后台做了一次记账,不应该仅仅因为改了更新时间,就让旧会话一直保持“刚有人聊过”的状态。生命周期说明
所以,重启后接不上话,不一定是历史文件丢了。也可能是路由选了另一个会话键,或者生命周期规则已经切换到了新的会话。排查时,先确认系统打开的是哪本笔记,再问里面少了哪一页。
还有一个容易过度简化的地方:会话条目确实可以保存运行状态,例如某些子任务的状态、结束时间,以及上次运行是否中断。不能因此说“运行相关的信息全都只在内存里”。但磁盘上的 running 字段,也不是一个仍然活着的进程。网络连接、执行栈和等待中的控制句柄,不会因为读到这个词就重新出现。
那本笔记,其实长着分支
打开 JSONL 文件,会看到一行一条 JSON。乍看很像把聊天消息逐条追加进去,重启时再从头读到尾。
Pi 的 SessionManager 比这个模型多了一层关系:每个普通条目都有自己的 id 和 parentId,管理器还维护当前的 leafId。追加条目时,新条目接到当前叶子后面;构建上下文时,则从当前叶子沿着父节点一路找回去。SessionManager 实现
用清单举个简化例子。A 是“帮我整理清单”,B 是第一次回答。后来你想换一种拆分方式,从 A 的位置生成了另一个回答 C。文件里可以同时保留 A、B、C,但当前这条对话路径是 A → C。
如果恢复时只把文件里的消息按行拼起来,模型就可能同时看到两份本来属于不同分支的回答,还以为它们是连续发生的。parentId 的价值就在这里:保存过的内容,与当前这次对话实际沿着哪条路径展开,可以分别表达。
这个模型还留了一个值得注意的边界:branch() 本身只是移动内存里的叶子指针,不会立即写出一条“以后从这里开始”的记录。我在局部实验里先切回 A,什么也不追加,然后重新打开文件,管理器仍然选中了最后保存的条目 B。等 C 真正追加之后,再打开文件,才会沿着 C 的父节点关系恢复这条新路径。
因此,“支持分支”不能直接翻译成“任何一次界面导航都会持久保存”。得继续看上层在切换之后究竟写入了什么。这里验证的是 Pi 底层管理器,不是在声称 OpenClaw 的某个界面操作一定丢失选择。分支与加载逻辑
保存过的内容,也未必都要交给模型
我们的会议笔记越聊越长,下一次请求却不可能无限带上所有原文。
buildSessionContext() 不是简单导出文件。它先找当前分支,再处理这条路径上的压缩记录:有压缩时,先加入摘要,再带上指定保留位置之后的相关消息,以及压缩之后的新消息。旧条目仍可能留在文件里,但模型本轮拿到的已经是另一种组织方式。上下文构建
还有一种更安静的区别。扩展可以追加 custom 条目保存自己的数据,这类条目不会自动成为模型上下文;需要把内容放进对话的扩展,则有 custom_message 这条专门的路径。一个文件可以承载不止一种用途,而“写进去了”并不意味着“模型一定读到了”。
这也解释了为什么排查遗漏时,直接搜索原始 JSONL 还不够。你在文件中找到了那句话,只能证明它被保存过;还要确认它在当前分支上,以及经过压缩和后续历史处理之后,是否进入了这次模型请求。本文先停在会话上下文构建这一层,压缩策略本身留到后面的文章展开。
聊天窗口出现了字,算不算已经保存?
现在回到重启前的那一刻。
Pi 的 AgentSession 收到事件以后,会先通知扩展和监听者,再处理 message_end 对应的会话持久化。对于普通用户、助手和工具结果消息,它会调用 sessionManager.appendMessage()。这意味着,看到一个事件,不足以单独证明磁盘记录已经完成。事件与持久化顺序
写入管理器本身也有一个小细节:新会话还没有助手消息时,Pi 会先把条目留在内存里。第一条助手消息到来后,才把前面的头部、用户消息和助手消息一起写入;后续通常继续追加。在实际临时目录里,我验证了“只有首条用户消息时文件尚未生成”,也验证了助手消息到来后,重新打开能够读回这段对话。
不过,不能把这个局部结果直接说成“OpenClaw 的用户消息一律等到回复后才保存”。外层还有预创建文件、写入守卫和主动刷新等路径。OpenClaw 的 prepareSessionManagerForRun() 就会处理一种特殊状态:文件已经存在,却没有助手消息。它会整理管理器和文件的初始状态,让后面的首次写入保持正确顺序。OpenClaw 初始化适配
这段代码也提醒我们,嵌入一个 Agent 库,工作远不止调用它的 prompt()。上层系统和下层库可能都碰到同一份会话文件;双方对“文件存在”“已经刷写”的理解,需要在边界上对齐。
再往下还有一层:同步文件写入成功,与断电后数据一定还在,也不能画等号。本期没有做断电、强制杀进程或磁盘故障实验,不为这些路径提供持久性保证。文章里的验证,是正常文件操作下的写入和重新读取。
最难补上的,是那个没有回来的工具结果
假设保存清单这一步发生了这样的顺序:模型提出 write 调用,工具实际写好了文件,随后进程退出,结果消息没有完整留下。
这是用来分析边界的假设故障窗口,不是本期复现出的 OpenClaw 故障。它的麻烦在于:外部世界可能已经改变,对话记录却缺少确认。
对模型接口来说,工具调用和工具结果通常还必须满足配对要求。如果历史里只有调用,没有对应结果,后续请求就可能被接口拒绝。OpenClaw 因此在写入和历史整理环节设置了工具结果守卫与修复逻辑。守卫跟踪待完成的调用;在允许合成结果的路径上,可以补入一个明确标记 isError: true 的缺失结果条目。是否合成、如何表达,还受具体策略影响,不是所有模型路径都会无条件补一条。写入守卫、缺失结果构造
但这条修复记录说的是“历史中缺少结果”,不是“目标文件一定没有写成功”。它没有神奇地回到工具宿主,撤销之前的写入,也没有提供一个可以盲目重做的许可证。
同样,也不能把合成结果理解成正常工具返回的成功回执。它的意义是帮助后续处理理解这个缺口,而不是把未知结果装扮成确定结果。
社区以前就为修复时机踩过坑:已合并的 PR #13746 调整了等待 Agent 空闲与补齐待完成结果的顺序。它说明,过早宣判“结果缺失”,可能和还在进行的工作冲突。这是历史修复记录,不是把旧问题重新列成当前待修的 bug。
如果将来要让助手自动发布文章、创建工单或调用机器人动作,这条边界会更重要。我的设计选择会是:给有副作用的动作稳定的业务标识,让目标系统支持结果查询或幂等请求;恢复时先核实已有结果,再决定是否重试。这是从本例推导出的工程建议,不是说 OpenClaw 的 JSONL 已经替所有工具提供了这套保证。
重新说一句“继续”,之前应该确认什么
这次研究共执行了 29 项局部检查:直接使用原始 Pi 会话管理器和消息构造代码、OpenClaw 初始化适配函数,以及原样抽取的缺失结果构造函数,在真实临时文件上验证了写入、重开、分支、压缩和错误标记。没有调用真实模型,没有运行完整 OpenClaw,也没有验证多进程竞争或故障恢复全链路。
如果让我从头实现一个小型助手,我会先把三件事拆开:用索引找到会话,用结构化记录重建上下文,用独立的动作结果确认决定是否重试。索引更新可以有自己的排他写入与原子写入机制;OpenClaw 的会话存储就是这样组织相应入口的。但这仍不意味着索引、对话文件和外部工具副作用天然组成一个事务。索引更新入口
回到那句“刚才第二项,再拆细一点”。当我们确认打开了同一段会话、当前上下文包含相关清单,助手就有条件继续讨论。至于刚才那次保存究竟有没有完成,最直接的证据仍然是工具的实际结果,必要时还要查看文件本身。
会话持久化让对话能够延续;可靠执行还要求系统承认并处理那些尚未确认的动作。下一篇,我们把重启暂时放到一边:如果你一边说“继续细化”,另一边又发来“先别改”,同一个会话里的两次请求应该怎样排队?

