@@ -145,7 +145,7 @@ export class DeepnoteFileChangeWatcher implements IExtensionSyncActivationServic
145145 * Consumes a self-write marker. Returns true if the fs event was self-triggered.
146146 */
147147 private consumeSelfWrite ( uri : Uri ) : boolean {
148- const key = uri . toString ( ) ;
148+ const key = this . normalizeFileUri ( uri ) ;
149149
150150 // Check snapshot self-writes first
151151 if ( this . snapshotSelfWriteUris . has ( key ) ) {
@@ -187,6 +187,7 @@ export class DeepnoteFileChangeWatcher implements IExtensionSyncActivationServic
187187 if ( liveCells . length !== newCells . length ) {
188188 return true ;
189189 }
190+
190191 return liveCells . some (
191192 ( live , i ) =>
192193 live . kind !== newCells [ i ] . kind ||
@@ -332,6 +333,7 @@ export class DeepnoteFileChangeWatcher implements IExtensionSyncActivationServic
332333 }
333334 }
334335
336+ // Apply the edit to update in-memory cells immediately (responsive UX).
335337 const wsEdit = new WorkspaceEdit ( ) ;
336338 wsEdit . set ( notebook . uri , edits ) ;
337339 const applied = await workspace . applyEdit ( wsEdit ) ;
@@ -341,13 +343,38 @@ export class DeepnoteFileChangeWatcher implements IExtensionSyncActivationServic
341343 return ;
342344 }
343345
344- // Save to sync mtime — mark as self-write first
345- this . markSelfWrite ( notebook . uri ) ;
346+ // Serialize the notebook and write canonical bytes to disk. This ensures
347+ // the file on disk matches what VS Code's serializer would produce.
348+ // Then save via workspace.save() to clear dirty state and update VS Code's
349+ // internal mtime tracker. Since WE just wrote the file, its mtime is from
350+ // our write (not the external change), avoiding the "content is newer" conflict.
351+ const serializeTokenSource = new CancellationTokenSource ( ) ;
346352 try {
347- await workspace . save ( notebook . uri ) ;
348- } catch ( error ) {
349- this . consumeSelfWrite ( notebook . uri ) ;
350- throw error ;
353+ const serializedBytes = await this . serializer . serializeNotebook ( newData , serializeTokenSource . token ) ;
354+
355+ // Write to disk first — this updates the file mtime to "now"
356+ this . markSelfWrite ( fileUri ) ;
357+ try {
358+ await workspace . fs . writeFile ( fileUri , serializedBytes ) ;
359+ } catch ( writeError ) {
360+ this . consumeSelfWrite ( fileUri ) ;
361+ logger . warn ( `[FileChangeWatcher] Failed to write synced file: ${ fileUri . path } ` , writeError ) ;
362+ }
363+
364+ // Now save — VS Code serializes (same bytes), sees the mtime is from our
365+ // recent write (which its internal watcher has picked up), and writes
366+ // successfully without a "content is newer" conflict.
367+ this . markSelfWrite ( fileUri ) ;
368+ try {
369+ await workspace . save ( notebook . uri ) ;
370+ } catch ( saveError ) {
371+ this . consumeSelfWrite ( fileUri ) ;
372+ logger . warn ( `[FileChangeWatcher] Save after sync write failed: ${ notebook . uri . path } ` , saveError ) ;
373+ }
374+ } catch ( serializeError ) {
375+ logger . warn ( `[FileChangeWatcher] Failed to serialize for sync write: ${ fileUri . path } ` , serializeError ) ;
376+ } finally {
377+ serializeTokenSource . dispose ( ) ;
351378 }
352379
353380 logger . info ( `[FileChangeWatcher] Reloaded notebook from external change: ${ notebook . uri . path } ` ) ;
@@ -523,6 +550,15 @@ export class DeepnoteFileChangeWatcher implements IExtensionSyncActivationServic
523550 return ( metadata ?. __deepnoteBlockId ?? metadata ?. id ) as string | undefined ;
524551 }
525552
553+ /**
554+ * Normalizes a URI to the underlying file path by stripping query and fragment.
555+ * Notebook URIs include query params (e.g., ?notebook=id) but the filesystem
556+ * watcher fires with the raw file URI — keys must match for self-write detection.
557+ */
558+ private normalizeFileUri ( uri : Uri ) : string {
559+ return uri . with ( { query : '' , fragment : '' } ) . toString ( ) ;
560+ }
561+
526562 private handleFileChange ( uri : Uri ) : void {
527563 // Deterministic self-write check — no timers involved
528564 if ( this . consumeSelfWrite ( uri ) ) {
@@ -581,7 +617,7 @@ export class DeepnoteFileChangeWatcher implements IExtensionSyncActivationServic
581617 * Call before workspace.save() to prevent the resulting fs event from triggering a reload.
582618 */
583619 private markSelfWrite ( uri : Uri ) : void {
584- const key = uri . toString ( ) ;
620+ const key = this . normalizeFileUri ( uri ) ;
585621 const count = this . selfWriteCounts . get ( key ) ?? 0 ;
586622 this . selfWriteCounts . set ( key , count + 1 ) ;
587623
0 commit comments