OpenClaw 源码解析(八):Session 会话模型与 sessionKey 设计
1. 本期目标
前几期我们已经分析了 OpenClaw 的 CLI 入口、初始化流程、agent 命令执行链路,以及 Gateway 控制平面。
这一期进入 OpenClaw 中非常关键的一个概念:
Session,会话。对于普通 Chatbot 来说,会话通常只是“聊天历史”。但在 OpenClaw 里,Session 不只是历史记录,它同时承担了:
1. 区分不同用户、不同渠道、不同群聊、不同任务来源;2. 决定一次消息应该继承哪段上下文;3. 记录当前会话对应的 transcript 文件;4. 保存模型、thinking、verbose、sendPolicy 等会话级状态;5. 支持 reset、idle 过期、daily reset、cleanup 和 compaction;6. 支持 Gateway、Control UI、CLI、Channel 统一查询和管理会话。官方文档也说明,OpenClaw 会根据消息来源将对话组织到不同 sessions 中,例如 DMs、群聊、房间 / 频道、cron jobs 和 webhooks 都会走不同的会话路由策略。(OpenClaw)
所以本期的核心问题是:
OpenClaw 如何用 sessionKey 和 sessionId,把多 Agent、多渠道、多用户、多任务的上下文组织起来?2. 为什么 Session 很重要?
前面讲 openclaw agent --message 的时候,我们已经看到,用户发送一条消息时,CLI 不只是传入 message,还会携带 agentId、sessionKey、sessionId、to、channel、replyChannel 等信息。
原因就在于:OpenClaw 需要先确定“这条消息属于哪一个会话”。
比如:
同一个用户在 Telegram 私聊里问的问题;同一个用户在 Slack 频道里问的问题;某个群聊里的消息;某个 cron 定时任务触发的消息;某个 webhook 触发的消息;某个 subagent 执行过程中的消息;这些消息不能全部混在一个上下文里。
如果混在一起,就会出现严重问题:
A 群聊中的上下文污染 B 群聊;Alice 的私聊内容被 Bob 的私聊继承;cron 后台任务把普通用户对话打乱;subagent 执行历史进入主会话;模型在错误上下文里继续回答。所以,OpenClaw 必须有一套稳定的会话路由机制。
一句话理解:
Session 是 OpenClaw 管理上下文边界的核心机制。3. sessionKey 是什么?
sessionKey 可以理解为“会话桶”的名字。
官方深度文档中说,sessionKey 用来标识当前消息属于哪个 conversation bucket,也就是哪一个路由和隔离上下文。常见形式包括主会话、群聊、房间 / 频道、cron 和 webhook 等。(GitHub)
例如:
主会话:agent:<agentId>:main群聊:agent:<agentId>:<channel>:group:<id>频道 / 房间:agent:<agentId>:<channel>:channel:<id>agent:<agentId>:<channel>:room:<id>Cron:cron:<job.id>Webhook:hook:<uuid>可以这样理解:
sessionKey 不是随机 ID,而是带有路由含义的结构化字符串。它回答的是:
这条消息应该进入哪个上下文桶?比如:
agent:main:main大概表示:
main agent 的主会话。而:
agent:main:telegram:group:123456大概表示:
main agent 在 Telegram 某个 group 中的会话。所以,sessionKey 的核心作用是“路由”。
4. sessionId 是什么?
如果说 sessionKey 是“会话桶”,那么 sessionId 就是“当前桶里正在使用的 transcript 文件 ID”。
官方文档中明确说明,每个 sessionKey 都会指向一个当前 sessionId,而 sessionId 对应继续记录对话的 transcript 文件。(GitHub)
可以这样理解:
sessionKey:稳定的会话入口,例如 agent:main:main。sessionId:当前实际对话记录文件的 ID。二者关系大概是:
sessionKey ↓sessions.json 中的一条 SessionEntry ↓当前 sessionId ↓<sessionId>.jsonl transcript 文件也就是说,sessionKey 通常比较稳定,而 sessionId 可能会变化。
例如用户在主会话中连续对话:
sessionKey = agent:main:mainsessionId = abc-001当用户执行 /new 或 /reset 后:
sessionKey = agent:main:mainsessionId = def-002会话入口没变,但实际 transcript 文件换了。
5. 为什么要同时有 sessionKey 和 sessionId?
这是 OpenClaw Session 设计中最关键的一点。
如果只有 sessionKey,那么每个会话桶只能永远追加到同一个历史文件中,时间久了上下文会越来越长。
如果只有 sessionId,那么系统又很难稳定地从“消息来源”找到“应该继续哪个会话”。
所以 OpenClaw 把它们拆开:
sessionKey 负责路由;sessionId 负责具体历史文件。这样就能同时满足两个需求:
第一,外部消息可以稳定路由到同一个会话入口。第二,会话入口内部可以因为 reset、daily reset、idle expiry 等原因切换到新的 transcript。官方文档也说明,/new、/reset 会为同一个 sessionKey 创建新的 sessionId;daily reset 默认会在 Gateway 主机本地时间 4:00 后的下一条消息处创建新 sessionId;idle expiry 则会在超过空闲窗口后创建新 sessionId。(GitHub)
这是一种很合理的设计:
对外保持稳定入口;对内允许历史轮换。6. 消息来源如何映射到 Session?
官方 Session 文档给出了几类来源的默认行为:
Direct messages:默认共享 session。Group chats:按 group 隔离。Rooms / channels:按 room 或 channel 隔离。Cron jobs:每次运行使用新鲜 session。Webhooks:按 hook 隔离。这些策略说明 OpenClaw 的会话路由是按“消息来源语义”设计的,而不是简单按用户输入文本设计的。(OpenClaw)
可以画成:
外部消息 ↓判断来源类型 ↓生成 sessionKey ↓查找 sessions.json ↓找到当前 sessionId ↓读取对应 transcript ↓构造本次 Agent 上下文也就是说,Session 是 Channel 和 Agent 之间的重要中间层。
7. DM isolation:为什么私聊也要隔离?
官方文档中有一个重要提醒:默认情况下,所有 DMs 会共享一个 session,这对单用户部署是可以的;但如果多个人都能私聊你的 agent,就应该开启 DM isolation,否则不同人的私聊上下文会共享。(OpenClaw)
这点非常重要。
默认共享 DM 的好处是:
单用户使用时,上下文连续;用户可以从不同私聊入口延续同一个助手上下文。但在多人场景下就有风险:
Alice 的私聊内容可能进入 Bob 的上下文;Bob 可能间接看到 Alice 之前告诉 Agent 的信息;Agent 的回答可能被其他人的历史影响。所以官方建议在多人可私聊场景中使用:
{ "session": { "dmScope": "per-channel-peer" }}几种常见策略可以这样理解:
main:所有 DM 共享主会话。per-peer:按发送者隔离。per-channel-peer:按渠道 + 发送者隔离。per-account-channel-peer:按账号 + 渠道 + 发送者隔离。这说明 Session 不只是上下文管理问题,也是隐私边界问题。
8. Session 状态存在哪里?
OpenClaw 的 session 状态由 Gateway 管理。官方文档明确说,所有 session state 都由 Gateway 拥有,UI 客户端需要向 Gateway 查询 session 数据。(OpenClaw)
在磁盘上,每个 agent 的 session 文件通常位于:
~/.openclaw/agents/<agentId>/sessions/其中主要有两类文件:
sessions.json<sessionId>.jsonl官方深度文档说明,OpenClaw 有两层 session 持久化结构:第一层是 sessions.json,它是 sessionKey -> SessionEntry 的 key/value map,用来保存 session 元数据;第二层是 transcript,也就是 <sessionId>.jsonl,用于保存真实对话、工具调用和 compaction summary,并在未来回合中重建模型上下文。(GitHub)
可以这样理解:
sessions.json:会话索引表。<sessionId>.jsonl:具体会话内容。9. sessions.json 负责什么?
sessions.json 更像一个会话元数据表。
它不直接保存完整聊天内容,而是保存当前会话状态,例如:
sessionKey 对应的当前 sessionId;会话开始时间;最后真实用户交互时间;最后更新时间;chatType;provider / subject / room / space / displayName;thinking / verbose / reasoning 等 toggles;sendPolicy;模型覆盖;token 计数;compaction 计数。官方深度文档列出了 SessionEntry 的关键字段,其中包括 sessionId、sessionStartedAt、lastInteractionAt、updatedAt、sessionFile、chatType、provider / subject / room / space / displayName、thinking / verbose / reasoning / elevated、sendPolicy、providerOverride / modelOverride / authProfileOverride、token counters、compactionCount 等。(GitHub)
可以举一个简化例子:
{ "agent:main:main": { "sessionId": "abc-001", "sessionStartedAt": 1760000000000, "lastInteractionAt": 1760000300000, "updatedAt": 1760000400000, "chatType": "direct", "thinkingLevel": "high", "modelOverride": "anthropic/claude-sonnet", "contextTokens": 12000 }}这个文件回答的是:
这个 sessionKey 当前指向哪个 sessionId?这个会话最近什么时候被用户真正使用?这个会话当前有哪些设置?这个会话大概用了多少 token?10. sessionStartedAt、lastInteractionAt、updatedAt 的区别
这三个字段很容易混淆。
可以这样理解:
sessionStartedAt:当前 sessionId 的开始时间,daily reset 主要看它。lastInteractionAt:最后一次真实用户 / channel 交互时间,idle reset 主要看它。updatedAt:这条 store row 最近被修改的时间,主要用于列表展示、清理和内部 bookkeeping。官方文档特别强调,updatedAt 不是 daily / idle reset freshness 的权威依据;daily reset 使用 sessionStartedAt,idle reset 使用 lastInteractionAt。(OpenClaw)
这很合理。
因为有些系统事件可能会更新 session 行,例如 heartbeat、cron、gateway bookkeeping,但它们不应该让一个会话“看起来像刚被用户使用过”。
否则会出现:
用户很久没说话;但后台系统事件一直更新 updatedAt;idle reset 永远不触发。所以 OpenClaw 把“真实用户交互时间”和“普通元数据更新时间”分开了。
11. transcript:.jsonl 负责什么?
如果 sessions.json 是索引表,那么 <sessionId>.jsonl 就是实际的对话记录。
官方深度文档说明,transcript 是 JSONL 文件,第一行是 session header,后续是带有 id 和 parentId 的 session entries,形成一种树结构。常见 entry 类型包括 message、custom_message、custom、compaction、branch_summary 等。(GitHub)
可以简化理解为:
第一行:描述这个 session 的基础信息。后续每一行:记录一次用户消息、助手消息、工具结果、扩展消息、压缩摘要等。示意:
<sessionId>.jsonl{"type":"session","id":"abc-001","cwd":"...","timestamp":...}{"type":"message","role":"user","content":"你好"}{"type":"message","role":"assistant","content":"你好,有什么可以帮你?"}{"type":"message","role":"toolResult","content":"..."}{"type":"compaction","summary":"..."}所以 transcript 承担的是:
保存真实上下文;未来重建模型输入;支持工具调用历史;支持 compaction;支持分支和恢复。12. 为什么 transcript 用 JSONL?
JSONL 的好处是适合追加写入。
一次对话中,模型可能逐步产生消息,工具可能返回结果,系统可能写入 compaction summary。如果每次都重写一个巨大的 JSON 文件,成本会比较高,也更容易出现写入冲突。
JSONL 则更像日志:
一行一个事件;顺序追加;方便 tail;方便局部读取;方便恢复和索引。OpenClaw 深度文档也提到,Gateway history readers 应避免在不需要完整历史时物化整个 transcript;first-page history、embedded chat history、restart recovery、token / usage checks 会使用 bounded tail reads,而完整扫描会走异步 transcript index。(GitHub)
这说明 OpenClaw 在 session 读写上考虑了性能问题。
13. Session 生命周期:复用、过期与重置
OpenClaw 的 session 不是无限复用。
官方 Session 文档说明,sessions 会被复用直到过期,常见过期方式包括 daily reset、idle reset 和 manual reset。daily reset 默认在 Gateway 主机本地时间 4:00 后的新消息处触发;idle reset 需要设置 session.reset.idleMinutes;manual reset 则通过 /new 或 /reset 触发。(OpenClaw)
可以画成:
用户消息进入 ↓根据 sessionKey 找到当前 sessionId ↓检查 daily reset 是否到期 ↓检查 idle reset 是否到期 ↓如果到期:创建新 sessionId ↓如果未到期:继续使用旧 sessionId这一设计解决了两个问题:
第一,保持短期连续上下文。第二,避免长期会话无限增长。14. /new 和 /reset 的含义
在使用层面,/new 和 /reset 都会让当前 sessionKey 切换到新的 sessionId。
也就是说:
sessionKey 不变;sessionId 改变;新的 transcript 开始记录。例如:
原来:agent:main:main -> sessionId = abc-001执行 /new 后:agent:main:main -> sessionId = def-002这样做比直接删除 sessionKey 更好。
因为系统仍然知道这是同一个会话入口,只是历史文件更新了。
15. Session 和 compaction 的关系
长会话会遇到上下文窗口限制。
OpenClaw 的 compaction 会把旧对话总结成 transcript 中的 compaction entry,同时保留近期消息。官方文档中说,compaction 会把较旧的对话压缩成持久化摘要,并保留近期消息;未来回合会看到 compaction summary 和 firstKeptEntryId 之后的消息。(GitHub)
可以这样理解:
reset:换一个新的 sessionId,历史上下文断开。compaction:不换会话入口,而是把旧历史压缩成摘要。二者区别是:
reset:适合彻底开始新话题。compaction:适合保留长期上下文,但压缩 token 占用。所以 Session 管理不只是“存历史”,还要处理“历史太长怎么办”。
16. Session maintenance:为什么需要清理?
OpenClaw 是长期运行的个人助手,会不断产生 session entry 和 transcript 文件。
如果不清理,磁盘上会逐渐积累:
长期不用的 session;旧 transcript;reset archive;cron 产生的临时 session;hook 产生的临时 session;subagent 产生的临时 session;trajectory sidecar。官方文档说明,OpenClaw 提供 session.maintenance 控制 session 存储维护,默认 mode 是 warn,可以设置为 enforce;还可以配置 pruneAfter、maxEntries、maxDiskBytes、highWaterBytes 等。(GitHub)
示例:
{ "session": { "maintenance": { "mode": "enforce", "pruneAfter": "30d", "maxEntries": 500 } }}这说明 OpenClaw 的 session 管理是有生命周期治理的。
17. openclaw sessions 命令能做什么?
从使用者角度,session 可以通过 CLI 和 Gateway 查询。
官方文档列出了几个常见方式:
openclaw statusopenclaw sessions --json/status/context listopenclaw sessions cleanup --dry-runopenclaw sessions cleanup --enforce其中 openclaw sessions --json 可以查看所有 sessions,/status 可以在聊天中查看上下文使用、模型和 toggles,openclaw sessions cleanup 可以预览或执行清理。(OpenClaw)
对于源码学习者来说,建议运行后重点观察:
sessions.json 里新增了什么;sessionKey 是如何生成的;sessionId 是否随着 /reset 改变;transcript 文件是否持续追加;不同 channel 是否进入不同 sessionKey;cleanup dry-run 会报告哪些可清理项。18. Gateway 中的 sessions.* 方法
在 Gateway 层,Session 不是只靠 CLI 文件操作,而是通过一系列 RPC method 暴露出来。
源码搜索结果显示,Gateway method 中包含:
sessions.listsessions.cleanupsessions.subscribesessions.unsubscribesessions.messages.subscribesessions.messages.unsubscribesessions.previewsessions.describesessions.resolvesessions.compaction.listsessions.compaction.getsessions.createsessions.compaction.branchsessions.compaction.restoresessions.sendsessions.steersessions.abortsessions.patchsessions.pluginPatchsessions.resetsessions.delete这些方法说明,Gateway 对 Session 的管理已经不仅是“列出历史记录”,而是包括订阅、消息预览、解析、创建、发送、steer、abort、patch、reset、delete、compaction branch / restore 等完整操作。(GitHub)
可以分成几类理解:
查询类:sessions.listsessions.describesessions.resolvesessions.preview订阅类:sessions.subscribesessions.messages.subscribe控制类:sessions.createsessions.sendsessions.steersessions.abortsessions.resetsessions.delete修改类:sessions.patchsessions.pluginPatch压缩类:sessions.compaction.listsessions.compaction.getsessions.compaction.branchsessions.compaction.restore维护类:sessions.cleanup这进一步说明,Session 是 Gateway 控制平面的一等对象。
19. 从一次消息看 Session 的参与位置
现在可以把完整链路串起来:
用户发送消息 ↓Channel adapter 或 CLI 接收输入 ↓Gateway 确定 agentId ↓根据来源生成或接收 sessionKey ↓查 sessions.json ↓找到当前 sessionId ↓读取 <sessionId>.jsonl 的相关上下文 ↓Agent Runtime 构造 prompt ↓模型生成回复 / 工具调用 ↓写入 transcript ↓更新 sessions.json 元数据 ↓通过 Gateway / Channel 返回结果其中 Session 出现了三次:
运行前:决定上下文来自哪里。运行中:影响模型输入和工具历史。运行后:保存新消息和更新元数据。所以 Session 是贯穿 Agent turn 的核心状态。
20. 初学者容易混淆的几个点
20.1 Session 不是单纯聊天窗口
普通聊天应用里,一个 session 可能就是一个聊天窗口。
但 OpenClaw 里的 session 更复杂,它同时绑定:
agent;channel;sender;group;room;cron job;webhook;model override;send policy;token counters;compaction state。20.2 sessionKey 不等于 sessionId
sessionKey:稳定路由入口。sessionId:当前 transcript 文件 ID。20.3 updatedAt 不等于真实交互时间
updatedAt:任意元数据更新都可能改变。lastInteractionAt:真实用户 / channel 交互时间。20.4 Reset 不一定删除旧 transcript
Reset 的核心是让当前 sessionKey 指向新的 sessionId。旧 transcript 可以作为历史文件继续存在,后续清理策略再决定是否删除或归档。
20.5 Gateway 是 session state 的权威来源
官方文档明确说明 session state 由 Gateway 拥有,UI 客户端应该向 Gateway 查询 session data。(OpenClaw)
所以不要只看本地某个文件就断言当前状态,尤其在 remote mode 下,本地文件可能不是 Gateway 正在使用的文件。
21. 本期源码阅读建议
这一期建议重点看这些文件和文档:
docs/concepts/session.md ↓先看 Session 的概念、路由、DM isolation、生命周期和维护策略。docs/reference/session-management-compaction.md ↓看 sessions.json、transcript、sessionKey、sessionId、compaction 的细节。src/config/sessions.ts ↓看 session 相关导出和路径解析。src/config/sessions/store.ts ↓看 session store 的读写、更新和维护。src/config/sessions/transcript.ts ↓看 transcript 文件的读取、摘要、尾部读取等逻辑。src/gateway/server-methods/sessions.ts ↓看 Gateway 的 sessions.* RPC 方法如何实现。src/auto-reply/reply/session.ts ↓看 Agent turn 前如何初始化 session state。阅读时可以带着几个问题:
1. sessionKey 是在哪里生成的?2. sessionKey 到 sessionId 的映射在哪里保存?3. /reset 后 sessions.json 如何变化?4. transcript 文件什么时候创建?5. Agent 运行前如何从 transcript 重建上下文?6. compaction entry 如何进入 transcript?7. sessions.list 和 sessions.preview 分别读取哪些内容?8. cleanup 是直接删文件,还是通过 Gateway 写队列处理?22. 我的理解
我认为 Session 是 OpenClaw 从“聊天工具”变成“个人 AI 助手系统”的关键设计之一。
因为一个真正的个人助手不会只面对一个窗口:
它可能同时接收 Telegram 私聊;同时在 Slack 频道里工作;同时有 cron 后台任务;同时有 WebChat 页面;同时有 mobile node;同时有 subagent 分支任务;同时有多个 agent identity。这些任务都需要上下文,但又不能互相污染。
所以 OpenClaw 用:
sessionKey 管路由;sessionId 管历史文件;sessions.json 管元数据;transcript 管真实对话;Gateway 管统一状态;maintenance 管长期清理;compaction 管长上下文压缩。这样才能支撑一个长期运行、多入口、多任务、多上下文的 Agent 系统。
23. 本期重点理解
这一期可以总结为五点:
第一,Session 是 OpenClaw 管理上下文边界的核心机制。第二,sessionKey 用来标识会话路由桶,负责把消息映射到正确上下文。第三,sessionId 是当前 transcript 文件 ID,一个 sessionKey 可以因为 reset、daily reset 或 idle expiry 指向新的 sessionId。第四,OpenClaw 使用两层持久化结构:sessions.json 保存会话元数据,<sessionId>.jsonl 保存真实对话和工具调用历史。第五,Session 由 Gateway 统一管理,并通过 sessions.* RPC 方法暴露给 CLI、Control UI 和其他客户端。一句话概括:
OpenClaw 的 Session 设计,本质上是在多 Agent、多渠道、多用户、多任务环境中,为每条消息找到正确的上下文边界。24. 本期小结
本期主要分析了 OpenClaw 的 Session 会话模型。OpenClaw 使用 sessionKey 标识会话路由桶,用 sessionId 标识当前实际 transcript 文件。sessions.json 负责保存 sessionKey -> SessionEntry 的映射和元数据,<sessionId>.jsonl 负责保存真实对话、工具调用、扩展消息、compaction summary 等内容。Session 会根据消息来源进行路由,Direct Message、群聊、频道、cron 和 webhook 都有不同隔离策略。Session 生命周期还包含 daily reset、idle reset、manual reset、cleanup 和 compaction 等机制。通过这些设计,OpenClaw 能够在多渠道、多用户、多任务环境中维持清晰的上下文边界。
这一期可以用一句话总结:
sessionKey 决定消息进入哪个上下文桶,sessionId 决定当前桶使用哪份对话历史,Gateway 则负责把这一切统一管理起来。下一期可以继续分析:
OpenClaw 源码解析(九):Channel 接入机制与消息路由流程
夜雨聆风