这篇不是转述 Issue,而是独立复现。测试使用本地 loopback HTTP SSE、独立 CODEX_HOME、800ms idle timeout、零重试,不调用模型和外部 API。
Codex CLI 为什么把 response.failed 变成 idle timeout waiting for SSE?0.153.4/0.154.0 复现
如果 Codex CLI 最后报的是:
text
stream disconnected before completion: idle timeout waiting for SSE
不要立刻把它理解成"服务端一直没有返回任何东西"。在 Codex CLI 0.153.4 和 0.154.0 上,我都独立复现了另一种情况:服务端已经通过 SSE 明确发送了 terminal response.failed,其中包含真实 server_error,但因为底层 socket 没有立即关闭,Codex 继续等待 EOF,直到 stream idle timeout 触发,最终把原始错误覆盖成了 idle timeout waiting for SSE。
这个区别很重要。生产环境里如果只保存 Codex 最后一条错误,你可能会把真正的 provider failure、容量拒绝或服务端错误误判成普通网络 idle timeout,然后去调 timeout、重试次数或代理配置,却没有处理最先到达的真实错误。
XBSTACK 在 2026 年 9 月 12 日用一个完全本地的 loopback HTTP SSE fixture 做了两组控制实验,不调用任何模型、不使用 OpenAI/Azure API Key,也不依赖外部网络。0.153.4 和当前稳定版 0.154.0 结果一致。
结论先说:0.154.0 仍然可以复现
我们的测试矩阵如下:
| Codex CLI | 服务端行为 | 原始错误保留 | 是否出现 idle timeout |
|---|---|---|---|
0.153.4 |
response.failed 后立即 EOF |
是 | 否 |
0.153.4 |
response.failed 后 socket 保持打开 |
否 | 是 |
0.154.0 |
response.failed 后立即 EOF |
是 | 否 |
0.154.0 |
response.failed 后 socket 保持打开 |
否 | 是 |
0.153.4 的 open-socket 组耗时约 1.08 秒,0.154.0 约 1.12 秒。这里不是在做性能测试,因为 fixture 故意把 stream_idle_timeout_ms 缩短到 800ms,只是为了让行为快速出现。
真正需要看的是错误内容。
EOF 组:
text
stream disconnected before completion: XBSTACK_LOCAL_TERMINAL_FAILURE
open-socket 组:
text
stream disconnected before completion: idle timeout waiting for SSE
两组服务端发送的是同一个 response.failed。唯一差别是terminal event 发出后是否立即关闭连接。
这次实验到底复现了什么
fixture 返回:
text
HTTP/1.1 200 OK
Content-Type: text/event-stream
然后发送一个 terminal failure:
text
event: response.failed
data: {
"type": "response.failed",
"response": {
"status": "failed",
"error": {
"code": "server_error",
"message": "XBSTACK_LOCAL_TERMINAL_FAILURE"
}
}
}
第一组发送后马上关闭 socket;第二组发送完全相同的 event,但保持 socket 打开。
从协议语义看,response.failed 已经告诉客户端这个 response 进入 failed terminal state。客户端至少已经有足够信息把这次执行判定为失败。问题在于当前受影响路径没有在这里结束 SSE consumption,而是把错误暂存后继续等 transport EOF。
于是出现了这条链:
text
HTTP 200
↓
SSE response.failed
↓
Codex 已解析真实 server_error
↓
连接仍然保持打开
↓
继续等待 EOF
↓
stream idle timeout
↓
用户最终看到 idle timeout waiting for SSE
这不是"服务端什么都没返回",而是服务端已经失败,客户端随后又被 transport timeout 覆盖了诊断信息。
为什么这个问题容易误导排障
假设真实 provider 返回的是:
text
server_error: backend capacity unavailable
但 socket 没有立即 EOF,Codex 最后只显示:
text
idle timeout waiting for SSE
如果只看最后一条,你很容易做这些动作:
- 把
stream_idle_timeout_ms从 5 分钟调成 10 分钟; - 增加
stream_max_retries; - 怀疑代理、DNS、TLS 或本地网络;
- 重新安装 Codex;
- 降低 context 或 output limit。
这些操作可能全部绕开了真正的 first failure。
调大 idle timeout 不是修复。 它只会把错误被覆盖的时间推迟。调小也不是修复,只会更早得到相同的 timeout 文案。
如何快速判断自己是不是这个 failure shape
生产日志里至少同时保留四层信息:
- HTTP status;
- SSE event type;
response.failed.response.error;- 最终 Codex CLI error。
如果你看到类似:
text
HTTP 200
response.failed: server_error / <real message>
... socket remains open ...
Codex: idle timeout waiting for SSE
就高度符合本文复现路径。
但如果根本没有收到 response.failed,只有 socket 一直没有数据,那么那是另一类真正的 idle timeout,不能套本文结论。
本地最小复现
XBSTACK 的公开 Repro 不依赖任何模型或 API 账号。核心只有一个 Python 标准库 HTTP server 和一个临时 CODEX_HOME。
执行:
bash
python3 repro.py \
--codex /Applications/ChatGPT.app/Contents/Resources/codex \
--output logs/result.json
fixture 会自动跑:
text
Case A: response.failed -> EOF
Case B: response.failed -> keep socket open
并检查:
json
{
"original_error_preserved": true,
"idle_timeout_reported": false
}
和:
json
{
"original_error_preserved": false,
"idle_timeout_reported": true
}
为了避免污染真实 Codex 配置,每个 case 都新建临时 CODEX_HOME,并把 retry 设为 0。
为什么我又补测了 0.154.0
上游 Issue #43140 最初针对的是 0.153.4,但 Codex 在 9 月 9 日已经发布 0.154.0。如果今天只重复 0.153.4,会有一个很明显的问题:用户不知道升级是否已经解决。
所以我额外下载 OpenAI Codex GitHub 官方稳定版 rust-v0.154.0 的 Apple Silicon 二进制,使用完全同一份 fixture 再跑一遍。
结果没有变化。
因此截至 2026 年 9 月 12 日,至少在本文控制条件下:
升级到 0.154.0 不能作为这个问题的已验证解决方案。
未来 0.155.x 或后续稳定版是否修复,需要重新跑 version matrix,不能从 main branch 或 alpha 版本推断稳定版行为。
上游 Issue 已经有 proposed patch,但它还不是正式修复
Issue #43140 给出了源码层分析和一个候选 patch:核心思路是在已经识别出 response.failed terminal event 后直接结束 producer,而不是继续等待 SSE EOF。
这个方向与本文控制实验高度一致,但这里必须区分三件事:
- upstream reporter 的 patch 已经本地测试;
- XBSTACK 独立确认了 bug 行为;
- OpenAI 是否已经 merge/release official fix。
目前我们只确认前两项。Issue 截至 9 月 12 日仍为 Open,因此本文不会写"升级到某个正式版本即可修复"。
生产环境现在应该怎么处理
1. 不要只保存最终 timeout 文案
如果使用自定义 provider、Azure OpenAI 或代理层,优先保留原始 SSE event。真正有诊断价值的通常是最先到达的 terminal failure,而不是几分钟之后的 timeout。
2. 把 provider request id 一起保存
如果服务端错误里有 request id、region、capacity admission、retry-after 或其他错误分类,必须和 Codex run id 放在同一条 trace 里。
3. 不要把所有 reconnect 都归因于这个 bug
本文证明的是:
text
terminal response.failed + socket hold-open
这条路径可能覆盖原始错误。
它并没有证明:
text
所有 server_error
所有 no healthy upstream
所有 TLS failure
所有 Azure capacity failure
所有 Codex reconnect
都来自 Codex SSE consumer。
真实上游故障仍然可能存在。
4. Patch 只适合你明确接受自编译 Codex 的场景
如果你维护自己的 Codex build,可以研究上游 Issue 中的 proposed patch 和 regression tests。但生产团队不应把社区/Issue patch 等同于官方二进制更新。
更稳妥的发布流程是:
text
官方新 stable release
↓
运行本文 repro
↓
EOF case 保留原始 error
↓
open-socket case 也立即保留原始 error
↓
再确认修复
这和 AI Agent 安全有什么关系
这篇文章本身是 reliability / observability 问题,不应该为了蹭"AI 安全"强行包装成安全文章。
但它和 Agent Security 有一个重要交叉点:如果执行层把真实失败覆盖成次级 timeout,审计、告警、自动恢复和风险分类都会失真。 对长运行 Agent 来说,"失败原因是否能被准确保留"本身就是 runtime control 与 incident response 的基础。
所以它应该从 AI Tools Lab / Codex 排障入口承接搜索流量,再内链到 AI Agent Security 的 Audit / Observability / Runtime Control,而不是反过来把这篇写成泛安全总览。
最终结论
截至 2026 年 9 月 12 日,XBSTACK 独立确认:
- Codex CLI
0.153.4受影响; - Codex CLI
0.154.0仍受影响; response.failed后立即 EOF 时,原始 error 可以正常保留;response.failed后 socket 保持打开时,客户端会继续等 idle timeout;- 最终
idle timeout waiting for SSE可能覆盖真实 server failure; - 调大或调小 idle timeout 都不是根本修复;
- 当前不能把上游 proposed patch 称为 OpenAI 官方 released fix。
如果你正在排查 Codex 的 SSE reconnect,第一步应该不是继续调 timeout,而是先确认:在 timeout 之前,服务端是否已经发过 response.failed。
如果问题还涉及更广的 Responses 流式链路,可以继续对照 Responses API stream abort 后 tool call 丢失问题;如果你使用的是 Astra 或自定义 provider,再看 GPT-6 Astra API 实测与迁移边界。这类运行时排障统一收在 AI Tools Lab,而权限、审计和 Runtime Control 的安全边界则归到 AI Agent Security。
一手证据
- 上游 Issue:github.com/openai/code...
- Codex Releases:github.com/openai/code...
- XBSTACK 独立 Repro:github.com/xbstack/cod...
原文:www.xbstack.com/ai/tools-la...
相关阅读: