跳转至

Agent 状态存储

状态

当前边界是 PR 2A:结构化会话使用 hook 服务器的完全作用域规范存储;未绑定的 PTY/relay 证据仍保留在隔离的遗留适配器中。不要在本切片中移除渲染器桥接或其发布过滤器:它们仍然承载 native-chat 子行。

以下章节记录原始的 2026-09-09 发布计划。其 PR 1a 和 PR 1b 已落地;其提议的 PR 2/3 顺序已被该边界取代:

  1. 仅 main:每个生产者写入同一个存储,并且 worktree ps 读取它,拆分为 1a(结构化会话加入存储)和 1b(删除运行时中重复保留的存储);
  2. 渲染器:侧边栏成为订阅者,并停止重新派生行;
  3. 共享:为每个读取者提供一个 worktree-status 汇总和一条新鲜度规则。

本方案解决的问题

Orca 在四个地方展示“这个 agent 正在做什么”:桌面侧边栏、orca worktree ps 命令、移动应用和 agent 仪表盘。在 #19217 之前,这些读取者甚至没有共享它们的输入。在 #19217 之后,它们共享结构化会话映射,且仅此而已。

2026-09-09 的一次审计发现了六个生产者和三个消费者,并且仅在 main 进程内部就有同一行的三个独立副本:

main 进程副本 键 所有者 持久化 驱逐
hook 服务器 lastStatusByPaneKey paneKey src/main/agent-hooks/server.ts last-status.json 标签页关闭、pty 退出、hydrate
运行时 RuntimeAgentRowStore paneKey runtime-agent-row-store.ts(在 PR 1b 中删除) 否 仅 pty 退出
结构化 feed published sessionId src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.ts 否 从不(一个广播缓存)

第二份副本是重复写入:在 main 中解析的 OSC 状态被转发到 hook 服务器 并且 从同一调用中保留在运行时存储中(orca-runtime-create-terminal-side-effect-command-code-detector.ts)。第三份副本使用不同的键,并且根本不会到达 hook 服务器,这就是为什么 worktree ps 在 #19217 中为它增加了自己的适配器。

每个读取者随后应用自己的优先级和新鲜度规则,因此同一个 pane 在桌面、手机和 CLI 中可能合理地读取到不同结果。

规则

执行主机拥有 agent 状态,位于一个存储中,并且每个读取者都订阅它。 这遵循 ssh-execution-boundary.md 中的边界:运行进程的主机是唯一能够观察它的当事方,客户端对于执行状态永远不是权威来源。

三个推论:

  • 每个执行主机一个存储。远程主机保留自己的存储,客户端将其镜像下来,就像 web-session 镜像已经做的那样。镜像不是合并:客户端永远不会将其观察结果写回主机。
  • 优先级在写入时一次性决定,并在行上记录来源。读取者永远不会重新裁定 hook、terminal 和 structured 之间的优先级。
  • 读取者仅保留展示策略和用户事实:30 分钟显示衰减、确认、关闭、未读。这些保留在读取者侧,但成为一个共享实现(PR 3)。

该存储已经存在

对于每个基于 PTY 的 agent,hook 服务器的状态如今就是那个存储。审计确认:

  • hook HTTP 请求、WSL 和 SSH relay 接收器,以及 main 自身的 OSC 解析,都汇聚到同一条 applyNormalizedStatus 路径,并打上权威 id main-agent-hooks;
  • 只有它持有 pane 权限:启动令牌及其哈希承诺、退役 pane 栅栏、pane-key 别名、每连接顺序水位线,以及必须比传输清除存活更久的证据年龄映射;
  • 只有它持久化,具有七天的 hydrate 窗口和 restoredUnconfirmed 戳,使已 hydrate 的行永远不会被读取为实时真相;
  • 它已经通过 agentStatus:set 和 agentStatus:clear 扇出到两个渲染器窗口,并提供 agentStatus:getSnapshot。

main 中没有其他东西承载这些保证,构建一个具有这些保证的第二存储会是错误方向。因此设计不是“添加一个存储”。而是:将绕过 hook 服务器的两个生产者通过它路由,然后删除副本。

PR 1a:结构化会话发布到存储

