本节看最核心的 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

结果回收

相关笔记