這個 bug 平常測試時很難發現,為什麼?
因為它是純粹的時機競態問題——只有當背景子任務「剛好」在主結果送達的那個瞬間完成,才會觸發。平常測試如果背景任務跑得比主結果快很多、或是慢很多,都不會踩到這個窗口期,連線關閉的時機跟背景任務完成的時機錯開,一切正常。這也是為什麼這類 bug 常常被描述成「間歇性」、「無法穩定重現」——不是測試不夠仔細,而是問題本身的觸發條件就是一個很窄的時間窗口,大量平行跑背景任務的生產環境反而更容易撞到這個窗口,比開發環境更容易出現。
CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS 設太長或太短,各自會有什麼後果?
設太短(例如幾秒鐘),等於幾乎沒有拉長等待時間的效果——如果背景任務本來就需要幾分鐘才能完成,太短的上限會讓 SDK 在任務還沒真正結束前就強制往下走,等於繞回了修復之前那種「提早關閉」的風險,只是觸發條件從「競態巧合」變成「上限設太短的必然結果」。設太長(例如拉到幾小時),則是在背景任務真的卡死、永遠不會回報 idle 的情境下,讓連線懸掛的時間拉得更長,使用者或呼叫端要等更久才會發現異常。
比較務實的做法是先觀察你實際的背景任務在正常情況下大概需要多久完成,抓一個略高於這個時間的緩衝值,而不是直接沿用預設的 10 分鐘或隨意設一個很大的數字。
如果我的部署環境 CLI 版本比較舊、不支援 session_state_changed,升級 SDK 還有意義嗎?
有意義,但效益會打折扣。SDK 這邊會自動偵測 CLI 是否有發出 session_state_changed 事件,沒有的話就退回舊版的「第一個結果送達即關閉」邏輯——也就是說,升級 SDK 本身不會觸發任何錯誤或相容性問題,但這個特定 bug 的修復效果不會生效,你仍然可能遇到原本的競態條件。
真正要吃到這個修復的好處,升級 SDK 跟升級 CLI 到支援 session_state_changed 的版本是兩件都要做的事,只做其中一項不會完整解決問題。
這個修復只對有用 hooks、can_use_tool、SDK MCP server 的情境有效嗎?沒用這些功能的應用需要關心這個 bug 嗎?
changelog 明確點出這個 bug 是在「使用 query() 搭配 hooks、can_use_tool、或 SDK MCP server」的情境下出現,這代表觸發條件跟背景子任務的生命週期管理有關,而這幾項功能都涉及額外的、可能跑在背景的處理流程。如果你的應用完全沒有用到背景子任務的機制(例如單純的同步問答、沒有並行工具呼叫或背景驗證流程),理論上不會踩到這個特定的競態條件,但升級 SDK 本身不會有副作用,作為例行更新還是合理的做法。
用 Claude Agent SDK 的 query() 搭配 hooks、can_use_tool 回呼、或是 SDK 內建的 MCP server 時,有一種特定情境曾經會穩定觸發連線中斷:如果一個背景子任務(background subagent)剛好在主對話輪次的結果即將送達的那個時間點完成,SDK 舊版的邏輯會把 stdin 關閉得太早。結果是接下來的那一輪對話會直接失敗,噴出「Stream closed」的錯誤,而且模型端看到的現象,會被誤判成「這個工具呼叫被拒絕了」——但實際上工具呼叫本身沒有問題,問題出在底層連線的生命週期管理時機不對。
修這個 bug 之前,SDK 判斷「這一輪可以關閉 stdin 了」的邏輯,是以「收到第一個 result 訊息」為準——這在大部分情境下沒問題,但當背景子任務剛好卡在主結果送達的瞬間完成,就會出現競態條件(race condition):SDK 以為這輪結束了,提早關閉連線,但 CLI 端其實還在處理背景子任務收尾的工作,後續需要靠這個連線才能繼續溝通。新版修法改成監聽 CLI 主動發出的 session_state_changed 訊息,等到 CLI 明確回報狀態是 idle(真正閒置、沒有任何背景工作在跑)才關閉 stdin,而不是用「收到第一個結果」這種間接的訊號去猜測。
只改成「等 CLI 回報 idle 才關」,理論上解決了提早關閉的問題,但也帶來新的風險:如果某個背景任務因為某種原因卡住、永遠不會回報 idle,連線就會無限期懸著,造成另一種形式的異常。新版加入了 CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS 這個環境變數,預設值為 10 分鐘,作為等待 idle 狀態的上限——超過這個時間,即使背景任務還沒回報完成,SDK 也會強制往下走,避免無限期懸掛。對於背景任務確實需要跑超過 10 分鐘的場景(例如長時間的資料處理任務),可以透過這個環境變數自行拉長上限。
這個修法依賴 CLI 主動發出 session_state_changed 訊息,但不是每個版本的 CLI 都有實作這個機制。對於沒有這個狀態事件的舊版 CLI,SDK 會自動退回舊的行為模式——以第一個結果送達作為關閉訊號。這代表這個 bug 修復的效果,實際上取決於你部署環境裡 CLI 的版本:如果 CLI 版本太舊,即使 SDK 更新了,背後用的仍然是舊的、有競態條件風險的邏輯。
如果你的應用大量使用背景子任務(例如並行處理多個子查詢、或是讓某個 subagent 在背景跑一個耗時的驗證流程),這類「Stream closed」加上「工具呼叫被誤判拒絕」的錯誤,過去可能被你的團隊當成隨機、難以重現的不穩定問題處理——現在知道了根因,升級 SDK 到含這個修復的版本,同時確認部署的 CLI 版本支援 session_state_changed,才能真正吃到這個修復的效益。如果你的背景任務常態性需要跑超過 10 分鐘,記得同步設定 CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS,否則預設的 10 分鐘上限可能會在任務還沒跑完時就把連線強制往下推進。