<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Apache-2.0 on AI博士 万戈</title>
        <link>https://www.yesmiracle.net/tags/apache-2.0/</link>
        <description>AI博士万戈的技术博客，聚焦 Agentic AI、AI Infra 与 Agent Security，分享 AI 基础设施与工程落地实践。</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <managingEditor>admin@yesmiracle.net (万戈)</managingEditor>
        <webMaster>admin@yesmiracle.net (万戈)</webMaster>
        <lastBuildDate>Tue, 03 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.yesmiracle.net/tags/apache-2.0/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>agent-desktop：vercel-labs 给 Agent 做的「桌面操作」CLI，不靠截图，直接读无障碍树驱动原生应用</title>
        <link>https://www.yesmiracle.net/post/20261103-agent-desktop-accessibility-tree-cli/</link>
        <pubDate>Tue, 03 Nov 2026 00:00:00 +0000</pubDate>
        <author>admin@yesmiracle.net (万戈)</author>
        <guid>https://www.yesmiracle.net/post/20261103-agent-desktop-accessibility-tree-cli/</guid>
        <description>&lt;img src="https://www.yesmiracle.net/post/20261103-agent-desktop-accessibility-tree-cli/cover.svg" alt="Featured image of post agent-desktop：vercel-labs 给 Agent 做的「桌面操作」CLI，不靠截图，直接读无障碍树驱动原生应用" /&gt;&lt;p&gt;上个月我写过 Neko，那篇的落点是「只出像素」为什么是个关键设计：把浏览器变成一条视频流，agent 拿到的和你的眼睛拿到的是同一样东西。这篇讲的工具走的是反方向，一个像素都不出。&lt;/p&gt;
&lt;p&gt;agent-desktop 是 vercel-labs 开源的桌面操作 CLI，Rust 写的，Apache-2.0，2026 年 2 月建仓，现在 10,019 星、673 个 fork。它的主张可以压成一句话：读操作系统的无障碍树，把每个 agent 能操作的元素变成带稳定 ref 的结构化 JSON，agent 点一个 ref，再读一次界面来验证结果。整个观察（observe）和行动（act）的循环留在调用它的 agent 里，它自己不含模型、没有界面。&lt;/p&gt;
&lt;p&gt;我的判断放在前面：这条路的价值不在「省 token」这类容易被说烂的账上，而在于它把「看清界面」从概率问题换成了确定性问题。截图方案的失败模式是「看错了」和「点歪了」，而这两个失败模式在结构化 ref 上根本不存在。代价也很清楚，文章最后会说：拿不到无障碍树的界面，它什么都看不见。&lt;/p&gt;
&lt;h2 id=&#34;它明确说自己不是什么&#34;&gt;它明确说自己不是什么&lt;/h2&gt;
&lt;p&gt;这类工具容易在宣传里含糊边界，agent-desktop 反过来，在 &lt;code&gt;AGENTS.md&lt;/code&gt; 里把边界写成了排比句。原文：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;agent-desktop is a tool that AI agents invoke, not an agent. It is a Rust CLI and C library that reads native accessibility trees and returns structured JSON with ref-based element ids. The observe-act loop lives in the calling agent. It never embeds an LLM and has no GUI or interactive prompt. It does not automate web pages (use agent-browser). It cannot see UIs that expose no accessibility tree.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;四句话钉了四条界：不含模型、没有 GUI 和交互提示、不做网页自动化（把这块明确让给同 lab 的 agent-browser）、看不到没有无障碍树的界面。最后那条是这类方案天生的短板，它自己不藏。&lt;/p&gt;
&lt;h2 id=&#34;架构一个-trait-劈成四份&#34;&gt;架构：一个 trait 劈成四份&lt;/h2&gt;
&lt;p&gt;调用链是直的。CLI 用单一个 clap 枚举定义全部命令（&lt;code&gt;src/cli/mod.rs:34&lt;/code&gt;），&lt;code&gt;src/dispatch/mod.rs&lt;/code&gt; 按命令分支调用 &lt;code&gt;crates/core/src/commands/&lt;/code&gt; 里对应的模块，每个命令一个文件。core 只认一个抽象：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;trait&lt;/span&gt; PlatformAdapter: &lt;span style=&#34;color:#a6e22e&#34;&gt;ObservationOps&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; ActionOps &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; InputOps &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; SystemOps {}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这行在 &lt;code&gt;crates/core/src/adapter/mod.rs:39&lt;/code&gt;。trait 本身零个方法，靠四个子 trait 组合出来，加起来约 72 个方法，其中 &lt;code&gt;SystemOps&lt;/code&gt; 一个就占 35 个。三个平台各写一份实现，各自只在本系统上编译。&lt;/p&gt;
&lt;p&gt;代码量的分布值得看一眼，因为和文档给人的印象是反的。&lt;code&gt;src&lt;/code&gt; 的 CLI 层只有 12,797 行，core 84,803 行，FFI 15,197 行，而三个平台适配器分别是 &lt;strong&gt;Linux 123,842 行、Windows 79,045 行、macOS 43,026 行&lt;/strong&gt;。README 的平台表第一列是 macOS，快速上手用的是 Finder，文档站的平台页顺序也是 macOS 在前。代码里最大的那个是 Linux。&lt;/p&gt;
&lt;p&gt;为什么，答案在 &lt;code&gt;crates/linux&lt;/code&gt; 的子模块里：&lt;code&gt;system&lt;/code&gt; 56,324 行、&lt;code&gt;tree&lt;/code&gt; 29,042 行。Linux 这一侧要同时应付 AT-SPI 的 D-Bus 语义、三个 toolkit（GTK3、GTK4、Qt 6、Chromium）和两个显示服务器（X11、Wayland）。它在 &lt;code&gt;docs/solutions/runtime-errors/&lt;/code&gt; 下留了一篇复盘，标题本身就是结论：《一个 AT-SPI 错误名不是对象已经消失的证据》，它指出把 D-Bus 的 &lt;code&gt;UnknownObject&lt;/code&gt;、&lt;code&gt;UnknownMethod&lt;/code&gt;、&lt;code&gt;UnknownInterface&lt;/code&gt; 当成「对象已销毁」是双向都会错的，得针对四个 toolkit 逐个实测。这 15 篇故障复盘是仓库里最像工程笔记的部分，比 README 好看。&lt;/p&gt;
&lt;p&gt;架构的约束不靠自觉。&lt;code&gt;scripts/check-architecture.py&lt;/code&gt; 强制三件事：core 不得依赖任何平台或系统 crate，平台之间不得互相依赖，core 里不得出现允许清单之外的系统条件编译。&lt;/p&gt;
&lt;h2 id=&#34;ref-机制这套东西的核心&#34;&gt;ref 机制：这套东西的核心&lt;/h2&gt;
&lt;p&gt;一次 snapshot 返回的 JSON 里，每个可操作元素都带一个 &lt;code&gt;ref_id&lt;/code&gt;，形如 &lt;code&gt;@s8f3k2p9:e8&lt;/code&gt;。冒号前后是两回事。&lt;/p&gt;
&lt;p&gt;后半段 &lt;code&gt;e8&lt;/code&gt; 是这个 snapshot 内部按深度优先分配顺序自增的元素号，代码就一行 &lt;code&gt;format!(&amp;quot;@e{}&amp;quot;, self.counter)&lt;/code&gt;。前半段是 snapshot 自己的 id，生成方式是取时间戳纳秒、进程号和进程内计数器三者做哈希，再转 base36 并补上 &lt;code&gt;s&lt;/code&gt; 前缀。也就是说 ref 只在它所属的那次 snapshot 里有效，跨 snapshot 复用必然过期。&lt;/p&gt;
&lt;p&gt;这个设计的直接后果是裸 &lt;code&gt;@e8&lt;/code&gt; 会被拒绝。解析函数要求必须带 &lt;code&gt;@&amp;lt;snapshot_id&amp;gt;:&lt;/code&gt; 前缀，错误提示还专门写了建议：&lt;code&gt;A bare @eN ref is not accepted.&lt;/code&gt; 看起来是个小气的限制，实际上是这一整套能不能工作的前提：如果允许裸 ref，agent 拿着上一轮的 &lt;code&gt;e8&lt;/code&gt; 去点，你根本无法判断它指的是哪个元素。它宁可让你多写几个字符，也不接受一个有歧义的引用。&lt;/p&gt;
&lt;p&gt;那谁才配拿到 ref？规则比想象中讲究。表面看是一张 18 个角色的白名单（button、checkbox、combobox、textfield、menuitem 这些），列表在 &lt;code&gt;crates/core/src/tree/roles.rs:6-25&lt;/code&gt;。但实际判据是两条取或：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;is_ref_able_role_actions(role, actions)
  = INTERACTIVE_ROLES.contains(role) || advertises_primary_action(actions)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;第二条的意思是：一个元素即使角色不在白名单里，只要它声明的动作里有不属于「通用装饰动作」的，也算可操作。而「通用装饰动作」被定义成一个只有三项的集合：&lt;code&gt;SetFocus&lt;/code&gt;、&lt;code&gt;RightClick&lt;/code&gt;、&lt;code&gt;ScrollTo&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里有一段注释解释了为什么必须把这三项剔掉，是我在这一版源码里读到的最实在的一段（&lt;code&gt;crates/core/src/refs/ref_alloc.rs:71-84&lt;/code&gt;）：Chromium 和 Electron 几乎给每一个节点都声明 &lt;code&gt;RightClick&lt;/code&gt; 和 &lt;code&gt;ScrollTo&lt;/code&gt;，如果把它们算进去，就会「给每一个惰性容器和文本片段都分配 ref，把 Web 应用上的 refmap 撑到超过大小上限」。反过来，&lt;code&gt;scrollarea&lt;/code&gt; 和 &lt;code&gt;disclosure&lt;/code&gt; 这类按角色不在白名单里、但实际可操作的节点必须能拿到 ref，哪怕它的边界框是零尺寸。&lt;/p&gt;
&lt;p&gt;一个判据被收紧到这种程度，说明设计者是拿着真实的界面在调的，不是照着一张角色表抄的。refmap 本身有 1MB 的单文件上限，超了会报错并建议改用 &lt;code&gt;--skeleton&lt;/code&gt;、&lt;code&gt;--max-depth&lt;/code&gt; 或 &lt;code&gt;--root @ref&lt;/code&gt; 缩小范围。&lt;/p&gt;
&lt;p&gt;用法是一个渐进循环，技能文档里给的原文是这四行：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop snapshot --skeleton --app &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Notes&amp;#34;&lt;/span&gt; -i --compact   &lt;span style=&#34;color:#75715e&#34;&gt;# 浅层地图&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop snapshot --root @s8f3k2p9:e3 -i --compact        &lt;span style=&#34;color:#75715e&#34;&gt;# 只展开一块&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop click @s8f3k2p9:e12                              &lt;span style=&#34;color:#75715e&#34;&gt;# 点一个 ref&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop snapshot --root @s8f3k2p9:e3 -i --compact        &lt;span style=&#34;color:#75715e&#34;&gt;# 重读同一块验证&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;先要骨架，再钻进需要的那一块，点，然后重读同一区域确认。元素在两次读取之间消失了，你会拿到 &lt;code&gt;STALE_REF&lt;/code&gt;，附一条结构化的恢复建议，标准动作是重新 snapshot 再重试。&lt;/p&gt;
&lt;h2 id=&#34;安全模型默认不出手四个命令直接拒绝&#34;&gt;安全模型：默认不出手，四个命令直接拒绝&lt;/h2&gt;
&lt;p&gt;这一层我认为比 ref 机制更值得抄，因为它回答了一个所有桌面 agent 都会碰到的问题：工具怎么保证自己不做出物理上有副作用的事。&lt;/p&gt;
&lt;p&gt;答案是 &lt;code&gt;InteractionPolicy&lt;/code&gt; 这个只有两个布尔值的结构：允不允许抢焦点、允不允许移动光标。它有三个预设，&lt;code&gt;headless()&lt;/code&gt; 是两个都假，&lt;code&gt;headed()&lt;/code&gt; 是两个都真，而 &lt;code&gt;Default&lt;/code&gt; 就是 &lt;code&gt;headless()&lt;/code&gt;。README 的原文是：ref 动作走无障碍 API，不移动指针、不抢焦点，全局的 &lt;code&gt;--headed&lt;/code&gt; 才允许物理输入。&lt;/p&gt;
&lt;p&gt;这个默认值不是写在文档里就算数的，它有一条测试专门逐命令断言。&lt;code&gt;crates/core/src/commands/ref_policy_tests.rs&lt;/code&gt; 里的 &lt;code&gt;default_ref_commands_use_least_permissive_supported_policy&lt;/code&gt;，会把 click、toggle、check、expand、set-value、select、type、scroll 等十来个命令在默认上下文下全部执行一遍，然后逐个请求断言它的策略等于最保守的那一档。文档会撒谎，这个不会。&lt;/p&gt;
&lt;p&gt;四个命令干脆无条件拒绝执行：&lt;code&gt;key-down&lt;/code&gt;、&lt;code&gt;key-up&lt;/code&gt;、&lt;code&gt;mouse-down&lt;/code&gt;、&lt;code&gt;mouse-up&lt;/code&gt;。理由写在 &lt;code&gt;input_hold_policy.rs&lt;/code&gt; 里，它返回的错误信息是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;hellip; is unavailable in stateless mode. Use the atomic &amp;hellip; command; held input requires a daemon-owned transaction that can guarantee release.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;翻译过来：CLI 每次调用是一个独立进程，一次调用结束，它没有任何机制保证那个被按下的键会被松开。所以它的选择是拒绝执行，而不是冒留下一个卡住的按键的风险。README 里这条被明确计数：60 个命令名，56 个可用，剩下四个等有了能保证释放的守护进程再开放。&lt;/p&gt;
&lt;p&gt;还有一处容易被忽略：为了让「不抢焦点」这条在失败路径上也成立，指针手势有一个「尚未送达」的保证。&lt;code&gt;pointer_action.rs&lt;/code&gt; 里那个 &lt;code&gt;before_first_event&lt;/code&gt; 函数的注释写得很准：它执行的是一个手势在第一个指针事件之前该做的事，到那时为止什么都还没到达目标，所以拒绝或超时都留下一个未送达的手势。失败时它会打上 &lt;code&gt;not_delivered&lt;/code&gt; 的标记，而不是含糊地报「操作失败」。&lt;/p&gt;
&lt;p&gt;数据出站这侧，它有 trace 脱敏：命中敏感键名（text、value、username、label、query、secret、token、password、title、url 等）的字段值会被替换成一个 &lt;code&gt;{&amp;quot;redacted&amp;quot;: true}&lt;/code&gt;。但它的 FAQ 里有一句我认为所有做 computer use 的人都该记住的话：脱敏不能让一张截图变得可以公开。屏幕上的内容就是用户的内容，这个性质不会因为你处理了 JSON 字段而改变。另外它明确写了，原生 CLI 不需要任何远程模型服务，仓库里只有那一个可选的 helper 会把屏幕描述发给配置好的模型 provider。&lt;/p&gt;
&lt;h2 id=&#34;工程质量400-行上限和那个会自检的脚本&#34;&gt;工程质量：400 行上限，和那个会自检的脚本&lt;/h2&gt;
&lt;p&gt;这段是我在写这篇时唯一一个先猜错、然后被打脸的地方，值得记下来。&lt;/p&gt;
&lt;p&gt;我先按文件行数排了个序，看到一批文件整齐地停在 400 行，包括平台适配器里那些明显还能拆的模块，于是判断「这里有个 400 行的软约定」。然后我用一条临时的 awk 管线数「超过 400 行的文件有几个」，得到 2。这个数字我先记下了。&lt;/p&gt;
&lt;p&gt;后来实测重数，结论是：2,741 个 Rust 文件里，&lt;strong&gt;超过 400 行的有 0 个&lt;/strong&gt;，恰好停在 400 行的有 12 个。我那个 2 是错的，原因是我的管线把 &lt;code&gt;wc -l&lt;/code&gt; 输出的 &lt;code&gt;total&lt;/code&gt; 行和几处统计口径混在了一起。规则本身则比我猜的硬得多，它明写在 &lt;code&gt;AGENTS.md:158&lt;/code&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;400 lines at most per hand-written &lt;code&gt;.rs&lt;/code&gt; file&lt;/strong&gt; (&lt;code&gt;scripts/check-rust-file-size.sh&lt;/code&gt;). Split by responsibility.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;带 &lt;code&gt;@generated&lt;/code&gt; 标记的生成文件豁免，探针脚本豁免，C 头文件由 cbindgen 生成。而且有脚本在 CI 里真的跑，跑出来是 exit 0。&lt;/p&gt;
&lt;p&gt;真正值得抄的是那个脚本本身，它有三处讲究。&lt;/p&gt;
&lt;p&gt;第一，它用 awk 数记录，不用 &lt;code&gt;wc -l&lt;/code&gt;。脚本里的注释解释了原因：&lt;code&gt;wc -l&lt;/code&gt; 数的是换行符个数，一个 401 行、末尾没有换行的文件会被少算一行，从而从行数上限里溜过去。awk 数的是记录，没换行的最后一行也是一条记录。&lt;/p&gt;
&lt;p&gt;第二，脚本在扫描之前先做一轮自检，往规则里灌入违规样本，验证它还能抓到东西。理由写得更狠：一个已经检测不到任何东西的规则，必须让这道闸门失败，而不是静默通过。&lt;/p&gt;
&lt;p&gt;第三，这个「不能静默通过」的执念有出处。脚本注释里记着一件真实事故：那条禁止行内注释的扫描器，它自己的测试套件从来没被执行过，因为 CI 里 unittest 的发现范围被限定在 &lt;code&gt;tests/e2e&lt;/code&gt; 和一个 &lt;code&gt;test_*.py&lt;/code&gt; 的模式上，那个测试文件名不匹配。结果是 7 个文件里 27 处注释违规一路进了 CI，而扫描器一直显示自己是绿的。&lt;/p&gt;
&lt;p&gt;顺着这个思路看它的其它约定就顺了：非测试代码禁止 &lt;code&gt;unwrap()&lt;/code&gt; / &lt;code&gt;expect()&lt;/code&gt; / &lt;code&gt;panic!&lt;/code&gt;（在 &lt;code&gt;main.rs&lt;/code&gt; 里用 clippy 的 &lt;code&gt;deny&lt;/code&gt; 钉住）；每个结构体最多 7 个字段，每个函数最多 5 个参数；每个文件一个领域结构；每个命令一个同名文件；禁止行内注释，理由一句话，「名字承载含义」，只允许 public 项上的 &lt;code&gt;///&lt;/code&gt;。最后这条会逼着人把命名写到能自解释的程度，而不是靠注释补。&lt;/p&gt;
&lt;p&gt;测试的体量也对得上这种纪律：&lt;code&gt;#[test]&lt;/code&gt; 出现 6,850 次，&lt;code&gt;*_tests.rs&lt;/code&gt; 文件 1,029 个，测试代码 164,643 行，占全部 Rust 代码的 44.7%，测试与非测试的行数比接近 0.81 比 1。而且命令模块禁止写 inline 测试，必须用 &lt;code&gt;#[path = &amp;quot;foo_tests.rs&amp;quot;] mod tests;&lt;/code&gt; 挂兄弟文件，这条也有测试在断言。&lt;/p&gt;
&lt;h2 id=&#34;装上试一下&#34;&gt;装上试一下&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm install -g agent-desktop
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop version
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop status
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop skills get desktop
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;npm 包的 &lt;code&gt;postinstall&lt;/code&gt; 会把 Node 启动器替换成原生二进制的链接，之后每次调用约 21 毫秒；如果你的包管理器屏蔽了 install 脚本（npm 12 起默认屏蔽），启动器兜底也能用，代价是每次约 146 毫秒。想从源码构建需要 Rust 1.89+。&lt;/p&gt;
&lt;p&gt;平台要求是三行：macOS 13+（Intel 或 Apple Silicon），Windows 10 1809+ 或 Server 2019+（x64 或 ARM64），Linux 需要 Ubuntu 24.04+、glibc 2.35 以上、一个跑着 AT-SPI 总线的图形会话。macOS 上需要授予辅助功能权限。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;agent-desktop skills get desktop&lt;/code&gt; 这条值得单独说：它打印的是一份写给 agent 看的技能文档，让 agent 自己学会怎么用这套命令，而不是让你去给 agent 解释。同 lab 还有一条 &lt;code&gt;npx skills add&lt;/code&gt; 的装法。&lt;/p&gt;
&lt;p&gt;第一次跑就三行，拿 Finder 开刀：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop snapshot --app Finder -i     &lt;span style=&#34;color:#75715e&#34;&gt;# 每个可操作元素带 ref&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop click @s8f3k2p9:e8           &lt;span style=&#34;color:#75715e&#34;&gt;# 用上一轮的 ref 点一下&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop snapshot --app Finder -i     &lt;span style=&#34;color:#75715e&#34;&gt;# 再读一次验证&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;有两个坑先讲清楚。第一，&lt;code&gt;--help&lt;/code&gt; 会列出一些当前平台根本不支持的表面，比如 Linux 上 start-menu 和 system-tray-overflow 都列在那儿但会拒绝执行，taskbar 永远拒绝。第二，它说的 headless 专指「不移动指针、不抢焦点」，不代表不需要显示环境：一个没有那个图形会话的 TTY 或 SSH 环境，看不到它里面的应用。这两个词在别的工具里含义常常相反，读文档时要按它的定义来。&lt;/p&gt;
&lt;h2 id=&#34;它做不到什么以及不该夸大什么&#34;&gt;它做不到什么，以及不该夸大什么&lt;/h2&gt;
&lt;p&gt;拿不到无障碍树的界面它看不见。网页自动化明确不归它管。Wayland 下应用指针输入会拒绝，原生窗口控制不支持。GTK4 应用会拒绝聚焦调用，得改用 focus-window。&lt;/p&gt;
&lt;p&gt;还有一条值得引用的话，写在它的 JSON 输出文档里：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ok: true means the command returned a delivered or already-satisfied result, not that the application effect was verified.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;ok: true&lt;/code&gt; 只说明这次调用送达了或者本来就已满足，不说明应用层面的效果被验证过。同页还有一句，说那个结果和验证过的元素状态都不能证明应用级别的持久化。一个工具肯在自己的文档里写清成功标志的边界，比多写十页宣传有用。&lt;/p&gt;
&lt;p&gt;锁屏状态下的行为我在源码和文档里都没找到明确声明，这里就不猜了。&lt;/p&gt;
&lt;h2 id=&#34;我的判断&#34;&gt;我的判断&lt;/h2&gt;
&lt;p&gt;把它和上个月那篇 Neko 放在一起看，桌面 agent 这条路现在有两条岔路：一条是把界面变成像素流，让模型的视觉能力去理解它；另一条是把界面变成结构化数据，让模型只做决策。Neko 那条路赢在通用性，什么界面都能看；agent-desktop 这条路赢在可靠性，能看到的界面它不会看错。&lt;/p&gt;
&lt;p&gt;两条路的胜负手不在模型能力，在于你要操作的界面有没有把无障碍树暴露出来。有，第二条路几乎全面占优，因为它连点击的坐标都不需要猜；没有，你只能回到第一条路。&lt;/p&gt;
&lt;p&gt;而我在这份源码里看到的最有意思的东西其实不在 ref 机制上，是那份把自己的失效当成 bug 来对待的工程纪律。一个 400 行的文件上限、一条禁止行内注释的规则，配上一个会先给自己灌违规样本、验证自己还有效的检查脚本，再配上一段解释「为什么这件事必须这么较真」的事故复盘。这套东西和 agent-desktop 要解决的问题没有半点关系，但它是这份代码库能被信任的原因。要抄的话，我建议先抄这一段。&lt;/p&gt;
&lt;p&gt;想先看另一条路是怎么走的，见&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261024-neko-virtual-browser-teardown/&#34; &gt;《Neko 源码级拆解：把浏览器变成一条视频流，以及「只出像素」为什么是关键》&lt;/a&gt;；想让这类工具真的敢放手操作，前面还有一层审批与权限要解决，见&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261008-agent-approval-permission-layer/&#34; &gt;《「在聊天框里点确认」撑不住了：Agent 的审批和权限，正在长成独立的一层》&lt;/a&gt;；想先看清浏览器 agent 这个赛道的格局，见&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20260910-browser-agent-trio-comparison/&#34; &gt;《browser-use 114k⭐ 称王！浏览器 Agent 三强争霸》&lt;/a&gt;。&lt;/p&gt;
</description>
        </item>
        <item>
        <title>claude-mem 拆解：110k star 的记忆层，怎么把「会话里干过什么」捕获、压缩、回注进下一次会话</title>
        <link>https://www.yesmiracle.net/post/20261103-claude-mem-memory-layer-teardown/</link>
        <pubDate>Tue, 03 Nov 2026 00:00:00 +0000</pubDate>
        <author>admin@yesmiracle.net (万戈)</author>
        <guid>https://www.yesmiracle.net/post/20261103-claude-mem-memory-layer-teardown/</guid>
        <description>&lt;img src="https://www.yesmiracle.net/post/20261103-claude-mem-memory-layer-teardown/cover.svg" alt="Featured image of post claude-mem 拆解：110k star 的记忆层，怎么把「会话里干过什么」捕获、压缩、回注进下一次会话" /&gt;&lt;p&gt;claude-mem 现在的星数是 110,816，fork 9,661，Apache-2.0，TypeScript 写的，作者 Alex Newman，仓库 2025 年 8 月底建，版本号已经跑到 15.9.0。一句话说清它做什么：它挂进你的编码 agent 生命周期，把会话里每次工具调用捕获下来，交给一个后台进程用模型压成结构化的「观察」，下次会话开始时按预算把相关的那几条回注进上下文。&lt;/p&gt;
