現場工作最怕的,不是第一次失敗。
最怕的是,第一次失敗後沒人知道下一步該怎麼辦。
你可能看過這種狀況:
這時候系統要做的就不是「裝沒事」,而是要有一套撐住現場的方法:
第 16 天我想講的就是這件事:
重試、回退、補救,系統怎麼撐住現場?
如果第 15 天是在分辨錯誤類型,這一天就是在看 OpenClaw 怎麼真的把那個錯誤現場穩住。
failed-retryable 到底怎麼往下接?OpenClaw 的現場處理,不是單一 retry loop。
它其實是四層一起工作:
這四層組起來,才像真的在撐現場。
因為現場錯誤通常不是乾淨的:
OpenClaw 不是假設這些都不會發生,而是直接把它們當常態來設計。
先看 failed-retryable 這個狀態在系統裡怎麼被用。
📄 原始碼:
extensions/telegram/src/telegram-ingress-spool.ts(本機版本行號已變動,僅標到檔案)
const deliveryFailureWithoutFinalResponse = !finalAnswerDelivered && (deliverySummary.skippedNonSilent > 0 || deliverySummary.failedNonSilent > 0);
const retryableDispatchFailure = dispatchError ?? (deliveryFailureWithoutFinalResponse ? new Error(`Telegram reply delivery failed without a final response (failed=${deliverySummary.failedNonSilent}, skipped=${deliverySummary.skippedNonSilent})`) : null);
if (retryableDispatchFailure && retryDispatchErrors && (dispatchError != null && !hasFinalResponse || dispatchError == null && deliveryFailureWithoutFinalResponse && !hasVisibleResponse)) return {
kind: "failed-retryable",
error: retryableDispatchFailure
};
這段很重要,因為它告訴我們:
再看 retry 進到哪裡。
📄 原始碼:
extensions/telegram/src/bot-processing-outcome.ts:59-59
if (result.kind === "failed-retryable") {
...
}
這種結果會被上層當成「不是完全死掉」。
也就是說,系統收到的不是單純 error,而是一種可接續處理的結果。
這點很關鍵,因為它會影響後面的 abort、queue、以及後續重送策略。
再看 backoff。
📄 原始碼:
extensions/telegram/src/bot-handlers.callback-interactions.runtime.ts:236-236
const retryDelayMs = TELEGRAM_PLUGIN_CALLBACK_SUBMIT_RETRY_DELAYS_MS[attempt];
if (!isReplySessionInitConflictResult(result) || retryDelayMs === void 0) throw new TelegramRetryableCallbackError(result.error);
logVerbose(`telegram plugin callback submitText hit active reply session; retrying in ${retryDelayMs}ms`);
await sleepWithAbort(retryDelayMs, spooledReplayParticipant?.abortSignal);
這段不是 OpenClaw 核心 agent loop 本身,但它很能代表整個系統的 retry 精神。
當衝突是暫時性的,它不會立刻狂打第二槍,而是先等一下:
這就是現場撐住的關鍵。
不是硬上,而是有節奏地重來。
再看另一個典型的 retry / fallback 結合:cron 投遞失敗。
📄 原始碼:
src/agents/subagent-announce-delivery.ts:602-613
async function retryTransientDirectCronDelivery(params) {
const retryDelaysMs = resolveDirectCronRetryDelaysMs();
for (const [retryIndex, delayMs] of retryDelaysMs.entries()) {
const nextAttempt = retryIndex + 2;
const maxAttempts = retryDelaysMs.length + 1;
await logCronDeliveryWarn(`[cron:${params.jobId}] transient direct announce delivery failure, retrying ${nextAttempt}/${maxAttempts} in ${Math.round(delayMs / 1e3)}s: ${summarizeDirectCronDeliveryError(err)}`);
}
}
這裡很像實際現場救火:
再看更底層的 retry 保護,像 Telegram replay 裡面對重試的處理。
📄 原始碼:
extensions/telegram/src/bot-message.ts:438-457
while (!turnAbortSignal.aborted) {
retryAttempt += 1;
...
const delayMs = resolveSpooledUpdatePersistenceRetryDelayMs(retryAttempt);
runtime.error?.(danger(`telegram spooled turn durable replay protection retry ${retryAttempt} failed after active steer commit; retrying in ${delayMs}ms: ${String(retryError)}`));
}
這段的味道很明顯:
這就是有節制的 retry。
再看 cleanup。
📄 原始碼:
src/agents/subagent-spawn.ts:1324-1324
async function cleanupFailedSpawnBeforeAgentStart(params) {
await cleanupProvisionalSession(params.childSessionKey, {
emitLifecycleHooks: false,
deleteTranscript: true
});
}
這段很短,但它代表一個很實際的原則:
失敗不是只要回報,還要把現場收掉。
如果子代理還沒正式開始就失敗了,那就不要留下半截 session。
該刪 transcript、該清 provisional 狀態,就要先做。
再看另一個 cleanup 路徑。
📄 原始碼:
src/agents/openclaw-tools.ts(本機版本行號已變動,僅標到檔案)
async function cleanupProvisionalSession(childSessionKey, options) {
...
}
OpenClaw 對 provisional state 很小心。
因為一旦 provisional 沒清乾淨,下一輪就會接到莫名其妙的殘影。
最後看 recovery。
📄 原始碼:
extensions/telegram/src/telegram-ingress-spool.ts:453-453
async function recoverStaleTelegramSpooledUpdateClaims(params) {
return await createTelegramIngressQueue(params.spoolDir).recoverStaleClaims({
...
});
}
這說明系統會主動去找那些失聯的 claim,試著恢復。
這不是補鍋,是把掉在地上的工作撿回來。
很多人把 retry 想得太簡單。
以為就是:
失敗了,再執行一次。
但 OpenClaw 的 retry 比這更像「重新進場」。
因為它還要確認:
所以 retry 其實是有前置條件的。
如果前置條件不對,硬 retry 只會讓錯誤複製一次。
當一個問題剛發生時,立刻重試常常沒用。
原因很簡單:
所以 OpenClaw 用 backoff 的概念,讓 retry 先退一步。
這很像你去敲門沒人回,不是立刻連敲十下,而是先等兩秒、五秒、十秒。
不是因為你懶,是因為現場本來就需要時間恢復。
很多系統失敗後最容易忘記的事,就是收尾。
但如果不 cleanup:
所以 OpenClaw 在失敗前後都會很重視 cleanup。
你可以把它想成:
有些問題不是 retry 可以解的。
例如:
這時候就要 recovery。
Recovery 的精神不是執行新的流程,而是把還沒完成的舊流程接回來。
這很像收善後,不是重做整份。
很多人把 fallback 誤會成「失敗了,只好隨便回一個」。
其實不是。
OpenClaw 的 fallback 比較像:
這種保底很重要,因為使用者最怕的是:
fallback 至少能保住可觀察性。
OpenClaw 不把修復押在單一點上。
它會分層處理:
這樣才像真的在撐系統。
如果只有 retry,系統很容易一直撞牆。
如果只有 fallback,系統很容易變成永遠不解決問題。
如果只有 cleanup,系統可能會很乾淨,但沒有把任務救回來。
所以 OpenClaw 的重點是:把每一層都放進來,然後按情況接力。
如果換成最簡單的做法,就是失敗就報錯,報錯就結束。
那種系統很乾脆,但一遇到現場波動就會很脆。
OpenClaw 選的是另一條路:
失敗可以發生,但系統不能亂掉。
前面這幾天一直在看系統怎麼撐住失敗。
下一步我想換一個角度,看系統怎麼主動把工作排進來:
cron是怎麼被 OpenClaw 掛進來的?
因為一個系統不只要會救火,也要會自己定時出手。