-
## 病症(真跑实测) 2026-08-20 那条 run 的 timeline 里,**每个节点**的产物索引写入都失败: node.output | 产物索引写入失败 contract.json: UNIQUE constraint failed: index 'idx_graph_files_path' node.output | 产物索引写入失败 sdd.md: UNIQUE constraint failed: index 'idx_graph_files_path' 19 次 agent 调用、每次两三个文件,无一成功。 ## 根因:加了新索引、没删旧索引 产物索引加「版本」之前,唯一索引是无条件的 `UNIQUE(company_id, COALESCE(task_id,''), rel_path)`。加版本之后建表块换成了 带谓词的 idx_graph_files_current(WHERE superseded_by IS NULL),**但没人删旧的那条** —— `CREATE ... IF NOT EXISTS` 只管不重复建,不会删已有的。 于是 recordFile 那套「先把旧行 superseded_by 指向新行、再插新行」的版本化写法 在存量库上整个失效:旧索引不认 superseded_by,同路径第二行直接撞。 而重试恰恰会让同一条 rel_path 被产出第二次。 ## 失效方向:静默 writeFileIndex 把 recordFile 的抛降级成一条 timeline,**节点照样 success**。 后果是产物索引在任何有重试的 run 上都不全 —— 而下游节点正是靠这份索引 按 kind 找上游产物。不报错、不影响测试、git status 干净。 ## 为什么没有任何测试照到 新建库压根没有那条索引(建表块里早就只剩 current 那条),**只有存量库会踩**。 这是同一类问题的**第三次**(tasks 加列的索引顺序、墙钟锚点迁移、这次)。 判据固定成一句话:**凡是碰 schema,「存量库重启」那条路必须单独走一遍。** ## 修 + 验 迁移里一句 DROP INDEX IF EXISTS idx_graph_files_path。 守卫 graph-files-stale-index.test.js 走存量库重启那条路,并验版本化语义没被削弱 (同路径两行、当前版本恰好一条、旧行指向新行)。反向验过:不删就红。 生产库已实跑验证:删索引后同路径连产两次不再抛,graph_files 24 行未受影响 (备份 /tmp/graph.sqlite.pre-idxdrop)。 全量:141 文件 / 1300 测试全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>