sessions_send 协作——每个会话都会为其他会话建立一些假设。一旦另一个参与者介入,这些假设就会立刻过时。会话状态感知是一套机制:它能检测到这种介入,只向受影响的会话通知一次,并在其继续行动之前,提供一种低成本的方式让它赶上最新状态。
这三部分协同工作:
- 持久化信号日志记录每个会话的选定状态变更。
- 观察者为每个目标维护游标,并接收一次合并后的陈旧状态通知。
- 对齐通过带有
changesSince的session_status拉取精确的差异。
信号日志
当被监视的会话发生实质性变化时,OpenClaw 会向共享状态数据库(session_state_events)追加一个带类型的事件。事件包含元数据和一行摘要——绝不包含消息内容。
每个事件都会标明其行为主体(
human、agent 或 system)。被取消和超时的子运行会作为失败记录,并在事件负载中保留精确结果(cancelled、timeout 或 error)。
会话的 状态版本 本质上就是其日志中的最高序号,由一个持久化的按会话保存的 head 跟踪,即使在裁剪后也会保留。sessions_list 行在会话记录了变更时会包含 stateVersion;session_status 总是会报告它。
仅记录日志的类型是为了对账历史,而不是通知:普通的子运行完成交付仍由 子代理公告 负责,信号日志不会重复记录它。
观察者
观察者是一个持有目标上游标(session_watch_cursors)的会话。游标来自两个地方:
- 隐式(spawn 边)。 当一个会话生成一个子代理或 ACP 子级时,父级的游标会自动在子级的生成版本处被设定。父级从不手动订阅。
- 显式(
sessions_send watch: true)。 任何协调者都可以观察一个非生成的目标:在sessions_send上传入watch: true,并且在发送成功分发后,发送者会被注册为实际接收该消息的会话的观察者。注册从目标当前的状态版本开始——之前的历史不会产生通知。工具结果会在设置了该参数时报告watched: true|false。
session.scope="global" 下,共享的 global 键在不同代理之间是有歧义的,因此这类会话会获得持久日志和 changesSince,但不会获得主动通知。
观察会自动清理:游标行会随着信号日志保留期过期,在观察者会话重置时被移除,并会随着任一会话被删除而删除。v1 中没有 unwatch 动词。
Watched Claude, Codex, OpenCode, and Pi sessions adopted from a session catalog are checked for direct upstream human activity on a fixed cadence. Pi monitoring starts after the session is in its append-only v3 format. Detected activity enters the same signal log and watcher flow as other direct human turns.
OpenCode detection is deliberately conservative. OpenCode’s v1 tables do not preserve message provenance, so reporting ambiguous rows would create false alarms; per-message provenance exists only in its v2 schema. OpenCode therefore does not report image-only turns, @file-mention-only turns, slash commands routed to a subagent, or turns from ACP clients that annotate content with an audience (mapped by OpenCode to synthetic or ignored). It also suppresses text matching any of the preceding 50 user messages to catch compaction replay, which means a human deliberately repeating the same text within that window can be missed.
如果某个已采纳会话的上游源被外部删除,连续三次缺失检查(约三个监控周期)会为其观察者生成一条 upstream_missing 信号,并移除上游链接。继续该目录会话会重新创建一条新的链接。
注意事项:一条,而非多条
当一个可通知事件到达且某个观察者的游标落后时,该观察者会在其下一次轮到时收到一条系统通知:- 每个观察者/目标对仅保留一条待处理通知。 通知文本在待处理期间保持字节级稳定,系统事件队列会对其去重,因此即使同一目标在短时间内发生二十次快速变化,观察者提示中也只会出现一行。
- 冻结水位线。 当通知入队时,游标会冻结其已通知的位置。后续的物料事件只会推进物料水位线;它们不会重新触发通知。
- 在清空时确认,仅在交错工作时重新开启。 当观察者轮到并消费该通知时,游标会前进。如果在入队和清空之间又有更多物料事件到达,则只会为剩余部分开启恰好一条新的通知。
- 自我抑制。 观察者永远不会收到自己引发的事件通知。
- 重启恢复。 待处理通知保存在内存队列中;在网关重启后,启动扫描会根据持久化游标重新物化这些通知。
对账
该通知会准确告诉 watcher 该做什么。带有changesSince: <version> 的 session_status 会返回该版本之后的已类型化事件(最多 200 条),且不会推进任何游标:
historyGap: true 表示请求的版本早于已保留的历史记录——应当刷新整个会话状态(sessions_history、session_status),而不是将响应视为一个精确的增量。这个缺口信号是精确的:它来自每个会话被裁剪后的水位线,而不是根据序号运算推断出来的。
存储和限制
历史记录保存在共享状态数据库中,受 30 天和 50,000 行的限制;在清理后,每个会话的头部仍保持单调递增。记录采用尽力而为的方式——失败的追加会被记录下来,但绝不会导致原始轮次失败——因此stateVersion 是一个信号日志头,而不是事务性的变更数据捕获版本。
当前限制:
- 通知投递假定只有一个网关进程拥有共享状态数据库。多个网关共享持久日志和
changesSince,但 v1 不会在进程之间推送通知。 - 压缩事件覆盖嵌入式运行时的压缩所有者;仅原生 harness 的压缩不会被完整记录。
- 已取消结果的负载细节当前由 ACP 子运行产生;原生子代理取消会显示为通用失败。
- 上游自回显检测比较的是归一化后的用户文本。与会话最近 10 条 OpenClaw 侧用户消息之一匹配的外部提示会被视为自回显。
- 单个本地 Claude JSONL 行如果大于每个周期 1 MiB 的扫描上限,会阻塞该会话在 v1 中的游标;未分类字节绝不会被跳过。
- 单个 Pi JSONL 行如果大于每个周期 1 MiB 的扫描上限,会阻塞该会话在 v1 中的游标;未分类字节绝不会被跳过。
- 旧版 Pi 会话是在没有上游链接的情况下接管的。先恢复一次以将文件迁移到 v3,然后再从目录中继续它,以开始监控。
- OpenCode 检查每个周期发出一次批量数据库查询。只有当该查询显示其持久事件序列已推进时,才会运行会话导出。
- 配对节点 Claude 检查每个周期会分类最近的 50 个转录项。更大的突发可能会落在 v1 的扫描窗口之外。
- 配对节点 Claude 历史读取不会暴露明确的 thread-not-found 结果,因此远程 Claude 删除在 v1 中不会被归类为
upstream_missing。 - 尚未被接管的目录会话在 v1 中不在感知层范围内。
- 在此功能之前接管的会话不包含上游链接;从目录中继续一次即可开始上游监控。
- 上游链接假定每个已接管的会话键映射到一个拥有代理(接管使用默认存储代理)。在 v1 中,不会监控同一外部线程的多代理接管。