tandem-editor 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -92,7 +92,13 @@ async function startHocuspocus(port) {
92
92
  console.error(`[Hocuspocus] Merged pre-existing content into document: ${documentName}`);
93
93
  }
94
94
  documents.set(documentName, document);
95
- onDocSwapped?.(documentName, document);
95
+ if (onDocSwapped) {
96
+ onDocSwapped(documentName, document);
97
+ } else {
98
+ console.error(
99
+ `[Tandem] WARN: onDocSwapped callback not registered during doc load for ${documentName}. Server-side observers will NOT be attached. Call setDocLifecycleCallbacks() before starting Hocuspocus.`
100
+ );
101
+ }
96
102
  return document;
97
103
  },
98
104
  async afterUnloadDocument({ documentName }) {
@@ -461,6 +467,74 @@ var init_manager = __esm({
461
467
  }
462
468
  });
463
469
 
470
+ // src/server/file-watcher.ts
471
+ import fs2 from "fs";
472
+ function watchFile(filePath, onChanged) {
473
+ if (watched.has(filePath)) return;
474
+ let watcher;
475
+ try {
476
+ watcher = fs2.watch(filePath, (eventType) => {
477
+ if (eventType !== "change") return;
478
+ const entry = watched.get(filePath);
479
+ if (!entry) return;
480
+ if (entry.suppressed) {
481
+ entry.suppressed = false;
482
+ return;
483
+ }
484
+ if (entry.timer !== null) {
485
+ clearTimeout(entry.timer);
486
+ }
487
+ entry.timer = setTimeout(() => {
488
+ entry.timer = null;
489
+ onChanged(filePath).catch((err) => {
490
+ console.error(`[FileWatcher] onChanged callback failed for ${filePath}:`, err);
491
+ });
492
+ }, 500);
493
+ });
494
+ } catch (err) {
495
+ console.error(`[FileWatcher] Failed to watch ${filePath}:`, err);
496
+ return;
497
+ }
498
+ watcher.on("error", (err) => {
499
+ console.error(`[FileWatcher] Watcher error for ${filePath}:`, err);
500
+ unwatchFile(filePath);
501
+ });
502
+ watched.set(filePath, { watcher, timer: null, suppressed: false });
503
+ console.error(`[FileWatcher] Watching ${filePath}`);
504
+ }
505
+ function suppressNextChange(filePath) {
506
+ const entry = watched.get(filePath);
507
+ if (entry) {
508
+ entry.suppressed = true;
509
+ }
510
+ }
511
+ function unwatchFile(filePath) {
512
+ const entry = watched.get(filePath);
513
+ if (!entry) return;
514
+ if (entry.timer !== null) {
515
+ clearTimeout(entry.timer);
516
+ }
517
+ try {
518
+ entry.watcher.close();
519
+ } catch (err) {
520
+ console.error(`[FileWatcher] watcher.close() failed for ${filePath}:`, err);
521
+ }
522
+ watched.delete(filePath);
523
+ console.error(`[FileWatcher] Unwatched ${filePath}`);
524
+ }
525
+ function unwatchAll() {
526
+ for (const filePath of [...watched.keys()]) {
527
+ unwatchFile(filePath);
528
+ }
529
+ }
530
+ var watched;
531
+ var init_file_watcher = __esm({
532
+ "src/server/file-watcher.ts"() {
533
+ "use strict";
534
+ watched = /* @__PURE__ */ new Map();
535
+ }
536
+ });
537
+
464
538
  // src/server/file-io/mdast-ydoc.ts
465
539
  import * as Y3 from "yjs";