渲染器行为没有变化。侧边栏继续接收它今天接收的相同 IPC 事件,外加它当前自行派生的结构化会话行。

结构化会话发布到 hook 服务器

结构化 feed 保留其将会话日志投影为摘要并将其流式传输给订阅者的职责。在每次发布时,它还会将摘要作为状态行摄取到 hook 服务器中:

行字段 来源
paneKey structuredAgentSessionPaneKey(tabId, sessionId),渲染器已经使用的键;其叶子是 UUID 形状,因此 pane-key 验证接受它
tabId structuredAgentSessionTabId(sessionId)
worktreeId summary.workspaceId(文件夹 workspace id 是一个有效值)
行字段 来源
state structuredAgentSessionStatusState(summary.status),#19217 中共享的映射
structuredHost 当 summary.hostExecutionOwned 已设置时为 'owned',否则为 'held';worktree ps 由此派生其行的 structuredHostOwned
prompt、tool、last message、model、provider session 摘要的字段

没有已持久化回合(status === null)的会话不会生成行,这与聊天显示的内容一致。当宿主撤销实时所有权时,该行会被重新设置且不带该标志;当宿主关闭或驱逐会话时,该行会被删除。这两者已经作为 feed 事件存在(revokeLive 以及 liveSessionSummaries 中的 roster 过滤);PR 1 将它们变为 store 写入。

从宿主的 map 中删除会话和删除其行是同一个操作,forgetStructuredAgentSession。store 会保留一行直到被告知,而宿主拥有的行会绕过陈旧检查,因此如果删除路径忘记删除该行,就会遗留一个看起来永久处于工作状态中的 agent。

ingest 必须遵守两条规则:

  • 绝不持久化结构化行。 journal 是结构化会话的持久事实来源,宿主在恢复时会重新发布。last-status.json 中的结构化行会以 restoredUnconfirmed 水合,然后与实时重新发布发生冲突。序列化器会跳过携带 structuredHost 的行,hydrate 会丢弃在磁盘上发现的任何此类行。因此应用一条这样的行也会跳过持久化调度:遍历和 stringify 只能复现已经位于磁盘上的文件,对于每个流式聊天,每个防抖窗口一次。
  • 绝不让它与 hook 行冲突。 结构化会话没有 PTY,因此没有 hook 或 OSC 事件携带其 pane key。ingest 仍然经过处置门控,因此已退役的 pane key 会像其他任何 key 一样被拒绝。

应用一条这样的行确实仍会运行两个状态 fan-out,这是有意为之,而非偶然。notifyStatusChangeListeners 为 agentAwakeService 的电源节省阻塞器提供输入,subscribeEnrichedStatus 为 AgentSessionTransitionRecorder 的统计提供输入,因此加入 store 会将原生聊天同时纳入这两者。正在工作的原生聊天是真实工作,应该像 PTY agent 一样让机器保持唤醒。

删除侧通过 dropStatusEntry 路由,而不是 clearPaneState:pane 状态清除会到达 renderer,而在 PR 2 之前,renderer 自己的 feed bridge 是该 pane key 的 writer。它还传递 preserveResumeIdentity: false —— 被用户关闭的 pane 保留的 providerSessionOnly 残留之所以存在,是为了让 agent 可以在该 pane 中恢复,而结构化会话没有 pane,并将恢复身份保留在 record store 中。与其他所有 dropStatusEntry 调用方一样,它不发出 pane 清除,因此在 working 中途被删除的会话会让 AgentSessionTransitionRecorder 保持一个打开的 stats 会话,直到其 LRU 将其驱逐;该缺口与用户关闭路径共享,并非结构化行特有。

ingest 位于 feed 中,而不是位于 structured-agent-session-host.ts 中,后者已达到文件长度上限。

worktree ps 变为读取器

