
回执坏了,为什么任务没有再跑一遍?一次真实调度链的反例实验
我们故意损坏一条命令的完成记录,再提交同一条命令。系统果然再次调用了负责执行的函数。
但函数被调用两次,任务就一定会启动两次吗?接上产品的真实调度模块后,我们发现:没有。实验中的模拟执行接口仍只启动了一次,也只留下了一条执行尝试记录。
如果只数函数被调用了几次,很容易过早得出“任务重复执行”的结论。
这里负责执行的函数,后文称为“执行回调”;模拟执行接口则代替 SDK,也就是运行时连接模型的接口。它只在内存中模拟启动,没有连接真实模型,更没有付款、发信或代码推送。
再次调用,不等于再次启动;再次启动,也还不是外部效果已经重复的证据。
这篇文章讲的不是一次生产重复派单事故,而是我们怎样通过反例,收回一个过早的工程判断。
1. 一张回执,为什么会影响下一次执行?
CodeFlowMu 是我们正在开发的本地多 Agent 协作系统。任务身份与业务记录由 FCoP 组织,运行时负责调度、会话和技术执行。
用户提交“重试这项任务”时,系统不仅要执行命令,还要留下命令处理记录。客户端可能丢失响应,进程也可能重新启动;之后收到相同请求,系统需要判断:这是新命令,还是已经处理过的命令又来了?
为此,请求携带幂等键,也就是标识“同一次意图”的稳定编号。命令回执通常先记为 pending,表示正在处理;完成后追加 completed,表示已有结果。再次收到同一个键时,可以直接返回旧结果,而不是把操作做一遍。
我们先检查了选定的真实历史文件,避免把日志行数误当事故数量。
| 9 月 8 日读取的历史样本 | 数量 |
|---|---|
| 任务命令回执行 | 10 |
| 不同幂等键 | 5 |
| pending / completed | 5 / 5 |
| 解析坏行 / 完全重复行 | 0 / 0 |
这些记录来自 9 月 5 日。十行是五条命令各自的状态事件,不是重复派发了五次,更没有观察到我们后来人为注入的损坏。
因此,接下来的实验是在隔离目录里提出一个问题:假如已完成的回执不能再被读取,系统会怎样判断同一条重试请求?
2. 第一轮:文件没删,读取结论却退回了旧状态
当前回执读取器逐行倒序查找。遇到无法解析的行,或者无法识别其格式版本的记录,它会跳过,继续寻找可读的匹配项。
这是一种容错方式:一条无关坏行,不应必然让整份文件都不能读取。但它也有一个后果。如果最新的完成回执坏了,读取器可能找到更早的 pending;如果两条目标记录都坏了,它可能返回 null,即没有找到可用记录。
本轮源码与实验没有发现读取过程删除原始字节。变化发生在“调用方能得到什么判断依据”,不是“磁盘上还剩什么”。
为了单独测这层语义,我们调用真实命令内核和回执存储,固定一份允许重试的治理状态,并使用只计数的模拟执行器。每次重试创建新内核实例,排除旧实例内存缓存的影响。
读下面的表,可以先抓住三件事:完整回执会复用;目标回执损坏后可能再次调用;即使再次调用,下层去重仍可能合并效果。 其余对照用于检查这三个判断的适用条件。
| 受控条件 | 第二次读取或处理结果 | 两次请求累计回调 / 模拟效果 |
|---|---|---|
| 完成回执完整 | 复用完成结果 | 1 / 1 |
| 仅插入无关坏行 | 仍复用目标完成结果 | 1 / 1 |
| 完成回执损坏,旧 pending 完整 | 再次进入回调 | 2 / 2 |
| 两条目标回执都损坏 | 未找到可用回执,再次进入回调 | 2 / 2 |
| 两条损坏,但当前状态版本已改变 | 拒绝旧版本请求 | 1 / 1 |
| 读取器明确抛错 | 存储不可用,不再调用 | 1 / 1 |
| 完成回执改成不支持的版本 | 找到旧 pending,再次进入回调 | 2 / 2 |
| 完成回执损坏,pending 完整,状态版本已改变 | 识别到状态已推进,不再调用 | 1 / 1 |
| 完成回执损坏,模拟执行器按相同键去重 | 回调再次进入,但效果合并 | 2 / 1 |
表中的显式读取错误是研究注入的反例,不是说当前读取器已经会报告这种损坏;不支持的版本也是人为构造。最后一行的去重集合位于实验进程内,不是持久化数据库。
因此,第一轮的结论应停在调用层:读取异常没有被明确表达时,真实内核可能再次调用执行器;重复效果是否发生,还取决于执行器。 现有内核本来就允许在 pending、状态未变的条件下重试,并要求下层按同一幂等键合并工作。
3. 第二轮:把“实际会不会再启动”测出来
第二轮不再让执行回调只增加一个计数,而是让它进入产品真正安排任务启动的流程。负责调度、保存执行尝试、管理任务状态和会话的组件,都使用当前产品实现。
实验固定了一份允许重试的治理状态,让请求能够进入下层;最后启动的仍是前面介绍的内存模拟接口。我们没有从网页发起请求,而是用研究代码按照网页服务的重试处理逻辑,传入同一个幂等键和调度参数。这测到了实际调度组件,不是整个网页入口的端到端验证。
这使我们能够回答一个更窄、但比第一轮更接近实际执行的问题:真实调度链是否会把这次重复调用变成第二个执行尝试、第二次 SDK 启动?
每组先验证第一次确实启动,再破坏隔离回执、执行第二步。下面的数量都包含第一次执行。先看两种结果:使用原命令键的场景没有第二次启动;换成新键的对照能够启动。 再看中间各行怎样区分取消、状态变化与并发。
| 场景 | 变化条件 | 第二次结果 | 累计 SDK 启动 / 执行尝试 |
|---|---|---|---|
| I0 | 完整回执 | 回执层直接复用 | 1 / 1 |
| I1 | 完成回执损坏 | 调度器返回已派发 | 1 / 1 |
| I2 | 两条目标回执都损坏 | 调度器返回已派发 | 1 / 1 |
| I3 | 同 I2,新进程读取原磁盘状态 | 调度器返回已派发 | 1 / 1 |
| I4 | 同 I2,先取消原会话,再用原键重试 | 调度器返回已派发 | 1 / 1 |
| I5 | 同 I2,治理状态版本已改变 | 内核拒绝旧版本请求 | 1 / 1 |
| I6 | 同 I2,八个内核并发调用同一调度器 | 八次均返回已派发 | 1 / 1 |
| I7 | 取消后使用新命令键 | 新建执行尝试并启动 | 2 / 2 |
图 1:I2 的分层计数与 I7 的区分对照,均包含第一次调用。中间列出调度模块的保护层,不表示 I2 依次执行了所有检查;合成 SDK 启动也不等于真实外部效果。来源:正式观测与证据说明。点击图片可查看原图。
I1、I2 直接修正了第一轮最危险的外推:回执确实没能告诉内核“已经完成”,但会话和租约仍告诉调度器“不要再启动一次”。
I4 则进一步说明,相同命令不会仅因旧会话已取消,就自动变成一个新命令。实际对账把旧尝试收敛到终结状态;相同幂等键仍复用这条尝试,没有再次启动。内部结果里的“已派发”在这里不能翻译成“旧进程仍在运行”。
I7 是不能删掉的对照。如果只展示前七行,读者可能以为我们把系统测试成了“任何时候都不执行”。换成不同命令键后,合成治理输入允许的新命令确实建立了第二条尝试。它不是同键重复效果,更不代表“随便改键就获得真人授权”;本实验没有测试完整认证与批准界面。
4. 挡住第二次启动的,不是一张万能回执
源码给出了与实验一致的解释。
首先,调度器会检查当前任务、当前工作轮次的会话与有效执行租约。租约在这里表示执行占用关系:谁持有哪项任务的哪次执行,不是任务已经完成的业务结论。有会话或有效占用,重复进入调度不应自然获得另一次启动资格。
其次,执行尝试存储会按幂等键复用已有记录。offer() 负责查找或提出执行尝试,claim() 负责争取执行占用;两者不能合并成“调用一次就启动一次”。终结的尝试和冲突的租约,还有各自的拒绝条件。
最后,单个调度器会串行处理同一任务的重新派发过程。仅保证尝试记录不重复创建,并不足以阻止两次流程同时修改任务位置,因此 I6 测的是整段过程的单实例并发保护。
I3 还揭示了一个必须解释的细节:新进程没有旧的内存运行句柄,但会话列表读取的是磁盘上的 running 记录,租约也仍然存在。这足以保守地挡住第二次启动,却不证明原执行者仍活着,也不证明工作已经恢复。
我们没有在这里跑完整启动恢复,也没有模拟硬件掉电。把这个结果叫作“新进程没有丢弃已有占用证据”是准确的,叫作“断电恢复通过”就越过了实验范围。
5. 外部日志修复,为什么不能直接套成本地缺陷?
这项研究的外部起点之一,是 Orca 的会话日志修复提案。它处理的是另一种风险:损坏位置后面的日志被修复流程删除,而模型服务端的对话记录又不能重建应用自身的回执。
截至 9 月 8 日,该提案仍为 Draft。方案是在保留原始日志的前提下,把可读部分发布为新一代记录,并对不支持的版本限制写入。它尚不是本文核验过的正式发布能力。Orca #19399
两者不能简单画等号。Orca 提案要消除破坏性的修复路径;本地实验里的读取器没有删原文。相同的问题意识是:恢复需要自己的证据,不能因为“能重新打开”或“模型还记得”,就认为恢复判断的依据没有变化。
本地也并非所有存储都采用跳过坏行的方式。我们检查的会话单记录存储,会区分“找不到文件”和“文件损坏”;操作审批记录损坏时,也会返回明确错误。它们同样保留原文。这些反例阻止我们把一个 JSONL 回执读取行为概括成“整个系统把损坏当不存在”。
6. 工程落点:先让异常可见,而不是宣判重复执行
第二轮没有让读取层的异常消失。完成回执坏了之后,调用方仍可能只看到旧 pending 或没有可用记录,却不知道这两种返回背后存在损坏。
已有执行保护在本轮场景中仍然有效,所以值得评审的是一个更窄的问题:读取结果能否同时表达“找到什么记录”与“读取过程是否健康”?找不到、内容损坏、格式版本不支持,是否需要不同的诊断信息?这些信息应该影响哪类命令,哪些只用于提示和对账?
这不是立即规定“有一条坏行就禁止整个系统工作”。无关坏行、仍可用的目标回执、版本兼容与真实执行占用,必须分别处理。粗暴阻断可能把原本有效的恢复路径也切断。
一个可复用的研究方法,是沿链逐层计数:保留了哪些原文,读取了哪份结果,进入了几次回调,形成了几条执行尝试,启动了几次执行,最后实际产生了几个外部效果。只有测到哪一层,才对哪一层下结论。
可靠性研究的价值,不是尽快证明系统有问题,而是把问题定位到不能再被下一层反例推翻的位置。
这一次,执行回调来了两次,任务没有因此再启动一遍。应该保留的是已经起作用的保护,应该补清的是尚未被表达的读取异常。
研究与证据说明
第一方源码固定于 2026 年 9 月 8 日提交 c008d9db91a2,完整值见文稿元数据。第一轮模块对照包含 18 项,第二轮包含 8 组调度与 4 组身份探针,各自正式复跑两轮;本文只使用相关子集,不把重复运行次数当事故样本或可靠性百分比。
所有损坏注入都发生在隔离夹具。内核、存储、调度和会话组件来自实际产品,治理输入与 SDK 为合成部分;没有真实外部效果实验。八路并发限于同一进程共享调度器,跨进程场景是顺序交接,未验证跨进程写锁、硬件掉电、所有命令或所有执行平台,也不是独立 QA。
逐轮脱敏观测、源码摘要、历史聚合与记录校验器已随本文公开:中文证据说明 · 英文说明。公开包核对已保存的结果,不等于重新执行产品实验;原始运行文件、完整夹具及产品复跑仍属于访问受限材料。研究没有修改产品代码,也没有授权新的恢复改造。
