排查Codex加载技能失败的真实原因:不是Markdown格式问题,而是Too many open files(EMFILE)系统错误,附修复过程与诊断命令。
最初发布于 hexisteme notes。
警告信息提到了我的文档:Codex 跳过了 13 个 skills 的加载,原因是它们的 SKILL.md 文件无效。但标题下方的详细说明指向了另一个故障:读取文件失败:打开文件过多(os error 24)。
这个区别决定了修复方向。一个无法打开的文件并不一定是因为 Markdown 或 schema 检查失败。编辑其内容无法回答为什么进程无法读取它。
这是我对 9 月事件的事后分析,基于日期为 2026-09-21 的修复记录。下面的观察来自该次运行。它们并非对每个 Codex 版本或每个带有"无效"字样的警告的声明。
有用的信号是 EMFILE,即进程级别的"打开文件过多"错误。这里的"文件"包括通信通道消耗的文件描述符。我的环境配置了通过 stdio 连接的 MCP servers,它们附加在一个长期运行的 Desktop app-server 上,所以普通的 skill 文档在和管道共享资源配额。
诊断记录包含多个不同的测量值:
这些是不同层面的数据。尤其需要说明的是,shell 的软限制并不是对已经运行的 Desktop 进程有效限制的测量。看到该进程中有 274 个描述符,并不能证明它运行在 shell 的 256 限制下。我需要目标进程自身的限制来做出这个判断。
快照确实显示了读取失败旁边有大量的管道占用。进程清单也显示了对应于多个会话的 MCP groups。因此资源假设除了顶层警告外还有证据支持,尽管仅凭快照无法确定具体是哪个描述符分配失败了。
我的清单范围也很广:agent 树下有 61 个 skills,Codex skill 目录下有 16 个,插件缓存中有 112 个。这些数字描述的是配置层面的覆盖范围,而不是同时打开的文件数量。我没有把它们转化为并发测量。
事件记录将症状与并行加载 skills 以及与会话相关的 MCP 子进程管道联系起来。它引用了上游讨论:issue #36755、issue #26984 和 issue #37971。这些是记录中的调查线索;它们当前的状态不是本 post 提供的证据。
我的工作解释是:加载 skills 需要短暂的文件描述符,而长期运行的进程已经持有大量的通信占用。一个文档恰好是下一个被拒绝开放的资源消费者。它的文件名使失败看起来像是文档本地的问题,而嵌套错误指向的则是进程。
我把这个解释作为测试资源干预的充分条件。我没有设计一个对照实验来分离加载器并发性与保留的 MCP 管道的各自贡献。下面的成功干预支持了资源诊断,但它无法为每个机制分配失败的份额。
我修改了 CLI 启动路径,在启动 Codex 前请求将软 NOFILE 限制提高到 65,536。wrapper 会尊重更低的硬限制,并在操作系统拒绝更改时保持其现有行为。domain launcher 收到了相同的目标。
我的证据在这里有一个局限性。验证记录记载了一个 wrapper probe 从软限制为 256 的父进程发出 ulimit -Sn 4096。这支持了较窄的声明:启动路径可以尝试更高的限制。它不能证明最终的 65,536 目标在每个启动的进程中都有效。配置意图和继承的运行时限制是不同的 факт,我的记录没有弥合这个差距。
另一个改变减少了常规需求。我将全局 MCP 默认值从 23 个 servers 削减到 8 个,默认禁用 Comfy 和 video-vision 插件,并为需要更大工具集的工作保留了 domain profiles。完整 profile 保留了之前更广泛的配置。
这给了我一个操作选择:普通工作使用更小的基线,在需要时选择更广泛的依赖。仅提高上限会保留原有的管道需求。仅降低基线不会解决 CLI 低继承软限制的问题。
GUI 路径有自己的边界。我尝试从用户会话更改 launchd maxfiles 限制时被拒绝,操作不允许。我没有把它记录为成功的 Desktop 限制增加。实际应用的更改是减少的默认需求和 CLI 侧的启动行为。
热重载后,记录的 Desktop app-server 快照有 116 个数字描述符、78 个 PIPE 条目和 26 个直接子进程。前后快照显示保留的占用减少了。它们不能确定上游生命周期管理缺陷已被永久修复。
验证记录比快照更广泛。wrapper 和 launcher 的 shell 语法检查都通过了。所有 11 个 profile 配置都通过了它们的解析和 MCP-list 检查。严格的 doctor 运行报告 23 项检查 OK。
我还用 base profile 和 full profile 分别运行了新的临时执行。每次都返回 exit 0,产生了请求的响应标记,且没有发出 FD 或 skill 警告。这些运行后的检查没有发现新的 MCP orphans。
这些新的运行是记录中最有力的恢复证据:系统执行了之前一直在失败的操作。full-profile 运行很重要,因为减少默认 server 集不是唯一被执行的路径。不过,这些都是有限的冒烟检查,而不是对会话变更的长时间测试。"在检查的运行中已恢复"是有支持的。"不会复发"则不是。
修复记录包含一个特定的证伪器,在 2026-09-21 评估:如果一个使用 base profile 且经验证 NOFILE 限制为 65,536 的新进程在 os error 24 消失后仍然反复在相同的 SKILL.md 文件上失败,则将这些文件重新分类为单独的语法或 schema 缺陷。
"verified"这个词很重要,因为 wrapper-probe 存在差距。我会在将重试作为证伪器的测试之前确认有效限制。否则,尝试应用缓解措施失败可能被误认为是反对该诊断的证据。
对于复发,我的 runbook 从 Desktop app-server 的管道计数和直接子进程计数开始。在更小基线下跨会话增长会重新打开生命周期管理问题。在这种情况下重启应用程序是一种恢复操作,而不是原因已被移除的证明。
新的会话在跳过 skill 警告后也很重要。记录的恢复过程会打开一个新会话,而不是假设失败的加载已在旧会话中修复。当受影响的进程持续存在时,保存活动工作并完全重启 Desktop 是更强的重置。
在调查期间,原始配置 diffs 和相邻配置行在工具输出中暴露了凭证值。修复记录明确地将凭证轮换作为未解决的跟进事项。我不会复制这些值,也不会将描述符修复视为安全修复。
操作教训是具体的:配置诊断应该打印 server 名称、enabled 标志、计数以及是否存在凭证。当 secrets 可以占据相邻行时,狭窄的行范围不是足够的过滤器。这个错误应该出现在这个 postmortem 中,因为它发生在收集证据的过程中,而不是因为它解释了 EMFILE。
资源修复始于我不再将"无效"作为诊断,而是跟随嵌套读取错误。验收检查同样具体:新的执行在没有原始警告的情况下完成,观察到的管道占用下降了。将这些声明保持比"所有限制已修复"更窄,为下一次调查留下了诚实的起点。
本文由我的事件和修复记录提供 AI 辅助编写。
这些笔记的邮件列表:hexisteme.beehiiv.com — 还没有发出过任何一期,所以你会在第一期之前订阅。没有欢迎序列,没有课程,没有追加销售。