&lt;p&gt;记忆类工具今年不少，站内前面也拆过几个，所以先说清楚这篇的价值在哪。它的星数不构成理由。我拆它的理由是两个：第一，它把记忆这件事拆成了一条可以逐段验证的流水线，而这条流水线里最讲究的部分落在&lt;strong&gt;注入预算&lt;/strong&gt;这一层，压缩反倒更容易做对；第二，我在读源码的过程中核出四处文档和代码打架的地方，这四处比任何功能列表都更能说明一个十万星项目现在处在什么阶段。&lt;/p&gt;
&lt;p&gt;先说四条 hook 和三层存储的骨架，再说这几个打架的地方。&lt;/p&gt;
&lt;h2 id=&#34;捕获层五个-hook和一次-250-毫秒的捎信&#34;&gt;捕获层：五个 hook，和一次 250 毫秒的捎信&lt;/h2&gt;
&lt;p&gt;它的钩子注册在一个文件里，全部命令形如 &lt;code&gt;claude-mem hook claude-code &amp;lt;event&amp;gt;&lt;/code&gt;。五个事件各自干一件事：会话开始拉上下文、用户提交 prompt 时记录身份、每次工具调用后捕获观察、agent 停止生成时写会话摘要、会话结束时收尾。&lt;code&gt;PostToolUse&lt;/code&gt; 和 &lt;code&gt;PostToolUseFailure&lt;/code&gt; 共用同一个 handler，工具成功和失败都会被记下来，这点比只记成功要诚实。&lt;/p&gt;
&lt;p&gt;真正值得说的是它怎么做到「不拖慢你的 agent」。多数同类工具的做法是 hook 里发一个 HTTP 请求给后台服务，尽量做成非阻塞。claude-mem 不是这么做的，它连那个请求都不发。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/cli/spool-hook-event.ts&lt;/code&gt; 里，handler 只做两件事：把事件序列化落盘到一个 spool 目录，然后「戳」一下 worker 让它来取。那个戳的动作有一个 250 毫秒的超时，而且源码注释把边界划得很死：这个调用永远不启动 worker，也永远不因为失败而报错。配套的一行注释解释了这么做的原因：一次写 hook 已经把事件放进 spool 并开始捎信了，就让它落地，最多 250 毫秒。&lt;/p&gt;
&lt;p&gt;会话结束那条更极端。&lt;code&gt;session-end.ts&lt;/code&gt; 的文件头注释写着：Claude Code 给插件的 SessionEnd hook 只有 1.5 秒预算，所以这个 hook 只把请求放进 spool 并戳一下 worker，它从不等待 worker。也就是说，你的会话关掉之后，压缩和入库还在后台继续跑。&lt;/p&gt;
&lt;p&gt;这个设计带来一个直接的工程结论：&lt;strong&gt;捕获的可靠性不依赖后台进程活着&lt;/strong&gt;。后台挂了，事件还在磁盘上排队，等它起来再消费。&lt;/p&gt;
&lt;p&gt;捕获什么、跳过什么，代码里有五道过滤。递归调用自己的记忆工具会直接短路返回，避免自己记录自己。被排除的项目整个跳过。subagent 产生的观察会被丢弃，注释写的理由很实际：要在任何 worker 请求或 provider 调用之前丢掉它们，防止 Dynamic Workflows 的扇出把 provider 配额耗光。还有工具白黑名单，以及环境变量级的整体关闭。&lt;/p&gt;
&lt;p&gt;每条记录叫 observation，字段有十六个左右，包括标题、副标题、叙事、概念、读过的文件、改过的文件、prompt 序号，以及两个很关键的：&lt;code&gt;discovery_tokens&lt;/code&gt; 和 &lt;code&gt;occurrence_count&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;类型标签一共九种，权威定义在一个 route 文件的常量里：&lt;code&gt;decision&lt;/code&gt;、&lt;code&gt;bugfix&lt;/code&gt;、&lt;code&gt;feature&lt;/code&gt;、&lt;code&gt;refactor&lt;/code&gt;、&lt;code&gt;discovery&lt;/code&gt;、&lt;code&gt;change&lt;/code&gt;，加上 &lt;code&gt;security_alert&lt;/code&gt;、&lt;code&gt;security_note&lt;/code&gt;、&lt;code&gt;sensitive&lt;/code&gt;。要注意这九种是观察模型按提示词自己挑出来的，代码里没有按规则判定的启发式。提示词里那一段写得很明确，要求必须是这九种之一，不许有别的值。&lt;/p&gt;
&lt;p&gt;最后一种 &lt;code&gt;sensitive&lt;/code&gt; 的定义值得单独抄下来，原文列的是「内部 URL、未发布计划、个人细节、业务指标、客户或合作方名字」这类信息。它是个语义标签，跟密钥检测没关系。这一点后面讲隐私时会再碰到。模型也可以选择什么都不记，判据是回一句特定的跳过标记，理由写 &lt;code&gt;noise&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id=&#34;压缩层跑在-bun-里的后台-worker&#34;&gt;压缩层：跑在 Bun 里的后台 worker&lt;/h2&gt;
&lt;p&gt;后台进程是 &lt;code&gt;src/services/worker-service.ts&lt;/code&gt;。它导入了 &lt;code&gt;import type { Database } from &#39;bun:sqlite&#39;&lt;/code&gt;，也就是说它跑在 Bun 而不是 Node 上，这在 TypeScript 项目里是个明确的技术选择。&lt;/p&gt;
&lt;p&gt;端口这里有个坑，因为不少二手资料写错了。默认端口是 &lt;code&gt;37700 加上你的 uid 对 100 取模&lt;/code&gt;，流传较广的 37777 属于另一个组件，host observer。仓库自己的 UI 代码里还硬编码了 37777 当本地 worker 地址，两处不一致，可以算是它自己的小 bug。&lt;/p&gt;
&lt;p&gt;默认用于压缩的模型是 Haiku 别名，provider 默认走订阅制的 Claude。模型别名的解析在 &lt;code&gt;model-aliases.ts&lt;/code&gt;，层级路由的注释写着一句很实在的话：默认设置下，简单层和主模型都是 Haiku。&lt;/p&gt;
&lt;p&gt;压缩就是把原始工具调用变成结构化观察，靠的是一段提示词。它要求的输出骨架长这样：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;&amp;lt;observation&amp;gt;
  &amp;lt;type&amp;gt;[ decision | bugfix | feature | refactor | discovery | change | security_alert | security_note | sensitive ]&amp;lt;/type&amp;gt;
  &amp;lt;title&amp;gt;…&amp;lt;/title&amp;gt;
  &amp;lt;subtitle&amp;gt;…&amp;lt;/subtitle&amp;gt;
  &amp;lt;facts&amp;gt;&amp;lt;fact&amp;gt;…&amp;lt;/fact&amp;gt;…&amp;lt;/facts&amp;gt;
  &amp;lt;narrative&amp;gt;…&amp;lt;/narrative&amp;gt;
  &amp;lt;concepts&amp;gt;&amp;lt;concept&amp;gt;…&amp;lt;/concept&amp;gt;…&amp;lt;/concepts&amp;gt;
  &amp;lt;files_read&amp;gt;&amp;lt;file&amp;gt;…&amp;lt;/file&amp;gt;…&amp;lt;/files_read&amp;gt;
  &amp;lt;files_modified&amp;gt;&amp;lt;file&amp;gt;…&amp;lt;/file&amp;gt;…&amp;lt;/files_modified&amp;gt;
