返回博客
官小西

Orca 源码拆解:当代码主要由 Agent 写,纪律必须编译成闸门

你同时开五个 coding agent 跑同一个需求。第一个真正难住你的问题不是"怎么合并",而是——第三个跑完了没有?

它在思考、在等你回答、还是十分钟前就崩了?终端里只有一串字节。CLI agent 没有状态 API,没有退出码可等(进程还活着),没有统一的事件流。你只能盯着屏幕看那个 spinner 转不转。

所有并行 agent 编排工具都卡在这一关。stablyai/orca 的答案,是我在开源项目里见过最不体面、也最有效的一套。

数字先摆出来

截至 2026 年 8 月 12 日:

指标
Star / Fork 43,205 / 3,014
建仓时间 2026-03-17(约 5 个月)
提交数 8,490
文件总数 13,310(TypeScript/TSX 12,526)
测试文件 5,421 个 .test. / .spec.(占 TS 文件 43%)
发布节奏 v1.4.180,几乎每日发版,带 rc 通道
License MIT
母公司 Stably AI(YC W22),另一条线是 AI 测试平台 Stably

商业模式是「自带订阅」:Orca 本体 $0、无按座席收费、无代理层——它不碰你的 token,直接在你本机调你自己的 Claude / Codex / Cursor 订阅。这个结构决定了它短期没有把功能锁进付费墙的动机,长期可持续性则押在母公司的测试主业上。

前 4 位贡献者提交了 6,653 次,占总量 78%;加上 CI bot 约 87%。这是"开源",不是"共治"。

Orca 分层架构:客户端、运行时 RPC、主进程、终端守护进程、四种执行宿主

一、状态检测:三条信道,按可靠度降级

src/main/agent-hooks/managed-agent-hook-registry.ts 里注册了 14 个 agent 的 hook service——claude、openclaude、codex、gemini、antigravity、amp、cursor、droid、command-code、grok、copilot、hermes、devin、kimi。每个都有 install / remove / refreshManagedScripts / getStatus 四个生命周期方法。

Agent 状态检测的三条信道及其 stdin 契约陷阱

信道 1(主):托管 hook。 Orca 把脚本写进用户的 agent 配置,脚本通过 loopback HTTP + per-pane token 把事件打回来:

ORCA_PANE_KEY / ORCA_AGENT_HOOK_PORT / ORCA_AGENT_HOOK_TOKEN
→ POST http://127.0.0.1:$PORT
  Header: X-Orca-Agent-Hook-Token: $TOKEN

接收端 agent-hooks/server.ts2,907 行

信道 2(寄生):劫持 Claude Code 的 statusLine。 这一手很妙,源码注释自己说明了动机:

Claude Code 每轮都把 rate_limits 管道给 statusLine 命令;转发它就能给 Orca 实时用量,而不必消耗 OAuth usage 端点很紧的调用预算。脚本不产生 stdout,所以终端里的状态栏视觉上毫无变化。

也就是说,你看到的 Claude 状态栏已经被接管了,而你看不出来。

信道 3(兜底):解析终端输出。 spinner 帧识别、pane 标题匹配。这是最脆的一层——2026 年 8 月的提交流里,出现频率最高的补丁就是这类:「把 Claude Code 的四分之一圆 spinner 判定为运行中」「从原生 OC | 会话标题识别活着的 OpenCode 面板」。上游改一个字符,这层就坏一次。

真正值钱的是 hook 脚本里的血

hook-stdin-contract.ts 里,POSIX 和 Windows 的规则完全相反,而且每条都标着 issue 号:

# POSIX:先吞 stdin 再退出
# Why: 被剥光的 PATH 不能阻止 hook 消费 stdin,否则 agent 看到 exit 127
# 和写到一半的断管(#8110)。`command -p` 从 shell 内建默认 PATH 解析,
# 因此也能在没有 /bin/cat 的 NixOS 上活下来,并且忽略 worktree 里
# 那个可能截获 payload 的本地 `cat`。
POSIX_HOOK_STDIN_READER = '{ command -p cat 2>/dev/null || cat; }'
:: Windows:先查 env 再碰 stdin
:: Why(#11549): 缺少 Orca 上下文说明 hook 跑在非 Orca 面板里,调用方可能
:: 放弃 stdin 而不是关闭它——读到 EOF 会永久阻塞,每个 hook 事件留一个
:: 可见窗口。
WINDOWS_HOOK_STDIN_READER = '"%SystemRoot%\System32\more.com"'

more.com 写全路径,是因为 Windows 先搜当前目录再搜 PATH——hook 的 payload 绝不能喂给仓库里的代码。

还有 statusline 的节流:Claude 流式输出时状态栏每秒 tick 约 3 次,所以 Windows 那版用纯 cmd 内建算出"当天秒数"来做限流,避免每个 tick 都 spawn 一次 findstr + curl;并且只在确定要发的时候才写时间戳,跳过的 tick 不会把下一次允许发送的时间往后推。

以及一个只有踩过才知道的细节:PTY 会活过 Orca 重启,env 里的 port/token 就成了陈的。所以 hook 每次先 call 一个 ORCA_AGENT_HOOK_ENDPOINT 文件刷新,失败才回落到 PTY env。

我的判断:这层是 Orca 唯一 fork 不走的东西。 功能可以照抄,这几百个平台级 corner case 是拿 issue 编号一个一个换来的。

二、换一个 socket 文件名,要走七步

src/main/daemon/AGENTS.md 是我在这个仓库里读到的密度最高的文档,没有之一。它讲的是一件听起来很小的事:终端守护进程如何在自己的规范 socket 路径上完成交接。

守护进程端点交接协议的七个步骤与五条已付出代价的陷阱

问题的根:net.Server.close() 会无条件 unlink 它绑定的路径名,不做任何所有权检查。于是一个离场的守护进程会删掉当时坐在规范路径上的那个 socket——包括一个活着的继任者的。继任者仍然活着、仍然持有所有 PTY,只是没有任何客户端能连到它。

用户看到的是什么?终端接受击键,但什么都不执行。

两条不变式:

只有正在把自己发布到规范端点上的守护进程才可以改动那个目录项,而且只能替换一个它刚刚亲自证明已死的条目。

没有任何参与者可以移除一个不是自己创建的名字。

协议本身七步:绑定私有 .p<hex> 名 → 尝试独占 link → 遇 EEXIST 就连过去证明在位者已死 → 复查条目没有易主 → 再探一次 → 一次 rename 完成替换 → 验证自己确实保住了它。

但比协议更值钱的,是文档里那节叫 "Traps That Already Cost Us" 的清单:

  • 绝不把"说不准"折叠成"已死"。 只有 connected 证明占用,只有 refused / missing 证明死亡。超时和 EPERM 什么都不证明,必须弃权——把它当作死亡,等于删掉一个仍在服务本机全部终端的端点。
  • link,绝不无条件 rename rename 会替换它找到的任何东西,等于允许一个正在启动的守护进程摧毁一个健康的。link 会响亮地失败,逼你回答存活性这个问题。
  • rename,不要 unlinklink 后者会让名字在两次调用之间消失;实测一次活跃交接中它几乎每次观测都出现空窗,而 rename 在约 14,500 次探测里一次都没有。
  • 不要用 birthtimeMs 识别条目。 Node 文档说它有时装的是 ctime,没有 birth time 的文件系统直接报纪元,粒度还常常粗于它要区分的事件。三次打补丁产生了三个新缺陷;inode 回收现在改由"有没有人在服务"来裁决。
  • 不要加清扫器。 "别人留下的东西能不能安全删掉"正是这套设计要退休的问题。上一个清扫器产生了五个缺陷,其中一个删掉了活跃监听者唯一的路径名。

而这段是全文最狠的一句:

针对旧的"启动器回收死进程名字"形状做的七轮评审,产生了二十三个缺陷,全部是同一种交错:第三方在 T 时刻观测存活,在 T+1 时刻动了目录项。

最后它还诚实地写下残留风险:最后一次探测和 rename 是两次系统调用,POSIX 没有"目标 inode 为 X 才 rename"这种原语。危害另有独立防线兜底——守护进程绝不在自己已不再持有的端点上创建会话,它排空而不是继续服务。

这份文档比它描述的协议值钱。 它记录的不是"我们怎么做的",而是"我们试过哪些做法、各自死在哪里、以及为什么不要再试一次"。绝大多数团队的这类知识只存在于三个人的脑子里,随离职蒸发。

三、它不是 worktree GUI,是一台编排引擎

src/main/runtime/orchestration/ 是这次拆解里最出乎意料的发现:一套 SQLite 支撑的 coordinator–worker 系统。

CREATE TABLE runs / tasks / dispatch_contexts / messages / deliveries
CREATE TABLE worker_dispatches / worker_terminal_resources / worker_terminal_archives
CREATE TABLE federated_dispatches / federation_relay_items / remote_questions
CREATE TABLE mutation_receipts

四个设计决定值得单独拎出来:

1)coordinator 是全仓极少数被批准破例的文件。 它第一行就是:

