度器修復實錄:復活節(jié)點導致的 run 誤終態(tài)(dag_2d12c2f7 事故復盤與回歸驗證))
oh-my-openagent senpi-task DAG 調(diào)度器修復實錄復活節(jié)點導致的 run 誤終態(tài)dag_2d12c2f7 事故復盤與回歸驗證【免費下載鏈接】oh-my-openagentOmO: Just type mass ulw keyword with your prompt. Now you are the master of graph engineering.項目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent導讀本文基于 .omo/evidence/20260821-dag-revive-terminalization/README.md 及 senpi-task 的 DAG 調(diào)度器源碼完整復盤一次真實線上事故一次三節(jié)點 DAG run 在節(jié)點 B 被dag send復活、節(jié)點 C 仍處于 pending 時調(diào)度器錯誤地發(fā)出了dag.run.completed終態(tài)事件。文章逐層還原事故時間線、根因分析、修復方案共享結(jié)算注冊表 終態(tài)斷言并給出可復現(xiàn)的 RED/GREEN 回歸測試與完整驗證命令幫助你理解 oh-my-openagent 中 DAG 波次調(diào)度、節(jié)點復活revive與 run 終態(tài)判定之間的微妙交互。事故背景一次真實的 DAG run 提前終態(tài)2026-08-21 的線上運行dag_2d12c2f7-38f3-487e-bead-2ceafd0ab87e在一個三節(jié)點、兩邊的 DAG 上執(zhí)行其定義與最終狀態(tài)快照保存在 incident-run.json 中節(jié)點依賴角色omo-sdk-error-surfacing記為 A無修復 omo 側(cè) dag SDK 錯誤透出senpi-js-localroots記為 B無修復 senpi 側(cè) JS kernellocal://路徑verify-merges記為 CA、B驗證兩個 PR 均已合并波次結(jié)構(gòu)為wave 0 [A, B]wave 1 [C]C 依賴 A、B 都完成后才能運行。事故發(fā)生時B 已被dag send復活且處于 running 狀態(tài)而調(diào)度器卻在 A 完成的那一刻錯誤地終結(jié)了整個 run。事件日志 incident-events.jsonl 完整記錄了當時的執(zhí)行序列seq 1 dag.run.created runIddag_2d12c2f7-... nodeCount3 edgeCount2 seq 2 dag.run.started generation1 seq 5 dag.wave.started waveIndex0 [A, B] seq 10 dag.node.transitioned B: running - failed seq 11 dag.node.steered B deliveryrevive seq 12 dag.node.transitioned B: failed - running (revived) seq 13 dag.node.transitioned A: running - completed seq 14 dag.wave.completed waveIndex0 # B 仍在 running seq 15 dag.run.completed counts{total:3, pending:1, running:1, completed:1}seq 15 的counts字段就是最直觀的病征total:3而pending:1C 還等著調(diào)度、running:1B 剛被復活還在跑、completed:1只有 A 完成這樣一個明顯未完成的 run 卻被標記為completed。事故時刻的快照 incident-run.json 中verify-merges節(jié)點狀態(tài)為pending、status卻已是completed形成了一副不可能被任何恢復路徑繼續(xù)認領(lǐng)的終態(tài)檢查點。根因分析波次局部的結(jié)算表與復活后的身份丟失復盤文檔給出的根因指向調(diào)度器內(nèi)部的一個狀態(tài)所有權(quán)問題The schedulersadmitAndSettleWavekept the waves attached settlement map local to the wave. After B failed, its settlement was removed.dag sendrevived B throughnode-send.ts, which armed a separate watcher but did not register Bs new settlement in the active wave map. When A settled, the wave appeared empty andrunWavesemitteddag.run.completeddespite B running and C pending.拆解來看問題由三個環(huán)節(jié)疊加而成結(jié)算表是波次局部的調(diào)度器把已掛載任務的結(jié)算settlement映射放在波次內(nèi)部。B 失敗后其結(jié)算從波次映射中被移除。復活沒有回到主結(jié)算循環(huán)dag send復活 B 走的是 node-send.ts 的sendToDagNode路徑它為復活的子任務單獨掛了一個 watcherwatchRevivedTask但沒有把 B 的新結(jié)算注冊進活躍波次的結(jié)算映射??詹ù斡|發(fā)誤終態(tài)A 結(jié)算后波次內(nèi)部結(jié)算集合已空調(diào)度器據(jù)此判定沒有活躍任務進而發(fā)出dag.run.completed——盡管 B 正在運行、C 尚在等待。值得注意的是事故文檔中提到的admitAndSettleWave/runWaves這些命名在當前 scheduler.ts 中已被**依賴前沿dependency-frontier**執(zhí)行循環(huán)取代runFrontier不再以波次為執(zhí)行屏障編譯出的 wave 僅作為信息性事件分組存在見emitWaveAdmissions/emitCompletedWavesscheduler.ts。這正是本次修復重構(gòu)的底層背景——波次已不再是執(zhí)行單元卻仍在結(jié)算歸屬上殘留著波次局部的舊語義才讓空波次鉆了空子。修復方案調(diào)度器生命周期共享的結(jié)算注冊表與終態(tài)斷言修復的核心是把結(jié)算注冊表的所有權(quán)從波次提升到調(diào)度器本身。當前源碼中的SchedulerContext持有兩個貫穿調(diào)度器生命周期的映射// scheduler.ts type SchedulerContext { ... readonly attachedTaskIds: MapDagNodeId, string readonly attachedTasks: MapDagNodeId, AttachedTask readonly settlementChanged: () Promisevoid readonly resolveSettlementChanged: () void ... }attachedTasks記錄每個已掛載任務節(jié)點的結(jié)算 Promisesettled與折疊完成信號folded其生命周期等于調(diào)度器實例本身而不是某個波次。關(guān)鍵路徑如下1. 復活結(jié)算重新進入共享注冊表sendToNode在投遞 revive 時通過watchRevived回調(diào)判斷當前 run 是否仍處于runningscheduler.tssendToNode: (runId, nodeId, message) sendToDagNode( options, runId, nodeId, message, (revivedNodeId, taskId) context.journal.refresh().status running ? watchRevivedInScheduler(context, revivedNodeId, taskId) : undefined, ),若 run 仍在運行則走watchRevivedInSchedulerscheduler.ts——它把復活的 taskId 寫回共享的attachedTaskIds/attachedTasks并等待結(jié)算被主循環(huán)折疊async function watchRevivedInScheduler( context: SchedulerContext, nodeId: DagNodeId, taskId: string, ): Promisevoid { context.attachedTaskIds.set(nodeId, taskId) const task attachTaskSettlement(context, nodeId, taskId) await task.folded }attachTaskSettlementscheduler.ts在注冊結(jié)算的同時調(diào)用context.resolveSettlementChanged()——這正好喚醒了settleOne中等待的波次等待者wave waiter使主結(jié)算循環(huán)立刻重新感知到 B 的存在。2. 復活結(jié)果在折疊完成后才 resolvesettleOnescheduler.ts等待attachedTasks中任一結(jié)算落地并在finally中調(diào)用task?.resolveFolded()。因此sendToDagNode返回的settledPromise 只有在調(diào)度器真正把復活節(jié)點的新終態(tài)折疊foldTaskOutcome之后才 resolve——調(diào)用方可以據(jù)此知道復活結(jié)果已被 run 主循環(huán)吸收。3. 終態(tài)發(fā)射前的全節(jié)點終態(tài)斷言runFrontier的主循環(huán)scheduler.ts在發(fā)射dag.run.completed/dag.run.failed之前先要求所有節(jié)點處于終態(tài)const current context.journal.snapshot() if (current.nodes.every((node) TERMINAL_NODE_STATES.has(node.state))) break if (context.attachedTasks.size 0) { if (hasCascadableDependent(current)) continue throw new Error(DAG run ${current.runId} cannot terminalize while nodes are active) } if (!await settleOne(context)) return cancelledSnapshot(context)其中TERMINAL_NODE_STATES定義為completed / failed / cancelled / skipped四態(tài)scheduler.ts。換句話說只要還有節(jié)點非終態(tài)B 復活后 running、C pending循環(huán)就不會 break自然也不會進入終態(tài)事件發(fā)射即使出現(xiàn)無掛載任務但節(jié)點仍活躍的反常局面也會直接拋錯而不是靜默完成。4. 已終態(tài) run 的復活保留獨立 watcher若 run 已經(jīng)進入終態(tài)completed/cancelled后仍有人對節(jié)點sendwatchRevived回調(diào)返回undefinedsendToDagNode會退回到 node-send.ts 的watchRevivedTask獨立 watcher——它通過自己的 control journal 折疊復活結(jié)果并持久化產(chǎn)物與主結(jié)算循環(huán)解耦。此外 node-send.ts 對已completed/cancelled的 run 直接拋出node_not_continuable只有failed狀態(tài)的 run 保留可 revive 語義復活正是失敗節(jié)點的文檔化補救手段?;貧w測試A/B/C 場景的完整復現(xiàn)修復對應的回歸用例位于 e2e-failure.test.ts測試名直接標注了事故編號test(#given a live wave with one failed node revived before its sibling settles \ #when the sibling completes #then the run awaits the revival and schedules their dependent, async () { // given - mirrors dag_2d12c2f7: A and B share wave 0; C depends on both. ... })測試路徑與事故完全一致A、B 同屬 wave 0C 依賴 A、B——B 先失敗runner.settle(B, failed(B))再通過與生產(chǎn)相同的sendToNode路徑復活fixture.send(runId, B, ...)斷言sent.delivery revive隨后 A 完成此時必須滿足run 狀態(tài)仍為running修復前此處斷言失敗事件流中不存在任何dag.run.completed/dag.run.failed終態(tài)事件。接著 B 完成runner.settle(B, completed(B-revived))、await sent.settled確認復活已被折疊、C 被調(diào)度并完成最后才允許出現(xiàn)且僅出現(xiàn)一次dag.run.completed終態(tài)節(jié)點列表嚴格為A:completed / B:completed / C:completed。同文件的第二個用例 L1109-L1141 覆蓋run 已失敗后復活駐留子任務的獨立 watcher 路徑failedResult.status failed時仍可 revive復活任務完成只折疊一次completed轉(zhuǎn)換且結(jié)果產(chǎn)物落盤output:stuck-revived。驗證流程RED → GREEN 與全套證據(jù)事故文檔將整個修復過程固化為標準的 TDD 證據(jù)鏈全部捕獲在 .omo/evidence/20260821-dag-revive-terminalization/ 目錄下文件階段內(nèi)容red.txt修復前聚焦回歸測試按預期失敗green.txt修復后聚焦回歸測試 17 pass / 0 failfocused-final.txt聚焦scheduler 專項測試全部通過package-suite-final.txt全量senpi-task 包級全套測試通過typecheck-final.txt類型檢查tsgo --noEmit -p tsconfig.json無錯誤incident-events.jsonl事故證據(jù)事故 run 的完整事件日志incident-run.json事故證據(jù)事故時刻的檢查點快照RED 階段的關(guān)鍵失敗信息位于 red.txt 中error: expect(received).toBe(expected) Expected: running Received: completed at e2e-failure.test.ts:1078:67 (fail) #given a live wave with one failed node revived before its sibling settles ... 16 pass 1 fail即修復前A 完成后 run 狀態(tài)被錯誤置為completed修復后同一斷言通過且終態(tài)事件在全 run 中只出現(xiàn)一次。驗證命令為在倉庫根目錄運行bun test packages/senpi-task/src/dag/e2e-failure.test.ts聚焦、完整包套件bun test對應文件以及tsgo --noEmit -p tsconfig.json類型檢查。相關(guān)源碼與事件模型索引調(diào)度器主循環(huán)與結(jié)算注冊表packages/senpi-task/src/dag/scheduler.tsrunFrontierL512、settleOneL839、attachTaskSettlementL809、watchRevivedInSchedulerL825、終態(tài)集合 L47復活/投遞路徑packages/senpi-task/src/dag/node-send.tssendToDagNodeL32、revive 折疊 L102、獨立 watcherwatchRevivedTaskL164回歸測試packages/senpi-task/src/dag/e2e-failure.test.tsL1052、L1109事件構(gòu)造器packages/senpi-task/src/dag/events.tsdag.run.completedL75、dag.node.steeredL168節(jié)點/狀態(tài)類型packages/senpi-task/src/dag/types.tsDagNodeState、DagNodeTransitionReason事故證據(jù)目錄.omo/evidence/20260821-dag-revive-terminalization/RED/GREEN 輸出、事件日志與快照從源碼結(jié)構(gòu)看本次事故的價值在于揭示了 DAG 調(diào)度器的一條不變式run 的終態(tài)必須以全部節(jié)點的終態(tài)為準而不是以當前波次是否還有已注冊結(jié)算為準。復活revive作為失敗節(jié)點的事實補救手段必須被納入主結(jié)算循環(huán)的可見范圍否則任何局部結(jié)算表 外部 watcher的組合都可能讓 run 在仍有活躍節(jié)點的前提下被誤判完成。這一修復同時為dag.send的調(diào)用方明確了語義返回的settledPromise 即復活結(jié)果已被 run 吸收的確認信號?!久赓M下載鏈接】oh-my-openagentOmO: Just type mass ulw keyword with your prompt. Now you are the master of graph engineering.項目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考