466
540
  function mdastToYDoc(doc, tree) {
@@ -1517,7 +1591,24 @@ function refreshRange(ann, ydoc, map) {
1517
1591
  }
1518
1592
  const newFrom = relPosToFlatOffset(ydoc, ann.relRange.fromRel);
1519
1593
  const newTo = relPosToFlatOffset(ydoc, ann.relRange.toRel);
1520
- if (newFrom === null || newTo === null) return ann;
1594
+ if (newFrom === null || newTo === null) {
1595
+ if (newFrom !== null || newTo !== null) {
1596
+ console.error(
1597
+ `[positions] refreshRange: partial CRDT resolution for ${ann.id} (from: ${newFrom !== null ? "ok" : "dead"}, to: ${newTo !== null ? "ok" : "dead"})`
1598
+ );
1599
+ }
1600
+ const fromRel = flatOffsetToRelPos(ydoc, ann.range.from, 0);
1601
+ const toRel = flatOffsetToRelPos(ydoc, ann.range.to, -1);
1602
+ if (fromRel && toRel) {
1603
+ const updated2 = { ...ann, relRange: { fromRel, toRel } };
1604
+ if (map) map.set(ann.id, updated2);
1605
+ return updated2;
1606
+ }
1607
+ const stripped = { ...ann };
1608
+ delete stripped.relRange;
1609
+ if (map) map.set(ann.id, stripped);
1610
+ return stripped;
1611
+ }
1521
1612
  if (newFrom > newTo) {
1522
1613
  console.error(
1523
1614
  `[positions] refreshRange: inverted CRDT range for annotation ${ann.id}: resolved [${newFrom}, ${newTo}] from flat [${ann.range.from}, ${ann.range.to}]`
@@ -2681,23 +2772,23 @@ var init_docx_apply = __esm({
2681
2772
  });
2682
2773
 
2683
2774
  // src/server/file-io/index.ts
2684
- import fs2 from "fs/promises";
2775
+ import fs3 from "fs/promises";
2685
2776
  import path4 from "path";
2686
2777
  function getAdapter(format) {
2687
2778
  return adapters[format] ?? plaintextAdapter;
2688
2779
  }
2689
2780
  async function atomicWrite2(filePath, content) {
2690
2781
  const tempPath = path4.join(path4.dirname(filePath), `.tandem-tmp-${Date.now()}`);
2691
- await fs2.writeFile(tempPath, content, "utf-8");
2692
- await fs2.rename(tempPath, filePath);
2782
+ await fs3.writeFile(tempPath, content, "utf-8");
2783
+ await fs3.rename(tempPath, filePath);
2693
2784
  }
2694
2785
  async function atomicWriteBuffer(filePath, content) {
2695
2786
  const tempPath = path4.join(path4.dirname(filePath), `.tandem-tmp-${Date.now()}`);
2696
- await fs2.writeFile(tempPath, content);
2787
+ await fs3.writeFile(tempPath, content);
2697
2788
  try {
2698
- await fs2.rename(tempPath, filePath);
2789
+ await fs3.rename(tempPath, filePath);
2699
2790
  } catch (err) {
2700
- await fs2.unlink(tempPath).catch(() => {
2791
+ await fs3.unlink(tempPath).catch(() => {
2701
2792
  });
2702
2793
  throw err;
2703
2794
  }
@@ -2760,6 +2851,58 @@ var init_file_io = __esm({
2760
2851
  }
2761
2852
  });
2762
2853
 
2854
+ // src/server/notifications.ts
2855
+ function pushNotification(notification) {
2856
+ buffer.push(notification);
2857
+ while (buffer.length > NOTIFICATION_BUFFER_SIZE) {
2858
+ buffer.shift();
2859
+ }
2860
+ for (const cb of subscribers) {
2861
+ try {
2862
+ cb(notification);
2863
+ } catch (err) {
2864
+ console.error("[Notifications] Subscriber threw during dispatch:", err);
2865
+ }
2866
+ }
2867
+ }
2868
+ function subscribe(cb) {
2869
+ subscribers.add(cb);
2870
+ return () => {
2871
+ subscribers.delete(cb);
2872
+ };
2873
+ }
2874
+ var buffer, subscribers;
2875
+ var init_notifications = __esm({
2876
+ "src/server/notifications.ts"() {
2877
+ "use strict";
2878
+ init_constants();
2879
+ buffer = [];
2880
+ subscribers = /* @__PURE__ */ new Set();
2881
+ }
2882
+ });
2883
+
2884
+ // src/shared/utils.ts
2885
+ function generateId(prefix) {
2886
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2887
+ }
2888
+ function generateAnnotationId() {
2889
+ return generateId("ann");
2890
+ }
2891
+ function generateMessageId() {
2892
+ return generateId("msg");
2893
+ }
2894
+ function generateEventId() {
2895
+ return generateId("evt");
2896
+ }
2897
+ function generateNotificationId() {
2898
+ return generateId("ntf");
2899
+ }
2900
+ var init_utils = __esm({
2901
+ "src/shared/utils.ts"() {
2902
+ "use strict";
2903
+ }
2904
+ });
2905
+
2763
2906
  // src/server/mcp/file-opener.ts
2764
2907
  var file_opener_exports = {};
2765
2908
  __export(file_opener_exports, {
@@ -2767,7 +2910,7 @@ __export(file_opener_exports, {
2767
2910
  openFileByPath: () => openFileByPath,
2768
2911
  openFileFromContent: () => openFileFromContent
2769
2912
  });
2770
- import fs3 from "fs/promises";
2913
+ import fs4 from "fs/promises";
2771
2914
  import fsSync from "fs";
2772
2915
  import path5 from "path";
2773
2916
  import { randomUUID } from "crypto";
@@ -2798,7 +2941,7 @@ async function openFileByPath(filePath, options) {
2798
2941
  { code: "UNSUPPORTED_FORMAT" }
2799
2942
  );
2800
2943
  }
2801
- const stat = await fs3.stat(resolved);
2944
+ const stat = await fs4.stat(resolved);
2802
2945
  if (stat.size > MAX_FILE_SIZE) {
2803
2946
  throw Object.assign(new Error("File exceeds 50MB limit."), { code: "FILE_TOO_LARGE" });
2804
2947
  }
@@ -2867,7 +3010,7 @@ async function openFileByPath(filePath, options) {
2867
3010
  }
2868
3011
  if (!restoredFromSession) {
2869
3012
  const adapter = getAdapter(format);
2870
- const fileContent = isDocx ? await fs3.readFile(resolved) : await fs3.readFile(resolved, "utf-8");
3013
+ const fileContent = isDocx ? await fs4.readFile(resolved) : await fs4.readFile(resolved, "utf-8");
2871
3014
  await adapter.load(doc, fileContent);
2872
3015
  }
2873
3016
  addDoc(id, { id, filePath: resolved, format, readOnly, source: "file" });
@@ -2876,6 +3019,9 @@ async function openFileByPath(filePath, options) {
2876
3019
  initSavedBaseline(doc);
2877
3020
  broadcastOpenDocs();
2878
3021
  ensureAutoSave();
3022
+ if (format !== "docx") {
3023
+ wireFileWatcher(id, resolved, format);
3024
+ }
2879
3025
  return {
2880
3026
  ...buildResult(doc, {
2881
3027
  documentId: id,
@@ -2933,7 +3079,7 @@ async function clearAndReload(id, doc, filePath, format, existing) {
2933
3079
  let preparedComments;
2934
3080
  let preparedContent;
2935
3081
  if (isDocx) {
2936
- const buffer3 = await fs3.readFile(filePath);
3082
+ const buffer3 = await fs4.readFile(filePath);
2937
3083
  [preparedHtml, preparedComments] = await Promise.all([
2938
3084
  loadDocx(buffer3),
2939
3085
  extractDocxComments(buffer3).catch((err) => {
@@ -2945,7 +3091,7 @@ async function clearAndReload(id, doc, filePath, format, existing) {
2945
3091
  })
2946
3092
  ]);
2947
3093
  } else {
2948
- preparedContent = await fs3.readFile(filePath, "utf-8");
3094
+ preparedContent = await fs4.readFile(filePath, "utf-8");
2949
3095
  }
2950
3096
  try {
2951
3097
  doc.transact(() => {
@@ -3019,6 +3165,90 @@ function buildResult(doc, base) {
3019
3165
  ...warnings.length > 0 ? { warnings } : {}
3020
3166
  };
3021
3167
  }
3168
+ async function reloadFromDisk(id, filePath, format) {
3169
+ if (reloadInProgress.has(id)) {
3170
+ console.error(`[FileWatcher] reload already in progress for ${id}, skipping`);
3171
+ return;
3172
+ }
3173
+ reloadInProgress.add(id);
3174
+ try {
3175
+ console.error(`[FileWatcher] reloadFromDisk: reloading ${id} from ${filePath}`);
3176
+ const doc = getOrCreateDocument(id);
3177
+ const fileContent = await fs4.readFile(filePath, "utf-8");
3178
+ doc.transact(() => {
3179
+ const awareness = doc.getMap(Y_MAP_AWARENESS);
3180
+ awareness.forEach((_, k) => awareness.delete(k));
3181
+ const userAwareness = doc.getMap(Y_MAP_USER_AWARENESS);
3182
+ userAwareness.forEach((_, k) => userAwareness.delete(k));
3183
+ if (format === "md") {
3184
+ loadMarkdown(doc, fileContent);
3185
+ } else {
3186
+ populateYDoc(doc, fileContent);
3187
+ }
3188
+ }, MCP_ORIGIN);
3189
+ const annotationMap = doc.getMap(Y_MAP_ANNOTATIONS);
3190
+ const annotations = [];
3191
+ annotationMap.forEach((val) => annotations.push(val));
3192
+ if (annotations.length > 0) {
3193
+ const refreshed = refreshAllRanges(annotations, doc, annotationMap);
3194
+ doc.transact(() => {
3195
+ for (const ann of refreshed) {
3196
+ if (!ann.textSnapshot) continue;
3197
+ const vr = validateRange(doc, ann.range.from, ann.range.to, {
3198
+ textSnapshot: ann.textSnapshot
3199
+ });
3200
+ if (vr.ok) continue;
3201
+ if (vr.code === "RANGE_MOVED") {
3202
+ const relocated = anchoredRange(doc, vr.resolvedFrom, vr.resolvedTo, ann.textSnapshot);
3203
+ if (relocated.ok) {
3204
+ const updated = {
3205
+ ...ann,
3206
+ range: relocated.range,
3207
+ relRange: relocated.fullyAnchored ? relocated.relRange : void 0
3208
+ };
3209
+ annotationMap.set(ann.id, updated);
3210
+ }
3211
+ }
3212
+ }
3213
+ }, MCP_ORIGIN);
3214
+ }
3215
+ attachObservers(id, doc);
3216
+ console.error(`[FileWatcher] reloadFromDisk: complete for ${id}`);
3217
+ } finally {
3218
+ reloadInProgress.delete(id);
3219
+ }
3220
+ }
3221
+ function wireFileWatcher(id, filePath, format) {
3222
+ try {
3223
+ watchFile(filePath, async () => {
3224
+ try {
3225
+ await reloadFromDisk(id, filePath, format);
3226
+ pushNotification({
3227
+ id: generateNotificationId(),
3228
+ type: "file-reloaded",
3229
+ severity: "info",
3230
+ message: `File changed on disk \u2014 reloaded: ${path5.basename(filePath)}`,
3231
+ documentId: id,
3232
+ dedupKey: `reload:${id}`,
3233
+ timestamp: Date.now()
3234
+ });
3235
+ } catch (err) {
3236
+ console.error(`[FileWatcher] reloadFromDisk failed for ${filePath}:`, err);
3237
+ pushNotification({
3238
+ id: generateNotificationId(),
3239
+ type: "general-error",
3240
+ severity: "warning",
3241
+ message: `Failed to reload ${path5.basename(filePath)} from disk`,
3242
+ documentId: id,
3243
+ dedupKey: `reload-error:${id}`,
3244
+ timestamp: Date.now()
3245
+ });
3246
+ }
3247
+ });
3248
+ } catch (err) {
3249
+ console.error(`[FileWatcher] wireFileWatcher failed for ${filePath}:`, err);
3250
+ }
3251
+ }
3022
3252
  function ensureAutoSave() {
3023
3253
  if (isAutoSaveRunning()) return;
3024
3254
  startAutoSave(async () => {
@@ -3028,6 +3258,7 @@ function ensureAutoSave() {
3028
3258
  }
3029
3259
  });
3030
3260
  }
3261
+ var reloadInProgress;
3031
3262
  var init_file_opener = __esm({
3032
3263
  "src/server/mcp/file-opener.ts"() {
3033
3264
  "use strict";
@@ -3035,6 +3266,10 @@ var init_file_opener = __esm({
3035
3266
  init_constants();
3036
3267
  init_queue();
3037
3268
  init_file_io();
3269
+ init_file_watcher();
3270
+ init_positions2();
3271
+ init_notifications();
3272
+ init_utils();
3038
3273
  init_markdown();
3039
3274
  init_docx();
3040
3275
  init_docx_html();
@@ -3042,6 +3277,7 @@ var init_file_opener = __esm({
3042
3277
  init_manager();
3043
3278
  init_document_model();
3044
3279
  init_document_service();
3280
+ reloadInProgress = /* @__PURE__ */ new Set();
3045
3281
  }
3046
3282
  });
3047
3283
 
@@ -3126,6 +3362,7 @@ async function closeDocumentById(id) {
3126
3362
  return { success: false, error: `Document ${id} not found.` };
3127
3363
  }
3128
3364
  const closedPath = docState.filePath;
3365
+ unwatchFile(docState.filePath);
3129
3366
  try {
3130
3367
  const doc = getOrCreateDocument(id);
3131
3368
  await saveSession(docState.filePath, docState.format, doc);
@@ -3210,34 +3447,13 @@ var init_document_service = __esm({
3210
3447
  init_manager();
3211
3448
  init_constants();
3212
3449
  init_queue();
3450
+ init_file_watcher();
3213
3451
  openDocs = /* @__PURE__ */ new Map();
3214
3452
  setShouldKeepDocument((name) => openDocs.has(name) || name === CTRL_ROOM);
3215
3453
  activeDocId = null;
3216
3454
  }
3217
3455
  });
3218
3456
 
3219
- // src/shared/utils.ts
3220
- function generateId(prefix) {
3221
- return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
3222
- }
3223
- function generateAnnotationId() {
3224
- return generateId("ann");
3225
- }
3226
- function generateMessageId() {
3227
- return generateId("msg");
3228
- }
3229
- function generateEventId() {
3230
- return generateId("evt");
3231
- }
3232
- function generateNotificationId() {
3233
- return generateId("ntf");
3234
- }
3235
- var init_utils = __esm({
3236
- "src/shared/utils.ts"() {
3237
- "use strict";
3238
- }
3239
- });
3240
-
3241
3457
  // src/server/events/types.ts
3242
3458
  var init_types3 = __esm({
3243
3459
  "src/server/events/types.ts"() {
@@ -3271,18 +3487,18 @@ function untrackPayloadId(event) {
3271
3487
  else emittedPayloadIds.set(id, count - 1);
3272
3488
  }
3273
3489
  function pushEvent(event) {
3274
- buffer.push(event);
3490
+ buffer2.push(event);
3275
3491
  trackPayloadId(event);
3276
- while (buffer.length > CHANNEL_EVENT_BUFFER_SIZE) {
3277
- const evicted = buffer.shift();
3492
+ while (buffer2.length > CHANNEL_EVENT_BUFFER_SIZE) {
3493
+ const evicted = buffer2.shift();
3278
3494
  if (evicted) untrackPayloadId(evicted);
3279
3495
  }
3280
3496
  const now = Date.now();
3281
- while (buffer.length > 0 && now - buffer[0].timestamp > CHANNEL_EVENT_BUFFER_AGE_MS) {
3282
- const evicted = buffer.shift();
3497
+ while (buffer2.length > 0 && now - buffer2[0].timestamp > CHANNEL_EVENT_BUFFER_AGE_MS) {
3498
+ const evicted = buffer2.shift();
3283
3499
  if (evicted) untrackPayloadId(evicted);
3284
3500
  }
3285
- for (const cb of subscribers) {
3501
+ for (const cb of subscribers2) {
3286
3502
  try {
3287
3503
  cb(event);
3288
3504
  } catch (err) {
@@ -3290,16 +3506,16 @@ function pushEvent(event) {
3290
3506
  }
3291
3507
  }
3292
3508
  }
3293
- function subscribe(cb) {
3294
- subscribers.add(cb);
3509
+ function subscribe2(cb) {
3510
+ subscribers2.add(cb);
3295
3511
  }
3296
3512
  function unsubscribe(cb) {
3297
- subscribers.delete(cb);
3513
+ subscribers2.delete(cb);
3298
3514
  }
3299
3515
  function replaySince(lastEventId) {
3300
- const idx = buffer.findIndex((e) => e.id === lastEventId);
3301
- if (idx === -1) return [...buffer];
3302
- return buffer.slice(idx + 1);
3516
+ const idx = buffer2.findIndex((e) => e.id === lastEventId);
3517
+ if (idx === -1) return [...buffer2];
3518
+ return buffer2.slice(idx + 1);
3303
3519
  }
3304
3520
  function attachObservers(docName, doc) {
3305
3521
  detachObservers(docName);
@@ -3476,7 +3692,7 @@ function attachCtrlObservers() {
3476
3692
  function reattachCtrlObservers() {
3477
3693
  attachCtrlObservers();
3478
3694
  }
3479
- var MCP_ORIGIN, docObservers, emittedPayloadIds, buffer, subscribers, ctrlCleanups;
3695
+ var MCP_ORIGIN, docObservers, emittedPayloadIds, buffer2, subscribers2, ctrlCleanups;
3480
3696
  var init_queue = __esm({
3481
3697
  "src/server/events/queue.ts"() {
3482
3698
  "use strict";
@@ -3487,8 +3703,8 @@ var init_queue = __esm({
3487
3703
  MCP_ORIGIN = "mcp";
3488
3704
  docObservers = /* @__PURE__ */ new Map();
3489
3705
  emittedPayloadIds = /* @__PURE__ */ new Map();
3490
- buffer = [];
3491
- subscribers = /* @__PURE__ */ new Set();
3706
+ buffer2 = [];
3707
+ subscribers2 = /* @__PURE__ */ new Set();
3492
3708
  ctrlCleanups = [];
3493
3709
  }
3494
3710
  });
@@ -3668,34 +3884,12 @@ function escapeRegex(str) {
3668
3884
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3669
3885
  }
3670
3886
 
3671
- // src/server/notifications.ts
3672
- init_constants();
3673
- var buffer2 = [];
3674
- var subscribers2 = /* @__PURE__ */ new Set();
3675
- function pushNotification(notification) {
3676
- buffer2.push(notification);
3677
- while (buffer2.length > NOTIFICATION_BUFFER_SIZE) {
3678
- buffer2.shift();
3679
- }
3680
- for (const cb of subscribers2) {
3681
- try {
3682
- cb(notification);
3683
- } catch (err) {
3684
- console.error("[Notifications] Subscriber threw during dispatch:", err);
3685
- }
3686
- }
3687
- }
3688
- function subscribe2(cb) {
3689
- subscribers2.add(cb);
3690
- return () => {
3691
- subscribers2.delete(cb);
3692
- };
3693
- }
3694
-
3695
3887
  // src/server/mcp/document.ts
3888
+ init_notifications();
3696
3889
  init_utils();
3697
3890
  init_offsets();
3698
3891
  init_file_io();
3892
+ init_file_watcher();
3699
3893
 
3700
3894
  // src/server/mcp/convert.ts
3701
3895
  init_provider();
@@ -3703,7 +3897,7 @@ init_document_model();
3703
3897
  init_file_io();
3704
3898
  init_file_opener();
3705
3899
  init_document_service();
3706
- import fs4 from "fs/promises";
3900
+ import fs5 from "fs/promises";
3707
3901
  import path7 from "path";
3708
3902
  async function findAvailablePath(basePath) {
3709
3903
  const dir = path7.dirname(basePath);
@@ -3714,7 +3908,7 @@ async function findAvailablePath(basePath) {
3714
3908
  let counter = 0;
3715
3909
  while (counter <= MAX_ATTEMPTS) {
3716
3910
  try {
3717
- await fs4.access(candidate);
3911
+ await fs5.access(candidate);
3718
3912
  counter++;
3719
3913
  candidate = path7.join(dir, `${name}-${counter}${ext}`);
3720
3914
  } catch (err) {
@@ -3763,7 +3957,7 @@ async function convertToMarkdown(documentId, outputPath) {
3763
3957
  });
3764
3958
  }
3765
3959
  try {
3766
- const stat = await fs4.stat(resolvedOutput);
3960
+ const stat = await fs5.stat(resolvedOutput);
3767
3961
  if (stat.isDirectory()) {
3768
3962
  const baseName = path7.basename(docState.filePath, path7.extname(docState.filePath));
3769
3963
  resolvedOutput = path7.join(resolvedOutput, `${baseName}.md`);
@@ -4070,6 +4264,7 @@ function registerDocumentTools(server) {
4070
4264
  });
4071
4265
  }
4072
4266
  const output = adapter.save(r.doc);
4267
+ suppressNextChange(r.filePath);
4073
4268
  await atomicWrite2(r.filePath, output);
4074
4269
  await saveSession(r.filePath, format, r.doc);
4075
4270
  const meta = r.doc.getMap(Y_MAP_DOCUMENT_META);
@@ -4207,6 +4402,7 @@ function registerDocumentTools(server) {
4207
4402
  init_docx();
4208
4403
  init_types2();
4209
4404
  init_positions2();
4405
+ init_notifications();
4210
4406
  init_utils();
4211
4407
  init_positions2();
4212
4408
  function getDocAndAnnotations(documentId) {
@@ -4588,6 +4784,7 @@ init_constants();
4588
4784
  init_document_model();
4589
4785
  init_file_opener();
4590
4786
  init_document_service();
4787
+ init_notifications();
4591
4788
 
4592
4789
  // src/server/mcp/docx-apply.ts
4593
4790
  init_document_service();
@@ -4596,7 +4793,7 @@ init_positions2();
4596
4793
  init_document_model();
4597
4794
  init_file_io();
4598
4795
  import { z as z4 } from "zod";
4599
- import fs5 from "fs/promises";
4796
+ import fs6 from "fs/promises";
4600
4797
  import path8 from "path";
4601
4798
  async function applyChangesCore(documentId, author, backupPath) {
4602
4799
  const r = requireDocument(documentId);
@@ -4678,21 +4875,21 @@ async function applyChangesCore(documentId, author, backupPath) {
4678
4875
  throw Object.assign(new Error("No accepted suggestions to apply."), { code: "NO_SUGGESTIONS" });
4679
4876
  }
4680
4877
  const ydocFlatText = extractText(ydoc);
4681
- const buffer3 = await fs5.readFile(filePath);
4878
+ const buffer3 = await fs6.readFile(filePath);
4682
4879
  const result = await applyTrackedChanges(buffer3, suggestions, {
4683
4880
  author: author ?? "Tandem Review",
4684
4881
  ydocFlatText
4685
4882
  });
4686
4883
  let resolvedBackup = backupPath ?? filePath.replace(/\.docx$/i, ".backup.docx");
4687
4884
  try {
4688
- await fs5.access(resolvedBackup);
4885
+ await fs6.access(resolvedBackup);
4689
4886
  const ext = path8.extname(resolvedBackup);
4690
4887
  const base = resolvedBackup.slice(0, -ext.length);
4691
4888
  resolvedBackup = `${base}-${Date.now()}${ext}`;
4692
4889
  } catch {
4693
4890
  }
4694
- await fs5.copyFile(filePath, resolvedBackup);
4695
- const [origStat, backupStat] = await Promise.all([fs5.stat(filePath), fs5.stat(resolvedBackup)]);
4891
+ await fs6.copyFile(filePath, resolvedBackup);
4892
+ const [origStat, backupStat] = await Promise.all([fs6.stat(filePath), fs6.stat(resolvedBackup)]);
4696
4893
  if (origStat.size !== backupStat.size) {
4697
4894
  throw Object.assign(new Error("Backup verification failed: file sizes do not match."), {
4698
4895
  code: "BACKUP_FAILED"
@@ -4747,17 +4944,17 @@ function registerApplyTools(server) {
4747
4944
  const { filePath } = r;
4748
4945
  const backupPath = filePath.replace(/\.docx$/i, ".backup.docx");
4749
4946
  try {
4750
- await fs5.access(backupPath);
4947
+ await fs6.access(backupPath);
4751
4948
  } catch (err) {
4752
4949
  if (err.code === "ENOENT") {
4753
4950
  return mcpError("FILE_NOT_FOUND", `No backup file found at ${backupPath}`);
4754
4951
  }
4755
4952
  throw err;
4756
4953
  }
4757
- await fs5.copyFile(backupPath, filePath);
4954
+ await fs6.copyFile(backupPath, filePath);
4758
4955
  const [backupStat, restoredStat] = await Promise.all([
4759
- fs5.stat(backupPath),
4760
- fs5.stat(filePath)
4956
+ fs6.stat(backupPath),
4957
+ fs6.stat(filePath)
4761
4958
  ]);
4762
4959
  if (backupStat.size !== restoredStat.size) {
4763
4960
  throw new Error("Restore verification failed: file sizes do not match.");
@@ -4861,7 +5058,7 @@ function notifyStreamHandler(req, res) {
4861
5058
  clearInterval(keepalive);
4862
5059
  unsubscribe2();
4863
5060
  }
4864
- const unsubscribe2 = subscribe2((notification) => {
5061
+ const unsubscribe2 = subscribe((notification) => {
4865
5062
  try {
4866
5063
  res.write(`data: ${JSON.stringify(notification)}
4867
5064
 
@@ -5221,7 +5418,7 @@ function sseHandler(req, res) {
5221
5418
  unsubscribe(onEvent);
5222
5419
  }
5223
5420
  };
5224
- subscribe(onEvent);
5421
+ subscribe2(onEvent);
5225
5422
  keepalive = setInterval(() => {
5226
5423
  res.write(": keepalive\n\n");
5227
5424
  }, CHANNEL_SSE_KEEPALIVE_MS);
@@ -5685,12 +5882,12 @@ init_manager();
5685
5882
  init_platform();
5686
5883
 
5687
5884
  // src/server/version-check.ts
5688
- import fs6 from "fs/promises";
5885
+ import fs7 from "fs/promises";
5689
5886
  import path9 from "path";
5690
5887
  async function checkVersionChange(currentVersion, versionFilePath) {
5691
5888
  let storedVersion = null;
5692
5889
  try {
5693
- storedVersion = (await fs6.readFile(versionFilePath, "utf-8")).trim();
5890
+ storedVersion = (await fs7.readFile(versionFilePath, "utf-8")).trim();
5694
5891
  } catch (err) {
5695
5892
  if (err.code !== "ENOENT") {
5696
5893
  console.error("[Tandem] Failed to read last-seen-version:", err);
@@ -5698,8 +5895,8 @@ async function checkVersionChange(currentVersion, versionFilePath) {
5698
5895
  }
5699
5896
  const result = storedVersion === null ? "first-install" : storedVersion === currentVersion ? "current" : "upgraded";
5700
5897
  if (result !== "current") {
5701
- await fs6.mkdir(path9.dirname(versionFilePath), { recursive: true });
5702
- await fs6.writeFile(versionFilePath, currentVersion, "utf-8");
5898
+ await fs7.mkdir(path9.dirname(versionFilePath), { recursive: true });
5899
+ await fs7.writeFile(versionFilePath, currentVersion, "utf-8");
5703
5900
  }
5704
5901
  return result;
5705
5902
  }
@@ -5723,6 +5920,7 @@ function isKnownHocuspocusError(err) {
5723
5920
  // src/server/index.ts
5724
5921
  init_queue();
5725
5922
  init_document_service();
5923
+ init_file_watcher();
5726
5924
  init_file_opener();
5727
5925
  init_document_model();
5728
5926
 
@@ -5860,6 +6058,7 @@ async function shutdown(signal) {
5860
6058
  isShuttingDown = true;
5861
6059
  console.error(`[Tandem] ${signal} received, saving session...`);
5862
6060
  try {
6061
+ unwatchAll();
5863
6062
  await saveCurrentSession();
5864
6063
  stopAutoSave();
5865
6064
  } catch (err) {