@disco_trooper/apple-notes-mcp 1.7.0 → 1.8.2

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.
@@ -1,24 +1,36 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
 
3
3
  vi.mock("../notes/read.js", () => ({
4
4
  getAllNotes: vi.fn(),
5
+ getNoteByFolderAndTitle: vi.fn(),
5
6
  }));
6
7
 
7
8
  vi.mock("../db/lancedb.js", () => ({
8
9
  getVectorStore: vi.fn(),
10
+ getChunkStore: vi.fn(),
9
11
  }));
10
12
 
11
13
  vi.mock("./indexer.js", () => ({
12
14
  incrementalIndex: vi.fn(),
13
15
  }));
14
16
 
17
+ vi.mock("./chunk-indexer.js", () => ({
18
+ hasChunkIndex: vi.fn().mockResolvedValue(false),
19
+ updateChunksForNotes: vi.fn(),
20
+ }));
21
+
15
22
  describe("checkForChanges", () => {
16
23
  beforeEach(() => {
17
24
  vi.resetModules();
18
25
  vi.clearAllMocks();
19
26
  });
20
27
 
21
- it("should return true if notes were modified after indexing", async () => {
28
+ afterEach(() => {
29
+ delete process.env.INDEX_TTL;
30
+ delete process.env.SEARCH_REFRESH_TIMEOUT_MS;
31
+ });
32
+
33
+ it("returns true if notes were modified after indexing", async () => {
22
34
  const { getAllNotes } = await import("../notes/read.js");
23
35
  const { getVectorStore } = await import("../db/lancedb.js");
24
36
 
@@ -27,8 +39,8 @@ describe("checkForChanges", () => {
27
39
  ]);
28
40
 
29
41
  vi.mocked(getVectorStore).mockReturnValue({
30
- getAll: vi.fn().mockResolvedValue([
31
- { title: "Note 1", folder: "Work", indexed_at: "2026-01-09T12:00:00Z" },
42
+ getIndexMetadata: vi.fn().mockResolvedValue([
43
+ { id: "1", title: "Note 1", folder: "Work", indexed_at: "2026-01-09T12:00:00Z" },
32
44
  ]),
33
45
  } as any);
34
46
 
@@ -38,7 +50,7 @@ describe("checkForChanges", () => {
38
50
  expect(hasChanges).toBe(true);
39
51
  });
40
52
 
41
- it("should return false if no changes", async () => {
53
+ it("returns false if no changes", async () => {
42
54
  const { getAllNotes } = await import("../notes/read.js");
43
55
  const { getVectorStore } = await import("../db/lancedb.js");
44
56
 
@@ -47,8 +59,8 @@ describe("checkForChanges", () => {
47
59
  ]);
48
60
 
49
61
  vi.mocked(getVectorStore).mockReturnValue({
50
- getAll: vi.fn().mockResolvedValue([
51
- { title: "Note 1", folder: "Work", indexed_at: "2026-01-09T12:00:00Z" },
62
+ getIndexMetadata: vi.fn().mockResolvedValue([
63
+ { id: "1", title: "Note 1", folder: "Work", indexed_at: "2026-01-09T12:00:00Z" },
52
64
  ]),
53
65
  } as any);
54
66
 
@@ -58,7 +70,7 @@ describe("checkForChanges", () => {
58
70
  expect(hasChanges).toBe(false);
59
71
  });
60
72
 
61
- it("should return true if new note added", async () => {
73
+ it("returns true if a new note was added", async () => {
62
74
  const { getAllNotes } = await import("../notes/read.js");
63
75
  const { getVectorStore } = await import("../db/lancedb.js");
64
76
 
@@ -68,8 +80,8 @@ describe("checkForChanges", () => {
68
80
  ]);
69
81
 
70
82
  vi.mocked(getVectorStore).mockReturnValue({
71
- getAll: vi.fn().mockResolvedValue([
72
- { title: "Note 1", folder: "Work", indexed_at: "2026-01-09T12:00:00Z" },
83
+ getIndexMetadata: vi.fn().mockResolvedValue([
84
+ { id: "1", title: "Note 1", folder: "Work", indexed_at: "2026-01-09T12:00:00Z" },
73
85
  ]),
74
86
  } as any);
75
87
 
@@ -79,15 +91,15 @@ describe("checkForChanges", () => {
79
91
  expect(hasChanges).toBe(true);
80
92
  });
81
93
 
82
- it("should return true if note deleted", async () => {
94
+ it("returns true if a note was deleted", async () => {
83
95
  const { getAllNotes } = await import("../notes/read.js");
84
96
  const { getVectorStore } = await import("../db/lancedb.js");
85
97
 
86
98
  vi.mocked(getAllNotes).mockResolvedValue([]);
87
99
 
88
100
  vi.mocked(getVectorStore).mockReturnValue({
89
- getAll: vi.fn().mockResolvedValue([
90
- { title: "Note 1", folder: "Work", indexed_at: "2026-01-09T12:00:00Z" },
101
+ getIndexMetadata: vi.fn().mockResolvedValue([
102
+ { id: "1", title: "Note 1", folder: "Work", indexed_at: "2026-01-09T12:00:00Z" },
91
103
  ]),
92
104
  } as any);
93
105
 
@@ -97,7 +109,7 @@ describe("checkForChanges", () => {
97
109
  expect(hasChanges).toBe(true);
98
110
  });
99
111
 
100
- it("should return true if no index exists and notes exist", async () => {
112
+ it("returns true if no index exists and notes exist", async () => {
101
113
  const { getAllNotes } = await import("../notes/read.js");
102
114
  const { getVectorStore } = await import("../db/lancedb.js");
103
115
 
@@ -106,7 +118,7 @@ describe("checkForChanges", () => {
106
118
  ]);
107
119
 
108
120
  vi.mocked(getVectorStore).mockReturnValue({
109
- getAll: vi.fn().mockRejectedValue(new Error("Table not found")),
121
+ getIndexMetadata: vi.fn().mockRejectedValue(new Error("Table not found")),
110
122
  } as any);
111
123
 
112
124
  const { checkForChanges } = await import("./refresh.js");
@@ -120,19 +132,41 @@ describe("refreshIfNeeded", () => {
120
132
  beforeEach(() => {
121
133
  vi.resetModules();
122
134
  vi.clearAllMocks();
135
+ delete process.env.INDEX_TTL;
136
+ delete process.env.SEARCH_REFRESH_TIMEOUT_MS;
137
+ });
138
+
139
+ afterEach(() => {
140
+ vi.useRealTimers();
141
+ });
142
+
143
+ it("skips refresh when INDEX_TTL is not configured", async () => {
144
+ const { incrementalIndex } = await import("./indexer.js");
145
+ const { getVectorStore } = await import("../db/lancedb.js");
146
+
147
+ const { refreshIfNeeded } = await import("./refresh.js");
148
+ const refreshed = await refreshIfNeeded();
149
+
150
+ expect(refreshed).toBe(false);
151
+ expect(incrementalIndex).not.toHaveBeenCalled();
152
+ expect(getVectorStore).not.toHaveBeenCalled();
123
153
  });
124
154
 
125
- it("should trigger incremental index if changes detected", async () => {
155
+ it("triggers incremental index when TTL expired and changes detected", async () => {
156
+ process.env.INDEX_TTL = "1";
157
+
126
158
  const { incrementalIndex } = await import("./indexer.js");
127
159
  const { getAllNotes } = await import("../notes/read.js");
128
160
  const { getVectorStore } = await import("../db/lancedb.js");
129
161
 
130
162
  vi.mocked(getAllNotes).mockResolvedValue([
131
- { title: "New Note", folder: "Work", created: "2026-01-10", modified: "2026-01-10T12:00:00Z" },
163
+ { title: "Note 1", folder: "Work", created: "2026-01-01", modified: "2026-01-10T12:00:00Z" },
132
164
  ]);
133
165
 
134
166
  vi.mocked(getVectorStore).mockReturnValue({
135
- getAll: vi.fn().mockResolvedValue([]),
167
+ getIndexMetadata: vi.fn().mockResolvedValue([
168
+ { id: "1", title: "Note 1", folder: "Work", indexed_at: "2020-01-01T00:00:00Z" },
169
+ ]),
136
170
  } as any);
137
171
 
138
172
  vi.mocked(incrementalIndex).mockResolvedValue({
@@ -146,21 +180,45 @@ describe("refreshIfNeeded", () => {
146
180
  const refreshed = await refreshIfNeeded();
147
181
 
148
182
  expect(refreshed).toBe(true);
149
- expect(incrementalIndex).toHaveBeenCalled();
183
+ expect(incrementalIndex).toHaveBeenCalledOnce();
150
184
  });
151
185
 
152
- it("should not trigger index if no changes", async () => {
186
+ it("skips refresh when TTL is not expired", async () => {
187
+ process.env.INDEX_TTL = "86400";
188
+
189
+ const { incrementalIndex } = await import("./indexer.js");
190
+ const { getVectorStore } = await import("../db/lancedb.js");
191
+
192
+ vi.mocked(getVectorStore).mockReturnValue({
193
+ getIndexMetadata: vi.fn().mockResolvedValue([
194
+ {
195
+ id: "1",
196
+ title: "Note 1",
197
+ folder: "Work",
198
+ indexed_at: new Date().toISOString(),
199
+ },
200
+ ]),
201
+ } as any);
202
+
203
+ const { refreshIfNeeded } = await import("./refresh.js");
204
+ const refreshed = await refreshIfNeeded();
205
+
206
+ expect(refreshed).toBe(false);
207
+ expect(incrementalIndex).not.toHaveBeenCalled();
208
+ });
209
+
210
+ it("returns false when refresh throws", async () => {
211
+ process.env.INDEX_TTL = "1";
212
+
153
213
  const { incrementalIndex } = await import("./indexer.js");
154
214
  const { getAllNotes } = await import("../notes/read.js");
155
215
  const { getVectorStore } = await import("../db/lancedb.js");
156
216
 
157
- vi.mocked(getAllNotes).mockResolvedValue([
158
- { title: "Note 1", folder: "Work", created: "2026-01-01", modified: "2026-01-08T12:00:00Z" },
159
- ]);
217
+ vi.mocked(getAllNotes).mockRejectedValue(new Error("JXA failed"));
160
218
 
161
219
  vi.mocked(getVectorStore).mockReturnValue({
162
- getAll: vi.fn().mockResolvedValue([
163
- { title: "Note 1", folder: "Work", indexed_at: "2026-01-09T12:00:00Z" },
220
+ getIndexMetadata: vi.fn().mockResolvedValue([
221
+ { id: "1", title: "Note 1", folder: "Work", indexed_at: "2020-01-01T00:00:00Z" },
164
222
  ]),
165
223
  } as any);
166
224
 
@@ -170,4 +228,67 @@ describe("refreshIfNeeded", () => {
170
228
  expect(refreshed).toBe(false);
171
229
  expect(incrementalIndex).not.toHaveBeenCalled();
172
230
  });
231
+
232
+ it("returns false when refresh exceeds timeout budget", async () => {
233
+ vi.useFakeTimers();
234
+
235
+ process.env.INDEX_TTL = "1";
236
+ process.env.SEARCH_REFRESH_TIMEOUT_MS = "5";
237
+
238
+ const { incrementalIndex } = await import("./indexer.js");
239
+ const { getAllNotes } = await import("../notes/read.js");
240
+ const { getVectorStore } = await import("../db/lancedb.js");
241
+
242
+ vi.mocked(getAllNotes).mockResolvedValue([
243
+ { title: "Note 1", folder: "Work", created: "2026-01-01", modified: "2026-01-10T12:00:00Z" },
244
+ ]);
245
+
246
+ vi.mocked(getVectorStore).mockReturnValue({
247
+ getIndexMetadata: vi.fn().mockResolvedValue([
248
+ { id: "1", title: "Note 1", folder: "Work", indexed_at: "2020-01-01T00:00:00Z" },
249
+ ]),
250
+ } as any);
251
+
252
+ vi.mocked(incrementalIndex).mockImplementation(
253
+ () => new Promise(() => {
254
+ // Intentionally never resolves
255
+ })
256
+ );
257
+
258
+ const { refreshIfNeeded } = await import("./refresh.js");
259
+ const pending = refreshIfNeeded();
260
+
261
+ await vi.advanceTimersByTimeAsync(10);
262
+ await expect(pending).resolves.toBe(false);
263
+ });
264
+
265
+ it("coalesces concurrent refresh calls into a single run", async () => {
266
+ process.env.INDEX_TTL = "1";
267
+
268
+ const { incrementalIndex } = await import("./indexer.js");
269
+ const { getAllNotes } = await import("../notes/read.js");
270
+ const { getVectorStore } = await import("../db/lancedb.js");
271
+
272
+ vi.mocked(getAllNotes).mockResolvedValue([
273
+ { title: "Note 1", folder: "Work", created: "2026-01-01", modified: "2026-01-10T12:00:00Z" },
274
+ ]);
275
+
276
+ vi.mocked(getVectorStore).mockReturnValue({
277
+ getIndexMetadata: vi.fn().mockResolvedValue([
278
+ { id: "1", title: "Note 1", folder: "Work", indexed_at: "2020-01-01T00:00:00Z" },
279
+ ]),
280
+ } as any);
281
+
282
+ vi.mocked(incrementalIndex).mockImplementation(async () => {
283
+ await new Promise((resolve) => setTimeout(resolve, 20));
284
+ return { total: 1, indexed: 1, errors: 0, timeMs: 100 };
285
+ });
286
+
287
+ const { refreshIfNeeded } = await import("./refresh.js");
288
+ const [first, second] = await Promise.all([refreshIfNeeded(), refreshIfNeeded()]);
289
+
290
+ expect(first).toBe(true);
291
+ expect(second).toBe(true);
292
+ expect(incrementalIndex).toHaveBeenCalledTimes(1);
293
+ });
173
294
  });
@@ -5,12 +5,26 @@
5
5
  */
6
6
 
7
7
  import { getAllNotes, getNoteByFolderAndTitle, type NoteInfo } from "../notes/read.js";
8
- import { getVectorStore, getChunkStore } from "../db/lancedb.js";
8
+ import {
9
+ getVectorStore,
10
+ getChunkStore,
11
+ type IndexMetadataRecord,
12
+ } from "../db/lancedb.js";
9
13
  import { incrementalIndex } from "./indexer.js";
10
14
  import { updateChunksForNotes, hasChunkIndex } from "./chunk-indexer.js";
11
15
  import { createDebugLogger } from "../utils/debug.js";
16
+ import { DEFAULT_SEARCH_REFRESH_TIMEOUT_MS } from "../config/constants.js";
17
+ import { shouldAutoRefreshByTtl } from "./refresh-policy.js";
12
18
 
13
19
  const debug = createDebugLogger("REFRESH");
20
+ let refreshInFlight: Promise<boolean> | null = null;
21
+
22
+ interface LegacyIndexRecord {
23
+ id?: string;
24
+ title: string;
25
+ folder: string;
26
+ indexed_at: string;
27
+ }
14
28
 
15
29
  /**
16
30
  * Detected changes in notes.
@@ -22,27 +36,144 @@ interface DetectedChanges {
22
36
  deleted: string[]; // note IDs
23
37
  }
24
38
 
39
+ async function getIndexMetadata(): Promise<IndexMetadataRecord[]> {
40
+ const store = getVectorStore() as {
41
+ getIndexMetadata?: () => Promise<IndexMetadataRecord[]>;
42
+ getAll?: () => Promise<LegacyIndexRecord[]>;
43
+ };
44
+
45
+ if (typeof store.getIndexMetadata === "function") {
46
+ return store.getIndexMetadata();
47
+ }
48
+
49
+ if (typeof store.getAll === "function") {
50
+ const legacyRecords = await store.getAll();
51
+ return legacyRecords.map((record) => ({
52
+ id: record.id ?? "",
53
+ title: record.title,
54
+ folder: record.folder,
55
+ indexed_at: record.indexed_at,
56
+ }));
57
+ }
58
+
59
+ throw new Error("Vector store does not expose index metadata methods");
60
+ }
61
+
62
+ function getLatestIndexedAtMs(records: IndexMetadataRecord[]): number | null {
63
+ let latestMs: number | null = null;
64
+
65
+ for (const record of records) {
66
+ const indexedAtMs = Date.parse(record.indexed_at);
67
+ if (Number.isNaN(indexedAtMs)) {
68
+ continue;
69
+ }
70
+ if (latestMs === null || indexedAtMs > latestMs) {
71
+ latestMs = indexedAtMs;
72
+ }
73
+ }
74
+
75
+ return latestMs;
76
+ }
77
+
78
+ function getSearchRefreshTimeoutMs(): number {
79
+ const raw = process.env.SEARCH_REFRESH_TIMEOUT_MS;
80
+ if (!raw) {
81
+ return DEFAULT_SEARCH_REFRESH_TIMEOUT_MS;
82
+ }
83
+
84
+ const parsed = Number.parseInt(raw, 10);
85
+ if (!Number.isFinite(parsed) || parsed <= 0) {
86
+ return DEFAULT_SEARCH_REFRESH_TIMEOUT_MS;
87
+ }
88
+
89
+ return parsed;
90
+ }
91
+
92
+ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | null> {
93
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
94
+
95
+ const timeoutPromise = new Promise<null>((resolve) => {
96
+ timeoutHandle = setTimeout(() => resolve(null), timeoutMs);
97
+ });
98
+
99
+ try {
100
+ return await Promise.race([promise, timeoutPromise]);
101
+ } finally {
102
+ if (timeoutHandle !== undefined) {
103
+ clearTimeout(timeoutHandle);
104
+ }
105
+ }
106
+ }
107
+
108
+ async function runRefresh(existingRecords: IndexMetadataRecord[]): Promise<boolean> {
109
+ const changes = await detectChanges(existingRecords);
110
+
111
+ if (!changes.hasChanges) {
112
+ return false;
113
+ }
114
+
115
+ // Update main index
116
+ debug("Changes detected, running incremental index...");
117
+ const result = await incrementalIndex();
118
+ debug(`Main index refresh: ${result.indexed} notes updated in ${result.timeMs}ms`);
119
+
120
+ // Update chunk index if it exists and there are changes
121
+ const hasChunks = await hasChunkIndex();
122
+ if (hasChunks && (changes.added.length > 0 || changes.modified.length > 0)) {
123
+ debug("Updating chunk index for changed notes...");
124
+
125
+ // Fetch full content for changed notes
126
+ const changedNotes = [...changes.added, ...changes.modified];
127
+ const notesWithContent = await Promise.all(
128
+ changedNotes.map(async (n) => {
129
+ const note = await getNoteByFolderAndTitle(n.folder, n.title);
130
+ return note;
131
+ })
132
+ );
133
+
134
+ // Filter out nulls (notes that couldn't be fetched)
135
+ const validNotes = notesWithContent.filter((n) => n !== null);
136
+
137
+ if (validNotes.length > 0) {
138
+ const chunksCreated = await updateChunksForNotes(validNotes);
139
+ debug(`Chunk index refresh: ${chunksCreated} chunks for ${validNotes.length} notes`);
140
+ }
141
+
142
+ // Delete chunks for deleted notes
143
+ if (changes.deleted.length > 0) {
144
+ const chunkStore = getChunkStore();
145
+ await chunkStore.deleteChunksByNoteIds(changes.deleted);
146
+ debug(`Deleted chunks for ${changes.deleted.length} notes`);
147
+ }
148
+ }
149
+
150
+ return true;
151
+ }
152
+
25
153
  /**
26
154
  * Check for note changes and return details about what changed.
27
155
  */
28
- export async function detectChanges(): Promise<DetectedChanges> {
156
+ export async function detectChanges(
157
+ existingIndexMetadata?: IndexMetadataRecord[]
158
+ ): Promise<DetectedChanges> {
29
159
  debug("Checking for changes...");
30
160
 
31
161
  const currentNotes = await getAllNotes();
32
- const store = getVectorStore();
33
162
 
34
- let existingRecords;
35
- try {
36
- existingRecords = await store.getAll();
37
- } catch {
38
- // No index exists yet
39
- debug("No existing index found");
40
- return {
41
- hasChanges: currentNotes.length > 0,
42
- added: currentNotes,
43
- modified: [],
44
- deleted: [],
45
- };
163
+ let existingRecords = existingIndexMetadata;
164
+ if (!existingRecords) {
165
+ try {
166
+ existingRecords = await getIndexMetadata();
167
+ } catch {
168
+ // No index exists yet
169
+ debug("No existing index found");
170
+ return {
171
+ hasChanges: currentNotes.length > 0,
172
+ added: currentNotes,
173
+ modified: [],
174
+ deleted: [],
175
+ };
176
+ }
46
177
  }
47
178
 
48
179
  // Build lookup maps
@@ -106,46 +237,75 @@ export async function checkForChanges(): Promise<boolean> {
106
237
  * @returns true if index was refreshed, false if no changes
107
238
  */
108
239
  export async function refreshIfNeeded(): Promise<boolean> {
109
- const changes = await detectChanges();
240
+ try {
241
+ const ttlRaw = process.env.INDEX_TTL;
242
+ if (!ttlRaw) {
243
+ debug("Auto-refresh disabled (INDEX_TTL not set)");
244
+ return false;
245
+ }
110
246
 
111
- if (!changes.hasChanges) {
112
- return false;
113
- }
247
+ const parsedTtlSeconds = Number.parseInt(ttlRaw, 10);
248
+ if (!Number.isFinite(parsedTtlSeconds) || parsedTtlSeconds <= 0) {
249
+ debug("Auto-refresh disabled (INDEX_TTL invalid)");
250
+ return false;
251
+ }
114
252
 
115
- // Update main index
116
- debug("Changes detected, running incremental index...");
117
- const result = await incrementalIndex();
118
- debug(`Main index refresh: ${result.indexed} notes updated in ${result.timeMs}ms`);
253
+ const timeoutMs = getSearchRefreshTimeoutMs();
254
+ if (refreshInFlight) {
255
+ debug("Auto-refresh already in progress, waiting for existing run");
256
+ const sharedResult = await withTimeout(refreshInFlight, timeoutMs);
257
+ if (sharedResult === null) {
258
+ debug(`Auto-refresh wait timed out after ${timeoutMs}ms; returning stale index results`);
259
+ return false;
260
+ }
261
+ return sharedResult;
262
+ }
119
263
 
120
- // Update chunk index if it exists and there are changes
121
- const hasChunks = await hasChunkIndex();
122
- if (hasChunks && (changes.added.length > 0 || changes.modified.length > 0)) {
123
- debug("Updating chunk index for changed notes...");
264
+ const refreshTask = (async () => {
265
+ let existingRecords: IndexMetadataRecord[] = [];
124
266
 
125
- // Fetch full content for changed notes
126
- const changedNotes = [...changes.added, ...changes.modified];
127
- const notesWithContent = await Promise.all(
128
- changedNotes.map(async (n) => {
129
- const note = await getNoteByFolderAndTitle(n.folder, n.title);
130
- return note;
267
+ try {
268
+ existingRecords = await getIndexMetadata();
269
+ } catch {
270
+ // No index yet - treat as empty metadata
271
+ existingRecords = [];
272
+ }
273
+
274
+ const lastIndexedAtMs = getLatestIndexedAtMs(existingRecords);
275
+ const shouldRefresh = shouldAutoRefreshByTtl(
276
+ ttlRaw,
277
+ Date.now(),
278
+ lastIndexedAtMs
279
+ );
280
+
281
+ if (!shouldRefresh) {
282
+ debug("Auto-refresh skipped by TTL policy");
283
+ return false;
284
+ }
285
+
286
+ return runRefresh(existingRecords);
287
+ })()
288
+ .catch((error) => {
289
+ debug("Auto-refresh failed; returning stale index results", error);
290
+ return false;
131
291
  })
132
- );
292
+ .finally(() => {
293
+ if (refreshInFlight === refreshTask) {
294
+ refreshInFlight = null;
295
+ }
296
+ });
133
297
 
134
- // Filter out nulls (notes that couldn't be fetched)
135
- const validNotes = notesWithContent.filter((n) => n !== null);
298
+ refreshInFlight = refreshTask;
136
299
 
137
- if (validNotes.length > 0) {
138
- const chunksCreated = await updateChunksForNotes(validNotes);
139
- debug(`Chunk index refresh: ${chunksCreated} chunks for ${validNotes.length} notes`);
300
+ const refreshed = await withTimeout(refreshTask, timeoutMs);
301
+ if (refreshed === null) {
302
+ debug(`Auto-refresh timed out after ${timeoutMs}ms; returning stale index results`);
303
+ return false;
140
304
  }
141
305
 
142
- // Delete chunks for deleted notes
143
- if (changes.deleted.length > 0) {
144
- const chunkStore = getChunkStore();
145
- await chunkStore.deleteChunksByNoteIds(changes.deleted);
146
- debug(`Deleted chunks for ${changes.deleted.length} notes`);
147
- }
306
+ return refreshed;
307
+ } catch (error) {
308
+ debug("Unexpected auto-refresh failure; returning stale index results", error);
309
+ return false;
148
310
  }
149
-
150
- return true;
151
311
  }