fix(watcher): degrade cleanly on watch exhaustion and prolonged lock contention (#891)

The live file watcher could stay "alive" after it had stopped being
trustworthy. EMFILE/ENFILE watch-resource exhaustion only logged (and was
silently tolerated on the Linux per-directory path), and prolonged
LockUnavailableError retried forever at the normal debounce cadence — both
left auto-sync dead while the index silently drifted stale. Especially bad
for long-running MCP/daemon sessions.

Add a one-way degrade(): on watch-resource exhaustion (any watch strategy)
or on lock contention past a bounded exponential-backoff budget, log once,
fire a new onDegraded callback, and stop. start() now returns false
consistently when the per-directory path degrades at startup — it previously
returned true on Linux, so the MCP server reported the watcher "active" when
it had degraded. Wire onDegraded into the MCP server so callers are actually
told, and expose isDegraded()/getDegradedReason().

Builds on the approach in #877 by @thismilktea. Validated on macOS
(recursive), Linux (per-directory, Docker) and Windows (recursive) — 30/30
watcher + watch-policy tests on each.

Closes #876

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-14 23:32:54 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 1bd9431879
commit cea4d086f9
4 changed files with 351 additions and 13 deletions
+8
View File
@@ -216,6 +216,14 @@ export class MCPEngine {
onSyncError: (err) => {
process.stderr.write(`[CodeGraph MCP] Auto-sync error: ${err.message}\n`);
},
onDegraded: (reason) => {
// Live watching gave up permanently (watch-resource exhaustion or a
// write lock held past the retry budget). Say so loudly and ONCE — the
// graph will no longer auto-update, so a long-running MCP session must
// not keep assuming it's fresh. The reason already names the remedy
// (`codegraph sync` / git sync hooks).
process.stderr.write(`[CodeGraph MCP] File watcher degraded — ${reason}\n`);
},
});
this.watcherStarted = true;