• ## 病症(真跑实测)
    
    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>
    yibo authored
     
    Browse File »