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
@@ -584,6 +584,23 @@ export class CodeGraph {
|
||||
return this.watcher?.isActive() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* True once live watching has permanently degraded (OS watch-resource
|
||||
* exhaustion, or a write lock held past the retry budget) and auto-sync is
|
||||
* disabled until the next {@link watch} call. Distinct from `!isWatching()`:
|
||||
* a stopped/never-started watcher is inactive but NOT degraded. MCP tools use
|
||||
* this to surface a whole-index "results may be stale" notice, since
|
||||
* `getPendingFiles()` goes empty once watching stops (#876).
|
||||
*/
|
||||
isWatcherDegraded(): boolean {
|
||||
return this.watcher?.isDegraded() ?? false;
|
||||
}
|
||||
|
||||
/** The reason live watching degraded, or null if it is healthy (#876). */
|
||||
getWatcherDegradedReason(): string | null {
|
||||
return this.watcher?.getDegradedReason() ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Files seen by the file watcher since the last successful sync —
|
||||
* the per-file "stale" signal MCP tools attach to responses so an agent
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user