随笔
一次 Codex 写盘风波后,怎样判断要不要先自救
从社区 issue 到本机 Codex 自查,用写入速率代理指标判断官方修复程度和临时止血的性价比。
起因是社区 issue 中一个关于“SSD 会不会被写坏”的讨论。学习了同行们的实践后,还是决定自己动手再验一遍,做到真正心中有数。
最近 Codex 社区一个反馈让我为手里的 Mac 捏了一把汗:Codex 的本地诊断日志,在某些使用方式下确实可能持续写盘,甚至把 SSD 写入量堆到很夸张的位置。
这个问题其实挺难被注意到。单看每一条日志,它们都符合“正常可观测性”的一部分:TRACE、SSE event、WebSocket event、stream event、连接池状态、后台心跳。每一类单独拿出来,都能解释为调试和复盘服务。
但这里最让人抓狂的是 “高频写入 + 插入后修剪”。这是一种表面逻辑完备、却缺少硬件体系素养的架构设计:为了防止日志文件快速膨胀,系统采取快速擦写的方式控制文件体积,这在一般设备上可以说已经做到了 80 分;但是回到 Codex 可能 7*24 小时高强度工作在个人电脑上的场景,判断标准就悄悄发生了偏移。SSD 的硬件特性决定了它的使用寿命强相关于反复擦写次数,而前面控制文件过快增长的小技巧,还会抹掉使用者快速发现问题、及时修正的机会,一下子好事变坏事。
既然问题已经暴露,修正方向其实很清晰;但 SSD 寿命这件事不能只靠转述判断,还是要亲自验一遍。
先把目标拆清楚
要护住 SSD,同时不因为恐慌放弃工具,先要把几个关键问题拆清楚:
- 当前 Codex 是否仍在高频写本地日志库?
- 社区上报的问题,在本机是否也能复现?
- 最新源码和当前发布包分别修到了哪里?
- 如果当前发布包还没完全闭环,是先关注等待官方包,还是先做临时止血?
社区里被反复提到的线索主要有几类:
| 线索 | 问题形态 |
|---|---|
#17320 / #22444 / #24275 / #28224 | 用户侧长期使用后看到日志库、WAL 或写入压力异常 |
#29432 | 停止记录每个 Responses WebSocket event,减少逐事件 payload 日志 |
#29457 | 给持久化 SQLite 日志增加默认过滤,试图屏蔽高噪声 target |
#29599 | 继续修补 #29457 没挡住的 target=log bridged log |
这里最关键的是 #29599。#29457 看起来已经过滤了 log,但后续又出现 #29599,说明问题没有在第一轮完全闭环:有些 bridged log 会绕过外层 target filter,必须在 SQLite sink 内部、格式化和入队前直接丢弃。
也就是说,只看“有修复 PR”不够。关键要看本机当前跑着的发布包是否真的包含这些修复,以及写入压力有没有降到可以接受的范围。
本机先验了一遍
先用一个本地 Codex session 做只读检查。
桌面 App 已更新到:
Codex App 26.616.71553
codex-cli 0.142.0日志库状态大致是:
logs_2.sqlite 319M
logs_2.sqlite-wal 10M
logs_2.sqlite-shm 32K先做了一个 15 秒窗口采样:
id_delta=770
count_delta=39折算约 51 条 id/秒。id_delta 增长很快,但 count_delta 只增加 39,符合“持续插入,同时修剪旧日志”的模式。
随后做了 60 秒采样:
before_id=56886305
after_id=56889261
id_delta=2956
before_count=32593
after_count=32615
count_delta=22这约等于 49.3 条 id/秒。更重要的是,60 秒只多保留了 22 行。这说明大量写入发生了,但很多行在窗口结束时已经被修剪掉。
当时新行主要集中在这些来源:
TRACE|log|980
TRACE|codex_api::sse::responses|30
TRACE|codex_mcp::connection_manager|6
DEBUG|codex_core::stream_events_utils|3
TRACE|codex_api::endpoint::responses_websocket|1这一步基本确认了两件事:社区上报的方向在本机也能复现;而且当前发布包还没有完整消除高频写盘。
最新源码修了什么,当前发布包还缺什么
接着核对本地最新源码、App 内置 CLI 和运行时日志。
最新源码层面,修复动作是存在的:
| 问题点 | 最新源码里的处理 |
|---|---|
| Responses WebSocket 每事件 payload 日志 | #29432 已处理 |
codex_otel.log_only / codex_otel.trace_safe 写进 SQLite | #29457 已处理 |
target=log bridged log 绕过外层过滤 | #29599 继续补洞 |
但本机正在跑的发布包不是最新源码 HEAD。关键证据是:App 内置 CLI 二进制时间早于 #29599 的合入时间;重启后的新进程仍在写 TRACE|log,不是旧进程残留。
重启后再看 60 秒窗口:
id_delta=1960
count_delta=21折算约 32.7 条 id/秒。
这组数据不能证明版本修复收益。两次采样都发生在当前发布包环境下,差异更可能来自任务强度、后台活动和采样时机不同。它真正说明的是:重启后的窗口里,写入仍然存在,且 target=log 仍然很显眼。
TRACE|log|962
TRACE|codex_api::sse::responses|26
DEBUG|log|13按这个窗口粗算:
(962 + 13) / 1960 ≈ 49.7%也就是说,当前发布包里仍有相当一部分 id 增长来自 target=log 这条路径。这和“当前发布包缺少完整 #29599 效果”的判断一致。
所以这里能下的结论只有这些:
| 层面 | 判断 |
|---|---|
| 最新源码 | 已经对 WebSocket 逐事件日志、部分 OTEL 噪声、target=log bridged log 做了修复 |
| 当前发布包 | 只包含部分修复,缺少完整的 #29599 效果 |
| 本机运行态 | 仍能复现持续插入和修剪,TRACE|log 仍是重点观察对象 |
| 遗留问题 | SSE 每事件 TRACE 仍存在,SQLite log sink 默认 TRACE 仍偏激进,插入后修剪机制仍会造成写放大 |
这也是不能只说“官方已修”的原因。它不是没修,也不是完全没用,而是当前发布包还没修到足够放心常驻运行的程度。
空闲状态暂时不展开
接着又做了一个 120 秒空闲窗口采样。表面结果是:
before_id=57241061
after_id=57243743
id_delta=2682
before_count=34147
after_count=34147
count_delta=0折算约 22.35 条 id/秒,且 count_delta=0。
但这条证据暂时不继续演进成强结论。原因是它没有进一步按日志记录时间去拆分,也没有排除 Codex 自身维护任务、后台连接、查询窗口干扰等因素。要把“Codex App 空转时是否持续写盘”说清楚,还需要继续复查库内记录时间的时段分布,或者从源码上探索写入时机与 App 空转运行之间的关系。
所以在这篇里,只保留一个谨慎判断:当前发布包在有任务活动时仍存在明显写入压力;空闲写入值得继续查,但不作为本轮处置依据。
评估口径
id_delta/sec 不是精确 TBW。它当然不是磁盘实际写入字节数,也不能直接换算 SSD 寿命。
但它很适合作为一个本地代理指标:它能反映 SQLite 日志层是否正在持续发生插入和修剪,也能比较不同运行状态之间的压力差异。
现在这套口径可以这样用:
| 观察结果 | 处理倾向 |
|---|---|
id_delta/sec 在 30 以上 | 不适合长时间常驻,倾向临时止血 |
id_delta/sec 降到 10 以下,且 WAL 不增长 | 可以先等待官方包 |
TRACE log 仍是主项 | 继续关注近期官方包是否包含 #29599 效果 |
| WAL 持续增长或出现 deleted WAL | 优先退出、备份、清理流程,不继续硬扛 |
按这套口径,这台机器仍然值得警惕。它不是立刻灾难,但也不适合被当成一个可以长期忽略的后台状态。
接下来怎么处理
先做日常止血。
在官方完整修复包到来之前,不让 Codex 长时间无任务常驻。用完就退出;需要长任务时再打开;不把它当成全天候后台服务。这是最稳、最不会破坏系统的方案。
同时保留一个只读复测口径。每次更新后只看三件事:
codex --version
logs_2.sqlite / WAL 文件大小
60 秒窗口 id_delta/sec 和新增 target 分布只要 TRACE|log 还在前列,就不能把它当作完整修复。
再看官方包。
可以等,但要带着条件等:
- 发布包包含
#29599或等效的 sink 内部过滤。 TRACE|log/DEBUG|log在新窗口里消失或不再是主项。- 60 秒窗口
id_delta/sec降到低位,最好小于 10。 - WAL 没有持续增长,也没有 deleted WAL 残留。
源码里已经能看到解决方案,按合并节奏,6 月 24 日或 6 月 25 日附近的发布包值得重点关注。但这不是确定承诺,最终仍要看发布包行为。
最后保留本地应急方案。
这部分不能直接写成“照抄命令就能执行”。因为任何对本地日志库、App bundle 或运行进程的处理,都有误伤风险。可操作原则是:
- 先退出 Codex,确认没有进程继续打开日志库。
- 先备份日志库和 WAL,不在运行中手删。
- 优先做只读复测和使用习惯止血;只有必须长时间开 Codex 时,才考虑临时过滤高噪声 target。
- 不直接替换 App bundle 内置 CLI,不用来源不明的二进制替换常驻工具。
- 记录所有临时动作,官方包修好后撤掉本地措施,再复测。
这不是最优雅的方案,但在当前信息不完整的情况下,“可回滚、可复测、少改系统”更适合作为临时处置方式。
真正值得记住的地方
这次风波表面上是 SSD 写入,底层其实还是一个老问题:可观测性不是记录越多越好。日志当然重要,没有日志,很多问题根本没法复盘;但如果一个桌面 AI 工具默认把 wire-level TRACE 长期写进 SQLite,再靠保留上限去修剪,它就把“调试时有价值的材料”变成了“日常运行时持续付费的成本”。这个成本不只包括磁盘写入,也包括隐私面、本地数据库体积、排障时的信息噪音。
更合理的边界应该是:
- 默认保留 WARN/ERROR 和关键 INFO。
- 高频 wire payload TRACE 默认不落库;需要深度诊断时,再用显式开关短时间开启。
- 诊断日志需要仔细设计,包括摘要、事件类型、字节数和关联 id,而不是完整 payload。
- 当压力形成,盯的点往往不是一个,而是日志级别、写入源、保留策略、WAL 状态、清理时机和回滚方案都要一起看。
最终也不会有一个放之四海皆准的最优解,只有对当前使用方式最合适的解。
其实,判断 coding agent 是否算一个工程化的佳作,除了看它会不会调用工具、能不能写代码,还要看它怎样对待这些后台成本:日志、缓存、会话恢复、权限、磁盘写入、隐私边界。这些东西不在炫技的第一屏,但它们决定工具能不能被长期放心地放在机器上。
所以最后的判断不是“Codex 很危险”,也不是“等官方就好”。更准确地说,是先把风险量化,再决定动作。
如果下一次发布包复测时,TRACE|log 消失、id_delta/sec 掉到低位、WAL 不再异常增长,就可以安心等官方节奏。如果这些指标还压不下来,那它仍然是一个需要本地止血的工程问题,而不是一个已经过去的社区讨论。