一个 Agent 执行进程退出了。过了一段时间操作系统把同一个进程号分给了另一个进程。旧执行记录再次被读取时系统查询这个号码还活着。如果恢复逻辑就此停止记录可能继续显示“执行中”。它查到的不是过去那个执行者而是现在恰好使用同一个号码的进程。这是一种需要防范的情形不是本文声称已经发生的线上事故。真正引起我们注意的是 OpenAI Codex 对 Windows 受管进程身份的一项修订。沿着这项外部变化检查 CodeFlowMu 后我们发现的问题也不是“完全没有进程身份保护”而是保护已经存在于一个地方却没有以同样的语义进入另一份执行记录。1. Codex 为什么不只保存一个 PIDCodex 是 OpenAI 的编程 Agent其 app-server 为客户端提供运行接口。当这类服务作为后台进程存在时启动、停止和升级都必须知道自己正在操作谁。在 Windows 生命周期 PR #42381 中进程管理不仅保存 PID还使用 Windows 进程句柄和创建时间核验身份终止目标前再次检查避免把后来复用同一号码的进程当成原目标。随后合入的 升级交接 PR #42392 又处理了 successor 启动就绪与 ownership 交接。这些是已合并的源码变化不等于本文测试过其全部发布形态。这里的关键不是 Windows 专属技巧而是一个关系PID 是定位号码不是永久身份。长期运行的 Agent 系统一旦把号码写入磁盘下一次读到它时就跨越了时间号码相同不足以证明责任连续。但借鉴外部项目不等于照抄一套 daemon。我们先要回答CodeFlowMu 是否已经解决过这个问题2. 本地已经有答案但答案没有覆盖所有账本CodeFlowMu 是我们开发的本地多 Agent 协作系统。我们把本次源码与实验固定在 V2.2.6、提交5c94d8c3b0147b779b17f620b811c6a17cc65288。它的 Runtime 写锁已经有进程身份保护。写锁用于防止不同实例同时占有同一写入范围这里的 owner 就是持锁进程。runtime-process-identity.ts会在 Windows 查询真实进程创建时间形成带时间精度的身份 token。runtimeLockOwnerIsStale()不只问进程是否存活有创建标识时比较标识对于没有标识的旧锁还会检查一个时间矛盾——如果当前进程是在锁创建之后才出生就不可能是最初持锁者。权限不足或无法获得身份时也不会直接推导“原 owner 已死”。这个发现先否定了一个过大的选题不能写“CodeFlowMu 只有 PID完全没有进程身份”。另一条路径是操作审批服务。它保存某个获准动作的执行记录包括executing状态、执行进程号和开始时间。在读取记录时它会尝试识别执行者中断。当前条件的核心是record.statusexecutingrecord.execution.executor_pid!process.pid!isProcessAlive(record.execution.executor_pid)满足条件后服务把记录转成partial_failed并要求检查目标结果。这条恢复逻辑没有像写锁那样比较进程创建身份如果记录里的 PID 正好等于当前进程 PID连这一轮死亡检查也不会进入。两个组件职责不同不能要求它们所有状态相同。但它们都在回答一个共同的前提这份旧记录是否还能归属于目前查到的那个进程3. 我们没有等待一次真实 PID 复用为了验证这个差异我们没有反复制造进程等待操作系统重新分配到指定号码也没有对真实业务操作做中断。实验先通过实际OperationApprovalService.prepare()和approve()创建隔离审批记录再受控设置其中的执行状态、PID 和开始时间。随后创建新的服务实例用真实get()读取记录并触发恢复判断。执行器始终没有被调用。比较侧使用真实操作系统的进程探针再把相同的 PID 和受控时间交给现有写锁判断函数。这里构造的是没有 process-start token 的旧格式锁记录让它走“当前进程出生晚于记录”的兼容检查不是虚构一对不同 token直接得到预设答案。三种输入各在新隔离目录中重复两轮受控记录审批服务读回的状态写锁函数对相同 owner 时间条件的判断当前进程号当前开始时间executing非 stale经操作系统确认不存在的进程号partial_failed要求检查结果stale旧开始时间配现在活着的当前进程号仍为 executingstale第三行是决定性的比较。夹具把开始时间设为 2000 年真实进程创建于本轮实验。写锁函数能发现明显的时间矛盾审批读取路径仍保留executing。图 1合成旧记录配合真实 OS 创建时间的比较。它不是线上 PID 复用事故识别原 owner 已不匹配也不能直接推出效果未发生或获准重试。点击查看高清原图。这里的 2000 年是主动构造的测试值不是历史日志。我们模拟的是“旧记录遇到现在同号进程”的可见状态没有诱发真实的操作系统 PID 复用事件。重新创建服务对象也不是重启 Runtime 进程。两轮验证的是同一受控方法的重复性而非跨进程重启可靠性。还要限定比较的性质我们把审批开始时间映射成了合成旧锁的持锁时间用来检查相同时间矛盾并没有证明线上存在一对时间完全一致的真实审批记录和真实写锁。完整输入、真实 OS 元数据与输出保存在随稿证据说明所列的 E2 记录中。六次观察的外部执行器调用数均为零。4. “没有认出中断”不等于“已经重复执行”这个结果支持一个窄结论当前审批执行记录的恢复判断对进程代次的识别弱于既有写锁判断。在构造的旧记录情形中它可以继续显示executing尽管已经有足以否定原 owner 身份的时间证据。这可能影响使用者判断“是否还应等待这个执行者”。但本实验没有证明系统因此发起第二次动作没有证明误杀了其他进程也没有统计线上有多少记录长期停在这个状态。第二行负对照同样不能删除。原进程号确实不存在时现有服务能够识别中断并明确要求检查动作结果。系统不是没有恢复处理缺的是活 PID 与原进程身份之间的区分。对应的现有回归中进程身份四项、写锁九项、操作审批十五项均在两轮中通过。这说明已有保护并非纸面设计也说明已有测试通过不能替未覆盖的交接条件作保证。它们是本轮 41 项相关回归的子集不能再额外相加成另一组“系统可靠性”数据。5. owner 失效以后仍然不能直接宣布可以重试最危险的修法是发现旧执行者不在以后立刻把动作交给另一个 Agent 再跑一次。旧进程可能已经完成了外部动作只是没写完最后的记录。进程身份只能帮助判断“谁还拥有这份执行责任”不能证明远端提交、文件改动或消息发送是否发生。需要分开的其实是四个问题存活性这个进程号当前有没有对应进程身份它是不是记录中的那个执行者效果事实原动作到底发生了没有当前权限现在是否仍允许执行相应动作前两个问题回答了也没有自动回答后两个。这同样适用于 CodeFlowMu 的 FCoP 文件协作TASK 文件和生命周期位置可以保留任务身份与业务上下文但不能仅凭文件还在就把中断动作视为未发生。此前讨论的恢复准入合同与本轮进程识别研究可以衔接却不能因为版本号更新就把合同中的所有组件写成已交付能力。值得评审的改进方向是让需要长期保存的执行 owner 记录绑定可核验的进程代次并区分匹配、不匹配和未知能复用当前身份机制的地方应先复用而不是另造一套含义稍有不同的 PID 检查。对于旧记录尤其不能把现在查到的创建时间直接回填成“原执行者身份”。那会把缺失证据伪造成已核验事实。缺少可靠元数据时应保留未知识别出中断时也应继续检查效果而不是将身份失效转换成重执行许可。这些仍是研究后的工程建议。本轮没有修改产品代码没有进行真实副作用恢复也不构成独立 QA 或开发授权。Codex 的外部变化值得借鉴恰恰因为它让我们问出了一个更具体的问题同一种身份事实是否已经在所有依赖它的记录中得到一致解释进程号仍然有人使用不代表过去那份责任仍然有人承担。中文原文 · English · 研究主页