feat(mcp): surface degraded watcher state to the agent in tool responses (#892)
When live file watching permanently degrades (watch-resource exhaustion, or a
write lock held past the retry budget), getPendingFiles() goes empty — so the
existing per-file staleness banner can't fire even though the index is now
frozen and silently drifting stale. The agent kept getting clean-looking
responses off a no-longer-updating index.
Read-tool responses now lead with a whole-index banner ("CodeGraph auto-sync
is DISABLED…") whenever the watcher is degraded, and codegraph_status gets a
dedicated "Auto-sync disabled" section. Both carry the degrade reason and tell
the agent to Read files directly. Expose isWatcherDegraded() /
getWatcherDegradedReason() on the CodeGraph class, and document the new banner
in the MCP server instructions.
Completes the agent-notification half of #876 (the operator-facing onDegraded
wiring shipped in #891).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cea4d086f9
commit
beca7116a0
@@ -26,7 +26,7 @@ import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import CodeGraph from '../src/index';
|
||||
import { ToolHandler } from '../src/mcp/tools';
|
||||
import { __emitWatchEventForTests } from '../src/sync/watcher';
|
||||
import { __emitWatchEventForTests, __setFsWatchForTests } from '../src/sync/watcher';
|
||||
|
||||
function waitFor(condition: () => boolean, timeoutMs = 2000, intervalMs = 25): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -71,11 +71,25 @@ describe('MCP staleness banner', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__setFsWatchForTests(null); // reset the injected fs.watch seam
|
||||
try { cg.unwatch(); } catch { /* ignore */ }
|
||||
try { cg.close(); } catch { /* ignore */ }
|
||||
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// Force watch-resource exhaustion at startup so the real watcher degrades
|
||||
// deterministically on any platform (recursive or per-directory strategy).
|
||||
const degradeWatcher = () => {
|
||||
__setFsWatchForTests(() => {
|
||||
const err = new Error('too many open files') as NodeJS.ErrnoException;
|
||||
err.code = 'EMFILE';
|
||||
throw err;
|
||||
});
|
||||
const started = cg.watch({ debounceMs: 1000 }); // real (non-inert) watcher
|
||||
expect(started).toBe(false);
|
||||
expect(cg.isWatcherDegraded()).toBe(true);
|
||||
};
|
||||
|
||||
it('prepends a stale banner when the response references a pending file', async () => {
|
||||
// Long debounce so the edit lingers in pendingFiles while we query.
|
||||
cg.watch({ debounceMs: 4000, inertForTests: true });
|
||||
@@ -170,4 +184,29 @@ describe('MCP staleness banner', () => {
|
||||
it('returns zero pending files when no watcher is active', () => {
|
||||
expect(cg.getPendingFiles()).toEqual([]);
|
||||
});
|
||||
|
||||
it('prepends a whole-index degraded banner once live watching has permanently stopped (#876)', async () => {
|
||||
degradeWatcher();
|
||||
|
||||
const res = await handler.execute('codegraph_search', { query: 'alphaOnly' });
|
||||
expect(res.isError).toBeFalsy();
|
||||
const text = res.content[0].text;
|
||||
|
||||
expect(text.startsWith('⚠️')).toBe(true);
|
||||
expect(text).toMatch(/auto-sync is DISABLED/i);
|
||||
expect(text).toMatch(/Read files directly/i);
|
||||
expect(text).toContain('OS watch/file limit exhausted'); // the degrade reason
|
||||
expect(text).toMatch(/alphaOnly/); // the real result still follows the banner
|
||||
});
|
||||
|
||||
it('surfaces the degraded state as its own section in codegraph_status (#876)', async () => {
|
||||
degradeWatcher();
|
||||
|
||||
const res = await handler.execute('codegraph_status', {});
|
||||
const text = res.content[0].text;
|
||||
expect(text).toContain('### Auto-sync disabled:');
|
||||
expect(text).toContain('OS watch/file limit exhausted');
|
||||
// status renders the notice inline, so the auto-banner is not also prepended.
|
||||
expect(text.startsWith('⚠️')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user