内部发布工具起一个 codex worker,让它更新一个仓库,期望拿回一个跑过测试的补丁。某天宿主进程重启了,你打开日志,看起来什么都正常,就是没有补丁。

这种时候真正难的问题不在“进程还在不在”。而在:刚才跑的是哪个任务、这些事件属于谁、超时结束的是进程还是只是那一次等待、现在从哪一步接着跑是安全的。

codex exec 把 Codex 变成了一个能被别的程序调用的工人。工人被起起来之后,生命周期就归宿主管了。aiopssre 上那篇讲 supervised worker process 的文章把这层责任拆得比较细,下面结合它和我自己踩过的坑说。

先选层,再写监督代码#

官方文档把 Codex 的集成方式分成三档,先挑对档再动手:

集成层适合什么
codex exec脚本、CI、有边界的后台任务
Codex SDK应用代码里要程序化地启动、恢复、流式取结果
App-server要持久会话、事件流、中断、工具暴露、审批处理的产品

一次性的仓库检查当子进程跑就行。产品里有多个并发用户、审批、跨天会话,那就是在要一套协议和会话管理器,用 shell 拼出来可能比直接上 SDK 更费劲。

JSONL 是过程记录,不是结果#

codex exec --json 吐的是按行分隔的 JSON 事件,命令执行和文件改动都以 item 事件出现。官方评测指南里的例子是:把 trace 存下来,逐行解析,再检查该出现的命令和文件有没有出现。

这个流能告诉你进度,但一个进度事件离“这件事被接受了”还很远。命令可能起了然后失败,文件可能改了然后构建不过,最后那条 agent 消息听起来像做完了,而该有的产物不在。完成与否要由宿主定义:期望的终止事件、该有的退出码、产物检查,再加一个针对业务的验证器。

两个细节:

  • 原始事件流先落盘,再做给界面用的加工。渲染器会合并重复输出、把 reasoning 项藏起来,后面查账的人需要按原始顺序还原,才分得清“终止事件没到”和“界面没显示”。
  • 敏感信息在源头脱敏,trace 的访问权限要比成品的访问权限更窄。

一个任务只能有一个进程所有者#

子进程起来之前先建 execution ID。这条记录里放:进程 ID、线程 ID(如果有)、工作目录、本次请求的沙箱和审批策略、开始时间、截止时间、期望产物。心跳从观察到的事件里更新。取消和清理归父进程,不归 agent。

同一个线程不允许两个写者同时恢复。按线程加租约,实现可以简单到数据库里一行原子记录,带所有者和过期时间。子进程活着就续租,子进程退出并且输出流抽干之后才释放。宿主启动时先拿过期租约和真实进程、执行记录对一遍,再决定要不要起新的。

那条 CAD 的现场报告就是这个问题的实例:Mac 应用从 codex exec --json 起 worker,把事件映射成聊天步骤,之后用 resume 接着聊。作者说,同一个线程前面那个写者还活着的时候,resume 会失败,所以应用只能等、重试、退出时把子进程收掉。

还有更难的一档:父进程改了文件,还没收到终止事件就崩了。这时候重新跑同一个任务,可能覆盖已经被接受的工作,也可能把一次对外副作用做第二遍。先看工作区、看 git status、看该有的产物、看 agent 有权改的目标,把结果分成四种:已完成、失败、还在跑、不知道。“不知道”是真实状态,得对账,不能当没发生。

取消不止一条边#

截止时间到了,关停要有顺序:先停止接受新输入,发它支持的打断信号,等一个有时限的宽限期,还活着就终止,最后才考虑硬杀。关停期间继续读输出,晚到的事件和错误要进 trace。

只杀父 shell 会把后代进程留在外面。要在进程组里起 worker,按组终止。别按进程名模糊匹配,别人机器上跑着的 Codex 会话可能是正经活。每个信号和最终看到的退出状态都记下来。

取消之后不要假设工作区干净。走一遍和正常结束时同一套产物检查,再决定留下、回滚还是隔离。代码任务是 diff、构建、测试、未跟踪文件;CAD 那种是源脚本、生成的模型,加一个几何验证器。

权限设置也是记录的一部分#

沙箱和审批策略决定了“哪些动作本来可能发生”,所以要跟这次执行存在一起。自动化里按最小权限给。

宿主不该因为第一次卡住就悄悄放宽权限重试。worker 要联网、要更大的写范围、要碰一个有后果的工具,那就把这步变成产品能解释、能批准的一次新状态。prompt 一个字没改,权限变了就是新决定。

还有一条分界线:让 worker 在沙箱里生产和检查产物,宿主验证完,再由应用显式执行不可逆的发布、发送、删除。这样模型重试不会让对外的副作用跟着做第二遍。

故意失败,用来测监督者#

拿这几种情况测你的 supervisor:超过截止时间、写完文件后崩溃、JSONL 格式坏掉、缺终止事件就退出、留下后代进程、同一个线程两个 resume 抢、任务跑着的时候重启宿主。每一种都应该落到一个有界状态,并且留下够判断下一步的证据。

然后把 agent 的活单独评一遍。官方建议是先做少量确定性检查,对着 trace 和产物,规则判不了的地方再人看。两组信号要分开:监督者做得完美,也可能稳稳地送出一份烂活;agent 再强,撞上一个丢了自己退出状态的宿主,也一样白搭。

最后#

这个模式做完的标志是:一个 execution ID 串起请求、子进程、事件流、线程、工作区变化、验证器,和被接受的结果。到这一步,codex exec 不再是一个黑箱子进程,而是一个能被观察、能停、能对账、并且只按证据相信的组件。

我自己在博客自动化里踩的是同一个坑的小号版本:用后台方式起发布脚本,外壳是 bash 的登录交互模式,Python 子进程根本没起来,零输出,没有任何东西提示你出事了,只看到一行“任务完成”。后来改成前台跑、给足超时,并且去读远端 refs 确认推送真的落地,才算把这个状态补上。超时不等于失败,也不等于成功,它只是个待对账的信号。

参考来源: