问题描述
在使用 rclone FUSE 挂载的 OneDrive 作为 workspace(HUABU_WORKSPACE=/home/yuqyang/OneDrive/huabu)时,POST /:canvasId/execute 频繁报错:
Space write failed for node "<nodeId>": multiple persisted names claim this id: <oldName>.md, <newName>.md
例如:
node-6c0f7d49-...:Huabu IPR Strategic Response Memo.md / Huabu Strategic IPR Response Memo.md
node-6399f579-...:Question 1.md / 项目研究机会分析.md
后者复现时,两个文件的 frontmatter id 与正文内容完全一致,仅 label 不同(Question 1 → 自动生成标题 项目研究机会分析,labelSource: auto)。旧文件 Question 1.md 在 unlinkSync 没有抛出任何异常、日志中也从未出现 "Failed to remove stale node sidecar" 警告的情况下,30+ 分钟后依然残留在磁盘上——即删除对调用方"假成功",但实际未在 rclone 后端真正落地/传播,这与 rclone --vfs-cache-mode full 配合 --dir-cache-time/--poll-interval 的异步、最终一致的删除语义吻合。
根因分析(两层)
表层(放大器):rclone FUSE 挂载的异步删除语义
--vfs-cache-mode full 下,删除操作先在本地 VFS 缓存生效并立即返回成功,实际到 OneDrive 后端的删除是异步/排队的。若该异步删除没有真正完成,--dir-cache-time 72h + --poll-interval 1m 之后的目录轮询可能让"已删除"的旧文件重新出现在本地视图中,而调用进程对此完全无感知(unlinkSync 早已返回成功)。
根本原因:文件名与可变 label 字段强绑定
apps/server/src/modules/storage/backends/disk/legacy/canvas-store.ts:
// line 224
function nodeFilenameFor(nodeId: string, label: string | null): string {
return `${toSafeFilename(label, nodeId)}.md`;
}
节点的磁盘文件名直接由 label 派生(为了让 .md 文件在文件系统里"见名知意")。writeNode()(同文件 ~1075-1245 行)区分两种写入路径:
- label 不变:
atomicWriteText 直接覆盖同名文件——单步、原子(temp 写 + renameSync 到同一路径),即使在网络挂载盘上也基本安全。
- label 变化(
isRename = true):先把新内容写到新文件名,成功后单独 unlinkSync 旧文件名——两步、非原子。这一步 unlink 是唯一的脆弱环节:任何失败(无论是显式抛错还是像本 issue 描述的"静默假成功")都会在磁盘上留下两个文件同时声称同一个 nodeId,被 nodeIndex() 重建时判定为 duplicate,此后该节点的所有写入都会被拒绝(nodeDuplicateIds 命中即直接短路返回 duplicate,见同文件 ~1128 行),需要人工介入清理。
而 label 变化在真实工作流中并不罕见:节点创建时先给默认标题(如 "Question 1"),agent 生成内容后自动打标题(labelSource: auto),用户手动改名等,都会触发这条"写新 + 删旧"的路径。label 越是被设计为"随内容自动演化",触发 rename 的频率就越高,风险敞口也越大——即使在本地磁盘上,这个设计仍然存在"任意一次 unlink 失败即产生残留重复文件"的结构性风险,只是本地磁盘上 unlink 失败概率极低,问题不易察觉;换到 rclone 这类网络挂载盘后,问题被放大到肉眼可见的频率。
建议修复方向
- 解耦文件名与可变 label:节点首次创建时基于标题生成一次性、稳定的文件名,之后 label 再变化只更新 frontmatter 里的
label 字段和文件内容(走 atomicWriteText 覆盖同名文件的安全路径),不再触发改名,除非用户显式执行"重命名文件"操作。
- 如果保留"文件名跟随 label"的产品特性,至少应该:
- 让 rename 路径对"删除旧文件失败/未确认"更健壮,比如改名后不采用"先写新再删旧",而是引入短暂过渡态或延迟清理 + 定期对账,避免把用户可见的写入直接绑定到一次不可靠的
unlink。
- 增加针对本类"重复 id"的自动修复:检测到同一
nodeId 的多个文件时,依据内容 hash + mtime 自动判定并清理明显是"改名残留"的旧文件,而不是永久拒绝写入直到人工介入。
问题描述
在使用 rclone FUSE 挂载的 OneDrive 作为 workspace(
HUABU_WORKSPACE=/home/yuqyang/OneDrive/huabu)时,POST /:canvasId/execute频繁报错:例如:
node-6c0f7d49-...:Huabu IPR Strategic Response Memo.md/Huabu Strategic IPR Response Memo.mdnode-6399f579-...:Question 1.md/项目研究机会分析.md后者复现时,两个文件的 frontmatter
id与正文内容完全一致,仅label不同(Question 1→ 自动生成标题项目研究机会分析,labelSource: auto)。旧文件Question 1.md在unlinkSync没有抛出任何异常、日志中也从未出现"Failed to remove stale node sidecar"警告的情况下,30+ 分钟后依然残留在磁盘上——即删除对调用方"假成功",但实际未在 rclone 后端真正落地/传播,这与 rclone--vfs-cache-mode full配合--dir-cache-time/--poll-interval的异步、最终一致的删除语义吻合。根因分析(两层)
表层(放大器):rclone FUSE 挂载的异步删除语义
--vfs-cache-mode full下,删除操作先在本地 VFS 缓存生效并立即返回成功,实际到 OneDrive 后端的删除是异步/排队的。若该异步删除没有真正完成,--dir-cache-time 72h+--poll-interval 1m之后的目录轮询可能让"已删除"的旧文件重新出现在本地视图中,而调用进程对此完全无感知(unlinkSync早已返回成功)。根本原因:文件名与可变
label字段强绑定apps/server/src/modules/storage/backends/disk/legacy/canvas-store.ts:节点的磁盘文件名直接由
label派生(为了让.md文件在文件系统里"见名知意")。writeNode()(同文件 ~1075-1245 行)区分两种写入路径:atomicWriteText直接覆盖同名文件——单步、原子(temp 写 +renameSync到同一路径),即使在网络挂载盘上也基本安全。isRename = true):先把新内容写到新文件名,成功后单独unlinkSync旧文件名——两步、非原子。这一步unlink是唯一的脆弱环节:任何失败(无论是显式抛错还是像本 issue 描述的"静默假成功")都会在磁盘上留下两个文件同时声称同一个nodeId,被nodeIndex()重建时判定为duplicate,此后该节点的所有写入都会被拒绝(nodeDuplicateIds命中即直接短路返回duplicate,见同文件 ~1128 行),需要人工介入清理。而 label 变化在真实工作流中并不罕见:节点创建时先给默认标题(如 "Question 1"),agent 生成内容后自动打标题(
labelSource: auto),用户手动改名等,都会触发这条"写新 + 删旧"的路径。label 越是被设计为"随内容自动演化",触发 rename 的频率就越高,风险敞口也越大——即使在本地磁盘上,这个设计仍然存在"任意一次 unlink 失败即产生残留重复文件"的结构性风险,只是本地磁盘上 unlink 失败概率极低,问题不易察觉;换到 rclone 这类网络挂载盘后,问题被放大到肉眼可见的频率。建议修复方向
label字段和文件内容(走atomicWriteText覆盖同名文件的安全路径),不再触发改名,除非用户显式执行"重命名文件"操作。unlink。nodeId的多个文件时,依据内容 hash + mtime 自动判定并清理明显是"改名残留"的旧文件,而不是永久拒绝写入直到人工介入。