Files
Agent-System/.doc/remediation/2026-09-13-document-revision-editor-hydration-race.md
T
weichenandCursor 9b43bd6a73 fix(web): bind editor saves to the opened doc and real PATCH
Drop test-only ETag helpers. App now calls saveControlledBlockEdit,
which PATCHes once on the bound editor docId or fail-closes. Resume
hydrate after a blocked first load, and refuse silent doc switches.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 20:00:47 +00:00

3.7 KiB
Raw Blame History

title, status, created, updated
title status created updated
编辑弹窗 hydration 竞态整改 in_progress 2026-09-13 2026-09-13

编辑弹窗 hydration 竞态整改

背景

链到 ../audit/2026-09-13-document-revision-editor-hydration-race.md。

第十二轮 UI-06:fill 瞬间后台 GET 完成 hydration,块 ID 被重建;保存按旧 ID 找不到目标后静默关窗,PATCH 次数为 0,修订停在 r0。

目标

  • 编辑期间后台 hydration(含 fill 与 save 之间完成的响应)不得替换块身份、不得覆盖编辑基线
  • PATCH 成功后迟到 hydration 不得回滚新正文 / 新 ETag
  • 找不到目标块时 fail-closed:保留弹窗与用户输入,明确提示,绝不关窗、不按正文改另一个块
  • 正常保存仍发 PATCH,采用打开时/hydrate 基线 ETag;409 语义不削弱
  • 不声称真实 UI/E2E 已通过

分波次计划

波次 内容 验收
Wave 0 hydrate 对齐 head_block_map;抽出 editorHydrationGate + blockSavePolicy;App 三条 hydrate 路径与保存接线 单测:同 map 两次 hydrate ID 稳定;缺 map 仍随机;deferred hydrate / stale id / 迟到 hydrate / PATCH+409;npm test + npm run build
Wave 1 占槽后真实 UI-06(本轮不做) 现场 PATCH 次数 ≥ 1 且 r0→r1

设计

两层,缺一不可;不靠 Playwright sleep:

  1. 身份对齐:hydrateControlledDocumentContent 保留完整 RevisionState(含 head_block_map),buildControlledDocumentAST 后 applyHeadBlockMap。同一 head 的重复 hydrate 不再因 Math.random 换 ID。
  2. generation / 编辑门闩:双击立刻 setEditorOpen(true);编辑中 beginHydration 返回 null;已在途 token 经 decideHydrationApply 丢弃。PATCH 成功先 markSaved() 再关窗,迟到 hydrate 即使 ID 已对齐也不得回滚新正文。
  3. stale-id fail-closed:resolveSaveTargetBlock 只按 id;找不到则提示并留窗,绝不按正文改另一块、绝不静默关窗。
  4. 真实 PATCH 编排:saveControlledBlockEdit 被 App.tsx 调用;依赖注入 patchDocumentContent。成功路径恰好一次 PATCH(绑定 editorDocId、hydrate ETag、client_block_map),采用响应 ETag。409/网络错误不关窗。
  5. 编辑会话绑 docId:editorSession = { docId, block }。activeDocId 被 canonical/session 改写后保存 fail-closed,无 PATCH。
  6. 关窗续跑:editor_open 跳过/丢弃的 hydrate 置 blockedNeedsResume;安全关闭后 hydrateResumeKey 重跑 effect,避免永久 loading skeleton。
  7. 切文档:modal fixed inset:0 z-index 100 挡住 SlimTabBar,但 chat/drawer 仍可调 handleSelectDoc。编辑中切换 fail-closed 并提示,不静默关窗。

验收标准

  • 纯函数门闩覆盖 UI-06 时序(hydrate 在 fill 后 resolve 不覆盖)
  • stale block id 保存不静默关闭、不假成功、不改同内容另一块
  • PATCH 成功后迟到 hydration 不回滚;409 保留草稿
  • saveControlledBlockEdit 成功路径断言恰好一次 PATCH(docId/ETag/body/map);doc 漂移/409/网络不关窗
  • 关窗后续跑被阻塞的 hydrate;编辑中切文档 fail-closed
  • 真实 UI-06 E2E(本轮不做,不得勾选)

相关链接