19217 中添加的结构化 adapter 已被删除,结构化行通过与其他所有行相同的快照到达 worktree ps。collectRuntimeWorktreePtyAgentSources 中的保留与 hook 对账一直保留到 PR 1b 删除了为其提供数据的 store。这一步确定的是准入门控,它决定 worktree 列表可以显示哪些行:

  • hook 或 OSC 行需要其 tab 被镜像或存在已连接的 pty,与当前一样,SSH 行仍然豁免,因为其 tab 可能只存在于远程;
  • 携带 structuredHost 的行在宿主持有会话期间被准入,而宿主在关闭时的删除才是将其移除的原因。没有 tab 镜像要求:结构化会话的 tab 位于 renderer 自己的 tab 状态中,无头宿主没有 renderer 可供镜像。该论证只有在无头宿主本身已连接到 store 时才成立,这是每个入口点的单独义务:Electron 宿主(desktop 和 orca serve)共享 main-process-runtime-service.ts,而 orcad 在 src/main/orcad/orcad-entry.ts 中构造自己的 runtime。缺少该连接的宿主不会列出任何 agent,而不仅仅是没有结构化 agent,因为 worktree ps 对每一行读取相同的快照。

针对宿主拥有的结构化行的新鲜度绕过已经存在于 isFreshNonDoneAgentStatus 中;现在标志位于行上,它成为唯一路径,runtime-worktree-agent-rows.ts 中的手工检查被移除。

线协议兼容性

AgentStatusIpcPayload 增加一个可选字段 structuredHost,worktree ps 行增加 structuredHostOwned。根据 remote-wire-compatibility.md 的规则 1,两者都是安全的:旧客户端会忽略它们。worktree ps 行保持其形状和词汇,因此移动应用看不到变化。

在 PR 2 之前,主进程不会通过 agentStatus:set 或 agentStatus:getSnapshot 将结构化行转发到 renderer。renderer 的 feed bridge 仍然自行写入这些行,如果也转发它们,就会让一个 pane key 拥有两个 writer。移除该过滤器是 PR 2 的第一步。

PR 1b:运行时保留行存储已删除

已落地。RuntimeAgentRowStore 已移除,随之移除的还有 collectRuntimeWorktreePtyAgentSources 中的保留与 hook 对账。hook server 的 store 现在是 PTY agent 行的唯一主进程副本。

五个调用点

调用点 之前 之后
orca-runtime-create-terminal-side-effect-command-code-detector.ts retain() OSC 负载的第二次写入,该负载已经发送到 hook 服务器 已删除;事件现在携带 pane 的 terminalHandle,hook 摄取保留唯一副本
...command-code-detector.ts clearPty() 在 pty 退出时丢弃行 已删除;pane 拆除已经清除 hook 行
orca-runtime-get-worktree-ps.ts values() 提供 retainedSnapshots 已删除;读取方只保留 hookSnapshots
orca-runtime-serialize-agent-prompt-submission.ts getFreshExplicit() 先保留行,后 hook 行 selectFreshExplicitAgentStatus,仅 hook 行
orca-runtime-prune-mobile-session-tab-group-layout.ts getFreshForMobile() pane 键,然后 pty id selectFreshAgentRowForMobileTab:pane 键,然后 terminalHandle

两个读取方都移入了 runtime-hook-agent-row-selection.ts,由于现在没有任何东西保留快照,它现在也拥有 RuntimeAgentRowSnapshot。

terminalHandle 是行回连到其终端的关联键

保留存储中唯一真正多出来的内容是 pty id,并且有两个读取方使用了它。 原计划是把事件的 ptyId 写入 terminalHandle;这是错误的。 终端句柄(term_<uuid>)和 pty id 是不同的标识符, 而 getFreshExplicit 已经在将 hook 行与真实句柄进行比较。实际落地的是:

  • AgentHookEventPayload 和运行时终端状态事件新增了一个可选的 terminalHandle。检测器通过 getAgentStatusTerminalHandleForPaneKey 对每个块解析一次——这与面向渲染器的 IPC 边界为每一行已经执行的查找相同, 因此两个表面不会就某个 pane 属于哪个终端产生分歧。
  • 当传入事件未解析出任何句柄时,applyNormalizedStatus 会向前传递该句柄。 只有 main 的 OSC 解析能够解析出句柄,否则针对同一 pane 的 HTTP hook 提交 会将其擦除。
  • 它永远不会被持久化。句柄属于发出它的运行时,而一个已恢复的句柄只会把某行 重新关联到别人的终端。
  • toAgentStatusIpcPayload 会发布它,这也让 getFreshExplicit 中早已失效的 句柄比较重新生效:运行时读取的是原始快照行,而在此之前没有任何东西曾写入 该字段。

