跳转至

会话状态感知

多个会话经常协同处理同一个问题。例如:管理者将任务委派给子代理,人类直接介入某个工作会话,以及两个代理通过 sessions_send 进行协调。每个会话都会对其他会话形成假设。一旦其他参与者介入,这些假设就会立刻过时。会话状态感知(session state awareness)正是检测这种介入的机制。它会一次性通知受影响的会话,然后为该会话提供一种低成本的方式,使其在采取行动前赶上最新状态。

三个组成部分协同工作:

  1. 持久化信号日志(durable signal log)按会话记录选定的状态变更。
  2. 监视器(Watchers)持有针对每个目标的光标,并接收一条合并后的状态过期通知。
  3. 对账(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 下复用完全相同键的链接会因歧义而被跳过。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 cl/openclaw