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创建新的sessionIddaily reset 默认会在 Gateway 主机本地时间 4:00 后的下一条消息处创建新sessionIdidle 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 这个会话最近什么时候被用户真正使用 这个会话当前有哪些设置 这个会话大概用了多少 token10.sessionStartedAt、lastInteractionAt、updatedAt的区别这三个字段很容易混淆。可以这样理解sessionStartedAt 当前 sessionId 的开始时间daily reset 主要看它。 lastInteractionAt 最后一次真实用户 / channel 交互时间idle reset 主要看它。 updatedAt 这条 store row 最近被修改的时间主要用于列表展示、清理和内部 bookkeeping。官方文档特别强调updatedAt不是 daily / idle reset freshness 的权威依据daily reset 使用sessionStartedAtidle reset 使用lastInteractionAt。(OpenClaw)这很合理。因为有些系统事件可能会更新 session 行例如 heartbeat、cron、gateway bookkeeping但它们不应该让一个会话“看起来像刚被用户使用过”。否则会出现用户很久没说话 但后台系统事件一直更新 updatedAt idle reset 永远不触发。所以 OpenClaw 把“真实用户交互时间”和“普通元数据更新时间”分开了。11. transcriptsessionId.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 用 JSONLJSONL 的好处是适合追加写入。一次对话中模型可能逐步产生消息工具可能返回结果系统可能写入 compaction summary。如果每次都重写一个巨大的 JSON 文件成本会比较高也更容易出现写入冲突。JSONL 则更像日志一行一个事件 顺序追加 方便 tail 方便局部读取 方便恢复和索引。OpenClaw 深度文档也提到Gateway history readers 应避免在不需要完整历史时物化整个 transcriptfirst-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.idleMinutesmanual 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 中的compactionentry同时保留近期消息。官方文档中说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可以在聊天中查看上下文使用、模型和 togglesopenclaw 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.2sessionKey不等于sessionIdsessionKey 稳定路由入口。 sessionId 当前 transcript 文件 ID。20.3updatedAt不等于真实交互时间updatedAt 任意元数据更新都可能改变。 lastInteractionAt 真实用户 / channel 交互时间。20.4 Reset 不一定删除旧 transcriptReset 的核心是让当前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 等外部消息如何进入 OpenClawChannel adapter 如何把平台消息转换成内部消息Gateway 如何根据 channel、sender、group、room 等信息生成 sessionKey并最终触发一次 Agent run。