Skip to content

fix(watcher): retain pending files on zero-result sync#450

Merged
colbymchenry merged 3 commits into
colbymchenry:mainfrom
thismilktea:fix/watcher-zero-result-pending-v2
May 26, 2026
Merged

fix(watcher): retain pending files on zero-result sync#450
colbymchenry merged 3 commits into
colbymchenry:mainfrom
thismilktea:fix/watcher-zero-result-pending-v2

Conversation

@thismilktea
Copy link
Copy Markdown
Contributor

Summary

  • treat zero-result watcher syncs as no progress when pending files still exist
  • preserve pending files instead of clearing them
  • let the existing retry path reschedule sync automatically
  • add a regression test covering the zero-result retry path

Problem

FileWatcher.flush() currently treats any resolved syncFn() result as a successful sync.

When CodeGraph.sync() cannot acquire the file lock, it can return a zero-result no-op instead of throwing. In that
case, the watcher may clear pendingFiles even though nothing was actually indexed.

This can incorrectly mark edited files as fresh until a later file event happens to trigger another sync.

Test plan

  • run __tests__/watcher.test.ts
  • verify pending files remain after a zero-result sync
  • verify onSyncComplete is not called for the zero-result no-op
  • verify the watcher retries automatically and clears pending files only after a real successful sync

Closes #449

thismilktea and others added 2 commits May 26, 2026 19:24
Replace the heuristic `(filesChanged === 0 && durationMs === 0)` check
inside `FileWatcher.flush()` with a typed `LockUnavailableError` thrown
by `CodeGraph.watch()`'s sync wrapper. The wrapper has access to the
full `SyncResult`, including `filesChecked` — which is **only** zero
when `sync()` failed to acquire the cross-process file lock (a real
empty sync always has `filesChecked > 0` because `scanDirectory` ran).
That eliminates the heuristic's edge case where a fast no-op sync
returns `durationMs === 0` by `Date.now()` rounding and gets mistaken
for a lock failure on tiny projects.

The watcher's `catch` block now distinguishes `LockUnavailableError`
from real errors: it logs at `logDebug` (not `logWarn`) and does NOT
call `onSyncError` — so a long-running external indexer holding the
lock doesn't spam stderr every debounce cycle via the MCP daemon's
`Auto-sync error` handler. The existing post-catch path already
preserves `pendingFiles` and reschedules, so no new control flow is
needed.

A/B validated end-to-end against the built dist on macOS with a
three-scenario repro (lock held, lock released mid-flight, real sync
error):

- main:           lock-held silently clears pendingFiles (BUG);
                  lock-released never recovers (no real sync runs).
- PR-as-is:       lock-held preserves pendingFiles; lock-released
                  drains. Same observable behavior as wrapper-level.
- wrapper-level:  same outcomes; lock-failure goes through the catch
                  path silently (logDebug only, no onSyncError noise);
                  real errors still surface via onSyncError.

Updates the regression test to throw `LockUnavailableError` (the real
contract surfaced to `FileWatcher` by `CodeGraph.watch()`), and
asserts `onSyncError` stays quiet during the lock-held cycle.

Closes colbymchenry#449.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves CHANGELOG.md conflict by interleaving the colbymchenry#449 entry with the
other Unreleased fixes that landed since this branch diverged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@colbymchenry colbymchenry merged commit 72c08c2 into colbymchenry:main May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

watcher treats zero-result sync as success and clears pending files

2 participants