本节看最核心的 Node companion。它是这套设计真正的桥:一边接 Claude 传来的命令,一边接 Codex app-server,中间维护 job 状态。
Node companion 是什么角色?
codex-companion.mjs 可以理解成一个本地任务网关。它不是 agent,不负责推理;它是协议层,负责把 Claude Code 世界里的命令转成 Codex runtime 能执行的 thread/turn 请求。
flowchart LR A[Claude subagent] --> B[codex-companion.mjs] B --> C[parse flags] B --> D[build job] B --> E[job store] B --> F[Codex app-server] F --> G[thread/turn]
它为什么要支持多个子命令?
因为一旦支持后台任务,就不能只有 task。至少需要一组任务控制协议:
| 子命令 | 作用 |
|---|---|
task | 创建一个 Codex 任务 |
task-worker | 后台 worker 真正执行任务 |
status | 查询 job 状态 |
result | 读取 job 结果 |
cancel | 取消后台任务 |
setup | 检查本机环境和 Codex 可用性 |
review | 走专门的 review 工作流 |
flags 是怎么变成任务语义的?
用户输入里有自然语言,也有 flags。Node companion 要把它们拆成结构化字段:
flowchart TD A["/codex:rescue --background --write --resume fix bug"] --> B[parse args] B --> C[prompt: fix bug] B --> D[background: true] B --> E[write: true] B --> F[resume: true] B --> G[sandbox: workspace-write] B --> H[resumeThreadId: lookup]
这里最重要的是:--write 不是一个普通布尔值,它会影响 Codex 的 sandbox;--resume 也不是普通布尔值,它会触发 job/thread 查找。
job store 为什么关键?
后台任务必须有一个地方存状态,否则 /codex:status 和 /codex:result 没法工作。
job store 至少要保存:
- jobId
- workspace / cwd
- prompt 或 prompt 文件路径
- 状态:queued、running、completed、failed、cancelled
- threadId
- turnId
- log 路径
- finalMessage / result
- worker pid 或取消标记
stateDiagram-v2 [*] --> queued queued --> running running --> completed running --> failed running --> cancelled completed --> [*] failed --> [*] cancelled --> [*]
前台和后台为什么用同一个 task 协议?
这是一个很好的设计点。前台和后台不应该是两套任务构造逻辑,否则很容易行为不一致。
更合理的结构是:
flowchart TD A[task request] --> B[buildTaskJob] B --> C{background?} C -->|no| D[runForegroundCommand] C -->|yes| E[enqueueBackgroundTask] D --> F[executeTaskRun] E --> G[spawnDetachedTaskWorker] G --> F
这样前台和后台共享 job 构造,只在执行方式上分叉。
这层最值得学什么?
Node companion 的核心不是“会跑 node”,而是它提供了一个任务协议层。以后你做任何跨工具桥接,都可以仿照这个模型:
入口命令
↓
本地 companion
↓
结构化 job
↓
状态存储
↓
目标 runtime
↓
结果回收