维护者分享Safari-MCP server的多标签页隔离机制在超预期部署场景(3个macOS Profile、15个并发实例)下暴露的三类bug:内存守卫失效、竞态条件、标签页回收池污染,以及针对性修复方案。
我维护着 safari-mcp,这是一个让 AI Agent 能在 macOS 上驱动真实 Safari 会话的 MCP 服务器。今年夏天几个版本里,我实现了 per-session 的 tab 隔离——这个功能允许多个 Agent 共享同一个浏览器而不会互相抢 tab。我在这里写了两篇文章讲它。当时我相当得意。
然后一个陌生人出现了,他们的部署方式我从未想象过——三个 macOS 桌面配置,多达十五个并发服务器实例——在七秒钟内提交了三个 bug 报告。
三个全对。
真正了不起的不只是命中率,而是报告的形式。每个 issue 都描述了部署情况、复现步骤,以及他们自己机器上观察到的数据。没有猜测,没有"感觉不太稳定"。他们读了代码,形成了关于在他们的规模下哪里会出问题的假设,然后看着它恰好在那里坏掉了。
服务器有一个 WebKit 内存监视器,会在 Safari 的内容进程膨胀之前清理空闲 tab。它还有一个门控机制:只有充当"扩展宿主"的那个实例——实际上是哪个实例抢到了 9224 端口谁就是——才被允许执行清理,而且只清理它自己的 tab。在一个十五实例的部署中,这意味着十四个实例的 tab 永远堆积,而只有一个实例在彬彬有礼地打扫自己。这个守卫在内存压力最严重的拓扑结构中实际上是完全失效的。
尴尬的是:这个门控是多余的。已经有锁文件保证每个周期只有一个清理器在运行。是有人(我)在已有安全机制之上又加了一层,而第二层破坏了这个功能。修复方案是删除。
实例在一个共享的 JSON 文件中记录自己拥有哪些 tab。读取路径是启动时读取、内存中修改、保存时覆盖。经典模式。一个实例时,无害。十五个实例时,每次保存都是快照覆盖,会静默丢弃其他十四个实例在此期间写入的内容。
注意这个 bug 不会做的事:它不会崩溃,不会操作错误的 tab,也不会记录任何日志。丢失一个所有权条目是故障安全的——一个 tab 只是停止被追踪。这正是它能存活下来的原因。会大声失败的 bug 在第一周就被修掉了。会优雅失败的 bug 则在等着某个运行十五个实例并注意到条目消失的人。
修复方案是写时合并:将磁盘状态与本地状态做 union,时间戳最新者胜出,并使用显式的删除 delta,这样删除操作不会被过期的对端复活。我考虑过文件锁并故意跳过了它——残留的竞争窗口在亚毫秒级且故障安全,而加锁反而会引入新的故障模式。这个权衡被写在了代码里,就放在 merge 逻辑旁边。
当一个桌面配置的 Safari 窗口关闭时,服务器会轮询它重新出现——固定的 3 秒间隔,每次都生成一个 osascript 子进程。永不停歇。他们的日志显示了后果:每个空闲实例每小时大约产生 1200 行日志和 1200 次子进程启动。乘以十五。
修复方案是你已经知道的那个无聊方案:自调度的退避,3 秒翻倍到 60 秒上限,成功时重置。修复过程中我发现了一个报告没提到的额外 bug——一个写入 __dirname 的 trace 日志,对 npm 包来说意味着写进了 node_modules 里。那个文件一直在悄悄地在一个已安装包的目录里增长。
你的测试拓扑是对世界的一个声明,而没有人会同行评审它。我测过多实例——但只是一个桌面配置、从一个地方启动、数量很少。报告者跑的是三个桌面配置各五个实例。三个 bug 精确地落在我和他们之间的拓扑缝隙里。在我的拓扑中没有一个是可见的。整个测试都是绿的;报告到来的那天早上测试套件 78/78 全通过。绿测试只验证了你想到要模拟的那个世界。
故障安全的失败是最长寿的。三个 bug 有一个共同点:系统继续运行。内存缓慢变差。条目静默消失。子进程静默生成。如果其中任何一个抛出了异常,它们早就在代码审查中死掉了。设计你的失败要大声,否则就要接受静默的 bug 会在多年后、被一个陌生人、在你没测过的规模上被发现。
有时候修复就是删除。内存守卫 bug 存在是因为两个安全机制重叠了,新的那个勒死了功能。我不需要写更巧妙的协调代码——我需要删掉冗余的那一半。修复这三个中听起来最可怕的 bug 的 diff 是净减少行数的。
一份在陌生规模下的优秀 bug 报告是你买不到的免费 QA。我负担不起为了一个我不确定有人会用得这么狠的功能,去搭建一个十五实例、三桌面配置的部署测试环境。有人因为需要它而搭了这个环境,然后把 findings 连同复现步骤一起交给了我。唯一正确的回应是:快速验证、诚实致谢、以及发出修复。所有三个修复都在第二天一个版本里发出去了,每个 issue 的回复都记录了不仅有什么变化,还记录了我拒绝了哪些替代方案以及为什么——包括我选择不加的那个锁。
最后这部分比看起来更重要。报告者现在知道 merge-on-write 有一个已知的、有界的、故障安全的竞争条件。下一个在那里遇到奇怪问题的人不需要从零开始——推理过程在讨论帖里,不是在我脑子里。
这个功能是能用的。关于它的故事,在我自己的规模下,是真的。花了别人在生产环境中的拓扑来揭示"支持多实例"实际上是"支持我恰好尝试过的多实例形状"。
你的测试拓扑和用户真实部署之间最大的差距是什么——你被它坑过吗?那个 bug 是大声失败还是优雅失败?