gitnexus 1.6.11-rc.44 → 1.6.11-rc.45

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.
@@ -179,13 +179,15 @@ export function shouldStopAfterWatchRefreshFailure(error, paths) {
179
179
  export async function startWatchFileLoop(repoPath, debounceMs, refresh, onError, onWatcherError = (error) => onError(error, [])) {
180
180
  let ignorePath = await createWatchIgnorePredicate(repoPath);
181
181
  let ignoreControlValid = true;
182
+ let rearmPending = false;
183
+ let closed = false;
182
184
  const queue = new WatchRefreshQueue(async (paths) => {
183
185
  if (paths.some(isIgnoreControlPath) || !ignoreControlValid) {
184
186
  const retryingInvalidControls = !ignoreControlValid;
185
187
  try {
186
188
  ignorePath = await createWatchIgnorePredicate(repoPath);
187
189
  ignoreControlValid = true;
188
- watcher.add(repoPath);
190
+ rearmPending = true;
189
191
  }
190
192
  catch (error) {
191
193
  ignoreControlValid = false;
@@ -196,6 +198,12 @@ export async function startWatchFileLoop(repoPath, debounceMs, refresh, onError,
196
198
  : error);
197
199
  }
198
200
  }
201
+ // Re-arm before refreshing, never after: the refresh reads the whole
202
+ // repository, so a write that lands while the replacement watcher is
203
+ // arming is still picked up by this refresh, and a write that lands
204
+ // afterwards is reported by the armed watcher.
205
+ if (rearmPending)
206
+ await rearmWatcher();
199
207
  await refresh(paths);
200
208
  }, onError, debounceMs, {
201
209
  maxWaitMs: Math.max(2_000, debounceMs * 10),
@@ -203,46 +211,90 @@ export async function startWatchFileLoop(repoPath, debounceMs, refresh, onError,
203
211
  holdEventsUntilInitialRefresh: true,
204
212
  isPriorityPath: (filePath) => isIgnoreControlPath(filePath) || isConfigControlPath(filePath),
205
213
  });
206
- const watcher = watch(repoPath, {
207
- ignoreInitial: true,
208
- atomic: true,
209
- followSymlinks: false,
210
- awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 20 },
211
- ignored: (candidate, stats) => {
212
- const relative = repoRelativeWatchPath(repoPath, candidate);
213
- if (relative !== null && isAnalyzerOwnedWatchPath(relative))
214
- return true;
215
- if (relative !== null && (isIgnoreControlPath(relative) || isConfigControlPath(relative))) {
216
- return false;
214
+ const createWatcher = () => {
215
+ const created = watch(repoPath, {
216
+ ignoreInitial: true,
217
+ atomic: true,
218
+ followSymlinks: false,
219
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 20 },
220
+ ignored: (candidate, stats) => {
221
+ const relative = repoRelativeWatchPath(repoPath, candidate);
222
+ if (relative !== null && isAnalyzerOwnedWatchPath(relative))
223
+ return true;
224
+ if (relative !== null && (isIgnoreControlPath(relative) || isConfigControlPath(relative))) {
225
+ return false;
226
+ }
227
+ return ignorePath(candidate, stats?.isDirectory() ?? false);
228
+ },
229
+ });
230
+ // Events from an instance being retired are kept: they overlap with the
231
+ // replacement's coverage and the queue coalesces the duplicates.
232
+ created.on('all', (event, changedPath) => {
233
+ if (event !== 'add' && event !== 'change' && event !== 'unlink')
234
+ return;
235
+ const relative = repoRelativeWatchPath(repoPath, changedPath);
236
+ if (relative && isRelevantWatchPath(relative) && !isAnalyzerOwnedWatchPath(relative)) {
237
+ queue.enqueue(relative);
217
238
  }
218
- return ignorePath(candidate, stats?.isDirectory() ?? false);
219
- },
220
- });
221
- watcher.on('all', (event, changedPath) => {
222
- if (event !== 'add' && event !== 'change' && event !== 'unlink')
239
+ });
240
+ created.on('error', (error) => {
241
+ // Replacement failures before the swap are reported through `waitUntilReady`.
242
+ if (created !== watcher)
243
+ return;
244
+ // Chokidar can surface a transient EPERM on Windows while an ignored
245
+ // analyzer-owned path is replaced. Re-arm the watcher and force one
246
+ // bounded catch-up refresh so a missed event cannot leave the graph
247
+ // stale. Other watcher errors may mean coverage was lost and stay fatal.
248
+ if (TRANSIENT_WATCH_ERROR_CODES.has(error.code ?? '')) {
249
+ rearmPending = true;
250
+ queue.enqueue(WATCH_FULL_REFRESH_PATH);
251
+ return;
252
+ }
253
+ onWatcherError(error);
254
+ });
255
+ return created;
256
+ };
257
+ let watcher = createWatcher();
258
+ // Chokidar emits `ready` once per instance and `add()` returns before the
259
+ // rescan it starts has finished, with no signal for that completion. A file
260
+ // an ignore-rule reload has just unignored is therefore still unregistered
261
+ // when `add()` returns, and because `ignoreInitial` suppresses the `add` the
262
+ // rescan would emit, an immediate rewrite of that file is dropped for good
263
+ // (reproduced on chokidar 4 and 5). So re-arm by arming a replacement
264
+ // watcher and awaiting its `ready` instead. The outgoing instance keeps
265
+ // reporting until the replacement is armed, so the swap has no blind window,
266
+ // and a replacement that fails to arm leaves the working instance in place.
267
+ const rearmWatcher = async () => {
268
+ rearmPending = false;
269
+ if (closed)
223
270
  return;
224
- const relative = repoRelativeWatchPath(repoPath, changedPath);
225
- if (relative && isRelevantWatchPath(relative) && !isAnalyzerOwnedWatchPath(relative)) {
226
- queue.enqueue(relative);
271
+ const replacement = createWatcher();
272
+ try {
273
+ await waitUntilReady(replacement);
227
274
  }
228
- });
229
- watcher.on('error', (error) => {
230
- // Chokidar can surface a transient EPERM on Windows while an ignored
231
- // analyzer-owned path is replaced. Re-arm the root and force one bounded
232
- // catch-up refresh so a missed event cannot leave the graph stale. Other
233
- // watcher errors may mean coverage was lost and remain fatal.
234
- if (TRANSIENT_WATCH_ERROR_CODES.has(error.code ?? '')) {
235
- watcher.add(repoPath);
236
- queue.enqueue(WATCH_FULL_REFRESH_PATH);
237
- return;
275
+ catch (error) {
276
+ rearmPending = true;
277
+ try {
278
+ await replacement.close();
279
+ }
280
+ catch {
281
+ // The instance never became live; the arm error is the one to report.
282
+ }
283
+ throw new WatchControlReloadError(new Error('Unable to re-arm the filesystem watcher', { cause: error }));
238
284
  }
239
- onWatcherError(error);
240
- });
285
+ const retired = watcher;
286
+ watcher = replacement;
287
+ await retired.close();
288
+ // `close()` can land between arming the replacement and the swap above.
289
+ if (closed)
290
+ await replacement.close();
291
+ };
241
292
  try {
242
293
  await waitUntilReady(watcher);
243
294
  await queue.runInitial();
244
295
  }
245
296
  catch (error) {
297
+ closed = true;
246
298
  await watcher.close();
247
299
  await queue.close();
248
300
  throw error;
@@ -250,6 +302,7 @@ export async function startWatchFileLoop(repoPath, debounceMs, refresh, onError,
250
302
  return {
251
303
  waitForIdle: () => queue.waitForIdle(),
252
304
  close: async () => {
305
+ closed = true;
253
306
  await watcher.close();
254
307
  await queue.close();
255
308
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.11-rc.44",
3
+ "version": "1.6.11-rc.45",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",