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:main
sessionId = abc-001
当用户执行 /new 或 /reset 后:
sessionKey = agent:main:main
sessionId = 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:<sessionId>.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 status
openclaw sessions --json
/status
/context list
openclaw sessions cleanup --dry-run
openclaw 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.list
sessions.cleanup
sessions.subscribe
sessions.unsubscribe
sessions.messages.subscribe
sessions.messages.unsubscribe
sessions.preview
sessions.describe
sessions.resolve
sessions.compaction.list
sessions.compaction.get
sessions.create
sessions.compaction.branch
sessions.compaction.restore
sessions.send
sessions.steer
sessions.abort
sessions.patch
sessions.pluginPatch
sessions.reset
sessions.delete
这些方法说明,Gateway 对 Session 的管理已经不仅是“列出历史记录”,而是包括订阅、消息预览、解析、创建、发送、steer、abort、patch、reset、delete、compaction branch / restore 等完整操作。(GitHub)
可以分成几类理解:
查询类:
sessions.list
sessions.describe
sessions.resolve
sessions.preview
订阅类:
sessions.subscribe
sessions.messages.subscribe
控制类:
sessions.create
sessions.send
sessions.steer
sessions.abort
sessions.reset
sessions.delete
修改类:
sessions.patch
sessions.pluginPatch
压缩类:
sessions.compaction.list
sessions.compaction.get
sessions.compaction.branch
sessions.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 接入机制与消息路由流程
下一期重点看 Telegram、Slack、Discord、WebChat 等外部消息如何进入 OpenClaw,Channel adapter 如何把平台消息转换成内部消息,Gateway 如何根据 channel、sender、group、room 等信息生成 sessionKey,并最终触发一次 Agent run。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)