Problem
bin/codex-call 在後端回 SSE error 事件時,把錯誤訊息吞成籠統的 Codex error,導致所有 Codex 失敗都長成同一個字串、無法區分原因。
實測(2026-07-30)繞過 wrapper 直接抓 raw SSE(Server-Sent Events,HTTP streaming 協定),後端實際回的是:
{"type":"error","error":{"type":"service_unavailable_error","code":"server_is_overloaded","message":"Our servers are currently overloaded. Please try again later.","param":null},"sequence_number":2}
而 codex-call 印出的全部資訊只有:
HTTP 是 200、SSE stream 正常建立、response.created / response.in_progress 都收到了,是在生成階段才被後端拒絕。
Type
bug
Root cause
兩個獨立缺陷疊加。
(1) 提取鏈漏了頂層 error 物件那條路徑(plugins/parallel-ai-agents/bin/codex-call line 255-258):
case "error", "response.failed":
let msg = (json["message"] as? String)
?? ((json["response"] as? [String: Any])?["error"] as? [String: Any])?["message"] as? String
?? "Codex error"
實際 payload 把 message 放在 json["error"]["message"],三條路徑無一命中:
json["message"] → nil(message 不在頂層,在 error 物件內)
json["response"]["error"]["message"] → nil(type: "error" 的 payload 沒有 response 鍵)
- → 落到 fallback
"Codex error"
(2) 第二條提取路徑實質是死碼(同檔 line 220-225):
while let range = buffer.range(of: "\n\n") {
...
processEvent(event)
if streamError != nil { return }
}
後端會先送 type: "error"、緊接著送 type: "response.failed",而後者確實含 response.error.message(實測 payload 已確認)。但第一個事件已把 streamError 設成 "Codex error",這個 early return 讓第二個事件永遠不會進入 processEvent —— 所以提取鏈的第二條路徑從未有機會生效。
Expected
錯誤訊息應帶出後端給的 code 與 message,例如:
error: stream error [server_is_overloaded]: Our servers are currently overloaded. Please try again later.
Actual
一律是 error: Codex error,不論根本原因為何。
Impact
實測(2026-07-30)確認影響範圍限於「HTTP 200 stream 內以 SSE error 事件回報的錯誤」:
| 失敗類別 |
後端回應形式 |
受本 bug 影響? |
server_is_overloaded(後端壅塞) |
HTTP 200 + SSE type: "error" |
是 — 訊息被吞成 Codex error |
| model 不可用 |
HTTP 400(實測) |
否 — 走 line 229-232,正確印 HTTP 400: {"detail":...} |
| auth / token 失效 |
HTTP 401(實測) |
否 — 同上 |
| rate limit |
推測 HTTP 429(未實測) |
推測否 |
本 issue 初稿宣稱「所有 Codex 失敗無法區分」,該宣稱已被實測推翻 —— HTTP status code 類的錯誤本來就走 codex-call 的 HTTP 錯誤路徑(line 229-232),會正確印出 code + body。
修正後的實質影響仍然成立:
- 後端壅塞是最常遇到的暫時性失敗(本 issue 觸發情境即是連續三次),而它恰好落在受影響的那一類
- 這類錯誤正是「該重試還是該停手」最需要資訊的場合 —— 被吞成
Codex error 後只能靠猜
- 200-stream 內的錯誤不只壅塞一種(quota / content filter 等可能同形),全部共用這條被吞的路徑
- 修法一行,成本極低
Severity 由「所有失敗」下修為「200-stream 內的失敗」,不影響 Fix 的必要性。
Fix
提取鏈補上頂層 error 物件那條路徑:
let msg = (json["message"] as? String)
?? ((json["error"] as? [String: Any])?["message"] as? String) // ← 新增
?? ((json["response"] as? [String: Any])?["error"] as? [String: Any])?["message"] as? String
?? "Codex error"
兩個可一併考慮的延伸(非本 issue 承諾):
- 把後端的
error.code 一起帶進訊息或 exit code,讓 caller 能機械判斷可否重試,而不是靠人讀字串
- 檢視 line 224 的 early return:是否該讓後續
response.failed 有機會覆寫成更完整的訊息,而非讓第一個較貧乏的事件定案
Clarity Surface(idd-clarify run 2026-07-30T08:28:17Z)
| Type |
Source |
Suggested canonical |
Status |
| ambiguity |
"繞過 wrapper 直接抓 raw SSE" / "SSE stream 正常建立"(全文多處) |
SSE 未 expand — 首次出現處補 Server-Sent Events;本 issue 讀者需知這是 HTTP streaming 協定而非 OpenAI 專有名詞 |
resolved @ 2026-07-30T14:55Z(已於 Problem 段首次出現處補 expand) |
| missing-context |
"以下失敗需要完全不同的應對,但目前無法區分" + 四列 code 類別表 |
僅 server_is_overloaded 經實測;rate limit / auth-token / model-unavailable 三類的實際 payload 形狀未驗證。若那些走 HTTP 4xx 而非 SSE type: "error" 事件,則落在 line 229-232 的 HTTP 錯誤路徑(會印 HTTP <code>: <body>)、不受本 bug 影響 — 此時 Impact 範圍被高估,Fix 的優先級也要重估 |
resolved @ 2026-07-30T14:55Z(實測:invalid-model→HTTP 400、invalid-token→HTTP 401,皆走 HTTP 路徑不受影響;Impact 已據此改寫、範圍下修為 200-stream 內錯誤) |
Current Status
Phase: closed
Last updated: 2026-08-01 by idd-close
Key Decisions
Scope Changes
- 初稿宣稱「所有 Codex 失敗無法區分」→ 經實測推翻並改寫(HTTP 4xx 類不受影響)
- R1–R3 曾把終端事件政策綁進來,R4 依 owner 裁決收斂移出
Blocking
Commits
Problem
bin/codex-call在後端回 SSEerror事件時,把錯誤訊息吞成籠統的Codex error,導致所有 Codex 失敗都長成同一個字串、無法區分原因。實測(2026-07-30)繞過 wrapper 直接抓 raw SSE(Server-Sent Events,HTTP streaming 協定),後端實際回的是:
而
codex-call印出的全部資訊只有:HTTP 是 200、SSE stream 正常建立、
response.created/response.in_progress都收到了,是在生成階段才被後端拒絕。Type
bug
Root cause
兩個獨立缺陷疊加。
(1) 提取鏈漏了頂層
error物件那條路徑(plugins/parallel-ai-agents/bin/codex-callline 255-258):實際 payload 把 message 放在
json["error"]["message"],三條路徑無一命中:json["message"]→ nil(message 不在頂層,在error物件內)json["response"]["error"]["message"]→ nil(type: "error"的 payload 沒有response鍵)"Codex error"(2) 第二條提取路徑實質是死碼(同檔 line 220-225):
後端會先送
type: "error"、緊接著送type: "response.failed",而後者確實含response.error.message(實測 payload 已確認)。但第一個事件已把streamError設成"Codex error",這個 early return 讓第二個事件永遠不會進入processEvent—— 所以提取鏈的第二條路徑從未有機會生效。Expected
錯誤訊息應帶出後端給的
code與message,例如:Actual
一律是
error: Codex error,不論根本原因為何。Impact
實測(2026-07-30)確認影響範圍限於「HTTP 200 stream 內以 SSE
error事件回報的錯誤」:server_is_overloaded(後端壅塞)type: "error"Codex errorHTTP 400: {"detail":...}本 issue 初稿宣稱「所有 Codex 失敗無法區分」,該宣稱已被實測推翻 —— HTTP status code 類的錯誤本來就走
codex-call的 HTTP 錯誤路徑(line 229-232),會正確印出 code + body。修正後的實質影響仍然成立:
Codex error後只能靠猜Severity 由「所有失敗」下修為「200-stream 內的失敗」,不影響 Fix 的必要性。
Fix
提取鏈補上頂層
error物件那條路徑:兩個可一併考慮的延伸(非本 issue 承諾):
error.code一起帶進訊息或 exit code,讓 caller 能機械判斷可否重試,而不是靠人讀字串response.failed有機會覆寫成更完整的訊息,而非讓第一個較貧乏的事件定案Clarity Surface(idd-clarify run 2026-07-30T08:28:17Z)
SSE未 expand — 首次出現處補 Server-Sent Events;本 issue 讀者需知這是 HTTP streaming 協定而非 OpenAI 專有名詞server_is_overloaded經實測;rate limit / auth-token / model-unavailable 三類的實際 payload 形狀未驗證。若那些走 HTTP 4xx 而非 SSEtype: "error"事件,則落在 line 229-232 的 HTTP 錯誤路徑(會印HTTP <code>: <body>)、不受本 bug 影響 — 此時 Impact 範圍被高估,Fix 的優先級也要重估Current Status
Phase: closed
Last updated: 2026-08-01 by idd-close
Key Decisions
e87f1df的根因記錄已發 errata 更正(真正變數是 bash 3.2 vs 5.x,非 bats 版本)Scope Changes
Blocking
Commits
968b8832875d87ab1d266(撤)b6fbb913506f7caabbb9ee87f1df970a27c4f110d0