跳到主要内容

数据截至 (上游 commit 4ec6bbf5884e)

Orca — 架构与原理

30 秒导读: Orca 是一个 Electron 桌面 IDE,专门用来同时跑很多个命令行编码 agent(Claude Code、Codex、Cursor CLI……)。它给每个 agent 配一个独立的 git worktree 和一条终端,把这些终端托管在一个独立于 App 的常驻守护进程里(App 退出终端不死),再把整套能力反向开放成 orca CLI,让 agent 自己也能创建 worktree、读别的 agent 的终端、给同伴派活。


1. 这是什么(零基础也能懂)

一句话定义: Orca 是一台"agent 车队的调度台"——一个把 N 个终端编码 agent 并行装进同一个仓库、同一个窗口的桌面 IDE。

它解决的是哪个具体痛点

假设你想让 Claude Code 和 Codex 同时改同一个仓库。裸着干会立刻撞车:

撞车点裸着干会发生什么
工作区两个 agent 改同一份 checkout,互相覆盖
终端你开一堆 tmux 窗口,关掉哪个就丢哪个的上下文
状态你得盯着屏幕才知道谁跑完了、谁卡在提问上
收尾五个分支五个目录,合哪个、删哪个,全靠脑子记

Orca 的回答是四句话:一个 agent = 一个 worktree终端托给守护进程状态由 agent 主动上报worktree 的元数据(关联的 issue/PR、父子关系、未读标记)由 Orca 存着

给谁用: 一个人想同时开 3~10 条 agent 支线的开发者;或者需要让一个"主 agent"去指挥若干"工人 agent"的编排场景。

它能做什么

  • 为任意 CLI agent 拉起独立 git worktree(含 sparse checkout、共享目录软链、setup hook)。
  • 内置 Ghostty 级终端(WebGL 渲染、无限分屏、滚屏跨重启存活)。
  • 侧栏实时显示每个 agent 是 working / blocked / waiting / done
  • 原生集成 GitHub / GitLab / Linear / Jira,一个 issue 直接开一个 worktree。
  • 内嵌 Chromium 浏览器 + Design Mode(点网页元素 → HTML/CSS/截图塞进 agent 的 prompt)。
  • SSH 远端 worktree、手机 App 伴侣、orca serve 无头模式。
  • 一整套 orca CLI 与 skills,让 agent 反过来驱动 Orca。

用起来什么样

对人来说是个 GUI。但对 agent 来说 Orca 是一个 CLI——这才是它区别于普通 IDE 的地方:

# 示意,非源码:一个 agent 在自己的终端里给自己开一条支线
orca worktree create --name fix-auth --agent codex --prompt "修 login 超时" --json
# → {"result":{"worktree":{...},"agentTerminalHandle":"term_abc123"}}

orca terminal wait --terminal term_abc123 --for tui-idle --timeout-ms 600000 # 等它干完
orca terminal read --terminal term_abc123 --cursor 0 --json # 收它的输出

依据:src/cli/specs/core.ts:86-136worktree create 的 usage 与 notes)、src/cli/specs/core.ts:198-233terminal read / send / wait)。

一句话直觉

把 Orca 想成"给 agent 用的 Kubernetes + 给人看的仪表盘":worktree 是 Pod(隔离的工作副本),守护进程是 kubelet(进程活着与否不看控制台在不在),RPC runtime 是 API Server(桌面窗口、手机、CLI 都只是它的客户端)。

⚠ 这个类比只用一次,下文所有承重术语(worktree / PTY / provider / runtime)都按代码里的原义使用。


2. 顶层全景(它大概怎么转)

Orca 的仓库体量很大(src 下约 9,677 个 .ts/.tsx 文件,as-of 本 commit),但骨架只有三层。

2.1 三层结构:客户端 / RPC 内核 / 执行宿主

怎么读这张图: 从左到右。左边是"谁在开车",中间是唯一的真相源,右边是"活儿实际在哪台机器上跑"。左右两边都可插拔,中间那根不动。

