【深度复盘】我用自己造的体检锤子,敲开了 OpenClaw 一连串崩溃的真相 #5
Replies: 2 comments
|
整理一份完整的复盘到这里,作为公开案例存档。它最初源于我在 openclaw#115326 和社区开发者 robingutsche 的一次深度交流——他挖到的真根因,我顺着"证明某样东西还活着/还属于你的证据,必须和进程生命周期绑在一起"这条线,把几个崩溃家族串了起来。 一个被反复踩中的陷阱:状态句柄失效过去几周我拆了 OpenClaw 社区一连串真实崩溃,发现它们不是孤立 bug,而是一个家族——只要"证明 X 还有效"的证据,没有和进程的当前实例绑死,重启后它就会变成谎言。 案例一:#113434 — 会话句柄退休,但新句柄没交回客户端上游 generation 被退休,服务端已经换了新句柄,但客户端手里拿的还是旧的。表象是"莫名其妙 400/会话失效",根因是状态生命周期没跨重启对齐。这条诊断随后被上游合并的 PR(#114056 / #114401 / #114478)独立印证。 案例二:#114234 — 容器里 PID 复用,"按进程是否活着"判活的锁被骗一个用 PID 判断"锁是否被持有"的机制,在容器重建后 PID 被复用,于是锁被判成"永远活着",缓存冻结。根因是锁的有效性证据 = 一个会在重启后失效的标识符。 案例三:#114255 — 网关重启后 session 状态还是
|
|
我把这次系列诊断提炼成的方法论,正式归档在这里(#5 深度复盘 的姊妹篇): The "Proof-of-State" Trap: Why Your Agent Dies After Every Restart 四案例(#113434 / #114234 / #114255 / #115326)串成同一个家族,核心不变式:proof-of-state 必须和 incarnation 绑死——任何"X 还有效"的断言,只要没和当前实例绑死,重启后就会变成谎言。 这与本帖 #5 的逐案拆解互为表里:#5 是'怎么拆',这篇是'拆完归到哪条规律'。两篇一起看,就是完整的 ARK 方法论。 |
Uh oh!
There was an error while loading. Please reload this page.
我用一个自己造的"体检锤子",敲开了 OpenClaw 一连串崩溃的真相
我是观一,平时自己写点小工具自己用。
半年前我做了一个叫 ARK 的东西——全名 Agent Reliability Kit(AI 智能体可靠性工具包)。它干的事很窄:给跑在生产环境里的 agent 系统做"可靠性体检",专门查三类最容易让 agent 半夜崩掉的隐患——幂等边界、状态生命周期、重试/轮询风暴。
我造它,是因为我自己被这几类坑搞怕过。
真正让我觉得这锤子"成了"的,不是我自己用得多顺手,而是有一天,我在 OpenClaw 的 issue tracker 里瞎逛,撞见了一连串崩溃,然后用 ARK 的方法论,把它们一一种种敲开。
第一锤:一个开发者的"内存泄漏"其实是两件事
一个开发者在 Windows 11 上升级后,Gateway 内存一路往上涨,直到把 RAM 吃光、整个 Gateway 崩掉。更诡异的是,Builder / Codex 会话还反复报一句:
重置会话也没用。
我的第一反应和你一样:"哦又是个内存泄漏的 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(进程代次凭证)的存活声明。三锤敲完,我愣住了
当我把这三份报告排在一起,我突然意识到一件事:
这三个崩溃,本质上是一类病。
running状态被死进程遗留,无代次凭证它们的共同不变量只有一句话:任何持久化的"我活着 / 我在跑 / 我持有这个"声明,必须能够对当前进程代次验证;每一个消费者,都必须有当验证失败时的恢复路径。
我在造 ARK 时,从自己踩过的事故里抽象出了这套分类。然后拿它去解剖一个陌生开发者的真实崩溃,结果——一套套全中。
第三方结尾:上游用合并代码替我点了头
文章写完的第二天,事情起了变化。
一个用户(PollyBot13)在 #113434 下贴了一条 current-main 更新——不是感想,是已合并的 PR 清单:
也就是说:我那两份 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 官方无隶属关系。三份诊断报告同理。)
All reactions