Repository navigation
bug(run): 「code-intel .」撞 create repository authority root exit 74 os error 183——跨盘 symlink artifact root 触发 create_dir_all 假阳性 AlreadyExists #279
Description
问题
README/--help 的 quick start 是:
code-intel .
code-intel <path> --mode lite|normal|full
在本仓自身跑(code-intel . --mode lite --json,等价于 bare code-intel .)会崩:
{"diagnostic":"create repository authority root: 当文件已存在时,无法创建该文件。 (os error 183)","exitCode":74,"failureNode":null,"mode":"lite","outcome":"error","publication":null,"repo":"\\?\D:\projects\code-intel-pipeline","schema":"code-intel-primary-result.v1"}
code-intel sentrux check . 等窄命令完全正常——只有这条"quick start"经过的 authoritative-run bootstrap 步骤(创建 authority root 目录)会崩。
复现与根因排查
字面串 "create repository authority root" 在源码里正好两处,两处都是同一个消息模板:
crates/code-intel-cli/src/project_context.rs:135-140(ProjectContext::run()——quick start 实际经过的这一处)crates/code-intel-cli/src/authoritative_run/execution_kernel.rs:211-222(nest_authority_root(),被run execute使用)
两处都是 std::fs::create_dir_all(path).map_err(|error| .../* 塞进 host_io/io,exit 74 */),没有把 path 放进消息里。
一开始按字面复现思路("跑两遍,第二遍撞")没能复现:对一个全新、非 symlink 的 --artifact-root(/tmp/...)跑两遍——不管是对本仓 checkout 还是对一个独立 worktree——两遍都 exit 0,从不撞车。这是符合预期的:create_dir_all 对一个已经存在的普通目录是幂等的(std 自带 Err(_) if path.is_dir() => Ok(()) 兜底),"authority root 从上一轮还在" 这件事本身并不会让它报错。
真正的触发条件是 artifact root 本身是一个跨盘 symlink。 本机 $env:LOCALAPPDATA\code-intel\artifacts 是:
lrwxrwxrwx ... code-intel\artifacts -> /e/cache/code-intel (即 E:\cache\code-intel)
对本仓 bare 跑 code-intel .(走默认 artifact root,不带 --artifact-root),第一次调用就 100% 稳定复现上面的 exit 74——不需要跑两遍。
写了一个完全脱离 code-intel 的最小 Rust 复现(不经过任何本仓代码,纯 std::fs)钉死了根因:
let base = Path::new(r"C:\Users\...\code-intel\artifacts"); // symlink -> E:\cache\code-intel
let target = base.join("zzz-repro-test"); // 从未存在过
fs::create_dir_all(&target)
// => Err(kind=AlreadyExists, raw_os_error=Some(183))
// target.exists() == false, target.is_dir() == false (创建前后都不存在!)
// 重试(含 sleep 500ms 后重试):同样报错,不是竞态
// fs::create_dir(&target)(非递归单层):同样报错——不是递归算法的产物
// fs::canonicalize(base) 得到 \?\E:\cache\code-intel 后,对 canonical_base.join("zzz-repro-test")
// 跑 create_dir_all:成功
// 直接对 E:\cache\code-intel\zzz-repro-direct-e(完全不经过 symlink)跑:成功
// 对比:同drive symlink(同一分区内 symlink_dir)不会复现——必须跨盘结论:Windows 上,CreateDirectoryW 为一个全新子目录创建时,如果路径的祖先分量是指向另一个卷/盘的 symlink(reparse point),会确定性地返回 ERROR_ALREADY_EXISTS(os error 183),即使目标在调用前后都不存在。 create_dir_all 自带的兜底("AlreadyExists 就去查 is_dir(),是目录就当成功")救不了这个场景,因为 is_dir() 也是穿过同一个 reparse point 解析的,同样返回 false。这不是竞态(sleep 重试不解决),是结构性的;canonicalize 祖先目录后重试可以绕过。
这完整解释了报告的症状:任何人只要把 $env:LOCALAPPDATA\code-intel\artifacts(或 --artifact-root)symlink 到另一块盘(常见做法:把缓存目录挪到更大/更快的盘),quick start 就会在第一次调用时崩,报文里没有目标路径、没有任何可执行的下一步。
与 #145 / #148 的关系
- precedent/sibling:fix(run): 发布名撞车给出独立退出码 73 与可解析 stdout,终结「exit 65 + 零字节」 #145(
fix(run): 发布名撞车给出独立退出码 73 与可解析 stdout)修的是同一个缺陷类——把一个具体的 IO/collision 情况折叠进泛化的 error arm,吐出裸 OS error、没有路径、没有退出码区分——但那处是发布一个已完成的 run 到已存在的 authority root(run_commit::CommitError::Collision)。这里是创建 authority root 目录本身这一步,是姊妹缺陷、不同调用点,和 fix(run): 发布名撞车给出独立退出码 73 与可解析 stdout,终结「exit 65 + 零字节」 #145 修的地方(execution_kernel.rs::map_commit_error/publication_failure)相邻但不同。 - dogfood: 自扫摩擦清单——help 静默绿灯 / 门禁只给数字不给文件 / dsm 结构性空矩阵 / 149 条警告直通发布 #148 B3(dogfood 摩擦清单)记录的是反方向的缺口:authority root 不存在时工具拒绝自动创建(
authority root is not a directory: <path>,exit 64,见run_cli.rs:285-290)。本 issue 是 authority root(看起来)已存在方向的缺口,两者互补但不是同一个 bug——B3 未修。
DR-0001 适用性
读过 docs/decisions/DR-0001-install-topology-gate.md:DR-0001 管的是"装好的产品拓扑"类 bug(安装器路径、doctor entrypoint 缺失等,3794 个 checkout 拓扑测试都盖不到的那类)。这个 bug 用普通 checkout + cargo build --release + 直接跑二进制就能稳定复现(--help、sentrux check 都正常,只有 authority-root 创建这一步坏),不属于"装不上/装上跑不了"那一类,不需要进 install-smoke 闸。
计划
不打算只是"把消息写清楚"(照抄 #145 的退出码/消息模式)——因为多数真实触发场景下(跨盘 symlink),目标压根不存在,说"已经发布在这里,换个名字或删掉" 是假话,会把人往错误方向引导。倾向于:让创建 authority root 这一步对这个 Windows 症状自愈(对最近一层已存在的祖先目录 canonicalize 后重试),同时把两处消息补上目标路径,让"真的撞车"(比如路径被一个同名文件占用)时的报错也可执行。退出码维持 74(host_io/RunError::io)不变——这本来就是"host/文件系统层面出问题"的正确分类,不需要像 #145 那样新开一档退出码。
Activity
2233admin commentedon Aug 17, 2026
Claimed. 分支:
agent/authority-root-symlink-279。方案一句话:给
project_context.rs/execution_kernel.rs两处 authority-rootcreate_dir_all调用点包一个共享的ensure_directoryhelper(artifacts.rs)——对"AlreadyExists 但is_dir()也是 false"的这个 Windows 跨盘 symlink 假阳性,canonicalize 最近一层已存在祖先目录后重试自愈;真撞车(比如同名文件占位)时两处报错都补上目标路径,退出码维持 74 不变。2233admin commentedon Aug 17, 2026
PR: #280
修复过程中把"跑两遍才撞"这个初始假设纠正了一下:
create_dir_all对一个普通、非 symlink 的已存在目录本来就是幂等的(试了两轮全新 checkout + 全新 artifact root,两轮都 exit 0,从不撞车)。真正的触发条件不是"第几次跑",是 artifact root(或其祖先)本身是不是跨盘 symlink——命中时第一次调用就 100% 稳定复现,不需要跑两遍。细节和多组对照实验见 PR #280 里artifacts.rs::ensure_directory的文档注释。另外发现这个 Windows 症状本身依赖某种"陈旧" OS 状态:只有本机那个长期存在(2026-08-08 建的)的 symlink 能稳定复现,同进程现建的跨盘 symlink——不管指向哪块盘、不管是本进程建的还是外部
mklink /D建的——都摇不出来。所以 PR 里的自动化回归测试对这条 Windows 分支是"尽力而为"(真复现就断言修复生效,摇不出来就 skip 不装),核心的恢复路径运算则有一个不依赖 OS 状态的确定性单测单独覆盖。fix(run): authority-root create_dir_all 自愈跨盘 symlink 假阳性 AlreadyExist…
fix(run): authority-root create_dir_all 自愈跨盘 symlink 假阳性 AlreadyExist…
fix(run): authority-root create_dir_all 自愈跨盘 symlink 假阳性 AlreadyExist…
fix(run): authority-root 跨盘 symlink 下 create_dir_all 假阳性自愈,两处消息补路径 (#279