客户端(谁在开车) RPC 内核(唯一真相源) 执行宿主(活儿在哪跑)
┌──────────────────┐ ┌──────────────────┐
│ 桌面窗口 renderer│───┐ ┌──▶│ 本地机器 │
├──────────────────┤ │ ┌────────────────────────┐ │ │ local │
│ 手机 App / Web │───┼──▶│ OrcaRuntimeRpcServer │───┼──▶├──────────────────┤
├──────────────────┤ │ │ ├ RpcDispatcher │ │ │ SSH 远端 │
│ orca CLI │───┘ │ └ OrcaRuntimeService │ │ │ ssh:<id> │
│ (agent 在用) │ └────────────────────────┘ └──▶├──────────────────┤
└──────────────────┘ │ 另一台 Orca │
│ runtime:<envId> │
└──────────────────┘

右侧这三种宿主不是概念图,是代码里的一个字面类型:ExecutionHostId = 'local' | \ssh:${string}` | `runtime:${string}`src/shared/execution-host.ts:9)。每个 Worktree记录里都带一个hostId 字段(src/shared/worktree/types.ts:60`),于是"这条 worktree 归谁执行"是数据,不是分支判断。

2.2 部件一句话职责

部件干什么在哪
OrcaRuntimeService领域大脑:worktree / 终端 / git / 集成的全部业务方法src/main/runtime/orca-runtime.ts:2991
OrcaRuntimeRpcServer对外服务器:绑定各种传输、发 token、管长轮询src/main/runtime/runtime-rpc.ts:481
RpcDispatcher收请求 → 查方法表 → zod 校验参数 → 调 runtimesrc/main/runtime/rpc/dispatcher.ts:31
ALL_RPC_METHODS一张扁平方法清单(538 条方法定义 = 526 defineMethod + 12 defineStreamingMethodsrc/main/runtime/rpc/methods/index.ts:47
IPtyProviderPTY 能力接口:本地 / 守护进程 / SSH 都实现它src/main/providers/pty-provider-contract.ts:123
TerminalHost / HistoryManager守护进程侧:管会话、把滚屏落盘src/main/daemon/terminal-host.ts:46history-manager.ts:35
AgentHookServer本机回环 HTTP 服务,收 agent hook 上报的状态src/main/agent-hooks/server.ts:693
COMMAND_SPECS / dispatchorca CLI 的命令表与路由(220 条命令路径)src/cli/specs/index.ts:21src/cli/dispatch.ts:39
OrchestrationDb / Coordinatoragent 之间派活、提问、收结果的 SQLite 状态机src/main/runtime/orchestration/db/orchestration-db.ts:24coordinator.ts:37

2.3 一条终端字节的路径

这是全项目最值得先看懂的一条线:agent 打印的每个字节要穿过几层,为什么滚屏能活过 App 重启

agent CLI 进程 (claude / codex / …)
│ stdout: 普通字节 + OSC 9999 状态包

node-pty ── 跑在 orca-daemon 进程里(detached fork,App 退出它照活)
│ 帧化 + 落 history 检查点文件

DaemonPtyAdapter ── 把守护进程伪装成一个普通 IPtyProvider


OrcaRuntimeService ── 剥掉 OSC 9999,状态给侧栏,干净字节继续走

├──▶ RPC 流 ──▶ 桌面 xterm 面板 / 手机 App
└──▶ RPC 流 ──▶ orca terminal read(另一个 agent 在偷看)

依据:src/main/daemon/daemon-init.ts:445createOutOfProcessLauncher,装配进 DaemonSpawner 在同文件 :703)、:506-536(fork options:detached: truestdio: ['ignore','ignore','pipe','ipc']——stdout ignore 免得阻塞退出,stderr 是 pipe,用来收启动期崩溃日志)、:629-632child.disconnect() + child.unref(),让 Electron 能先退)、src/main/daemon/daemon-pty-adapter.ts:227DaemonPtyAdapter implements IPtyProvider)、src/shared/agent-status-osc.ts:35createAgentStatusOscProcessor)。这一段的完整展开见第 3 章 §3.2

2.4 主线走一遍(不进代码)