&amp;lt;/observation&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;输入那边是另一层包装，把这次工具调用包成一个「从主会话观察到的东西」的块，里面有工具名、发生时间、工作目录、参数和结果。参数或结果太长时会被截成头尾两段并插一个省略标记，而不是丢掉整条。这个细节说明它更怕丢信息，不怕截断。&lt;/p&gt;
&lt;p&gt;提示词里塞了一段系统身份，要求很直接：记录学到了什么、建了什么、修了什么、部署了什么、配了什么，而不是记录观察者自己在做什么。角色那段更狠，明确写着这个观察者是静默的、绝不去联系或通知任何其它 agent 或会话。&lt;/p&gt;
&lt;p&gt;把这两段放在一起看，能看出它的自我定位：它不是一个协作的 agent，它是一个躲在旁边的记录员。这个定位决定了它不参与你的任务，也因此不会改变你的任务结果。&lt;/p&gt;
&lt;h2 id=&#34;存储层三张表靠迁移长宽&#34;&gt;存储层：三张表，靠迁移长宽&lt;/h2&gt;
&lt;p&gt;数据库默认在 &lt;code&gt;~/.claude-mem/claude-mem.db&lt;/code&gt;，SQLite。三张主表：会话、观察、会话摘要。&lt;/p&gt;
&lt;p&gt;这里有个我觉得挺值得抄的细节。基础版的 observations 表非常窄，只有 id、会话 id、项目、文本、类型、时间这么几列。后来它靠一连串迁移把这张表重建了两次，才长成现在带标题、副标题、facts、narrative、concepts、读写文件列表、token 统计的样子。代码里是 &lt;code&gt;observations_new&lt;/code&gt; 这种重建写法。&lt;/p&gt;
&lt;p&gt;也就是说，它一开始只想存一段文本加一个类型，是后来才意识到「一条观察需要这么多维度才能被有效检索回来」。这个演化路径比最终的 schema 更有信息量。&lt;/p&gt;
&lt;p&gt;全文检索用的是 SQLite 的 FTS5，而且是外部内容表：索引表不存内容，指回 observations 表，配触发器同步。这个选择很省空间。有意思的是它还准备了一个探测语句，如果目标环境编译的 SQLite 不支持 FTS5，就降级到向量库加 LIKE 查询，而不是直接崩掉。&lt;/p&gt;
&lt;p&gt;向量层是可选的 Chroma，默认开、默认本地模式，靠一个 MCP 子进程管理，依赖 &lt;code&gt;uv&lt;/code&gt;。混合检索就是 FTS5 的关键词加 Chroma 的向量。&lt;/p&gt;
&lt;h2 id=&#34;回注层功夫在预算上&#34;&gt;回注层：功夫在预算上&lt;/h2&gt;
&lt;p&gt;回到我最开始说的那句话，这条流水线里最讲究的是回注，不是压缩。&lt;/p&gt;
&lt;p&gt;会话开始时注入的是什么，在 &lt;code&gt;AgentFormatter.ts&lt;/code&gt; 里有模板原文。它先给一个带项目名和时间戳的标题行，然后是模式名，接着一行说明格式，然后是统计行，形如「多少条观察、读了多少 token、干了多少 token 的活、省了多少、省了百分之几」。再往下是按天分组的日期标题，每一条是一行表格：ID、时间、类型图标、标题。最后一行告诉你还能怎么取更多。&lt;/p&gt;
&lt;p&gt;这个格式里藏着它的设计意图：注入给你的是一张&lt;strong&gt;索引&lt;/strong&gt;，不是原文。你知道有哪些活干过、大概是什么、花了多少 token，需要细节时再用 ID 去取。&lt;/p&gt;
&lt;p&gt;数字上，预算是硬性的：注入输出上限一万个字符，默认最多五十条观察，并且默认只包括当前这个 harness 的记录。候选的裁剪有一个专门的预算函数，还有一个终端横幅的预留量。&lt;/p&gt;
&lt;p&gt;三层检索的成本阶梯是它反复宣传的东西：第一层搜索返回 ID、标题、日期、类型的紧凑索引，每条约 50 到 100 token；第二层时间线给一条观察前后发生了什么，每条约 100 到 200 token；第三层按 ID 批量取全文，每条约 500 到 1000 token。README 里给的总账是省了大约十倍 token。&lt;/p&gt;
&lt;p&gt;这里我要把几个数字的性质说清楚，因为二手转述经常把它们混成一件事。 那个被引用最多的对照是：天真的做法一次性把三万五千 token 塞进上下文，其中真正相关的只有两千左右，信号率 6%。这个数字有一手出处，在仓库的 &lt;code&gt;docs/public/progressive-disclosure.mdx&lt;/code&gt; 里，配了一张 ASCII 图，还写了一句话：把 94% 的注意力预算浪费在不相关的上下文上。同一个文档的另一处写着「相关 token 占总上下文超过 80%」。&lt;/p&gt;
&lt;p&gt;但这两处的位置不一样。6% 那处是在&lt;strong&gt;描述现状&lt;/strong&gt;，80% 那处出现在一个&lt;strong&gt;评估标准或目标&lt;/strong&gt;的段落里。仓库里没有任何可复现的 benchmark 脚本或数据能证明 80% 达成过。所以准确的写法是：6% 是作者文档给出的现状描述，80% 是同一份文档给出的目标指标，而那个十倍的节省也只是 README 的说法，没有基准支撑。这三个数字都不该被当成实测结论引用。&lt;/p&gt;
&lt;p&gt;这不是要否定它的方向。索引加按需展开这个设计本身是对的，而且它连「取一条要花多少 token」都摊开告诉模型，这一点比多数记忆系统都诚实。我只是认为，一个项目自己文档里的目标值，不该在传播过程中长成实测值。&lt;/p&gt;
&lt;h2 id=&#34;它已经不只是一个本地插件了&#34;&gt;它已经不只是一个本地插件了&lt;/h2&gt;
&lt;p&gt;这是我读源码时发现的更大的变化，也是站内前面几篇记忆层文没有覆盖到的部分。&lt;/p&gt;
&lt;p&gt;仓库根目录下有三个服务目录：&lt;code&gt;services/sync-hub&lt;/code&gt;、&lt;code&gt;services/sync-api&lt;/code&gt;、&lt;code&gt;services/viewer-host&lt;/code&gt;。第一个是部署在 Cloudflare Workers 上的同步中枢，后两个部署在 Fly.io。客户端那侧，&lt;code&gt;CloudSync.ts&lt;/code&gt; 的文件头注释把协议写得很完整：数据库本身就是队列，每一行记忆带一个 synced_at 列，为空表示还没进中枢的日志；线上契约是往 &lt;code&gt;{hubUrl}/v1/sync/ops&lt;/code&gt; 发 POST，协议版本 2；单个正文最大 256,000 个 UTF-8 字节，单个请求压在 4 MB 和 500 个操作以下。&lt;/p&gt;
&lt;p&gt;服务端那侧有一个 2,363 行的 Postgres 路由文件，注册了二十六条 &lt;code&gt;/v1/*&lt;/code&gt; 路由，从用量、密钥、事件、观察、搜索、时间线一直到 MCP over HTTP，配了认证、限流、月度配额三个中间件。它还有一份 docker-compose，里面有 Postgres 和 Valkey。&lt;/p&gt;
&lt;p&gt;托管端点是 &lt;code&gt;sync.cmem.ai&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现在说边界。默认配置下云同步是关闭的，判据是中枢 URL 为空，源码注释就写着「空表示同步关闭」。默认运行时也还是本地 worker 而不是 server。所以「本地优先」这个说法在默认配置下是成立的。&lt;/p&gt;
&lt;p&gt;但边界文档和仓库内容之间有个张力，值得写出来。项目的许可文档和一份 IP 边界文档都明确说，托管的云、团队同步、管理后台、SSO、企业权限这些属于「保留在 Apache-2.0 公开实现之外」的区域。然而这些同步服务的代码就躺在公开仓库里，而且三个服务目录里都没有 LICENSE 文件，其中 sync-hub 的 &lt;code&gt;package.json&lt;/code&gt; 明确标着 &lt;code&gt;private: true&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我的判断是这样的：这不是偷偷闭源，因为代码确实公开可读；但它意味着这个仓库的许可覆盖范围比看上去要窄。Apache-2.0 那份 LICENSE 覆盖的是记忆引擎、CLI、SDK、适配器和 MCP 工具那一套，作者自己那份生产部署的产物既没有跟着开源协议，也没有标成私有仓库。对要拿它进企业的团队来说，这是一条需要自己走一遍法务的线：你能用的是引擎，不能默认那三个服务的代码也在同样的许可下。&lt;/p&gt;
&lt;p&gt;顺带一句成本上的一手数据：那个同步中枢的部署文档里写了它的降级路径成本，大约每人每月三分钱。这是个很小的数目，但它是作者自己写在仓库里的，比任何推测都可靠。&lt;/p&gt;
&lt;h2 id=&#34;隐私默认值选得好但有两个开关要自己想清楚&#34;&gt;隐私：默认值选得好，但有两个开关要自己想清楚&lt;/h2&gt;
&lt;p&gt;默认不出数据，这一条是站得住的。默认运行时是本地 worker，云同步 URL 为空，截图和图像推理默认关，媒体采集默认关。&lt;/p&gt;
&lt;p&gt;但有两个开关的默认值会让不读文档的人意外。&lt;/p&gt;
&lt;p&gt;第一个是脱敏。它内置了二十多个厂商的密钥正则，从 AWS 的这两个前缀、GitHub 的几种 token、OpenAI 和 Anthropic 的 key、Slack、Stripe、Google、GitLab、HuggingFace、npm、Docker、Shopify、PyPI，一直到它自己的 key 前缀，写得相当全。还有一层低置信的通用规则（密码、通用 token、Bearer、Basic），默认关。超长字段会整体替换成一个 oversize 标记，属于失败时朝安全的方向倒。&lt;strong&gt;但整个脱敏功能默认是关的&lt;/strong&gt;，源码那一行的注释解释了原因：默认 false，这也是面向用户的文档默认值。&lt;/p&gt;
&lt;p&gt;第二个是 &lt;code&gt;sensitive&lt;/code&gt; 这个类型。前面说过它只是个语义标签，由模型判定。而配置里它被用作推送触发条件：Telegram 触发器默认包含 &lt;code&gt;security_alert&lt;/code&gt; 和 &lt;code&gt;sensitive&lt;/code&gt;，Grok bot 的 awareness 推送默认包含四种，其中有 &lt;code&gt;sensitive&lt;/code&gt;。Grok bot 那条会把这类观察追加写进一个按月组织的 markdown 日志里，默认开着。&lt;/p&gt;
&lt;p&gt;我没有在同步或压缩路径里找到任何「把 sensitive 类型排除在外」的过滤逻辑，也没有找到加密相关的代码。同步走 HTTPS 加 Bearer token，本地库是明文 SQLite。&lt;/p&gt;
&lt;p&gt;所以结论是：&lt;strong&gt;默认配置是收敛的（都关），但如果你同时打开云同步又不开脱敏，你等于把原始的工具调用内容推到了外部基础设施上，而那个 &lt;code&gt;sensitive&lt;/code&gt; 标签不会替你挡住任何东西。&lt;/strong&gt; 它挡的是「模型下次不该把这条带进新任务」，不是「这条不该出你的机器」。这两件事长得很像，但完全不是一回事，我认为这是这类工具最容易被误读的一点。&lt;/p&gt;
&lt;p&gt;还有一份代码值得给个好评：&lt;code&gt;PrivacyCheckValidator.ts&lt;/code&gt; 只有五十一行，它区分两种情况，一种是用户 prompt 的记录行根本不存在（注释明确写着这不是隐私信号，放行并告警），另一种是行存在但被剥空了（说明用户真的隐藏了这一轮，抑制）。能把「数据缺失」和「用户主动隐藏」分开处理，是很细的地方。&lt;/p&gt;
&lt;h2 id=&#34;工程体量和昨天那篇正好是两个极端&#34;&gt;工程体量：和昨天那篇正好是两个极端&lt;/h2&gt;
&lt;p&gt;这里可以直接和昨天那篇对照。&lt;/p&gt;
&lt;p&gt;claude-mem 的源码是 654 个 TypeScript 文件、159,659 行。测试是 1,156 个文件、219,924 行，&lt;strong&gt;测试比源码多出 38%&lt;/strong&gt;。而且我去看了测试目录里装的是什么：最大的一项 &lt;code&gt;tests/worker/&lt;/code&gt; 占 62,235 行，而 fixtures 只有 1,555 行。也就是说那 22 万行里绝大部分是按源码模块镜像写出来的测试代码，不是测试数据。这个比例不是摆设。&lt;/p&gt;
&lt;p&gt;体量上它高度集中：&lt;code&gt;src/services/&lt;/code&gt; 一个目录占全部源码的 54%。最大的单文件是 &lt;code&gt;src/services/sqlite/SessionStore.ts&lt;/code&gt;，5,055 行；第二是 npx 安装器的 &lt;code&gt;install.ts&lt;/code&gt;，3,548 行；后面还跟着一串两三千行的文件，从 worker 工具、Postgres 路由、Chroma 同步，一直到 worker 主服务。&lt;/p&gt;
&lt;p&gt;行数约定方面，它有 109 个源文件超过 400 行，最大的七个都超过两千行。也就是说没有任何文件长度约束。&lt;/p&gt;
&lt;p&gt;而昨天那篇的 agent-desktop 是另一个极端：2,741 个 Rust 文件，最大值恰好 400 行，超过 400 行的 0 个，而且这条规则被写进开发规范、由一个会给自己灌违规样本的脚本在 CI 里强制。&lt;/p&gt;
&lt;p&gt;这两个极端没有谁对谁错。一个 400 行硬上限的代码库，说明它把「可被人逐行读完」当成前提；一个单文件 5,055 行的代码库，说明它把「改动的速度和覆盖面的完整」放在了前面。前者的更新频率应该很慢，后者的更新频率很快：claude-mem 的 CHANGELOG 已经 9,308 行，最近几天几乎每天都在发新版本，11 月 1 日到 2 日之间发了五个版本。&lt;/p&gt;
&lt;p&gt;近每日多次发布加上二十二万行测试，这两个数字是配套的。发得快就得测得多，否则回归面撑不住。这一点是它工程上最值得认可的地方。&lt;/p&gt;
&lt;h2 id=&#34;它能挂在哪些-agent-上&#34;&gt;它能挂在哪些 agent 上&lt;/h2&gt;
&lt;p&gt;安装是一条命令加一个 IDE 参数，装完会在宿主的配置里写两处：指定 claude-mem 作为记忆 provider，以及注册它的 MCP 服务。&lt;/p&gt;
&lt;p&gt;有一点很多人会踩：&lt;code&gt;npm install -g claude-mem&lt;/code&gt; 只装 SDK 和库，它不会帮你注册 hook，也不会起 worker 服务。要真正用起来得走安装器。&lt;/p&gt;
&lt;p&gt;它宣称支持的宿主里包括 Hermes，所以我专门看了那份集成。它是 642 行纯标准库的 Python，一张明确的事件映射表：轮次开始映射到初始化会话、工具调用后映射到记录观察、预取映射到语义检索、系统提示词块映射到取注入上下文，连宿主自己写的记忆笔记都会被转成 claude-mem 的笔记。超时给得很克制：写 5 秒、读 3 秒，连续失败 3 次就开 30 秒熔断；所有写操作走一个后台线程保证顺序，注释写着宿主永不等待它们。这份集成来自社区，署名和主项目作者并非同一人。&lt;/p&gt;
&lt;p&gt;这里也有一处文档与实现对不上：README 的徽章里列了 Copilot，但仓库根目录下没有对应的集成目录。&lt;/p&gt;
&lt;h2 id=&#34;文档和代码打架的四处&#34;&gt;文档和代码打架的四处&lt;/h2&gt;
&lt;p&gt;把这几处集中列一下，因为它们的性质不一样。&lt;/p&gt;
&lt;p&gt;工具数量。README 说四个 MCP 工具，正文里只点出了三个名字。而源码里实际注册了二十六个，多出来的包括语料库系列、工作状态、团队检索这些。这个不一致属于纯文档没跟上代码。&lt;/p&gt;
&lt;p&gt;端口号。默认 worker 端口是 &lt;code&gt;37700 + uid 对 100 取模&lt;/code&gt;，不是流传较广的 37777，后者是另一个组件的。而且仓库自己的 UI 里还硬编码着 37777，属于代码内部的不一致。&lt;/p&gt;
&lt;p&gt;许可边界。前面说过，这是四处里唯一有实质影响的一处，因为它直接影响你能不能把某段代码当成 Apache-2.0 来用。&lt;/p&gt;
&lt;p&gt;默认模型。网上不少介绍会说它默认用某个便宜的 Gemini 小模型来压缩。代码里的默认是 Haiku，Gemini 那条线只有在你显式选择 provider 时才用。我在仓库里没有找到任何支撑「默认 Gemini 加对应成本」的说法，唯一出现过那个模型名的地方是一个第三方的模型流行度数据文件。这属于二手传言。&lt;/p&gt;
&lt;p&gt;把这四处放在一起，我的判断是：一个十万星、近每日多次发布的项目，文档已经明显跟不上代码了。前两处只是需要读者自己核一下源码；第三处需要读它的人自己判断合不合规；第四处说明这个项目的传播量已经超过了它自己能维护准确度的范围。这不是它的独有问题，任何涨到十万星的工具都会经历这一阶段，但作为要用它的人，你得知道自己在读哪一版真相。&lt;/p&gt;
&lt;h2 id=&#34;我的结论&#34;&gt;我的结论&lt;/h2&gt;
&lt;p&gt;站内前面拆过记忆层的三个方向：腾讯那套四层金字塔管线、用明文文件树当第三极的做法、以及一篇把记忆拆成四层的通用工程方案。claude-mem 的路子是第三种形状：不去设计记忆的层次，而是设计一条&lt;strong&gt;从会话到下一次会话的流水线&lt;/strong&gt;，把中间每一步都做成可以单独替换和验证的环节。&lt;/p&gt;
&lt;p&gt;它的价值我认为集中在两点。第一，捕获那一层的 spool 设计是对的：把可靠性从「后台进程活着」里解耦出来，代价只是一次 250 毫秒的捎信。第二，注入那一层的索引化是对的：给你一张带 token 成本的地图，而不是一堆原文。&lt;/p&gt;
&lt;p&gt;它需要你留心的地方也很清楚。文档里那几个漂亮的百分比，性质各不相同，其中至少一个是目标值。以及那条「引擎开源、部署产物不开源」的边界，落在同一个仓库里，却没有许可证跟着。&lt;/p&gt;
&lt;p&gt;真正决定这类工具好不好用的，从来不在「记不记得住」上，而在它记下了什么、什么时候把什么拿出来、以及那些被记错的东西会不会被反复放大。它在这三件事上前两件做得比多数同类扎实，第三件我还没看到明确的治理机制：仓库里有近重复检测和合并，但我没有找到针对过时记忆的保留策略或半衰期逻辑。这大概是记忆层下一个要打的仗。&lt;/p&gt;
&lt;p&gt;想把记忆拆成四层的通用工程方案先看一遍，见&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261020-agent-memory-system-engineering/&#34; &gt;《Agent 记忆系统设计：从上下文窗口到跨会话持久化，一套能落地的工程方案》&lt;/a&gt;；想知道另一条完全不同的路（不建库、直接写明文文件树）怎么走，见&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261013-agent-memory-plaintext-file-tree/&#34; &gt;《AI Agent 记忆层的「第三极」：明文文件树用 200–900 个 token 记住一切》&lt;/a&gt;；而记忆再多也救不了长程会话的思维死局，那个问题见&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261030-context-inertia-long-running-agents/&#34; &gt;《长程运行的 Agent 如何保证不会陷入思维死局》&lt;/a&gt;。&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
