Trelix v2.7 到 v2.9 踩坑实录
最离谱的一次是 v2.7.0 发布时,我盯着 GitHub Release 页面看了半天,发现本该有三个二进制文件的地方只有两个。原因极其低级:release.yml 在构建 macOS 和 Linux 的 PyInstaller 二进制文件时,输出路径都叫 dist/trelix。而 softprops/action-gh-release 这个 Action 是按文件名上传的,导致两个同名文件直接打架,后上传的覆盖了先上传的。最绝的是,Workflow 显示绿色通过,但我根本分不清最后留在页面上的那个文件到底是哪个系统的。
为了修这个破洞,我在接下来的两周里疯狂发了 6 个版本。这次迭代我没打算按 changelog 顺序写,直接聊聊几个硬核的坑。
一、 交付基建的“隐形”崩溃

二进制文件冲突在 v2.7.1 解决了,方法很简单:在上传前给每个 binary 强制重命名,加上 OS 标识。但顺藤摸瓜发现,PR 阶段的 CI 流程竟然一直没跑 Linux 构建,只有打 Tag 发布时才跑。这导致很多 Linux 平台的兼容性问题在开发阶段完全不可见,我赶紧补了 matrix 矩阵配置。
还有一个让我脸红的细节:trelix-mcp 的测试套件在 CI 里竟然一直没运行。结果一个回归 Bug 潜伏了很久——测试用例里写着“MCP server 必须恰好有 6 个工具”,但实际上 v2.5.0 之后已经注册了 8 个。最可怕的是,这个测试居然一直显示 Pass。这种“错误的通过”比直接报错更危险。
二、 压力测试挖出的 5 个并发 Bug

在 v2.7.2 中,我尝试引入 Qdrant Cloud 支持和并行 BM25 读取池,但这次我没敢直接信任 check_same_thread=False,而是写了专门的压力测试。结果直接挖出 5 个并发 Bug:
1. TOCTOU 竞态条件:在稀疏嵌入模型的懒加载逻辑中,代码先检查模型是否加载,然后再获取锁。结果两个线程同时看到“未加载”,然后同时触发加载流程。
解法: 引入双重检查锁定(Double-Checked Locking)。
if not self._model_loaded:
with self._lock:
if not self._model_loaded:
self._load_model()2. MCP 标准输出冲突:并发写入通知时,部分 JSON-RPC 行被交织在一起,导致客户端解析崩溃。
解法: 给 write 和 flush 操作加整体锁。

3. 订阅注册表内存泄漏:客户端如果异常,会导致注册表无限增长。
解法: 设定 max-subscriber 上限并引入 TTL 扫描机制。
三、 极其隐蔽的图数据损坏
最让我后怕的是一个关于外键损坏的 Bug。在部分重新索引(partial re-index)时,我将父符号、调用关系等列设为 ON DELETE SET NULL。
逻辑看似安全,但实际情况是:当你删除一个修改过的旧符号行时,所有引用该行的记录都会被静默地设为 NULL。这意味着即使某些行根本没被编辑,它们的图边(graph edges)也会在不知不觉中被切断。没有报错,没有日志,只有随着文件修改而不断累积的空指针。
为了解决这个问题,我重构了索引更新机制,改为先快照(snapshot)再重新映射,确保在删除旧行之前,所有的关联关系已经正确迁移到了新行。
这次迭代让我意识到,对于一个复杂的 AI Agent 或检索系统,代码的正确性仅仅是第一步,并发控制和交付链路的鲁棒性才是决定它能否在生产环境活下来的关键。