/* eslint-disable max-lines -- Why: the coordinator keeps message processing, task
   dispatch, gate handling, escalation, and convergence checking in one class so the
   polling loop can make atomic decisions across all these concerns without
   split-brain behavior. */

根目录 AGENTS.md 白纸黑字禁止添加 max-lines 豁免。这里破例,并把理由写在破例现场。规矩硬、例外有据——这是成熟团队的样子。

2)worker 靠一段 preamble 接入。 coordinator 生成前言注入 worker 的 prompt,规定它用 orca CLI 上报 worker_done 和心跳。心跳间隔 5 分钟,coordinator 的陈旧判定阈值 10 分钟——注释解释了为什么正好是这两个数:一次 tick 内能抓到挂掉的 worker,又不至于刷爆收件箱。

3)派发前先探 base 漂移。 worktree 落后于跟踪远端就拒绝派发(返回 stale-base-refused),除非显式 allow-stale-base: true。然后是这一段,我认为是整个仓库最有含金量的注释:

调用方不得用空数据预填这个字段。drift 段落是"响亮但罕见"的信号,为新鲜 worktree 污染它,会训练 worker 忽略它

这是把告警疲劳当作提示工程的一级约束在管。写过 agent 系统的人应该都能立刻体会到这句话的分量——Prime Agent 的 harness 拆解里那些"证据必须真实"的规则,本质是同一条定律的另一面。

4)federation。 本地 runtime 和远程主机之间同步派发,带 ack checkpoint、relay 队列、schema 版本偏移迁移。也就是说编排本身可以跨机器

四、混版本兼容:静默丢帧才是真敌人

docs/reference/remote-wire-compatibility.md 的前提写得很清楚:桌面客户端和远程 Orca 运行时各自独立更新,所以「混版本是常态,不是边界情况」。

  • 规则 1:给已有帧新增可选 JSON 字段是安全的(RPC 参数走 zod .strip(),流帧走 JSON.parse,都忽略未知键)。但这个安全只在所有读者都把它当可选的前提下成立——一旦某个新客户端"要求"它,那和删字段是同一种缺陷,只是发现得更晚。
  • 规则 2:新增 stream opcode 不安全,必须做能力协商。因为:
const frame = decodeTerminalStreamFrame(bytes)
if (!frame) {
  return // silently dropped — the sender never learns
}

未知 opcode 解码为 null,然后被无声丢弃,发送方永远不会知道。用户侧的表现是「功能挂住了」——最难查的那种 bug。

这一条几乎可以原样搬进任何"两个独立发版的进程互相说话"的系统。

