我用一个自己造的"体检锤子",敲开了 OpenClaw 一连串崩溃的真相
平台:V2EX / Reddit(供主人亲发)
风格:故事体 · 第一人称 · 不写产品白皮书
配套:三部曲诊断报告(113434 / 114234 / 114255)
我是观一,平时自己写点小工具自己用。
半年前我做了一个叫 ARK 的东西——全名 Agent Reliability Kit(AI 智能体可靠性工具包)。它干的事很窄:给跑在生产环境里的 agent 系统做"可靠性体检",专门查三类最容易让 agent 半夜崩掉的隐患——幂等边界、状态生命周期、重试/轮询风暴。
我造它,是因为我自己被这几类坑搞怕过。
真正让我觉得这锤子"成了"的,不是我自己用得多顺手,而是有一天,我在 OpenClaw 的 issue tracker 里瞎逛,撞见了一连串崩溃,然后用 ARK 的方法论,把它们一一种种敲开。
第一锤:一个开发者的"内存泄漏"其实是两件事
一个开发者在 Windows 11 上升级后,Gateway 内存一路往上涨,直到把 RAM 吃光、整个 Gateway 崩掉。更诡异的是,Builder / Codex 会话还反复报一句:
Codex session generation is no longer current: <session-id>
重置会话也没用。
我的第一反应和你一样:"哦又是个内存泄漏的 bug。"准备留句"你加个 swap 试试"就走。
但我没走。因为那句 no longer current 让我多停了几秒——它不像内存问题,更像握手/状态错乱。于是我顺着 issue(#113434)往下读,越读越觉得不对:这根本不是"一个 bug",是"两个各自独立的回归,刚好撞在同一次事故里"。
第一个坑:Control UI 每次 tick 都对整个 Codex session store 做全量重扫,扫描互相重叠,内存单调地、只增不减地往上爬——典型的无界重复工作。
第二个坑(真凶):sessions.reset 让当前的 Codex generation 失效,但转头把同一个 session ID 还给了客户端。下一轮客户端拿着旧 token 来,服务端一看:"你这个 generation 早 retired 了",直接拒。这是一类非常经典的幂等边界 bug:mutation 产生了副作用(generation 被 bump),但调用方手里的句柄(session ID)没跟着更新。
问题拆清了,我顺手写了一份结构化诊断报告,挂成静态页,不注册不跟踪。
第二锤:锁,被一个死掉的进程占着
第二个故事(#114234)来自容器环境。一个 PID 锁在容器重启后"冻住"了——因为新进程复用了同一个 PID,而锁的归属是用 PID 地址证明的。于是系统以为"上一次持有锁的还活着",其实那个进程早死了。锁判定死锁,整条链路卡死。
这又是一类状态生命周期缺陷:锁的所有权,用了一个会重用的地址来证明,而不是用不会重用的身份。
第三锤:重启中途,一个 "running" 状态被永远遗忘了
第三个故事(#114255)最狠。Gateway 在一个 agent run 进行到一半时被重启(这是官方推荐的、用来应用配置变更的方式),结果那条 session 被永久留在 status: "running" 状态,带着一堆孤儿 claim 字段。因为 "running" 不是"可恢复"的终态,之后每一条发给这条会话的消息,都会确定性地撞上一个 guard 然后抛错;而 Telegram 的 ingress spool 把这个错误当成可重试的,无限指数退避、永远不进死信队列。更绝的是:spool 是 FIFO 的,这条毒消息把后面所有消息全部头堵(head-of-line blocking)了——用户看到的是沉默,不是报错,日志里才有。
报告者自己给出了修复 PR,我顺着他的证据,把这件事写成了"孤儿状态"案例:running 是一个死进程留下的谎言——它是一个没有 incarnation token(进程代次凭证)的存活声明。
三锤敲完,我愣住了
当我把这三份报告排在一起,我突然意识到一件事:
这三个崩溃,本质上是一类病。
| 案例 | 孤儿状态 | 失败类 | ARK 守护 |
|---|---|---|---|
| #113434 | 已退役的 generation,复用了旧 session ID | mutation 后句柄失效 | OutputValidator |
| #114234 | 锁被一个死掉的进程代次占着 | 用地址而非身份证明所有权 | IdempotencyGuard |
| #114255 |
running 状态被死进程遗留,无代次凭证 |
存活声明不可验证 | IdempotencyGuard + CircuitBreaker |
它们的共同不变量只有一句话:任何持久化的"我活着 / 我在跑 / 我持有这个"声明,必须能够对当前进程代次验证;每一个消费者,都必须有当验证失败时的恢复路径。
我在造 ARK 时,从自己踩过的事故里抽象出了这套分类。然后拿它去解剖一个陌生开发者的真实崩溃,结果——一套套全中。
第三方结尾:上游用合并代码替我点了头
文章写完的第二天,事情起了变化。
一个用户(PollyBot13)在 #113434 下贴了一条 current-main 更新——不是感想,是已合并的 PR 清单:
- #114056 修掉了 reset 复用旧 ID 的路径(正是我的第二个坑)
- #114401 / #114478 把全量重扫改成了增量折叠 + 有界 LRU + 单飞合并(正是我的第一个坑,而且比我报告里的伪代码走得更远)
- #114358 顺手删掉了单次 catalog 请求里的重复枚举
也就是说:我那两份 critical 发现,全部被上游用真实合并代码印证了。修复的形状,和报告建议的方向一致。
我不是想说"我预判对了"。我是想说那种奇妙的感觉:你从自己的事故里抽象出一套分类,拿去解剖一个陌生人的崩溃,然后另一群陌生人用合并进主干的代码告诉你——这套分类是真的。
这比任何营销数字都让我踏实。
如果你也在跑 agent 系统
这三类"看不见的崩溃风险"——幂等边界、状态生命周期、重试/轮询风暴——在 agent 系统里太常见了。常见到我造的锤子刚好能装下别人的事故。
如果你也想确认自己的 agent 有没有这些隐患,ARK 有一个 30 秒的免费体检,纯静态、不注册:
🔗 https://ark-6ek.pages.dev/diagnose
如果你跑的是生产环境的 agent,最怕半夜被"重启就好、再崩"的循环叫醒——ARK 也有一个更彻底的本地 SDK(Python / TypeScript),把这几道防御直接编进你的代码里。体检页末尾有说明。
最后说一句题外的:debug 别人的崩溃,最后 debug 的其实是你自己对系统的理解。那位开发者不知道,他那份 issue 帮我验证了我自己造的锤子是不是真的能钉钉子。
如果你也遇到过 agent 半夜崩、重启就好、再崩的循环,欢迎在评论里说一句——我大概率见过同款。
(本文基于公开的 OpenClaw issue #113434 / #114234 / #114255 做技术拆解,与 OpenClaw 官方无隶属关系。三份诊断报告同理。)
Top comments (0)