Split incident remount from outbox/governance so Wave 0 is executable without identity bootstrap, and record preempt-but-wait authorization. Co-authored-by: Cursor <cursoragent@cursor.com>
8.9 KiB
title, status, created, updated, severity
| title | status | created | updated | severity |
|---|---|---|---|---|
| dev rebuild 从临时 worktree 挂载空数据根导致正文投影 409 | done | 2026-09-14 | 2026-09-14 | critical |
dev rebuild 从临时 worktree 挂载空数据根导致正文投影 409
现象
用户刷新项目 143 后,前端请求:
GET /api/v1/docgov/documents/doc_proj143_intake/content 409
DOCUMENT_CONTENT_PROJECTION_MISSING
docgov: document content projection missing
错误发生在 projectDocuments.ts → controlledDocHydrate.ts 的受控正文水合链。它不是并发编辑的 HEAD_CONFLICT,也不是此前已修过的 canonical hash drift。
本轮只做只读现场核对与整改设计:没有操作用户会话、没有修改数据库/正文、没有重建共享容器。
结论
直接根因是 dev 应用、PI Runner、Cursor Runner 被重建成挂载临时集成 worktree 的空 data/dev,而 PostgreSQL 仍沿用原 dev 持久卷,形成数据库与文件事实源的 split-brain:
PostgreSQL persistent volume
document_records.work_dir=/app/data/workspaces/bizdev/intake_1789270446856290128
│
├─ 主仓 data/dev:output.md 存在,SHA 与 DB/head 一致
│
└─ 当前容器 /app/data:绑定 /tmp/as-dev-wf409-merge/data/dev,文件不存在
loadControlledDocumentContent 先取得账本 head,再调用 InspectProjection;容器内看不到 output.md,因此正确分类为 MISSING 并返回 409。前端只是如实暴露后端错误,不是本次根因。
现场证据
E1 — 容器挂载源是临时 worktree
2026-09-14 08:54 UTC 的 docker inspect shuimu-agent-app-dev:
| 项 | 现场值 |
|---|---|
| Compose working dir | /tmp/as-dev-wf409-merge |
| Compose config | /tmp/as-dev-wf409-merge/docker-compose.dev.yml |
app /app/data source |
/tmp/as-dev-wf409-merge/data/dev |
pi-agent /app/data/workspaces source |
/tmp/as-dev-wf409-merge/data/dev/workspaces |
cursor-agent /app/data/workspaces source |
/tmp/as-dev-wf409-merge/data/dev/workspaces |
| app created | 2026-09-14T08:40:44Z |
数据库容器的数据仍来自命名卷 agent-dev-context-pgdata,不会随 Compose 工作目录切换。由此产生“旧数据库 + 新空文件根”的不一致组合。
E2 — 项目 143 数据本身当前完整
document_records:
| 字段 | 值 |
|---|---|
| doc_id | doc_proj143_intake |
| work_dir | /app/data/workspaces/bizdev/intake_1789270446856290128 |
| content_sha256 | f283ba94c9c037897f021960f51abacee7242c3db8d10f9f2a5e32cde6acf039 |
| updated_at | 2026-09-14 05:14:10.302146+00 |
账本 open version 1 的 head snapshot 302 是人工修订,SHA 同为 f283...f039;主仓文件:
/home/weichen/projects/Agent-System/data/dev/workspaces/bizdev/intake_1789270446856290128/output.md
size=40075
sha256=f283ba94c9c037897f021960f51abacee7242c3db8d10f9f2a5e32cde6acf039
同一路径在当前 app 容器内 stat 返回 No such file or directory。所以无需再次对项目 143 做内容修复;需要恢复正确数据根挂载。
E3 — 影响不是单文档
对 dev 数据库 100 条“非模板且 work_dir 非空”的记录做只读比较:
| 根目录 | aligned | missing |
|---|---|---|
主仓 /home/weichen/projects/Agent-System/data/dev |
32 | 68 |
当前容器挂载 /tmp/as-dev-wf409-merge/data/dev |
0 | 100 |
主仓下的 68 条 missing 包含历史测试/临时路径,不能直接等同于 68 个真实用户缺陷;但当前容器对本次抽样 100/100 都不可见,足以确认环境级事故。恢复前任何真实文档读取或 PI 写入都不可信。
错误根在容器启动后已出现 27 个文件,其中 9 个 CAS blob 的 mtime 为 08:40:45 UTC;虽然 08:54 UTC 尚未找到新的 output.md,错误根已经不是可直接丢弃的“绝对空目录”,恢复前仍须做完整差异保全。
只读核对显示 test/main 当前 app 与 PI Runner 仍挂主仓 data/test、data/main,本轮没有观察到同样的运行事故;但其 Compose 仍使用相对 bind,仍受同一设计缺陷影响。
E4 — 健康检查假阳性
同一时刻 /health 与 /ready 都返回 200,只检查数据库、schema 和 task store;未验证:
- 容器绑定的数据根身份;
- app 与 runner 是否挂载同一根;
- 活跃项目的正文投影是否可达;
- 数据库记录与文件根是否属于同一环境实例。
因此“healthy + schema 33”不能证明文档运行态可用。
根因分析
R1 — Compose 相对 bind mount 与调用 worktree 耦合(直接根因)
docker-compose.dev.yml 使用:
- ./data/dev:/app/data
- ./data/dev/workspaces:/app/data/workspaces
Compose 按 project working directory 解析相对路径。scripts/env-service.sh 又把 ROOT_DIR 定义为脚本所在 worktree 并 cd 进去,所以从任何复制/linked worktree 执行 dev rebuild,都会把该 worktree 自己的 data/dev 当成运行数据根。
R2 — 纪律是文档约束,不是可执行门禁
scripts/agent-worktree.sh 只打印“禁止在此目录运行 env-service”,AGENTS.md 也规定 worktree 不得操作共享 Docker;但 env-service.sh 没有检测 linked worktree、主 worktree、分支、数据根或集成槽位。一次误调用即可静默替换全套容器挂载。
R3 — 数据库与文件没有共享存储身份
数据库使用稳定 named volume;正文使用随调用目录变化的 bind mount。两者没有共同的 environment/storage UUID,也没有启动握手,因此任意一个被指向另一份状态时,进程仍可启动并连接成功。
R4 — 账本权威仍被兼容投影阻断
当前设计声明 ledger head 是权威正文,但 GET 在账本存在时仍要求 output.md 投影必须存在且 aligned。于是兼容投影丢失会阻断 UI 读取权威账本。该 fail-closed 对真实 fork 有价值,却把“可安全重建的 missing”与“内容分叉”按同样的可用性后果处理。
R5 — 正文更新仍是非原子双写
人工 PATCH 先提交 PostgreSQL 账本,再单独写 output.md;候选 commit/reject 也采用 DB 状态与文件投影分步更新。失败后只依赖同幂等键重试,没有持久 projection job/status 和后台补偿。即使修复本次挂载问题,进程崩溃、权限错误或磁盘故障仍可能再次留下 head 已更新、投影缺失的状态。
R6 — 发布验收没有“从不同源码目录 rebuild 后仍接回同一状态”的负例
既有真实 UI 验收验证过 restart 后恢复,但 restart 不会重新解析 Compose bind mount;本次事故由 rebuild/recreate 触发。现有门禁没有覆盖 linked worktree、临时集成目录或错误数据根的启动拒绝。
为什么会“每次修改后又出现”
修改本身并不会删除项目 143 正文。本次时间线表明:最新人工修订和主仓 output.md 在 05:14 UTC 已一致落盘;08:40 UTC 后续 dev rebuild 把运行容器切到另一份空数据根,刷新才出现 409。
因此用户感知的“改完再刷新就坏”来自两个事件经常相邻:开发修改 → Agent 在临时 worktree 合并/rebuild → Compose 改绑数据目录。只修某个文档、某个 hydrate effect 或某次 hash 漂移,都无法阻止它复发。
严重度与影响
critical:
- dev 环境所有受控正文、工作流产物和 runner 工作区均可能不可见或写入错误根;
- 错误根上的新输出可能与主仓状态分叉;
- health/ready 仍为绿色,集成报告可能产生假 PASS;
- test/main 使用同类相对 bind 写法,设计缺陷可横向复制,虽本轮未观察其被错挂载。
关联整改
- Remediation: dev 运行数据根隔离与文档投影自愈整改
- Short-term: dev runtime data root guard 诊断与设计
- 前序问题: 项目 143 canonical hash 漂移
开放问题
- 恢复前需再次冻结并备份错误根,确认 08:40 UTC 后是否已有新会话、附件、runner 输出或 agent session 写入;08:54 UTC 抽样未发现
output.md,不能替代完整差异清单。 - test/main 当前挂载源需在实施 Unit A 只读核对;本轮未扩大操作范围。
审查附记(2026-09-14 ~09:23 UTC)
- 现场复核:
shuimu-agent-app-dev仍 created08:40:44Z,bind source 仍为/tmp/as-dev-wf409-merge/data/dev;/health/ready仍 200,schema_version=33。 - 主仓 CURRENT 集成槽仍为
implementer_cursor/e2e。worktree 副本不得写成 idle。 - 方案已审查修订:见 整改方案。用户选择事故抢占 + 先改方案再批实施。08:54 的
f283...f039不得再当 Wave 0 硬门槛。