五、本文的论点:把纪律编译成闸门

前面四节讲的都是「他们解决了什么难题」。但如果只能带走一件东西,我认为是这个:当代码主要由 agent 写出来时,靠人 review 逐行把关的模式失效了,于是所有约束都必须下沉成机器可执行的闸门。 Orca 是我见过把这件事做得最彻底的开源项目。

行数棘轮:只准缩,不准涨

check-max-lines-ratchet.mjs 的注释解释了机制:

oxlint 本来就会让任何超限且没有豁免的文件失败,所以文件唯一能长过预算的方式,是加一条 max-lines disable 注释或在 mobile/.oxlintrc.json 里给单文件抬预算。这个检查把当前允许这么做的文件集合冻结成基线,一旦出现新的绕过就让 CI 失败——已经超限的老文件按祖父条款保留,新的必须拆。基线只能缩小。

默认预算:普通 .ts 300 行、.tsx 400 行、.mjs 600 行、测试文件 800 行。基线里当前有 351 个被豁免的文件。

还有一个可爱的细节:棘轮脚本把自己和自己的测试排除在扫描之外,因为它们的正文里合法地包含那些指令文本(正则和 fixture)——"棘轮不监管棘轮自己"。

74 条可靠性闸门:一份形式化的可靠性台账

config/reliability-gates.jsonc 是我第一次在开源项目里看到这种东西。每条闸门是一条结构化记录:

{
  "policy": {
    "maturityLevels": ["experimental", "soak", "blocking", "accepted-gap", "deprecated"],
    "blockingPromotion": {
      "minimumSoakRuns": 100,
      "minimumSoakDays": 14,
      "maximumUnexplainedFlakes": 0
    }
  }
}

单条闸门要填:invariant(这条不变式到底是什么)、oracle(用什么手法证伪它)、coveredPlatforms / coveredProviders(覆盖到哪、哪没覆盖到)、motivatingLinks(哪个 issue/PR 催生了它)、commands(怎么跑)、assertionRefs(具体哪个测试文件里的哪几条断言支撑它)。

几个设计决定值得注意:

  • maturity 里有一档叫 accepted-gap——已知没覆盖,且这是一个被明确接受的决定,不是遗漏。把"我们知道这里有洞"变成一条可查询的记录,比假装全覆盖诚实得多。
  • 晋升成 blocking 需要 100 次浸泡运行 + 14 天 + 0 次无法解释的抖动。抖动不是"重跑一下就好",是升级的硬阻断项。
  • coverageNotes 会明写还没拿到的证据,例如某条闸门写着"确定性的 store/controller 测试覆盖了 local、直连 SSH、配对运行时……实机 headed/headless 配对运行时和建立连接后的 Electron IPC 仍未采集"。

E2E 断言必须打在 DOM 上,不许打在 store 上

tests/e2e/AGENTS.md 里有一个真实事故,值得完整引用:

'create-worktree' 这个 modal key 在 AddWorktreeDialog.tsx 于 #710 被删除之后,仍然活在 activeModal 联合类型里。于是 store.openModal('create-worktree')store.activeModal === 'create-worktree' 的往返断言,是对着一个什么都不渲染的 modal 成功的。正是这个同义反复让 #1186(StartFromField 里的 React error #31)得以发布——store 层测试通过,而 composer 对真实用户是崩的。

结论被写成硬规则:用 store 抵达某个状态,用 DOM 证明这个状态是对的。 一个既写 store 又读 store 的规格,断言的是 Zustand 的 setter 能用,不是 Orca 能用。

同一份文档还给了另一条反向纪律:如果一个 E2E 规格只是在 page.evaluate 里调 store.getState().someAction(...),那它是一个花了 1.5 秒 Electron 启动开销却没多拿到任何覆盖率的单元测试——去写 store slice 单测。

