Two coupled changes addressing the issue's underlying ask — "how does the
agent know when the index lags" — without resorting to a static wait.
Per-file staleness banner
-------------------------
FileWatcher now tracks per-path `pendingFiles` (path, firstSeenMs,
lastSeenMs, indexing) — events since the last successful sync, cleared
only after a sync whose `syncStartedMs >= lastSeenMs` commits. Chokidar
initial-scan events are gated behind a `ready` flag (with `waitUntilReady()`
exposed so tests can deterministically wait through it) so a fresh startup
doesn't falsely flag every existing file as pending.
ToolHandler now wraps every code-returning response (search, context,
callers, callees, impact, trace, explore, node, files) with
`withStalenessNotice`: intersects "files referenced in the response" with
`getPendingFiles()` and emits a hybrid signal —
* banner at the top for files referenced AND pending (with edit age +
indexing/pending-sync state, telling the agent to Read those specific
files directly; the rest of the response stays fresh and codegraph
stays authoritative for it),
* compact footer for pending files elsewhere in the project not
referenced above (capped at 5).
Cost is one boolean check + N substring matches when pending; zero
allocation when idle. `codegraph_status` surfaces the same data as a
first-class `### Pending sync:` section so the agent can ask "is the index
caught up?" in one call.
Cross-project quirk: when an agent passes `projectPath` matching the
default session's project, the staleness wrapper switches from the cached
cross-project CodeGraph (no watcher) to the default one (with watcher) so
the signal still fires. Same fix applied to `handleStatus`.
CODEGRAPH_WATCH_DEBOUNCE_MS
---------------------------
MCP `serve --mcp` now reads `CODEGRAPH_WATCH_DEBOUNCE_MS` and forwards it
to `cg.watch({ debounceMs })`. Clamped to [100ms, 60s]; out-of-range or
non-numeric values fall back to the FileWatcher default (2000ms). Active
value is logged to stderr on watcher startup so it's discoverable. The
docs in `server-instructions.ts`, `installer/instructions-template.ts`,
and `.cursor/rules/codegraph.mdc` no longer claim "~500ms"; they now
describe the banner mechanism instead — since per-file staleness replaces
the "wait N ms" guidance entirely, the docs become accurate at any
debounce value.
Validation
----------
* 847 unit/integration tests pass (added 15 new ones — pending-file
tracking, banner/footer routing, status section, env-var parsing).
* Direct MCP probe through a real `codegraph serve --mcp` process: edit a
file, query within the debounce window, banner fires naming the
edited file with edit-age.
* Real Claude TUI session via `scripts/agent-eval/itrun.sh` with
`CODEGRAPH_WATCH_DEBOUNCE_MS=10000`: agent edits `math.ts`, calls
`codegraph_explore`, reads the banner, **and discloses it unprompted in
its final reply**: "note: symbol index is mid-sync for the new `divide`,
but the source it returned is verbatim from disk."
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
4a4a37d135
commit
b48170e69f
+27
-2
@@ -46,7 +46,7 @@ import {
|
||||
import { GraphTraverser, GraphQueryManager } from './graph';
|
||||
import { ContextBuilder, createContextBuilder } from './context';
|
||||
import { Mutex, FileLock } from './utils';
|
||||
import { FileWatcher, WatchOptions } from './sync';
|
||||
import { FileWatcher, WatchOptions, PendingFile } from './sync';
|
||||
|
||||
// Re-export types for consumers
|
||||
export * from './types';
|
||||
@@ -75,7 +75,7 @@ export {
|
||||
defaultLogger,
|
||||
} from './errors';
|
||||
export { Mutex, FileLock, processInBatches, debounce, throttle, MemoryMonitor } from './utils';
|
||||
export { FileWatcher, WatchOptions } from './sync';
|
||||
export { FileWatcher, WatchOptions, PendingFile } from './sync';
|
||||
export { MCPServer } from './mcp';
|
||||
|
||||
/**
|
||||
@@ -499,6 +499,31 @@ export class CodeGraph {
|
||||
return this.watcher?.isActive() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Files seen by the file watcher since the last successful sync —
|
||||
* the per-file "stale" signal MCP tools attach to responses so an agent
|
||||
* can fall back to {@link Read} for just the affected file without
|
||||
* waiting for a debounced sync to complete (issue #403).
|
||||
*
|
||||
* Returns an empty list when the watcher isn't active, or no events have
|
||||
* arrived. Each entry includes `firstSeenMs` and `lastSeenMs` (wall-clock
|
||||
* `Date.now()` values) so callers can render "edited Nms ago", plus an
|
||||
* `indexing` flag indicating whether the in-flight sync (if any) will
|
||||
* absorb that file.
|
||||
*/
|
||||
getPendingFiles(): PendingFile[] {
|
||||
return this.watcher?.getPendingFiles() ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves once the file watcher has finished its initial chokidar scan.
|
||||
* Useful for tests that need a deterministic boundary before asserting on
|
||||
* `getPendingFiles()`. Resolves immediately when no watcher is active.
|
||||
*/
|
||||
waitUntilWatcherReady(timeoutMs?: number): Promise<void> {
|
||||
return this.watcher ? this.watcher.waitUntilReady(timeoutMs) : Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files that have changed since last index
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user