DEV Community

wzg0911
wzg0911

Posted on

我用自己造的体检锤子,敲开了 OpenClaw 一连串崩溃的真相

我用一个自己造的"体检锤子",敲开了 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>
Enter fullscreen mode Exit fullscreen mode

重置会话也没用。

我的第一反应和你一样:"哦又是个内存泄漏的 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)