用户在侧栏点「新建 worktree,用 Codex,prompt = 修 login 超时」之后发生了什么:

  1. 定 base。 解析要从哪个 ref 切分支;必要时先 fetch,UI 上先亮 fetching 再亮 creatingsrc/main/ipc/worktree-remote.ts:1502)。
  2. 建 checkout。 拼出 git worktree add --no-track -b <branch> <path> <base> 并执行(src/main/git/worktree.ts:978-988)。
  3. 登记元数据。 生成 Worktree 记录,id 直接是 `${repoId}::${path}`src/shared/worktree/types.ts:61)。
  4. 起终端。 通过当前 IPtyProvider 在守护进程里开一条 PTY,cwd 指向新目录,命令行是 Codex 的启动命令。
  5. 注入 hook。 PTY 的环境变量里带上 ORCA_AGENT_HOOK_*,agent 的 hook 脚本据此把状态 POST 回本机回环端口。
  6. 投喂 prompt。 首条 prompt 通过 terminal.send 打进去。
  7. 点亮侧栏。 agent 上报 working,任务卡开始转;转成 waiting(在问你话)或 done(干完了)时发通知。

3. 阅读地图

六章按"由浅入深"排。只想看一处的话,看第 3 章——终端底座是这个项目工程密度最高的部分。

顺序章节讲什么什么时候该读
1领域模型与状态骨架 — Project / Worktree / Tab 怎么被表达和存住Repo / Worktree / Tab / TabGroup 的字段设计、id 的构造法、可选字段为什么这么多想读任何一章之前
2一条 worktree 的一生 — 从选 base 到安全清场选 base、worktree add 的参数取舍、sparse checkout、共享目录、休眠/唤醒、删除前的安全断言关心 git 集成、关心"删错目录"这类事故
3终端底座 — 常驻守护进程、Provider 抽象与滚屏存活守护进程协议、IPtyProvider 的可选能力、history 检查点、背压与多路复用想学"终端怎么做才不丢"
4认得出 agent — 目录、OSC 状态协议、hook 注入与会话考古agent 目录、OSC 9999 协议、hook 安装与端点文件、会话恢复(AI Vault)想接第三方 agent、想做状态感知
5一个 runtime,多种客户端 — RPC 内核与远程执行方法表与 zod schema、Unix socket / WebSocket / relay 三种传输、bootstrap 文件、E2EE 配对想做多端同源、想理解 SSH 远端
6让 agent 反过来开车 — skills、Orca CLI、编排与动作面skills 的 stub/guide 双份设计、CLI 命令表、编排 SQLite 与 Coordinator 循环想学"怎么把一个 App 做成 agent 的工具"

按任务反查:

你想干的事去哪章
加一个新 agent 类型第 4 章
让某个新能力能被 CLI 调用第 5 章(加方法)+ 第 6 章(加命令)
排查"终端重启后滚屏没了"第 3 章
排查"worktree 删不掉 / 删错了"第 2 章
理解 agent 派活给 agent第 6 章

4. 巧妙之处(可借鉴的技术)

4.1 PTY 托给一个 detached 子进程,而不是留在 Electron 里

妙在哪: 大多数终端 IDE 的 PTY 和窗口进程同生共死——App 崩了、更新了、退出了,跑了两小时的 agent 就没了。Orca 把 node-pty 整个搬进一个 fork 出来的 orca-daemon,用 detached: true + unref() + disconnect() 让它脱离 Electron 的生命周期;App 下次启动时按 socket + token 重新连回去认领已有会话。

依据:src/main/daemon/daemon-init.ts:445createOutOfProcessLauncher)、:506-536(fork options)、:629-632disconnect() + unref())、:703(装配进 DaemonSpawner);会话认领在 src/main/daemon/terminal-host.ts:46TerminalHost.createOrAttach)。

诚实标注一处:src/main/daemon/production-launcher.ts 里也有一份同形状的 detached fork 实现,但 createProductionLauncher 在本 commit 里只被它自己的测试文件引用,不是生产路径。读代码请以 daemon-init.ts 为准。(同类未接线模块还有 binary-frame.ts,见第 3 章 §4.3。)

4.2 守护进程失败时静默降级,而且这条降级是被显式吐槽过的

