Skip to content

fix(mcp): re-arm the watcher after lock contention, report freshness, refuse answers from changed files (#1959) - #1980

Closed
danusha2345 wants to merge 5 commits into
colbymchenry:mainfrom
danusha2345:fix/1959-watcher-rearm
Closed

danusha2345 wants to merge 5 commits into
colbymchenry:mainfrom
danusha2345:fix/1959-watcher-rearm

Conversation

@danusha2345

Copy link
Copy Markdown
Contributor

Addresses asks 1, 2 and 3 of #1959. Ask 6 (the lock's age test) is a separate PR, and ask 5 already holds on main. The fallback walk honours .git/info/exclude and core.excludesFile; a probe with a corrupted .git/index confirmed it.

Stacked on #1977. The first commit is that PR's one-line watcher fix. Without it a re-armed watcher whose catch-up sync fails would never retry, since the catch-up is a full scan with no pending files.

Problem

With many sessions on one checkout, another writer holding the lock is normal. When it holds the lock past the watcher's retry budget, the watcher degrades and stays off for the rest of the session. Queries keep answering from the frozen index under a prose banner. The report has codegraph_explore returning unrelated symbols for code edited after the watcher stopped.

Fix

  • Re-arm (ask 3). A watcher degraded by lock contention (only that cause) is restarted on the next MCP tool call. It runs a full scan, and until that scan commits the index is reported as recovering, not fresh. Re-arm attempts are throttled to one per 30 s, so many sessions can't poll a long-running writer. Resource exhaustion and deterministic sync failures still latch as before.
  • Freshness as data (ask 1). codegraph_status over MCP reports the last index time and how many tracked files were added, changed or removed since. It uses the same computation as codegraph status --json, run in a worker thread so the daemon's event loop isn't blocked by a git or file walk. A timeout or failure reports unknown, never a false zero.
  • Refuse answers from changed files (ask 2). While the watcher is degraded, a response is checked before it is sent. The files it answers from are codegraph_explore's rendered files, and the indexed files that codegraph_search, codegraph_callers, codegraph_callees and codegraph_impact name as locations. They are hashed against the index even when size and mtime look unchanged. If any changed, the answer is withheld and the changed files are named, so the agent reads them. Answers from unchanged files are still served, under the degraded banner. The refusal is a success-shaped response, not isError, and a refused explore is not recorded as source the session has seen. codegraph_node keeps its own drift gate from Stale index + fresh disk read: codegraph_node/codegraph_explore return a DIFFERENT symbol's code under the requested name, while asserting "verbatim, current on-disk source … do not Read" — verified through a real serve --mcp #1474, which serves a changed file's current bytes.

Tests

  • watcher.test.ts: re-arm after lock contention keeps the stale state until a full scan commits; a failed re-arm is throttled; a watcher degraded by a persistent generic sync failure is not re-armed.
  • mcp-staleness-banner.test.ts: the recovering banner and status section, and re-arm on the next tool call.
  • mcp-status-freshness.test.ts: the counts go from 0/0/0 to one added, one changed and one removed, both for working-tree edits and for a commit made after indexing.
  • mcp-stale-refusal.test.ts: explore refuses a changed file even with its size and mtime restored, and serves an unchanged one; callers and search refuse when a file they name changed.
  • extraction.test.ts: the git-failure walk honours .git/info/exclude and core.excludesFile.

Full suite on this branch (Linux, Node 22, native kernel): all tests pass except the known extraction.test.ts pool-worker crash from #1779 (fixed by #1883), which is intermittent and also occurs on main; that file alone passes 655/655.

🤖 Generated with Claude Code

danusha2345 and others added 5 commits September 26, 2026 15:58
…#1964)

A directory removal adds no pending file, only needsFullScan. The retry
after a failed sync (lock contention or a generic failure) was gated on
pending files alone, so the owed full reconcile was dropped and the
removed directory's files kept their nodes. Reschedule while needsFullScan
is set as well. A regression test also pins the mid-sync scope-change case,
which already follows a scoped pass with a full scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… degradation (colbymchenry#1959)

Explore already refuses to render source from a file that changed while
auto-sync was off. search, callers, callees and impact answer from the same
frozen graph but kept replying, naming the changed file as if its edges were
current. They now take the same refusal: the indexed files an answer names
as locations are hashed against the index, and a drifted one is named
instead of the answer. codegraph_node keeps its own drift gate, which serves
the changed file's current bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry

Copy link
Copy Markdown
Owner

Thanks @danusha2345! Your commits here were carried into #2043 (authorship preserved) together with #1978, with follow-up changes from review, and that is now merged. Closing this one in favour of it. It will be in the next release.

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.

2 participants