Longhand 是一个本地 AI 记忆工具,读取 Claude Code transcript 存入 SQLite + ChromaDB;作者 4 个月内迭代 28 次解决「自信给错误答案」的归因 bug。
四月的一天,我问自己的记忆工具,想看看一个当天下午刚处理过的项目进展到哪里了。
recall_project_status("bsoi-mesh-kit") → "No session history found for this project."
磁盘上有四份转录。其中一份有 2526 行。
我以为找到了一个 bug。实际上我看到的是困扰我四个月的 bug——它换了各种马甲出现,历经 28 个版本。
它从不崩溃。从不丢数据。只是自信满满地给出一个错误的答案。
这个工具是什么,一段话说清
Longhand 是一个 Python CLI 和 MCP server,它读取 Claude Code 的会话转录(~/.claude/projects/**/*.jsonl),把所有工具调用、文件编辑和思考块索引到 SQLite + ChromaDB,让你对整个历史做语义检索。纯本地、零 API 调用、不做任何摘要。一句话定位:模型不需要背住记忆——让磁盘来背。pip install longhand。
这是我最初打算做的部分。这篇文章其余的部分是我没有计划到的。
那份 2526 行的会话索引没问题。每个事件都在 SQLite 里。问题是归属:工具判断一个会话属于哪个项目,是通过看第一个事件的工作目录。
我大多数时候从 $HOME 启动会话,然后 cd 进入项目。所以第一个事件的 cwd 是 /Users/natenelson,这个会话被归档到了空——project_id → NULL。所有按项目查的请求都找不到它。
修复方案是统计所有事件的工作目录,排除 $HOME 和没有项目标记的目录,然后取众数。回头看显而易见。有意思的不是修复——而是工具把这个报成"找不到会话历史"而不是"我有这个会话但不知道归到哪个项目"。这是两句完全不同的话。只有前一句会把你引向正确的查找方向。
两个月后,我发现主目录显示的会话数量不可思议地高。我查了一下 sessions 表。
2068 条"会话"对应 264 条真实的。53952 次文件编辑对应大约 7200 次实际编辑。
原因几乎让人啼笑皆非:upsert_project() 在每次摄入会话时都会增加计数器。同一份会话会被摄入多次——SessionEnd 钩子、实时尾随钩子的分析过程,以及任何协调重摄入都会触碰同一行。那些列不是在统计会话数,而是在统计我查看某份会话的次数。
同一个版本还修了一个相关问题:session.cwd 和 project_id 是两条独立代码路径写入的,可能失去同步,所以 265 个会话中有 35 个(13%)被归档到了错误的项目。
原始数据从未受影响。搜索和检索都正常。但工具展示给自己的每一个数字都被夸大了,而且已经持续了好几周,因为系统中没有任何地方在检查派生数字和源数字是否一致。
这个版本改变了我对这个问题的思考方式。
工具报告了一个"解决率"——你的会话中发现的问题最终被修复的比例。我的看起来很难看。大约三分之一。
分母是错的。它把每一次低置信度提取都算了进去——探测、工具抖动、只是包含了"error"这个词的行。这些不是我没有解决的问题;它们根本不是问题。按照我现在的语料库,把它们排除后,真实的数字是 501 个实质性事件中有 423 个被解决——84%。
我花了好几周以为自己的解决率很平庸,因为自己的工具自信满满地告诉我是这样。
这让整个项目的认知都重新框架化了。我之前把诚实理解成一个方向——不要夸大、保守估计、往少了说。不是这样的。诚实意味着准确。低估真相的工具和夸大真相的工具说谎程度完全一样,而且更难被发现,因为低估听起来像是谦虚。
还有两个同类型的:
错误计数被搜索结果放大了。如果你 grep 代码库,搜索结果中包含字符串 Error:,提取器就把它算作你遇到的一个问题。其实不是。那只是搜索命中。修复方案是让错误检测感知到是哪个命令产生了输出。
钩子静默失败。如果一个钩子死掉了,它会连带你整个会话的摄入一起完蛋,然后你几周后才在检索结果稀少时发现。0.13 让每个钩子失败都 exit 0——永远不打断用户的 prompt——但在磁盘上留一个面包屑,并在 longhand doctor 里写一行记录。失败可以。静默失败不行。
"今天"不是你的今天。检索窗口锚定在 UTC。如果你不处于 UTC 时区,问"我今天做了什么"会静默切掉你自己的上午。
到了 1.0 我以为已经学到了教训。然后我读到了在 0.13 加入的那一行——用于暴露钩子失败的:
⚠ 23 in the last 7 days — see ~/.longhand/logs/hook-errors-*.log;
longhand reconcile --fix heals missed ingests
问题是 reconcile 是通过遍历磁盘来找工作的。而这 23 个失败中有 21 个是 missing-transcript——那些转录文件根本没有落到磁盘上的会话。我查了:所有 21 个文件今天仍然缺失。它们从来没有存在过,以后也不会。
所以我专门为了诚实报告失败而加的那一行,对它报告的 23 件事中的 21 件推荐的是一个空操作。它没有说任何假话。准确地讲。它只是自信地指向一个根本不可能有帮助的命令。
修复方案是把补救措施按失败类别分开。只有一种类别实际可修复。其他的现在显示"这些从未被写入——没什么可修复的",听起来不那么有帮助,但更简短,而且是真的。
Longhand 1.0.0 发布后。一小时内,在自己的语料库上dogfooding又发现了三个。
最好的那一个已经在我的笔记里躺了好几周,标注着"原因不明,不阻塞"。doctor 版本行说的是:
Version ⚠ could not reach pypi.org (offline?)
而从同一个终端执行 curl https://pypi.org/pypi/longhand/json 返回 200。
从来不是网络问题。在 python.org 的 macOS 构建上,urllib 验证证书针对的是 OpenSSL 自己的信任存储,而不是系统密钥链,所以请求会报 CERTIFICATE_VERIFY_FAILED。一个 except Exception: return None 吞掉了真正的错误,而我写的消息猜的是"离线"——这让我一次又一次地去调试一个根本没问题的网络。
这个猜测让我好几周都不知道更新检查在 macOS 上从未起过作用。在最常见的 macOS Python 环境中,从来没有人被告知过有新版本存在。
这些没有产生任何堆栈跟踪。没有崩溃。没有丢失一个字节。日志里没什么可 grep 的,因为从程序的角度看什么都没出错。
它们能存活是因为一个听起来对的错误答案不会被调查。"离线?"是合理的。"找不到会话历史"是合理的。三分之一的解决率是合理的——虽然让人沮丧,但是合理。每一个都通过了嗅觉测试,而这恰恰就是它们每个都持续了好几周的原因。
它们背后的模式都是一样的:探测失败了,然后程序描述的是世界而不是描述探测本身。证书检查失败 → "你离线了"。磁盘查找没找到 → "历史不存在"。每一个都是程序在没有任何证据的情况下推断了一个原因。
防御方法不是更好的逻辑。是拒绝在证据之外回答。"pypistats.org 无法访问"是真的有用的。"下载数据不可用"既不真也不有用——这是一个从一个失败请求对世界做出的推断。
我对这个措辞非常清楚 precisely because 我在写这篇文章的时候又犯了同样的错。问有多少人在安装 Longhand,我查了 pypistats API,什么都没查到,然后报告说下载数据是黑的。不是的。PyPI 项目页面一直有那些数字。同样的 bug,出现在研究这篇关于 bug 的文章的过程中,距离发布修复了三个实例的版本才过去四个小时。
这听起来像个奇怪的发布,直到你注意到这就是 1.0 应该做的事。你不能一边承诺"什么都不消失"一边还带着四个你完全打算删除的东西。所以你删掉它们——先在一个完整版本的警告之后——然后再做出承诺。
这个承诺是五个具体承诺,每个都在仓库里有具名的执行产物(COMPATIBILITY.md):
表面稳定 — CLI 和 MCP 在 1.x 内冻结;删除只在 2.0,且只有在提前一整个 minor 版本警告之后。
数据前向兼容 — 由 0.11+ 写入的数据库在任何后续 1.x 上都能打开。测试套件里有一份真实的 v0.11.2 schema dump,由执行那个 tag 自己的迁移代码生成,可以证明这一点。
钩子保证 — 钩子永不抛异常、永不联网、永不阻塞你的 prompt。
上游漂移永不正沉默 — 未知转录条目被保留、在 doctor 中暴露、并有回归测试把关。
诚实指标 — 计数反映真实信号,且没有任何东西会推荐一个不可能起作用的补救方案。
承诺 5 是整个这段历史付出代价的那一条。这是唯一一个我在四月会当作"承诺"一笑置之的。
没有执行产物的承诺只是一个愿望,所以每一个承诺都点名了当它破坏时会让哪个测试或守卫失败。这才是 1.0 真正的交付物——不是功能,而是一组别人可以验证的声明。
28 个发布版本,4 月 15 日到 8 月 12 日。1.0 之前有九条 minor 线。
546 个测试。Python 3.10 到 3.14,全部在 CI 中把关。
上月 522 次下载,上周 134 次。
最后一对数据有意思。12 个 star 对比几百次月度安装——没有人给一个记忆工具点 star,他们安装了就忘了它正在运行。如果你用 star 来评判自己的项目,你在看错误的仪表。
真实版本的趋势是这样的:5 月约 175/周,现在约 134/周。三个月轻微下滑,期间我做了零分发工作——没有 Show HN、没有 newsletter、没有帖子。这是一条休眠渠道的样子,不是对工具的裁决。我宁愿把它印出来也不假装曲线在往上拐——这是我 4 月写的版本。写的时候是真的。只是我从未回去核实。
pip install longhand
longhand setup
setup 会回填你现有的 Claude Code 历史,安装钩子,并注册 MCP server。可以安全地重复运行。
longhand recall "that webhook fix from last week"
longhand doctor # 如果它告诉你有问题,现在它说的是真的了
MIT 许可。Python 3.10+。零 API 调用。所有东西都留在你本地。
仓库:github.com/Wynelson94/longhand
如果这四个月有什么值得偷走的话:去读一读你累的时候写的那些错误消息。不是逻辑——是消息。找到你代码中每一个在猜测原因而不是报告观察到的东西的地方。我的 bug 就藏在那里,28 个版本都是。
这篇文章的信息来源是仓库:github.com/Wynelson94/longhand/blob/main/docs/devto-v1-post.md。编辑走 git。