会话状态感知
多个会话经常协同处理同一个问题。例如:管理者将任务委派给子代理,人类直接介入某个工作会话,以及两个代理通过 sessions_send 进行协调。每个会话都会对其他会话形成假设。一旦其他参与者介入,这些假设就会立刻过时。会话状态感知(session state awareness)正是检测这种介入的机制。它会一次性通知受影响的会话,然后为该会话提供一种低成本的方式,使其在采取行动前赶上最新状态。
三个组成部分协同工作:
- 持久化信号日志(durable signal log)按会话记录选定的状态变更。
- 监视器(Watchers)持有针对每个目标的光标,并接收一条合并后的状态过期通知。
- 对账(Reconciliation)通过
session_status的changesSince拉取精确的增量变更。
信号日志¶
当被监视的会话发生实质性变更时,OpenClaw 会向共享状态数据库(session_state_events)追加一条带类型的事件。事件携带元数据和一行摘要——绝不包含消息内容。
| 类型 | 记录时机 | 是否通知监视器 |
|---|---|---|
created |
新会话具有可信的创建归属 | 否(仅记录日志) |
human_direct_message |
人类直接向被监视的会话发送一轮对话 | 是 |
upstream_missing |
已接管会话的上游来源消失 | 是 |
goal_changed |
会话的目标状态被创建、更新或清除 | 是 |
child_spawned |
创建了子代理或 ACP 子会话 | 否(初始化光标) |
run_completed |
子运行成功结束 | 否(仅记录日志) |
run_failed |
子运行失败、超时或被取消 | 否(仅记录日志) |
compacted |
会话的历史记录被压缩 | 否(仅记录日志) |
adopted |
目录会话被接管到 OpenClaw 中 | 否(仅记录日志) |
每个事件都标明其执行者(human、agent 或 system)。被取消和超时的子运行会记录为失败事件,精确结果(cancelled、timeout 或 error)保留在事件载荷中。
会话的状态版本(state version)就是其日志中的最大序号,由一个可持久化的按会话头部(head)跟踪,该头部在日志修剪后依然保留。当会话记录过变更时,sessions_list 的行会包含 stateVersion。session_status 始终报告该值。
仅记录日志的事件类型用于对账历史,而非通知:普通子运行完成的投递仍由子代理通告负责,信号日志绝不会重复记录它。
会话创建还会默认单独排入一条一次性的 Home 通知,由 session.notifyOnCreate 控制。它不会注册监视器,也不会唤醒 Home。与持久化的监视器通知不同,它仅使用有界的内存系统事件队列。可见性排除项请参阅新会话感知。
监视器¶
监视器(watcher)是持有针对某个目标的光标(存储在 session_watch_cursors 中)的会话。光标来自三个来源:
- 隐式(派生边)。 当会话派生(spawn)子代理或 ACP 子会话时,父会话的光标会自动初始化到子会话的派生版本。父会话从不手动订阅。
- 环境组(ambient groups)。 在
session.groupScope: "per-group"下,代理的主会话会在其隔离的群组、房间和频道会话收到首个来自人类的轮次后监视它们。这与session.dmScope无关。将房间路由到主会话无需监视,因为它已经共享主会话对话。 - 显式(
sessions_send watch: true)。 任何协调者都可以监视非派生的目标。在sessions_send上传递watch: true。发送成功投递后,发送者即被注册为实际收到消息的会话的监视器。注册从目标的当前状态版本开始——先前历史绝不会产生通知。当设置了该参数时,工具结果会报告watched: true|false。
监视器身份必须是代理限定的会话键。在 session.scope="global" 下,共享的 global 键在不同代理之间存在歧义,因此这类会话会获得持久化日志和 changesSince,但不会收到主动通知。
监视还会记录其监视器所在的物理存储。更改 session.store 不会将已排队的通知转移到具有相同键的其他会话中。存储来源未知的旧监视关系会保留历史记录,但需要重新注册后才能恢复主动通知。下一次组会话轮次会针对当前存储重新注册其环境监视。
监视关系会自行清理:光标行随信号日志的保留期限过期,在监视器会话重置时被移除,并随任一会话的删除而移除。已提交的重置即使后续清理步骤失败,仍会清除其监视。v1 中没有取消监视(unwatch)的动作。
从会话目录接管的受监视 Claude、Codex、OpenCode 和 Pi 会话会按固定节奏检查是否存在直接来自上游的人类活动。Pi 的监视在其会话进入只追加(append-only)v3 格式后开始。检测到的活动会像其他直接人类轮次一样,进入相同的信号日志和监视器流程。
OpenCode 的检测刻意保持保守。OpenCode 的 v1 表不保留消息来源,因此报告歧义行会产生误报。逐消息的来源信息仅存在于其 v2 模式中。因此,OpenCode 不报告仅包含图片的轮次、仅提及 @file 的轮次、路由到子代理的斜杠命令,或来自 ACP 客户端的轮次——这些客户端会用受众(audience)标注内容(OpenCode 将其映射为 synthetic 或 ignored)。它还会抑制与先前 50 条用户消息中任意一条文本匹配的文本,以应对压缩回放(compaction replay)的情况。因此,人类在该窗口内故意重复相同文本可能会被漏报。
如果已采用会话的上游源被外部删除,连续三次缺失检查会为其监视器产生一个 upstream_missing 信号,并移除上游链接。连续三次检查大约相当于三个监控节拍。再次继续目录会话会创建一个新的链接。
通知:一个,而不是许多¶
当可通知事件到达且某个监视器的游标落后时,该监视器会在其下一轮收到一条系统通知:
Session "agent:main:subagent:child" changed (other actor). Reconcile before acting: session_status sessionKey "agent:main:subagent:child" changesSince 12.
主会话监视器也会通过心跳唤醒立即被唤醒。嵌套子代理监视器会在其下一轮收到该通知。
该协议刻意防垃圾:
- 每个监视器/目标对只有一条待处理通知。 通知文本在待处理期间保持字节稳定,系统事件队列会基于它去重。对同一目标的二十次快速变更仍只会在监视器的提示中产生一行。
- 冻结水位线。 当通知入队时,游标会冻结其已通知位置。后续实质性事件只会推进实质性水位线。它们不会重新通知。
- 排空时确认,仅为交错工作重新打开。 当监视器的轮次消费该通知时,游标会前进。如果在入队和排空之间又有更多实质性事件到达,则只为剩余部分打开一条全新通知。
- 自我抑制。 监视器永远不会收到其自身引发的事件的通知。
- 重启恢复。 待处理通知保存在内存队列中。网关重启后,启动扫描会从持久游标中重新物化它们。
协调¶
通知会告诉监视器确切该做什么。带有 changesSince: <version> 的 session_status 会返回该版本之后的类型化事件(最多 200 条),且不会推进任何游标:
{
"stateVersion": 19,
"stateChanges": {
"events": [
{
"sequence": 14,
"kind": "human_direct_message",
"actorType": "human",
"summary": "human message via telegram"
},
{ "sequence": 19, "kind": "goal_changed", "actorType": "human", "summary": "goal updated" }
],
"historyGap": false
}
}
historyGap: true 表示请求的版本早于保留的历史——应刷新整个会话状态(sessions_history、session_status),而不是将响应视为精确增量。缺口信号是精确的:它来自每个会话的修剪水位线,而不是从序列算术推断得出。
存储与限制¶
历史记录保存在共享状态数据库中,限制为 30 天和 50,000 行。修剪后,每个会话的头部保持单调。记录是尽力而为的。追加失败会被记录日志,并且永远不会导致源轮次失败。因此,stateVersion 是信号日志头部,而不是事务性变更数据捕获版本。
子运行结果会异步记录,因此等待共享数据库不会阻塞 Gateway 事件处理。完成会并入记录工作,且被替换或临时运行所有者无法声明该运行的首个终止事件。
当前限制:
- 通知投递假设只有一个网关进程拥有共享状态数据库。多个网关共享持久日志和
changesSince,但 v1 不会跨进程推送通知。 - 压缩事件覆盖嵌入式运行时的压缩所有者。仅原生 harness 的压缩未被完整记录。
- 取消结果的有效负载详情目前由 ACP 子运行产生。原生子代理取消会表现为通用失败。
- 上游自回声检测会比较规范化用户文本。外部提示如果匹配该会话最近 10 条 OpenClaw 侧用户消息之一,则被视为自回声。
- 在 v1 中,单条本地 Claude JSONL 行如果大于每个节拍的 1 MiB 扫描上限,会阻塞该会话的游标。未分类字节永远不会被跳过。
- 在 v1 中,单条 Pi JSONL 行如果大于每个节拍的 1 MiB 扫描上限,会阻塞该会话的游标。未分类字节永远不会被跳过。
- 旧版 Pi 会话被采用时没有上游链接。恢复一次以将文件迁移到 v3,然后再次从目录继续它以开始监控。
- OpenCode 检查每个节拍发出一个批量数据库查询。只有当该查询显示其持久事件序列已前进时,才会运行会话导出。
- 配对节点 Claude 检查每个节拍对最新 50 条转录项进行分类。更大的突发可能落在 v1 扫描窗口之外。
- 配对节点 Claude 历史读取不会暴露明确的线程未找到结果,因此在 v1 中,远程 Claude 删除不会被分类为
upstream_missing。 - 在 v1 中,未被采用的目录会话仍位于感知层之外。
- 在此功能之前采用的会话没有上游链接。从目录中继续一次以开始上游监控。
- 上游监控要求每个已采用的会话键有一个拥有代理。采用会使用已解析的代理并返回带代理限定的键。不同的被监视键可以监视同一个原生线程。在多个代理 ID 下复用完全相同键的链接会因歧义而被跳过。
相关¶
- 会话工具 —
sessions_send、session_status、sessions_list - 子代理 — 生成边和完成公告
- 心跳 — 排队通知如何唤醒主会话
- 会话管理 — 会话键、作用域、生命周期
- Codex 会话目录与监督 — 原生会话发现和采用
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 cl/openclaw