妙在哪: 守护进程起不来时 Orca 不会崩,它退回普通本地 PTY——功能都在,只是不再跨退出存活。代码里的注释直接写明这条降级"曾经在 v1.4.129-rc.1 里是完全不可见的",所以现在强制打日志 + 打点。这是"优雅降级"和"降级要能被观测"两件事一起做对的样本。

依据:src/main/index.ts:1028-1034onDaemonError 的注释与 track('daemon_start_failed', …))。

4.3 IPtyProvider可选方法表达能力差异,而不是用类型分叉

妙在哪: 本地 PTY、守护进程 PTY、SSH relay PTY 的能力天差地别——有的能做生产者侧背压(pauseProducer),有的不能。Orca 没有为此拆出三个接口或塞一堆 if (provider.kind === …),而是把差异做成可选成员,并在注释里写死"调用方必须在没有它时照常工作"。能力探测方法(supportsAgentSessionClaims 之类)还允许返回 Promise,因为远端能力得问过去才知道。

依据:src/main/providers/pty-provider-contract.ts:123-179

4.4 agent 状态走 OSC 9999,而不是猜终端标题

妙在哪: 判断"agent 是不是在忙"的土办法是解析终端标题或 grep 输出,两者都脆。Orca 定义了一个私有 OSC 转义序列 ESC ] 9999 ; <payload>,由 agent 的 hook 主动打进 stdout,Orca 在流里把它剥掉再转发。状态枚举只有四个:working / blocked / waiting / done

三个细节值得抄:

  • 解析器是有状态的,跨 chunk 缓存半截前缀,因为 PTY 数据会在任意字节处切断(src/shared/agent-status-osc.ts:35-72)。
  • 缓存有上限 MAX_PENDING = 64KB,超了就丢,防止一个畸形序列把内存吃穿。
  • agent 类型不是闭集AgentType = WellKnownAgentType | (string & {}),代码注释明说"自定义 agent 存在,任何非空字符串都收"(src/shared/agent-status-types.ts:22-45)。

4.5 hook 端点写成文件、在调用时才读,解决"老 PTY 认识新进程"

妙在哪: hook 是在 PTY 创建时通过环境变量传入的。但 Orca 重启后端口和 token 都变了——那些活过重启的老 PTY 里,环境变量还是旧值。Orca 的解法是:环境变量里给的不是端口,而是一个端点文件路径,hook 脚本每次触发时现读文件。老 PTY 于是自动指向新进程。

依据:src/main/index.ts:1022("hooks source this endpoint file at invocation time so old PTY env reaches the current process after restart")、src/shared/agent-hook-endpoint-file.ts:16parseAgentHookEndpointFile)。

4.6 git worktree add--no-track,就为了 git status 不说谎

妙在哪:main 切新分支,如果继承了 base 的 upstream,git status 会在你还没 push 时就报 "behind by N"——对着一屏 agent 面板,这种假信号是噪音。Orca 显式加 --no-track,把 upstream 留给第一次 push(配 push.autoSetupRemote)去建立。

依据:src/main/git/worktree.ts:987(Why 注释)与 :948args.push('--no-track', '-b', branch, worktreePath))。

4.7 删 worktree 前先跑一遍"危险路径"断言

妙在哪: 自动化删目录是最容易出灾难的操作。Orca 把判断抽成一个纯函数 isDangerousWorktreeRemovalPath,逐条拦截:路径为空、路径等于仓库本身、路径解析后等于文件系统根。另有一条独立断言防止删掉一个"里面还套着别的已注册 worktree"的目录。

依据:src/main/worktree-removal-safety.ts:65:131assertWorktreeDoesNotContainRegisteredWorktree)。

4.8 记下"这条 worktree 是 agent 建的,还是人建的"

妙在哪: CliWorkspaceProvenance 里有个 callerTerminalHandle 字段,注释写得很直白:它用来区分 agent 发起的创建和人在外部 shell 里手敲的创建。当 Orca 自己成为 agent 的工具时,"谁调用了我"就变成了必须持久化的领域信息。

依据:src/shared/worktree/types.ts:142-150

4.9 CLI 把重依赖延迟加载,只为了 --help

