Skip to content

bin/codex-call 把 SSE error 事件訊息吞成籠統 "Codex error",200-stream 內的失敗無法區分原因 #25

Description

@kiki830621

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 印出的全部資訊只有:

error: Codex error

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

錯誤訊息應帶出後端給的 codemessage,例如:

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

  • (none)

Commits

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions