
CodeFlowMu 工程化实录(一):响应丢失之后,重试为什么必须先解决持久化幂等边界
对一个 Agent Runtime 来说,“工具报错后能不能重试”并不是一个网络层问题。
真正危险的窗口是:副作用已经发生,但调用方没有收到成功响应。
任务已经写入磁盘,响应却在返回途中丢失。调用方恢复后只知道“这次调用没有拿到结果”,于是按同一业务意图再次提交。如果 Runtime 无法证明第一次到底做了什么,第二次“重试”就可能变成第二次真实创建。
CodeFlowMu 是什么,为什么这个问题值得单独工程化
CodeFlowMu 是一个本地优先的多 Agent 协作与数字员工运行体。它不只负责把提示词交给模型,而是让 PM、DEV、QA、OPS、EVAL 等不同职责的 Agent 在受控工作空间里持续执行真实任务,并由 Runtime 管理任务对象、执行上下文、工具调用、事件、恢复和证据。
这类系统与一次性聊天最大的不同,是工作会跨越更长时间,也会遇到进程退出、Host 中断、网络异常、调度恢复和重复唤醒。一次工具调用如果已经产生真实副作用,Runtime 就不能只依赖“调用方有没有收到成功响应”来判断动作是否发生。
因此,幂等在 CodeFlowMu 里不是一个接口优化项,而是数字员工能否可靠恢复的基础边界:系统必须能够区分“第一次创建”“同一次业务提交的恢复”和“另一份新的业务意图”。
这次问题正是在这样的运行条件下被暴露出来。
完整实验、版本边界与公开证据见:RBE-20260828-01 证据页。
外部研究起点:异常并不能证明副作用没有发生
这条研究链的外部起点是 LlamaIndex 的 PR #22841:fix(core): avoid retrying failed function tools。
它处理的具体问题是:调用层先用一种参数形式真实执行 FunctionTool,遇到异常后又换另一种形式再执行。危险之处在于,第一次异常并不必然发生在副作用之前;如果第一次其实已经完成写入、发送或创建,那么第二次“尝试另一种调用方式”就可能重复执行真实动作。
LlamaIndex 的修复思路,是把参数形式的选择移动到真实调用之前,通过函数签名判断调用方式,避免用一次可能带副作用的执行去试探接口。
这个外部案例给 CodeFlowMu 的价值,不是提供一份可以照抄的补丁,而是提出了一个值得进入自身 Runtime 的故障模型:
当调用方看见异常时,系统是否能够证明副作用尚未发生?如果不能,重试依据是什么?
外部研究只负责提出问题。CodeFlowMu 是否存在同类工程缺口,必须由自己的实现和第一方实验回答。
第一方复现:同一个故障窗口,两条写路径给出相反结果
最初实验固定在 CodeFlowMu V2.0.4 提交 2ba1ad9b。实验不是生产事故统计,而是主动制造“动作已经完成、调用方却失去成功结果”的响应丢失窗口。
| 受测层次 | 历史实验观察 | 工程含义 |
|---|---|---|
| 上层内存去重 | 第一次动作已经发生,但成功结果没有进入可复用缓存;恢复后相同调用再次进入执行路径 | 进程内缓存不能解决跨恢复的结果未知 |
write_report | 相同任务、内容和提交标识重试,deduplicated=true,报告文件数保持为 1 | 受测报告路径已经具备持久结果复用 |
write_task | 相同业务创建意图再次进入真实创建路径,分配新任务身份,TASK 数变为 2 | 任务创建合同没有把稳定提交身份绑定到第一次结果 |
图 1:V2.0.4 的历史故障对照,不代表 V2.1.2 当前行为。图中计数只覆盖受测路径,不表示生产环境发生频率。来源:RBE-20260828-01 证据页。
这组结果很重要,因为它否定了一个过于宽泛的结论:并不是“CodeFlowMu 所有写工具都没有幂等”。
恰恰相反,write_report 是一个重要反例。它说明上层允许重试,并不必然制造第二份副作用,只要底层接口已经拥有可恢复的持久身份和结果复用机制。
真正的缺口集中在任务创建。当时 write_report 已暴露 client_submission_id,而 write_task 还没有相应的正式持久幂等合同。调用方主观上认为“这还是刚才那一次提交”,并不等于 Runtime 已经持久保存并认可了这个业务身份。
工程判断:可靠重试不是“查重”,而是恢复第一次结果
发现两张 TASK 之后,最容易想到的修法是:创建前查一下有没有同标题任务,有就不创建。
这不是可靠幂等。
标题、正文摘要、发送者甚至时间窗口都可能相同,却代表两次真正独立的业务意图。反过来,同一次业务提交在恢复时也可能带着不同的 trace、连接、进程或重试次数。模糊内容匹配既可能漏掉重复,也可能误伤合法新任务。
因此 CodeFlowMu V2.1.2 没有把问题定义成“怎么更聪明地查重”,而是定义成:
如何给一次创建意图一个稳定、可持久化、可恢复、可冲突检测的提交身份。
这也是这次工程化最关键的变化:重试从调用方策略,变成创建接口合同的一部分。
V2.1.2:把“一次业务提交”变成持久事实
V2.1.2 在任务创建路径中建立了这条关系:
client_submission_id → request_digest → task_id / task_path → creation_result
client_submission_id 描述的是一次业务创建意图,而不是某一次 HTTP、MCP 或进程调用。需要跨响应丢失、跨进程恢复的调用方,必须在重试时复用同一个稳定 ID。
如果重试时重新生成一个 ID,Runtime 会把它理解为新的业务意图。这不是幂等失败,而是调用方给出了一个新的身份。
只有提交 ID 仍然不够。同一个 ID 如果第一次要求 DEV 修复登录,第二次却变成 OPS 发布环境,Runtime 不能因为 ID 相同就错误复用第一次结果。因此 V2.1.2 同时保存规范化的 request_digest。
实现中的摘要方案为 write-task-v1:从 sender、recipient、subject、body、priority、thread_key、parent、references、depends_on、risk_level 等语义字段构造确定性 JSON,再计算 SHA-256。对象键排序,字符串统一 Unicode NFC 与换行,数组保留顺序;调用时间、重试次数、trace ID 等本次执行信息不进入业务摘要。
Runtime 同时保存 digest_schema_version。同一提交 ID 如果出现不同摘要或不同摘要版本,不会被静默解释成同一任务,而是返回 conflict,且不产生新的 TASK 副作用。
这是一份任务创建接口合同,不是把整个 Runtime 宣称成通用事务数据库。既有 write_report 去重语义保持不变,FCoP 的 TASK / REPORT / REVIEW / EVAL 协议也没有因为这次补丁而改版。
为什么还需要三阶段创建回执
如果只在 TASK 创建完成后再写一条“这个 submission 已经成功”的索引,仍然存在一个更窄但真实的崩溃窗口:
- TASK 已经成功落盘;
- 回执索引还没来得及写;
- 进程退出;
- 调用方带同一提交 ID 重试。
如果 Runtime 只看“最终回执不存在”,仍然可能错误地再创建一次。
因此 V2.1.2 把创建结果持久化为三个阶段:
reserved → task_created → committed
| 状态 | 已有持久事实 | 恢复规则 |
|---|---|---|
reserved | 提交 ID、摘要版本、摘要、预分配任务身份与路径 | TASK 尚不存在时沿原身份继续;已存在且内容匹配时接管原任务,不换号 |
task_created | TASK 已创建及文件摘要已经确认 | 匹配时继续提交回执;文件反而缺失时返回类型化冲突,不能假装“从未创建” |
committed | 完整机器可读创建结果 | 同 ID、同摘要直接复用第一次结果,不再次执行创建 |
提交级互斥与任务序号保护解决并发占位;原子持久化和中间态恢复规则解决崩溃恢复。目标路径如果已经存在,但身份或内容不匹配,也不能通过覆盖文件或另建第二张任务绕开不确定性。
对于长期停留在中间态的 reservation,Runtime 提供只读诊断:可以观察当前状态、目标 TASK 是否存在、摘要是否一致以及 reservation 是否长期未推进。诊断只报告事实,不因为“超时”自动删除记录,也不自行替业务层重新创建或裁决任务。
created、reused、conflict:把重试结果变成机器语义
disposition | action_taken | 含义 |
|---|---|---|
created | true | 本次调用完成首次创建 |
reused | false | 对应创建结果已经存在,返回第一次的权威任务身份 |
conflict | false | 同一提交身份与已有摘要或恢复事实冲突,没有新增 TASK |
action_taken=false 并不必然意味着请求失败。在 reused 场景里,它恰好说明 Runtime 没有再执行第二次创建,却仍然找回了第一次的成功结果。
这里的“成功”只表示任务对象创建结果被恢复,不代表任务已经执行完成、QA 已通过或业务已经交付。技术幂等不能替代上层治理状态。
兼容路径也没有被包装成更强保证。旧调用方不提供 client_submission_id 时,仍然可以使用原有 legacy 创建方式;但这种调用不会被宣称拥有跨响应丢失、跨进程的强幂等语义。
独立 QA:不是看有没有报错,而是看最终只剩几个权威对象
实现完成后,未参与实现的 QA 独立执行响应丢失和并发场景。验收关注的是实际 TASK 数量、task_id 是否一致,以及创建回执是什么。
| 场景 | 独立 QA 实际观察 |
|---|---|
| A2:创建成功后模拟响应未收到,再用同一提交身份重试 | TASK 数为 1;前后 task_id 相同;第二次返回 reused / action_taken=false |
| A4:8 路并发使用同一提交 ID 和同一摘要 | created=1 / reused=7;唯一 task_id;TASK 数为 1 |
进程重启后的结果复用、同 ID 异摘要冲突、legacy 返回兼容、中间态文件缺失与 stale reservation 诊断另有定向测试。
这里必须保留证据边界:A2/A4 的独立 QA 证明的是受测任务创建合同在两个关键故障模型中收敛,不等于“所有工具、所有 Host、所有网络环境都已经被独立验证”。
但对本次工程问题而言,前后差异已经足够明确:
历史受测版本中,同一创建意图在响应丢失后可以产生两张 TASK;V2.1.2 中,提供稳定提交 ID 和一致摘要的受测重试、并发与重启路径会收敛到同一个权威任务身份。
从补丁到产品版本:还必须经过发布门禁
V2.1.2 于 2026-08-30 完成私有母版 Runtime/Shell 发布。公开文章不直接链接私有 CodeFlowMu 仓库;对外可核查的版本事实、脱敏 QA 结果与范围说明统一保留在本文的公开证据页。
最终发布验证记录为:
- Runtime:1842 pass / 0 fail / 1 skip;
- Shell:1037 pass / 0 fail / 0 skip;
- V2.1.1 与 V2.1.2 同协议关键矩阵分别连续 10 轮通过:Runtime 1630/1630、Shell 550/550;
- typecheck、Shell build、安装器契约、规则和版本一致性通过。
这些数字是发布测试集合的实际结果,不应该被相加成所谓“幂等可靠率”。
本次发布对象是 CodeFlowMu 私有母版 Runtime/Shell,不是 Open Dev Team Edition,也不代表真实 LAN/Gateway、浏览器 profile 或用户生产项目已经被同一套夹具覆盖。Windows 符号链接权限导致的 1 项 skip、既有依赖审计告警以及 Python 定向夹具 warning 继续保留在证据说明中。
这次工程化真正改变了什么
过去,调用方看到的是“请求没有成功返回”,只能自行决定要不要再试。
现在,在提供稳定提交身份的前提下,Runtime 可以回答三件不同的事:
- 这是第一次创建,执行并返回新结果;
- 这是同一次业务提交的恢复,返回原来的结果;
- 这次请求与已经占用的提交身份冲突,拒绝产生新的副作用。
这使“重试”从一种带猜测的调用策略,变成可以被持久记录、恢复、审计和测试的工程合同。
对于数字员工运行体,这个变化比“自动重试几次”重要得多。数字员工会长时间运行、跨进程恢复、由调度器重新唤醒,也可能在 Host、网络和工具层遇到不确定失败。如果每一次恢复都要靠 Agent 猜“刚才是不是已经执行成功”,系统就没有真正跨过从 Demo 到生产运行体的边界。
可以迁移到其他 Agent Runtime 的检查方法
对每一个会产生副作用的工具,至少检查:
- 副作用已经落盘、响应丢失后,进程重启能否找回第一次结果?
- 相同稳定 ID、相同请求是结果复用,还是再次执行?
- 相同 ID、不同请求是否明确冲突,并保证零新增副作用?
- 并发请求的“检查 + 占位 + 创建”是否真的受到一致并发保护?
- 中间状态能否沿原身份恢复,而不是靠另建对象逃避不确定性?
- 旧调用是否保持兼容,同时没有被错误宣传为拥有同等级别的幂等保证?
- 已经正确的其他工具路径是否保持原有保护,没有在统一改造中退化?
如果一个 Runtime 只能回答“失败了就再试一次”,它还没有真正定义副作用边界。
更可靠的答案应该是:每一次可重试的业务意图,都有一个能够跨响应丢失、并发和进程恢复继续存在的持久身份。
工程结论
这次 CodeFlowMu 工程化没有证明“所有 Agent 工具都可以安全重试”。它证明了一个更窄、也更有价值的命题:
外部研究提出的失败模型,可以通过第一方正反实验收窄为具体接口缺口,再通过持久提交身份、请求摘要、创建回执、恢复状态机和独立 QA 变成一个可执行的 Runtime 合同。
响应丢失真正暴露的问题,从来不是“要不要再调用一次”。
而是系统有没有足够持久的事实,在第二次调用到来时回答:
第一次究竟做了什么。
证据范围与主要来源
- 历史实验、V2.1.2 工程更新与公开版本说明:公开 JSON fixture、Reader 与 check 用于验证冻结历史材料的一致性;公开页同时给出脱敏实现、独立 QA、发布门禁和残余风险说明。
- LlamaIndex PR #22841:提供外部故障模型与调用形式修复背景,不作为 CodeFlowMu 的实现或验收证据。
- CodeFlowMu V2.1.2 的实现、独立 QA 与发布原始日志位于受限私有母版中;本文不向公共读者提供不可访问的私有仓库链接。
- 真实生产发生频率、全部操作系统、真实 LAN/Gateway 与用户生产项目不在本文证据覆盖范围内。