妙在哪: RuntimeClient 的依赖图占了 CLI 全部 199 个 eager 模块中的 153 个(zod、ws、tweetnacl)。而 --help、命令拼错这些路径根本用不到它。于是加载被推到 main() 里一次动态 import,恰好在 dispatch 之前 await——既省了启动开销,又保住了 ctx.client 的同步 getter 签名。给 agent 用的 CLI 会被调用几千次,冷启动时间是真成本。

依据:src/cli/index.ts:39-46

4.10 编排状态放 SQLite,而不是内存

妙在哪: agent 之间派活、提问、等回答,这些状态必须活过 App 重启和 agent 崩溃。Orca 建了一个独立的 SQLite:runs / messages / tasks / dispatch_contexts / worker_dispatches / decision_gates 等十余张表,并且带 user_version 迁移——注释点明 CREATE TABLE IF NOT EXISTS 不会改已有库,所以迁移必须在事务里做、成功才 bump 版本。

依据:src/main/runtime/orchestration/db/orchestration-db.ts:24-32(构造开 WAL 并跑 migrate)、db/schema/ 目录的建表 SQL。

4.11 skills 交付 stub,正文由二进制自己吐

妙在哪: skills/*/SKILL.md 只是发现用的存根,明确写着"这不是使用手册"——真正的命令参考由 orca 二进制自己输出。理由是版本漂移:装在 agent 目录里的文档会跟实际要跑的二进制脱节。文档跟着可执行文件走,而不是跟着仓库走。

依据:skills/orca-cli/SKILL.md:17-20;配套的 skill-guides/ 全文被生成进 src/cli/bundled-skill-guides.ts:1("Generated by config/scripts/generate-bundled-skill-guides.mjs. Do not edit."),CI 跑 verify:bundled-skill-guides 防漂移(package.jsonlint 脚本)。

4.12 值得注意的取舍:OrcaRuntimeService 是一个巨型类

诚实说一句:src/main/runtime/orca-runtime.ts 单文件约 35,900 行,OrcaRuntimeService:2652 一路到文件尾部。项目自己的 AGENTS.md 里禁止关闭 max-lines 检查、还有 check:max-lines-ratchet 棘轮脚本,说明这是被承认的历史债而非设计意图。想学结构的读者应该看 rpc/methods/(按域拆成 40 个领域模块,methods/index.ts 里正好展开 40 项)和 providers/(接口清晰)这两处,而不是这个类。


5. 代码地图(导航索引)

按符号名 grep 定位比按行号稳;行号 as-of 95c431f,与各章代码地图对齐。

5.1 领域模型

主题文件路径符号名
仓库记录(含 fork、SSH、外部 worktree 可见性)src/shared/repo-types.ts:42Repo
worktree 记录(id = repoId::pathsrc/shared/worktree/types.ts:60Worktree
CLI 创建来源溯源src/shared/worktree/types.ts:142CliWorkspaceProvenance
统一 Tab / 分组 / MRU 栈src/shared/tab-types.ts:40:856TabTabGroup
执行宿主标识src/shared/execution-host.ts:9ExecutionHostId
worktree id 拆解src/shared/worktree/id.ts:20splitWorktreeId

5.2 worktree 生命周期

主题文件路径符号名
创建 worktree(git 层)src/main/git/worktree.ts:941addWorktree
sparse checkout 变体src/main/git/worktree.ts:1062addSparseWorktree
删除 + 清理前置检查src/main/git/worktree.ts:1148:1346removeWorktreeassertWorktreeCleanForRemoval
危险路径拦截src/main/worktree-removal-safety.ts:65isDangerousWorktreeRemovalPath
创建编排(本地/远端两条)src/main/ipc/worktree-remote.ts:1512:1894createRemoteWorktreecreateLocalWorktree
两段式进度事件src/main/ipc/worktree-remote.ts:1502emitCreateWorktreeProgress
拆除时清进程src/main/runtime/worktree-teardown.ts:89killAllProcessesForWorktree
跨 worktree 共享目录/软链src/main/git/worktree-shared-directories.ts:53getWorktreeSharedLinkPaths

5.3 终端与守护进程

主题文件路径符号名
PTY 能力接口(可选成员表达差异)src/main/providers/pty-provider-contract.ts:123IPtyProvider
本地实现src/main/providers/local-pty-provider.ts:545LocalPtyProvider
守护进程适配成 providersrc/main/daemon/daemon-pty-adapter.ts:227DaemonPtyAdapter
守护进程启动入口src/main/daemon/daemon-main.ts:30startDaemon
detached fork launcher(真正被装配的那个)src/main/daemon/daemon-init.ts:445:703createOutOfProcessLauncher
守护进程发现/PID 文件src/main/daemon/daemon-spawner.ts:49DaemonSpawner
会话表 + 认领/重连src/main/daemon/terminal-host.ts:46TerminalHost
滚屏检查点落盘src/main/daemon/history-manager.ts:35HistoryManager

5.4 agent 层

主题文件路径符号名
OSC 9999 流式解析src/shared/agent-status-osc.ts:35createAgentStatusOscProcessor
状态枚举与 agent 类型开集src/shared/agent-status-types.ts:18:20AGENT_STATUS_STATESWellKnownAgentType
hook 接收服务(回环 HTTP)src/main/agent-hooks/server.ts:693:2731AgentHookServeragentHookServer
hook 端点文件(跨重启寻址)src/shared/agent-hook-endpoint-file.ts:16parseAgentHookEndpointFile
hook 安装(跨平台命令包装)src/main/agent-hooks/installer-utils.ts:49:309buildManagedCommandHookwriteHooksJson
传输无关的 hook 管线(relay 复用)src/shared/agent-hook-listener.ts模块级导出
agent → 遥测类型映射src/shared/agent-kind.ts:16TUI_AGENT_KIND_BY_AGENT

5.5 RPC runtime 与远程

主题文件路径符号名
领域大脑src/main/runtime/orca-runtime.ts:2991OrcaRuntimeService
RPC 服务器src/main/runtime/runtime-rpc.ts:481OrcaRuntimeRpcServer
请求分发 + zod 校验src/main/runtime/rpc/dispatcher.ts:31RpcDispatcher
方法总表(538 条)src/main/runtime/rpc/methods/index.ts:47ALL_RPC_METHODS
传输抽象src/main/runtime/rpc/transport.ts:19RpcTransportRpcMessageContext
bootstrap 文件(客户端如何找到 runtime)src/shared/runtime-bootstrap.ts:17:29:47RuntimeMetadatafindTransportgetRuntimeMetadataPath
孤儿 socket 清扫src/main/runtime/runtime-rpc.ts:1755sweepOrphanedRuntimeSockets
SSH 远端执行体src/relay/context.ts:30RelayContext
启动期服务编排(守护进程 + hook)src/main/index.ts:978startTerminalRuntimeStartupServices

5.6 面向 agent 的表面

主题文件路径符号名
CLI 入口(含延迟加载)src/cli/index.ts:44:51loadRuntimeClientClassmain
命令表(220 条路径)src/cli/specs/index.ts:21src/cli/specs/core.tsCOMMAND_SPECSCORE_COMMAND_SPECS
命令路由src/cli/dispatch.ts:39dispatch
CLI → runtime 客户端src/cli/runtime/client.tsRuntimeClient
skills 存根(发现用)skills/orca-cli/SKILL.mdfrontmatter name / description
skills 全文(生成物,勿手改)src/cli/bundled-skill-guides.ts:1BundledSkillGuide
编排持久层src/main/runtime/orchestration/db/orchestration-db.ts:24OrchestrationDb
编排主循环src/main/runtime/orchestration/coordinator.ts:37:133CoordinatorexecuteLoop
无头服务模式src/cli/specs/serve.ts:4SERVE_COMMAND_SPECS

5.7 从哪读起(三条路线)

  • 只想知道它怎么组织的: src/shared/types.tssrc/main/runtime/rpc/methods/index.tssrc/cli/specs/core.ts
  • 想学终端工程: src/main/providers/pty-provider-contract.tssrc/main/daemon/terminal-host.tssrc/main/daemon/history-manager.ts
  • 想学"App 怎么变成 agent 的工具": skills/orca-cli/SKILL.mdsrc/cli/specs/core.tssrc/main/runtime/orchestration/coordinator.ts