剩下的闸门

  • 基准测试当 CI 门禁:启动耗时、空闲 CPU、主线程卡顿、终端打字延迟、zustand selector 扇出、worktree 删除、WSL git shell、挂起看门狗内存……每一项都有 bench: 脚本和预算检查。
  • global-fetch-call-site-audit.test.ts:用一个测试审计全局 fetch 的所有调用点。这招我强烈建议抄——它把"不要偷偷加出网请求"从一条口头纪律变成了 CI 里的红灯。
  • 本地化三重校验:catalog 一致性、抽取完整性、覆盖率,三个独立脚本,都挂在 pnpm lint 上。
  • 命名禁令:AGENTS.md 明令禁止 helpersutilscommonmiscshared-stuff——"它们零信息量,且倾向于变成垃圾场。如果你伸手去拿 helpers,这个文件多半有不止一个职责"。

这套东西合起来,就是Harness Engineering 那篇指南里讲的思路在一个真实产品上的完整落地:agent 的产出质量由它周围的脚手架决定,而不是由模型决定。

六、顺手做出来的两个东西

AI Vault(166 个文件):一个跨 agent 的会话考古层。Claude、Codex、Antigravity 各自在本地留下的 session 文件格式完全不同,Orca 给每一种写了一个 parser(session-scanner-codex-parser.tssession-scanner-antigravity-parser.tssession-scanner-claude-subagents.ts …),再统一成可搜索、可删除、可远程扫描(带并发和批处理控制)的索引。你所有 agent 的历史对话,第一次有了一个统一入口。

skills 目录:Orca 自带 8 个给 agent 用的技能(orchestrationorca-clicomputer-useorca-linearorca-emulator …)。有意思的是 skills/orchestration/SKILL.md 明确说自己只是一个发现桩

这个文件是发现桩,不是使用指南。完整的、版本匹配的编排参考由 orca 二进制自己提供——故意不放在这个文件里,这样它永远不会和真正要执行你命令的那个二进制发生漂移。

这是对技能文档最常见死法(文档和实现各自演进)的一个直接解法,也是技能生态那篇里讨论过的分发问题的一个务实答案。

同一个文件里还有一条让我笑出声的规则:在 Linux 上、且不在 Orca 托管的终端里时,绝不要运行裸 orca——那通常会解析到 GNOME 的 Orca 屏幕阅读器(/usr/bin/orca),然后在用户机器上开始朗读。

七、横向对比

选这四个维度,是因为它们决定了你在什么时候会撞墙:执行位置决定你能不能把活推到远程大机器上;状态回传决定编排器知不知道发生了什么;治理模式决定你提的 PR 会不会石沉大海;形态决定它跑起来吃多少资源。

项目 ★(2026-08-12) 形态 执行位置 状态回传 治理
Orca 43,205 Electron 桌面 + 移动 + VPS 本地 / WSL / SSH / 临时 VM hook + statusline + 终端解析三层 公司主导,4 人 78%
Vibe Kanban 27,745 Rust,任务看板 本地 进程级 母公司 2026-04 关停,转社区维护
Claude Squad 8,285 Go,终端 TUI + tmux 本地 tmux 面板 轻量社区
Crystal 3,108 TS 桌面 本地 进程级 小团队
Conductor 闭源 Mac 原生 本地 未公开 商业

我的推荐很分明:

  • 只在一台 Mac 上、只跑 Claude Code 和 Codex、想要打磨过的体验 → Conductor,Orca 的面对你是浪费。
  • 想要一个能嵌进已有流程的任务队列、不想要 IDE → Vibe Kanban,但要接受它已经没有商业实体在推。
  • 要把活推到远程盒子、要在手机上盯进度、要一次跑五个不同厂商的 agent → Orca 是目前唯一的选项,没有第二家把执行位置抽象和状态回传同时做到这个程度。