worktree ps 也使用它。ConnectedPtyEvidence 将其扁平的 ptyIds 集合 替换为 ptyIdByTerminalHandle,因此某行仍然可以解析其背后的已连接 PTY ——这既是工作终端汇总的匹配键,也是当某行的 pane 绑定因控制器实例变化 而被置空时的最后补救。

变更检测器必须随存储一起迁移

retain() 不仅仅是一个存储:它的布尔返回值是用于在仅状态转换时重新发布 session.tabs 的信号,而任何标题变更都不覆盖这种情况(#7970)。 hook-status-session-tabs-invalidation.ts 已经镜像了该投影变更集, 包括恢复来源和终端句柄关联,因此替代方案是将信号从存储中路由出去, 而不是构建第二个比较器。 installHookStatusSessionTabsRepublish 现在拥有全部三个分支——增强状态、 pane 清除以及关闭操作发出的状态丢弃 tap——并且两个宿主都会安装它。

两个宿主,而不仅是桌面宿主

orcad 构造其运行时时没有 onTerminalAgentStatus,因此 main 的 OSC 解析 在那里从未到达存储,保留的副本是唯一载体。如果删除它而不接入 orcad, 无头宿主将完全无法列出任何 PTY 代理。orcad-entry.ts 现在绑定生产者 并安装重新发布信号,同时保留它已有的快照和结构化 sink。

预期的行为变更

用户在桌面上关闭的行会立即从 worktree ps 和手机中消失, 而不是一直保留到 pty 退出。一个存储意味着一次关闭。

遗留的数字 pane 键仍然是一个有界的兼容情况。持久化布局会为其稳定的叶子所有者 注册别名;进程内 OSC 观察也只有在运行时提供匹配的 tab、PTY 和终端句柄时, 才可能保留一个数字键。HTTP 和 relay 入口仍然要求稳定的键或已注册的别名, 并且数字行永远不会被持久化。

PR 2:渲染器订阅

随着结构化行通过 agentStatus:set 到达,渲染器的 StructuredAgentSessionStatusBridge 不再需要写入状态;其卸载清理变成 向宿主发送的 tab 关闭信号。IPC 应用器是观察状态的唯一写入方。 2026-09-09 的审计整理了其他写入方:

写入方 处置
Command Code 输出种子、停放 pane 种子、pty 退出移除 删除;main 已经发出相同的事实
写入方 处置
结构化桥接状态写入 删除;main 现在发布该行
启动占位种子(用户使用 prompt 启动了 agent) 暂时保留;main 持有启动配置,之后可以播种
关闭、确认、卸载 保留;用户事实和组件生命周期
远程运行时 OSC 解析(字节从不经过本地 main) 保留,在主机足够新时置于主机已发布行的围栏之后;线协议文档规则 3 适用
web-session 镜像接收时钟 保留;衰减规则需要来自同一台机器的两个时钟

Command Code 的 done-settle 窗口是渲染器策略,main 中没有对应项。PR 2 要么将其移入 main 的检测器,要么保留,并说明选择哪一种。

PR 3:一个汇总,一个时钟

worktree 卡片状态被派生三次:渲染器中的 lib/worktree-status.ts、main 中的 runtime-worktree-status-projection.ts,以及 mobile 中的 agent-row-display.ts,后者手工复制了 30 分钟常量。PR 3 将汇总和衰减移入 src/shared,并让这三处都调用它。

不变的内容

  • 钩子脚本、OSC 9999 线格式和 relay 协议。
  • 状态词汇表。行使用 working / blocked / done,结构化摘要使用 working / attention / idle,只映射一次。
  • 远程工作的 live / unverifiable / exited 判定。失去联系不会清除任何内容;准入门中的 SSH 豁免保持不变。
  • 水合诚实性:恢复的非 done 行是 restoredUnconfirmed,并且永远不会是新鲜状态。

PR 1b 可靠性契约

  • 不变量(agent-session.status-host-ownership): 每个执行主机有一个 agent-status 存储;OSC、钩子和结构化会话写入它,而 desktop、worktree ps 和 mobile 只投影它。关闭、经过认证的 PTY 退出以及 provider-generation 替换会在所有位置移除同一行;仅传输丢失不会移除任何内容。
  • 故障来源: 被删除的运行时行存储重复了 OSC 观测,使用不同的终端身份作为键,并且在来自钩子存储的关闭之后仍然存活。Relay 重放也可能在读取方使用其新的投递时间戳时,让旧证据看起来是新鲜的。
  • Oracle: 一个 OSC 观测通过钩子快照出现在 worktree ps 和 mobile 中,一次存储关闭会从两者中移除它,而不会停止 PTY。聚焦测试还要求 leaf/incarnation-handle 重新加入、旧版 numeric-pane 兼容性、认证退出和 provider-generation 清理、证据年龄新鲜度,以及恰好一次的启动/停止清理。
  • 门: terminal-performance.osc-status-scan-budget 覆盖未更改的有界 OSC 解析器和运行时投影。目前还没有专门的多表面状态存储阻塞门;下面的聚焦测试套件是被接受的缺口,直到它们积累可靠性门浸泡证据。
  • Provider/平台覆盖: 本地和 daemon 支持的 PTY 由运行时测试覆盖,SSH relay 丢失/重放语义由 relay 集成测试覆盖。该投影由 git worktrees 和文件夹工作区共享。WSL 使用相同的存储和准入代码,但这里没有实时运行;Linux 和 Windows 运行时执行、原生 mobile 客户端以及混合版本配对客户端仍然是验证缺口。
  • 性能预算: 发布保持事件驱动,没有新的轮询或子进程。一个 mobile 投影克隆一次状态快照,构建一次 pane/handle 索引,并有一个确定性的调用次数测试;生命周期清理受现有状态和 handle 清单约束,orcad 测试证明监听器在启动失败和重复停止时各清理一次。
  • 诊断: 现有 hook-listener 错误会指明 pane 和 PTY,而 status-store 测试固定投递时钟与证据时钟。不会发出新的遥测或原始终端数据。
  • 剩余缺口: 渲染的 Electron/mobile 行为、实时 SSH 重连以及 Linux/Windows/WSL 执行需要平台 QA 通过。当前跨版本门不覆盖 session.tabs 内容。

验证

  • 单元测试:摄取一个结构化摘要,并通过 getStatusSnapshot、worktree ps 和 mobile 投影读回;断言序列化器从不写入携带 structuredHost 的行;断言某个水合文件如果以某种方式包含一个,则会被丢弃。
  • 单元测试:针对保留存储编写的 worktree ps 测试套件被重新接线到真实的 AgentHookServer(agent-status-store-wiring.test-fixture.ts),而不是删除,因此每个测试仍然断言其命名的列表行为。关闭变更在 orca-runtime-tests/worktree-ps-agent-row-dismissal.spec.ts 中端到端固定,如果恢复保留存储,该测试会失败。
  • 实时:将 #19217 中的 parity 检查(working、done、close、reload)针对合并后的存储重复执行,两个表面都从同一行读取。

已退役 OMP 窗格恢复

桌面渲染器退役通过现有的 agentStatus:retirePaneAuthority IPC 消息携带一个可选 UUID。钩子服务器使用其有界退役围栏保留它。经过验证的实时 OMP 新回合会消费该 UUID,并仅在实时通知中回显 authorityRestartId。缓存行、持久化和启动重放从不携带该确认。较旧的对等端会省略或忽略它,并保留显式 attach 恢复。

渲染器将其 UUID 保留在现有的非持久化退役墓碑中; 每次重新退役都会生成一个新的 UUID。匹配的确认只有在针对现有窗格且 匹配的 workspace/connection 成功写入状态后,才能清除该墓碑。已关闭的墓碑保持 true, 包括在标签页 LRU 逐出其条目之后。关闭一个已退役的物理别名会撤销其整个组。 这是控制平面退役关联,而不是第二个 agent-status 存储。

回退通过现有的 attach-restoration 路径恢复 hook 服务器记录的状态别名。 被接受的渲染器写入也会恢复匹配的状态别名路由,从而为下一次退役保留组成员身份。它不会 恢复编排或启动凭据。 其作用范围限定为发起请求的桌面渲染器。不同窗口的退役 UUID 无法通过该确认清除,Web 镜像保持其现有的 host-snapshot/attach 行为。

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