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:
Colby Mchenry
2026-06-14 23:47:31 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent cea4d086f9
commit beca7116a0
5 changed files with 115 additions and 3 deletions
+1 -1
View File
@@ -66,7 +66,7 @@ typically one to a few calls; a grep/read exploration is dozens.
- **Don't chain \`codegraph_search\` + \`codegraph_node\`** to understand an area — ONE \`codegraph_explore\` returns the relevant symbols' source together in a single round-trip.
- **Don't loop \`codegraph_node\` over many symbols** — one \`codegraph_explore\` call returns them all grouped by file, while each separate call re-reads the whole context and costs far more. Use \`codegraph_node\` for a single symbol.
- **Don't reach for the \`Read\` tool on an indexed source file** — \`codegraph_node\` with a \`file\` reads it for you (same \`<n>\\t<line>\` source, \`offset\`/\`limit\` like Read, faster, with its blast radius), and with a \`symbol\` it returns the source plus the caller/callee trail. Reach for raw \`Read\` only for what codegraph doesn't index (configs, docs) or when the staleness banner flags a file as pending re-index.
- **After editing, check the staleness banner.** When a tool response starts with "⚠️ Some files referenced below were edited since the last index sync…", the listed files are pending re-index — Read those specific files for accurate content. Every file NOT in that banner is fresh, so still trust codegraph.
- **After editing, check the staleness banner.** When a tool response starts with "⚠️ Some files referenced below were edited since the last index sync…", the listed files are pending re-index — Read those specific files for accurate content. Every file NOT in that banner is fresh, so still trust codegraph. A different, rarer banner — "⚠️ CodeGraph auto-sync is DISABLED…" — means live watching stopped entirely (the whole index is frozen, not just a few files); until it's resolved, Read files directly to confirm anything that may have changed.
## Limitations
+56
View File
@@ -347,6 +347,23 @@ export function formatStaleFooter(stale: PendingFile[]): string {
);
}
/**
* Whole-index degradation banner (issue #876). Emitted at the top of a read
* tool response when live watching has permanently stopped — at which point
* `getPendingFiles()` is empty, so the per-file banner above can't fire even
* though the index is now FROZEN and silently drifting stale. Leads with the
* agent-actionable instruction (Read directly) and carries the reason, which
* already names the operator remedy (`codegraph sync` / git hooks).
*/
export function formatDegradedBanner(reason: string | null): string {
return (
'⚠️ CodeGraph auto-sync is DISABLED — live file watching stopped, so the index is ' +
'frozen and any file edited since then is stale here. Read files directly to confirm ' +
'current content before relying on it.' +
(reason ? `\n Reason: ${reason}` : '')
);
}
/**
* MCP Tool definition
*/
@@ -1016,6 +1033,32 @@ export class ToolHandler {
}
}
// Whole-index degradation (#876): once live watching has permanently
// stopped, getPendingFiles() is empty so the per-file banner below can't
// fire — but the index is now FROZEN and silently drifting stale. Surface
// one global notice instead, so the agent Reads for current content rather
// than trusting a response off a no-longer-updating index. (Cross-project
// calls open a watcher-less CodeGraph, so this is false there — correct: we
// only know degraded state for the default session project.)
let degraded = false;
try {
degraded = cg.isWatcherDegraded?.() ?? false;
} catch {
degraded = false;
}
if (degraded) {
const [head, ...tail] = result.content;
if (!head || head.type !== 'text') return result;
let reason: string | null = null;
try {
reason = cg.getWatcherDegradedReason?.() ?? null;
} catch {
reason = null;
}
const composed = `${formatDegradedBanner(reason)}\n\n${head.text}`;
return { ...result, content: [{ type: 'text', text: composed }, ...tail] };
}
// Defensive: some test fakes inject a partial CodeGraph stub without the
// newer pending-files API. Treat missing/throwing as "no pending files."
let pending: PendingFile[] = [];
@@ -3325,6 +3368,19 @@ export class ToolHandler {
}
}
// Whole-index degradation (#876): when live watching has permanently
// stopped, getPendingFiles() is empty (so no "Pending sync" section below)
// but the index is frozen — call that out explicitly here, the one place an
// agent asks "is the index caught up?".
if (cg.isWatcherDegraded()) {
lines.push(
'',
'### Auto-sync disabled:',
`- ${cg.getWatcherDegradedReason() ?? 'live file watching stopped'}`,
'- The index is frozen; Read files directly for current content.'
);
}
// Per-file freshness — the inverse of the auto-prepended staleness banner
// (issue #403). Surfacing it inside `status` gives the agent a single
// place to ask "is the index caught up?" rather than inferring from