八、代价(诚实的部分)

  1. src/main 平铺了 200 多个顶层文件,文件名长到荒谬:worktree-removal-session-partition-fencing.test.tspowershell-osc133-bootstrap-windows-clm.test.ts。这是"禁止 utils/helpers + 行数棘轮"的必然产物——为 agent 优化的代码库靠文件名检索,不靠目录直觉。 对人类新人是高门槛,但他们的新人本来就主要是 agent。
  2. 棘轮拦不住核心状态机。 2,907 行的 hook server、555 行的 coordinator 说明复杂度不会因为规则消失,只会集中到少数被批准破例的地方。好消息是那几处确实是"该复杂"的地方。
  3. 14 个 agent 适配是一笔线性增长的税。 每接一个新 agent = 一个 hook service × 四个生命周期 × POSIX/Windows/WSL/SSH 四套脚本,而上游随时可能改 hook 格式或 spinner 字符。这是它维护面里最脆的一块。
  4. Electron 的账:约 250MB 安装包、跑几个 agent 时 400–800MB 常驻。跑五个 agent 时这只占总开销的一小部分,低配机器上仍然体感明显。
  5. 治理集中 + 社区 PR 积压。 最近合入的 30 个 PR 全部来自 4 位核心;最老的还开着的社区 PR 停在 4 月 11 日,四个月没动。仓库里有个 track-community-prs.yaml workflow,说明他们知道,但没解决。
  6. hook 会写用户的全局 agent 配置。 有安装锁、owner 身份、脚本刷新一整套机制说明他们很小心,但对企业环境这仍是需要过合规的动作。
  7. 日更等于回归常态化。 README 自己写"我们每天发版,所以这个功能列表永远滞后"。团队采用要锁版本,别跟 latest。

另外必须说清楚:本文所有结论来自读源码和公开数据,不是长期生产使用的评测。上面那些吃内存、发版回归的判断引自公开评测和项目自述,不是我实测的数字。

九、这对不用 Orca 的人意味着什么

三个可以直接拆下来用的结论:

1)任何"我们约定了 X"的纪律,寿命不超过三个月,除非它在 CI 里有一盏红灯。 Orca 的做法是给每条纪律配一个脚本:命名约定配 lint 插件、文件大小配棘轮、可靠性配台账、出网请求配调用点审计。写在 CONTRIBUTING.md 里的规矩,agent 不会遵守,人也不会。

2)静默失败必须被当成一类专门的敌人来打。 未知 opcode 静默丢帧、hook 读 stdin 永久阻塞、birthtimeMs 在某些文件系统上返回纪元、超时被当成"已死"——Orca 踩过的坑里,最贵的那些全部有同一个特征:它不报错,它只是让某个东西悄悄不工作。 设计接口时问一句"如果对面比我老,这条消息会怎么静静地消失",比写十个单测有用。

3)把"我们试错过什么"写进仓库。 那份 daemon 的 AGENTS.md,价值不在协议,在于它列出了五条走不通的路和各自的死因,还标了代价(七轮评审、二十三个缺陷、五个缺陷、14,500 次探测)。下一个人——无论是新同事还是新一轮的 agent——不会再提议加一个清扫器。 这是我见过成本最低的知识沉淀方式。

结论

Orca 值得关注的地方不是它做了什么功能——那些功能三个月后会被抄光。值得关注的是它证明了一件事:一个 4 人核心团队用 agent 集群,在 5 个月里造出了 VS Code 量级的表面积,而质量控制的手段不是"人 review 得更仔细",是把每一条能形式化的纪律都编译成一盏 CI 红灯

如果你正在决定要不要用它:需要远程执行、跨厂商 agent、移动端监督 → 装,没有替代品,但锁版本。只在单机跑一两个 agent → 它的复杂度对你是负担。

如果你只是想学点东西:把 src/main/daemon/AGENTS.mddocs/reference/remote-wire-compatibility.mdtests/e2e/AGENTS.mdconfig/reliability-gates.jsonc 这四个文件读一遍。四十分钟,比大多数工程博客有营养。

参考资料