Gateway 是 OpenClaw 生态中的常驻网关进程。它以单实例运行,持有所有消息渠道的连接——WhatsApp、Telegram、Slack、Discord、Signal、iMessage——是所有消息进出的唯一入口。用户从任何渠道发来的消息,都先到 Gateway,由它路由到对应的 AI agent,再把响应送回去。控制面客户端(macOS app、CLI、Web UI)通过 WebSocket 连接 Gateway,Gateway 再对接各消息平台和 AI 后端。换句话说,它既是消息的中枢,也是会话状态的管理中心。
这篇文章要聊的就是后者——Gateway 怎么管理会话状态。它最核心的工作不是转发请求,而是把一条条离散的消息串成连贯的对话。OpenClaw 的会话管理设计在小规模场景下优雅得令人舒适,但当用户和任务规模上来之后,它的文件系统架构会成为瓶颈。
一、会话串联机制
Session Key:一切的起点
用户从 WhatsApp 发来一条消息,Gateway 要解决的第一个问题是:这条消息属于哪个对话?
OpenClaw 的做法是给每个对话分配一个 Session Key——一个结构化的路由标识符。格式长这样:
agent:<agentId>:<channel>:dm:<peerId>
agent:<agentId>:<channel>:group:<groupId>
agent:<agentId>:<channel>:channel:<channelId>
Channel 是消息来源(WhatsApp、Telegram、Slack),peerId 是发送者的唯一标识,agentId 是处理这条消息的 AI agent。三层组合,精确锁定一个对话上下文。
关键在于,同一个用户从不同渠道发来的消息,可以通过 Identity Links 映射到同一个 canonical identity,进而路由到同一个 Session。也就是说,你在 WhatsApp 上跟 AI 聊了半截,切到 Telegram 继续聊,上下文不丢。
两层持久化模型
Session 找到了,接下来要把历史上下文加载出来。OpenClaw 用了两层存储:
第一层:Session Store(sessions.json)
一个 JSON 文件,存在 ~/.openclaw/agents/<agentId>/sessions/sessions.json,是所有 session 的索引。每个 entry 记录了 session 的元数据——最近活跃时间、token 用量、关联的 transcript 文件路径。
读取时走内存缓存(TTL 45 秒),避免每次请求都读磁盘。写入时用文件锁 + 临时文件原子重命名,保证并发安全。
第二层:Transcript 文件(*.jsonl)
每个 session 对应一个 JSONL 文件,存完整的对话历史。格式是追加写入的树结构——每条记录有 id 和 parentId,支持对话分叉和回溯。
~/.openclaw/agents/<agentId>/sessions/
├── sessions.json # session 索引
├── abc123.jsonl # session abc123 的完整对话
├── def456.jsonl # session def456 的完整对话
└── ...
一次请求的完整链路
把流程拉直了看:
1. 消息进来 → 提取 channel + peerId + agentId
2. 构造 Session Key → "agent:bot1:whatsapp:dm:user42"
3. 查 Session Store → 命中缓存或读 sessions.json
4. 加载 Transcript → 读 JSONL,重建对话树
5. 构建上下文 → 从树中提取最近的对话路径
6. 发给模型 → 带着完整上下文调 Claude/GPT
7. 追加响应 → 写入 JSONL,更新 sessions.json
8. 返回给用户
步骤 4 和 5 是精髓。对话不是一个扁平的消息列表,而是一棵树。当对话太长触发压缩时,旧的消息会被 AI 总结成摘要,保留在树的根部。新消息挂在摘要节点下面,模型看到的是”摘要 + 最近的消息”——既不丢上下文,又控制了 token 用量。
这就是 Gateway 把前后串起来的方式:用 Session Key 路由,用 JSONL 存历史,用树结构管理上下文窗口。
二、架构优势
1. 跨渠道的上下文连续性
用户不需要关心自己在哪个平台。WhatsApp 上问了一半的问题,切到 Slack 上继续追问,AI 记得之前说了什么。
这在实际使用中的体验提升是巨大的。特别是对于需要在手机和电脑之间切换的场景——手机上用 WhatsApp 快速发个指令,回到电脑上在 Slack 里看详细结果,对话不会断。
2. 对话树而非对话列表
JSONL 的树结构允许对话分叉。你可以从对话的某个中间节点重新开始,不需要清掉后面的内容。这对调试和实验特别有用——“从第三轮重来,换个方向试试”。
append-only 的写入模式也意味着不会丢数据。即使进程崩溃,最多丢当前这一轮的响应,历史对话完好无损。
3. 自动压缩保持上下文窗口可控
当对话超过 token 阈值时,自动触发 compaction:旧消息被总结成摘要,释放 token 空间。默认预留 16,384 token 给新的对话轮次。
这个设计让长对话成为可能。你不需要手动”清理上下文”或者担心”聊太多了模型忘了前面的”——系统自动处理。代价是压缩过程中可能丢失一些细节,但对大部分场景来说是可接受的 trade-off。
4. Session Scope 的灵活配置
Gateway 提供了四档 DM 隔离级别:
| 级别 | 说明 | 适用场景 |
|---|---|---|
main | 所有 DM 共享一个 session | 个人使用,只有自己在聊 |
per-peer | 按发送者隔离 | 多人共用一个 agent |
per-channel-peer | 按渠道+发送者隔离 | 多渠道多人 |
per-account-channel-peer | 按账号+渠道+发送者隔离 | 多账号多渠道多人 |
一个配置项就能调整隔离粒度。个人用选 main,团队用选 per-channel-peer,SaaS 用选最细粒度。
5. 零外部依赖
不需要 Redis,不需要 PostgreSQL,不需要任何外部存储。一个文件系统就够了。部署时不需要操心数据库连接池、Redis sentinel、存储集群——装好 Node.js 就能跑。
对于个人开发者和小团队,这是一个巨大的优点。运维复杂度低,调试方便(直接看 JSON 文件),备份简单(cp -r 就行)。
三、规模化瓶颈
1. 文件系统架构的天花板
所有 session 的写操作都通过 withSessionStoreLock() 序列化——同一时刻只有一个写入能执行。当并发写入多了,后面的请求就在队列里排队,超时设定是 10 秒。
来算一笔账。假设一个 gateway 实例服务 100 个活跃用户,每人每分钟发 2 条消息。每次写入涉及更新 sessions.json + 追加 JSONL 文件。如果单次写入耗时 5ms,每秒 3.3 次写入,看起来还好。
但实际上,文件 I/O 的耗时波动很大。磁盘繁忙时一次写入可能飙到 100ms 甚至更高。再加上 sessions.json 会随着 session 数量增长变大(上限 10MB),读写时间也会增长。当写入队列开始积压,用户感知到的就是”发了消息,AI 很久不回”。
正常情况:消息 → 5ms 锁等待 → 处理 → 响应
高负载时:消息 → 800ms 锁等待 → 处理 → 响应(用户已经发了第二条催促消息)
极端情况:消息 → 10s 超时 → 写入失败 → session 状态不一致
文件锁的瓶颈还只是纵向的问题。横向上,Gateway 的架构是单进程、单主机——一个实例持有所有 messaging surface 的连接(WhatsApp session、Telegram bot token、Slack WebSocket),session 数据存在本地文件系统。这意味着不能启动第二个实例来分担负载(WhatsApp 只允许一个活跃 session),不能把 session 数据迁移到另一台机器(没有分布式存储),也没有内置的集群方案、session 复制或 failover。当你的用户从 50 人涨到 500 人,唯一的出路是升级这台机器的配置。垂直扩展总有尽头。
2. 默认配置下的上下文泄露
dmScope 的默认值是 main——所有 DM 共享同一个 session。这在个人使用时完全没问题,但当多个用户同时跟 agent 对话时,A 能看到 B 的对话上下文。
官方文档明确警告了这个问题:
If your agent receives DMs from multiple people, you MUST enable secure DM mode. Without it, all users share the same conversation context, which can leak private information between users.
但默认值就是不安全的。对于不看文档的开发者(多数人),部署上线后才发现用户之间能看到彼此的对话——这是一个严肃的隐私事故。
3. Session 膨胀与可观测性缺失
每个对话一个 JSONL 文件。文件维护策略是:默认保留 500 个 session entry,超过 30 天的自动清理。听起来还行?
但实际场景中,session 的增长速度往往超出预期:
- 一个 Slack workspace 里 50 个人跟 agent 聊天 → 50 个 session
- 每个人同时在 DM 和 3 个 channel 里跟 agent 互动 → 200 个 session
- 再加上定时任务(cron job)触发的 session → 每天多几十个
- 一个月下来轻轻松松破千
session 文件的 JSONL 格式是 append-only,长对话的文件会持续膨胀。一个活跃用户一天几百轮对话,JSONL 文件几 MB。sessions.json 作为索引文件也在膨胀,接近 10MB 时触发 rotation——但 rotation 本身就是一个耗时操作。文件系统层面,一个目录下几千个文件,ls 都开始变慢,更别说 Gateway 启动时要扫描这个目录了。
更棘手的是,这种膨胀几乎是悄无声息的。当前 Gateway 没有提供 session 级别的指标仪表板(活跃 session 数、平均对话长度、压缩频率),没有 session 生命周期事件的结构化日志,也没有异常 session 的告警机制(如某个 session 的 JSONL 文件异常膨胀)。运维人员想回答”现在有多少活跃对话”这个问题,只能去数文件;想知道”哪些 session 占用了最多 token”,只能自己写脚本解析 JSONL。
在小规模使用时这不是问题——出了事直接看文件。但当 session 数以千计时,缺乏可观测性意味着你只能等问题爆发,而不是提前发现。Session 膨胀本身是可以管理的,前提是你能看见它——而这套系统恰恰让你看不见。
4. 压缩是有损的
对话压缩看起来很美好——自动总结旧消息,释放 token 空间。但总结本身就是有损压缩。
被压缩掉的内容包括:
- 具体的数字和参数(“上次你说预算是 50 万” → 压缩后可能变成”之前讨论了预算”)
- 对话中的细微转折(“你先说要 A,后来改主意要 B” → 可能只保留了 B)
- 工具调用的详细参数和返回值
- 用户纠正 AI 错误的过程
在任务型对话中,这些细节可能很关键。用户说”按上次说的那个方案来”,但”上次说的方案”已经被压缩成一句摘要了——AI 只能猜。
更麻烦的是,压缩触发时机不透明。用户不知道哪些上下文还在、哪些已经被压缩了。当 AI 突然”忘了”之前讨论过的细节,用户会困惑:是模型的问题还是上下文丢了?
5. 多 Agent 协作与长时任务
OpenClaw 支持多个 agent,每个 agent 有独立的 session 空间。但 agent 之间没有共享上下文的机制。一个典型场景:Agent A 负责客服,Agent B 负责技术支持,用户先跟 A 聊了问题背景,A 判断需要转给 B——转过去之后,B 对之前的对话一无所知,用户要重新描述一遍。没有 session 共享、没有 session 转移、没有跨 agent 的上下文传递,每个 agent 是一个孤岛。
跨 agent 协作的缺失只是表象,更深层的问题是系统缺乏高于 session 的任务抽象。OpenClaw 的典型任务——代码生成、文档分析、多轮工具调用——常常运行数十分钟,但系统没有一等公民的 Task 概念。当前的做法是用 sub-agent session 隐式承担 task 角色:sessions_spawn() 创建一个独立的 session,完成后通过 announce queue 通知主 session。这在简单场景下够用,但 sub-agent session 与真正的 Task 之间有明显的 gap:
- 无显式状态机——session 没有
pending → running → succeeded/failed的生命周期,只有”存在”和”不存在” - 无进度追踪——主 session 不知道 sub-agent 执行到哪一步了,只能等最终结果
- 无重试机制——sub-agent 失败了就失败了,没有自动重试或断点续跑
- 无持久化队列——如果 Gateway 进程崩溃,正在运行的 sub-agent 任务全部丢失
有趣的是,CronJob 反而有最完整的任务生命周期管理——runningAtMs 追踪运行状态、lastRunStatus 记录成功失败、还有卡住检测机制。但这套逻辑仅限定时触发场景,没有泛化成通用的 Task 原语。
GitHub issue 也反映了这些痛点:
- #6042:sub-agent 达到 context 上限后直接终止,没有优雅降级或任务移交
- #10334:announce 机制阻塞主 session,长时任务拖慢整个对话
- #10467:缺乏 lane 隔离,多个长时任务互相干扰
本质上,OpenClaw 选择了”渐进增强 session”而非引入独立的 Task 原语。这与其”两个抽象解释一切”的设计哲学一致——Session 和 Agent 已经足够解释系统中的大部分行为,加入 Task 会打破这种简洁性。但无论是跨 agent 协作还是长时任务管理,用 session 硬扛更高层抽象的职责,付出的都是可靠性的代价。
四、根源与出路
上面列的这些缺点不是因为代码写得烂。恰恰相反,OpenClaw 的 session 管理代码质量很高——原子写入、文件锁、缓存失效、优雅降级,每一个细节都处理得很用心。
问题出在架构假设上——不止一个,而是两个层面。
基础设施层的假设
这套系统是为”一个人或一个小团队在一台机器上运行一个 AI 助手”设计的。在这个假设下,文件系统是最好的选择——简单、可靠、零依赖。
但当使用场景从”Peter 的个人 WhatsApp 助手”变成”100 人团队的 AI 基础设施”时,这个假设就不再成立了。文件锁扛不住高并发,单机扛不住大流量,本地存储扛不住多实例,默认配置扛不住多用户。
领域模型层的假设
还有一个更隐蔽的问题:Session 的抽象直接耦合了外部平台的概念,而不是建立在平台无关的内部模型上。
回头看 Session Key 的格式:agent:<agentId>:<channel>:dm:<peerId>。这里的 channel——WhatsApp、Telegram、Slack——是一个外部平台标识,被硬编码进了路由层的最核心位置。系统的会话路由、隔离粒度、甚至 agent 协作,都建立在”消息来自哪个平台”这个外部概念上。
这带来了几个连锁效应:
-
Identity Links 是补丁,不是地基。 跨平台的身份关联通过 Identity Links 实现——把 WhatsApp 的 user42 和 Telegram 的 user42 映射到同一个 canonical identity。但这是一层事后补上的映射关系,不是系统从一开始就内建的统一身份。如果系统有一个平台无关的 Person 概念,跨平台身份就不是”需要额外配置的特性”,而是”默认行为”。
-
dmScope 的隔离粒度用平台维度定义。
per-channel-peer、per-account-channel-peer——这些隔离级别的命名方式本身就说明问题:隔离粒度是按”渠道”和”账号”切分的,而不是按”这是谁”和”这是哪个群”切分的。如果系统内建 Person 和 Group 概念,隔离粒度是天然内建的——一个 Person 就是一个对话上下文,不需要额外配置 scope。 -
跨 agent 协作困难,部分原因在这里。 第三章提到 Agent A 转给 Agent B 时上下文全丢。Session 绑定在
agent + channel + peerId上,换一个 agent 就是换一个 session。但如果 session 绑定在 Person 上,agent 只是服务这个 Person 的执行者——换 agent 不需要换对话上下文,因为上下文属于 Person,不属于 agent。
本质上,OpenClaw 把”消息来自 WhatsApp 还是 Telegram”当成了一等公民,而把”消息来自张三还是李四”当成了需要额外配置才能识别的东西。这个优先级是反的。在一个 IM 网关系统里,Person 和 Group 才应该是一等公民,外部平台应该只是适配器。
两层假设叠加,形成了经典困境
基础设施层假设文件系统够用,领域模型层假设外部平台概念可以直接作为内部抽象。两个假设在小规模场景下都合理——一个人在一台机器上通过一两个平台聊天,文件系统够用,平台概念和内部模型也确实没什么区别。但当规模增长,这两个假设同时失效:文件系统扛不住并发,平台耦合扛不住多样性。
这是一个经典的架构演进困境:让你快速起步的设计,恰恰是阻碍你规模化的那个东西。
出路
如果 OpenClaw 要认真地解决规模化问题,需要在以下方向上做出改变:
- 引入外部 session 存储——Redis 或 PostgreSQL,支持多实例共享 session 状态
- Gateway 集群化——支持多实例部署,session 数据通过外部存储共享
- 默认安全的隔离策略——
dmScope的默认值应该是per-channel-peer而不是main - session 的可观测性——内置 metrics endpoint,暴露 session 级别的指标
- 跨 agent session 传递——允许 agent 之间共享或转移对话上下文
- 引入 Task 抽象——为长时运行的操作提供显式的生命周期管理(状态机、进度追踪、重试、持久化队列),而不是继续用 session 隐式承载
- 引入平台无关的内部会话模型——以 Person 和 Group 为一等公民构建 session 路由,外部平台(WhatsApp、Telegram、Slack)降级为适配器层;Session Key 不再编码 channel,Identity Links 从可选配置变成默认行为,隔离粒度自然内建于 Person/Group 的边界中
但这些改变中的每一项都会增加复杂度,牺牲当前架构”零依赖、开箱即用”的核心优势。这又回到了那个永恒的 trade-off:简单和可扩展,你只能选一个。
OpenClaw 选了简单。对它的目标用户来说,这可能是对的。