ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Plannotator 源码编辑竞态与冲突恢复 SPIKE 深度解析:从乱序磁盘快照到原子保存冲突自愈

Plannotator 源码编辑竞态与冲突恢复 SPIKE 深度解析:从乱序磁盘快照到原子保存冲突自愈 【免费下载链接】plannotatorAnnotate and review coding agent plans and code diffs visually, share with your team, send feedback to agents with one click.项目地址https://gitcode.com/gh_mirrors/pl/plannotator点击查看免费下载本篇文章围绕 Plannotator 仓库中的 SPIKE 研究文档《Source Edit Race and Conflict Recovery》展开系统剖析源码直存source-save链路中 5 项 PR 审查发现乱序磁盘快照覆盖、保存冲突恢复依赖二次请求、恢复草稿不可见、未验证保存阻塞反馈提交以及 Bun/Pi 双端谓词重复。读者读完将掌握 Plannotator 的编辑器‑服务端保存架构、竞态防护的序列号 已知磁盘哈希双重守卫原理以及如何借助冲突快照将 409 响应直接转换为可恢复冲突状态。一、SPIKE 背景审查 PR #936 并决定合并前必须修复什么该 SPIKE 于 2026-06-18 立项核心问题是在合并 PR #936 之前哪些发现必须真正修复哪些可以暂缓。PR #936 的审查暴露出源码直存功能的一系列隐患主要集中在五个方面异步文件监听file-watch对账可能应用过期的磁盘快照保存冲突可能进入有冲突状态但无足够数据可恢复的僵局源文件缺失时恢复的草稿可能不可见未经校验的已保存编辑会阻塞反馈提交Bun 与 Pi 两端重复实现了同一份源码保存资格判断谓词。SPIKE 的考察范围横跨编辑端、共享层与两个服务端实现涉及的文件包括编辑端入口 App.tsxSSE 文件监听触发对账、保存请求发起与冲突状态呈现可编辑文档状态机 editableDocuments.ts保存状态机、磁盘快照对账结果分类共享层 source-save.ts保存能力、请求/响应类型与冲突快照判定Node 实现 source-save-node.tssaveSourceFileAtomic原子保存Bun 服务端 annotate.ts 与 Pi 扩展服务端 serverAnnotate.ts/api/source/save端点与资格谓词以及三组测试sourceDocumentReconciliation.test.ts、source-save-node.test.ts、editableDocumentsHook.test.tsx。SPIKE 明确注明本次研究期间未改动任何产品代码全部结论先以研究文档沉淀再指导后续实现。这一点在仓库中可以得到印证——SPIKE 文档adr/research/SPIKE-source-edit-race-and-conflict-20260618-095558.md与实现文档adr/implementation/source-edit-reliability系列是分开沉淀的。二、发现一乱序磁盘快照会覆盖更新的状态必须修复2.1 现状流程与漏洞点当前对账链路是SSE 文件监听事件触发reconcileOpenSourceDocuments见 App.tsx 中 SSE 订阅段对/api/reference/files/stream的事件做防抖后调用对账对每个打开的源文档向/api/doc发起快照拉取拉取返回后将返回文本与sourceSave快照应用到reconcileDiskSnapshot。原有的唯一防线是文档正被标记为saving时跳过。这远远不够一次拉取可以在保存开始之前发起却在保存完成之后才返回。此时文档状态已经从saving变成了saved那条过期的拉取结果会被当作最新的磁盘真相应用上去。由此产生连锁破坏活动文档可能回退到旧磁盘文本已保存的 Edits 卡片被清空反馈feedback提交丢失已保存编辑上下文下一次保存因为客户端基准哈希被移回旧值而触发假冲突。同样的 bug 也存在于两次快速的外部写入场景两个请求乱序返回时较旧的快照会获胜。2.2 为什么单一防护不够SPIKE 给出了严谨的论证仅靠序列号能防止旧请求压过更新的对账请求但拦不住某个旧请求在用户保存之后才返回并应用仅靠起始哈希校验能防止文档前进后应用旧快照但当两个重叠的外部快照中较旧者先被应用时会丢掉两者中较新的那一个。因此最佳修复是同时使用两个维度每文件最新请求的单调递增序列号每请求携带发起时文档已知的磁盘哈希expected disk hash。只有在它仍是该文件的最新请求且文档已知磁盘哈希仍是请求发起时那个哈希两个条件同时满足时快照才被应用。2.3 实现证据双重守卫已在当前代码中落地当前仓库中packages/editor/sourceDocumentReconciliation.ts的reconcileSourceDocuments正是按这一建议实现的完整实现const startRecord getDocument(doc.key); if (startRecord?.saveStatus saving) continue; // 守卫 1保存中跳过 const expectedDiskHash getEditableDocumentKnownDiskHash(startRecord); const seq (sequenceByKey.get(doc.key) ?? 0) 1; sequenceByKey.set(doc.key, seq); // 守卫 2每文件最新请求序列 const snapshotResult await fetchSnapshot(doc.sourceSave.path); if (sequenceByKey.get(doc.key) ! seq) continue; // 守卫 2 复核仍是该文件最新请求 const currentRecord getDocument(doc.key); if (!canApplyEditableDocumentDiskSnapshot(currentRecord, expectedDiskHash)) continue; // 守卫 3磁盘哈希未前进其中第三个条件canApplyEditableDocumentDiskSnapshot定义在 editableDocuments.ts#L119-L133export function canApplyEditableDocumentDiskSnapshot(record, expectedDiskHash) { return ( record?.sourceSave?.enabled true record.saveStatus ! saving getEditableDocumentKnownDiskHash(record) expectedDiskHash ); }也就是说当前实现把 SPIKE 建议的序列号 期望磁盘哈希双条件都落实了同时还保留了原始saving守卫作为第一道快速过滤。对应测试 sourceDocumentReconciliation.test.ts 用 deferred promise 精确构造乱序场景验证ignores an older disk read after a newer reconcile starts旧拉取与新拉取重叠旧结果后返回时被丢弃应用结果只包含新哈希sha256:newignores a disk read when the document changed while fetch was pending拉取挂起期间文档前进到sha256:newer旧快照返回后不应用。这两个用例直接对应 SPIKE 描述的两种竞态形态是序列号防旧请求压新请求、哈希校验防请求期间文档前进的回归防线。三、发现二保存冲突恢复依赖第二次/api/doc请求必须修复3.1 现状流程与僵局保存链路原本是客户端 POST/api/source/save服务端调用saveSourceFileAtomic若当前磁盘哈希与请求的baseHash不匹配服务端返回409 conflict及当前哈希元数据客户端随后调用reconcileOpenSourceDocuments(..., { includeSavingKey })再通过/api/doc拉取当前磁盘文本填充diskConflict。问题在于如果第二步的/api/doc跟随请求失败客户端会调用markConflict把saveStatus置为conflict但diskConflict没有数据。这是一个糟糕的状态冲突横幅banner依赖diskConflict没有数据则操作按钮不出现保存按钮看到冲突状态重试仍使用过期的基准哈希。3.2 最佳修复让 409 响应直接携带冲突快照SPIKE 的建议非常干净服务端在saveSourceFileAtomic检测到冲突时手中已经握有当前文件快照直接在 409 响应里返回它客户端从保存响应中直接构造冲突状态即可需要的字段是当前文本current text当前哈希current hash当前 mtimecurrent mtime当前大小current size当前 EOLcurrent eol若响应中缺少该快照则应把文档标记为error而非可恢复的 disk conflict——可恢复冲突必须拥有磁盘文本。这比努力让跟随拉取更可靠更省事、更健壮。3.3 实现证据冲突快照已成为保存协议的一等公民当前仓库的共享类型 source-save.ts 已把这一建议固化进协议export type SourceSaveResponse | { ok: true; hash: string; mtimeMs: number; size: number; eol: SourceFileEol } | { ok: false; code: conflict; message: string; currentText: string; currentHash: string; currentMtimeMs: number; currentSize: number; currentEol: SourceFileEol; } | { ok: false; code: not-writable | write-failed | invalid-request; message: string };并配套hasSourceSaveConflictSnapshot判定函数校验冲突分支是否携带完整的五元组快照。在 Node 实现 source-save-node.ts 中saveSourceFileAtomic检测到before.hash ! baseHash时返回的正是readSourceFileSnapshot读出的完整快照if (before.hash ! baseHash) { return { ok: false, code: conflict, message: The file changed on disk since Plannotator opened it., currentText: before.text, currentHash: before.hash, currentMtimeMs: before.mtimeMs, currentSize: before.size, currentEol: before.eol, }; }值得注意的是saveSourceFileAtomic本身也是原子保存的实现把新内容写入同目录下的临时文件.plannotator-pid-ts-rand.tmp保留原文件 mode再renameSync覆盖目标文件保存主体。对于allowMissingBase的重建场景还特意用linkSync保证目标只在仍缺失时才创建避免覆盖其他工具刚重建的文件若发现路径已被占用则再次返回带快照的 conflict。EOL 策略也在这里落实detectSourceEol统计\r\n与孤立\r/\napplySourceEolPolicy在保存时保持 CRLF/LF 一致性。服务端端点 annotate.ts#L1022-L1036 将conflict映射为 HTTP 409、invalid-request为 400、not-writable为 403、其他失败为 500。客户端 App.tsx#L5113-L5181 收到 409 后先用hasSourceSaveConflictSnapshot判断快照是否完整完整则直接用reconcileDiskSnapshot构造冲突状态diskConflict携带文本与 sourceSavesaveStatus 变conflict弹出覆盖磁盘或重新加载提示缺失则走markError分支提示无法加载最新磁盘版本请重试保存——这与 SPIKE缺快照就标记 error 而非 conflict的建议完全一致。测试层面source-save-node.test.ts#L99-L119 的 detects hash conflicts instead of clobbering external edits 用例验证了外部改动后保存会返回 conflict 及currentText/currentHash/currentMtimeMs/currentSize/currentEol且磁盘内容不被覆盖editableDocumentsHook.test.tsx#L80-L151 则验证了冲突后的两种处置路径覆盖磁盘markSaved以冲突快照为基准记录savedChange的前后文本与前后哈希与重新加载reloadDiskConflict丢弃本地缓冲、采纳磁盘文本回到clean。四、发现三源文件缺失时恢复的草稿可能不可见有效但本次不修草稿恢复路径可以在源文件无法正常打开时重新水合editableDocuments记录如果此时没有活动的可编辑文档恢复出的编辑不会立刻显示。SPIKE 判定此发现有效但超出本迭代范围理由有三它与过期快照损坏不是同一类问题它需要产品决策是展示孤立源编辑、允许保存即重建还是只允许复制/丢弃贸然加自动打开行为会在删除文件场景制造混乱状态。仓库当前行为与之呼应编辑端markEditableDocumentFileMissingeditableDocuments.ts#L262-L275会把记录置为missing并清空savedChange与diskConflict同时在 UI 侧给出 toastFile no longer exists on disk / Savebasenameto recreate it.App.tsx#L3172-L3188。也就是说缺失文件的可视化与重建 UX已有基本兜底但 SPIKE 建议的孤儿草稿自动展示仍需单独的产品设计迭代。五、发现四未经校验的已保存编辑阻塞反馈提交有效但属有意为之如果存在已保存的文件编辑而/api/doc无法确认其是否仍然是最新的提交submit路径会阻塞并要求用户重试。这在瞬时故障期间确实恼人但 SPIKE 的结论是fail-closed关闭式失败比把过期或虚假的编辑上下文发送给 agent 更安全因此建议保留现状。从仓库结构看这一设计是有意为之savedChange记录markEditableDocumentSaved携带beforeText/afterText/beforeHash/afterHash正是为了给 agent 反馈提供可校验的编辑上下文一旦无法校验其时效性宁可让用户重试也不发送可能失真的上下文。这类宁可阻塞不可失真的取舍值得读者在实现类似 agent 反馈链路时参考。六、发现五Bun/Pi 重复的源码保存资格谓词有效但属吹毛求疵单文件 annotate 会话能否暴露源码保存行为的谓词同时存在于 Bun 与 Pi 两套服务端代码中。SPIKE 指出当前两者是一致的抽取到packages/shared/source-save.ts可以降低漂移风险但这与竞态/冲突损坏无关属于可选清理项不阻塞本次修复。验证当前仓库重复确实仍在Bun 端 annotate.ts#L579mode annotate !sourceConverted !(renderHtml rawHtml) !/^https?:\/\//i.test(filePath)Pi 端 serverAnnotate.ts#L635-L639sourceMode annotate !options.sourceConverted !(options.renderHtml options.rawHtml) !/^https?:\/\//i.test(options.filePath)两处逻辑一致要求单文件 annotate 模式、源码未被转换、非 HTML 渲染、非远程 URL。正如 SPIKE 所判断这属于低优先级的去重优化当前没有造成行为偏差。顺带一提资格判定通过后真正的保存能力由createSourceSaveCapabilitysource-save-node.ts#L206-L238基于文件扩展名SOURCE_SAVE_FILE_REGEX /\.(md|mdx|txt)$/i与本地可读性构建并给出not-annotate-mode、not-local-file、unsupported-extension、missing-file、unreadable-file等 12 种禁用原因source-save.ts#L3-L13。七、SPIKE 最终结论与行动清单SPIKE 给出了清晰的合并前修复清单优先级动作状态对照当前仓库修复为源文档对账增加单调性守卫序列号 期望磁盘哈希已落地sourceDocumentReconciliation.ts双条件守卫 2 个乱序回归测试修复/api/source/save返回冲突快照客户端直接应用已落地SourceSaveResponseconflict 分支携带五元组快照App.tsx直接构造冲突态修复冲突响应无法提供磁盘文本时标记 error 而非 conflict已落地hasSourceSaveConflictSnapshot判定失败走markError不修删除/不可读文件的恢复草稿救援后续单独 UX 迭代不修未验证提交的 fail-closed 行为有意保留不修Bun/Pi 谓词清理可选除非顺带零成本完成八、总结从 SPIKE 到实现的工程启示这份 SPIKE 文档的价值不仅在于修复了 3 个具体缺陷更在于它示范了一套可复用的工程方法先论证防护的充分性再动手一个守卫不够的推演序列号 vs 起始哈希各自的能力边界直接决定了最终实现采用双重条件避免了修一半留一半把服务端已握有的数据随协议一起返回冲突快照随 409 返回消除了依赖第二次请求的脆弱依赖也让客户端状态机clean/dirty/saving/saved/conflict/error/missing七态见 editableDocuments.ts#L6永远可以区分可恢复冲突与不可恢复错误用 deferred promise 精确构造竞态测试乱序返回与请求期间状态前进两种场景都有对应回归用例保证修复不复发明确有意为之与待产品决策的边界fail-closed 提交阻塞被保留孤儿草稿救援被推迟到独立 UX 迭代——不是所有发现都要在当轮修复。对于任何需要编辑器本地编辑 外部工具同时修改同一文件 异步对账的系统这份 SPIKE 及其落地代码对账实现、原子保存实现、冲突协议都是一份可以直接借鉴的参考实现。赞分享【免费下载链接】plannotatorAnnotate and review coding agent plans and code diffs visually, share with your team, send feedback to agents with one click.项目地址https://gitcode.com/gh_mirrors/pl/plannotator点击查看免费下载相关推荐Glass Browser如何在5分钟内实现Windows透明悬浮浏览器的终极多任务体验Glass Browser如何在5分钟内实现Windows透明悬浮浏览器的终极多任务体验 你是否厌倦了在多个应用窗口之间不断切换是否希望在编写代码时能同时查r3f-game-demo技术栈全面解析react-three-fiber three.js如何撑起2D瓦片游戏r3f game demo技术栈全面解析react three fiber three.js如何撑起2D瓦片游戏 想用 Web 技术做一款俯视角 2D 瓦MemGPT项目中内存块标签冲突问题解析MemGPT项目中内存块标签冲突问题解析 在使用MemGPT项目开发AI代理时开发者可能会遇到一个常见但容易被忽视的问题——内存块标签冲突。这个问题通常表现为上一篇Bilibili-EvolvedAPI错误日志聚合ELK Stack使用下一篇OpenLayers 地图图层管理完全指南从基础到高级技巧创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表