From 72b38fed1d75fcb3420beeeeefd3dc2fc442e64b Mon Sep 17 00:00:00 2001 From: haoyuren <13851610112@163.com> Date: Sun, 15 Mar 2026 15:02:15 -0500 Subject: Fix sync race condition: external edits overwritten by remote ops When Claude Code writes to disk, a debounce timer is set. If a remote Overleaf op arrives during the debounce, onEditorContentChanged would write the remote content back to disk, overwriting Claude Code's edit. Fix: skip disk write and lastKnownContent update in onEditorContentChanged when a debounce is pending (external disk change waiting to be processed). Also increase writesInProgress guard to 800ms to exceed chokidar's 500ms polling interval. Co-Authored-By: Claude Opus 4.6 --- src/main/fileSyncBridge.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'src/main') diff --git a/src/main/fileSyncBridge.ts b/src/main/fileSyncBridge.ts index 7d7274a..73f3e70 100644 --- a/src/main/fileSyncBridge.ts +++ b/src/main/fileSyncBridge.ts @@ -509,8 +509,10 @@ export class FileSyncBridge { if (this.editorDocs.has(docId)) { // Doc is open in editor → send to renderer via IPC + // Don't update lastKnownContent here — let the renderer confirm via syncContentChanged. + // This prevents race conditions where remote OT ops overwrite lastKnownContent + // before the disk change is fully processed through the editor's OT pipeline. bridgeLog(`[FileSyncBridge] → sending sync:externalEdit to renderer for ${relPath}`) - this.lastKnownContent.set(relPath, newContent) this.mainWindow.webContents.send('sync:externalEdit', { docId, content: newContent }) } else { // Doc NOT open in editor → bridge handles OT directly @@ -723,6 +725,14 @@ export class FileSyncBridge { const relPath = this.docPathMap[docId] if (!relPath) return + // If there's a pending debounce for this file, an external tool (e.g. Claude Code) + // just wrote to disk and the change hasn't been processed yet. Don't overwrite the + // disk file or update lastKnownContent — let processDocChange handle it. + if (this.debounceTimers.has(relPath)) { + bridgeLog(`[FileSyncBridge] onEditorContentChanged: skipping ${relPath} (pending disk change)`) + return + } + // Update last known content this.lastKnownContent.set(relPath, content) @@ -778,7 +788,7 @@ export class FileSyncBridge { setTimeout(() => { this.writesInProgress.delete(relPath) - }, 150) + }, 800) // Must exceed chokidar polling interval (500ms) } private async deleteFromDisk(relPath: string): Promise { -- cgit v1.2.3