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,还会携带 agentIdsessionKeysessionIdtochannelreplyChannel 等信息。

原因就在于: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. 为什么要同时有 sessionKeysessionId

这是 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 的关键字段,其中包括 sessionIdsessionStartedAtlastInteractionAtupdatedAtsessionFilechatType、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. sessionStartedAtlastInteractionAtupdatedAt 的区别

这三个字段很容易混淆。

可以这样理解:

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,后续是带有 idparentId 的 session entries,形成一种树结构。常见 entry 类型包括 messagecustom_messagecustomcompactionbranch_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 存储维护,默认 modewarn,可以设置为 enforce;还可以配置 pruneAftermaxEntriesmaxDiskByteshighWaterBytes 等。(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。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