Skip to content

fix(mcp): self-heal stale read-tool responses after external file edits - #180

Merged
zzet merged 2 commits into
mainfrom
fix/read-family-staleness-self-heal
Jun 27, 2026
Merged

fix(mcp): self-heal stale read-tool responses after external file edits#180
zzet merged 2 commits into
mainfrom
fix/read-family-staleness-self-heal

Conversation

@zzet

@zzet zzet commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Problem

After an external file change — a native Edit/Write, a git checkout, or any edit that does not go through Gortex's own edit_file/write_file — the index-backed read tools (get_symbol_source, get_symbol, get_file_summary, get_editing_context, get_cfg) could return a stale body until the file watcher caught up. The visible symptom is a read returning an old symbol body while the file's metadata (e.g. line count) already reflects the new content.

Root cause

These tools call ensureFresh() to re-index a drifted file before serving it. But ensureFresh bailed out unconditionally whenever a multiIndexer was present:

if s.multiIndexer != nil {
    return nil
}

So in multi-repo mode the on-read self-heal did nothing at all. It had been disabled because the staleness check keyed off the lone single-Indexer, whose mtime map is empty for cross-repo paths — making IsStale true for every file, so the resulting mass re-index raced the live read surface and crashed the transport. It was switched off rather than fixed.

Fix

1. ensureFresh is now multi-repo-aware.
Each path is routed to its owning per-repo indexer (MultiIndexer.IndexerForFile), and re-indexed only when IsTrackedStale reports genuine drift. IsTrackedStale returns false for untracked / new / already-current files, so only a known-and-changed file is touched — no mass churn, which is what removes the original crash hazard. The recorded mtime is restamped after re-index so a follow-up read in the same window does no redundant work. Single-repo behaviour is unchanged (an active watcher still owns freshness, and the auto-refresh stands down rather than fight it).

2. read_file flags overlay-served responses.
When an editor-buffer overlay is active, read_file serves the buffer over disk; it now stamps served_from: "overlay" (plus an omission note) so an agent can tell a live buffer view from disk content. Drifted overlays were already rejected upstream by the overlay view guard, so this only annotates the live-buffer case.

Tests

  • TestEnsureFresh_MultiRepoSelfHealsStaleFile — the regression test: a file changed on disk after indexing is re-indexed on read in multi-repo mode. Fails on the pre-fix code (which returned nil).
  • TestEnsureFresh_SingleRepoSelfHealsStaleFile — same for the single-repo path.
  • TestReadFile_FreshOverlayIsServed / TestReadFile_OverlayDriftSurfacesError — overlay provenance flag + the existing drift protection.
  • The existing ensureFresh tests (watch-mode-skip, nil-indexer, rate-limit) still pass.

Verification

  • go build -o /tmp/gortex-build ./cmd/gortex/ (CGO) — clean.
  • go test -race ./internal/mcp/2417 passed.
  • golangci-lint / go vet — clean on the touched files.

zzet added 2 commits June 27, 2026 11:22
…-repo mode

ensureFresh stood down entirely when a multiIndexer was present, so after a
native edit or any external write the index-backed read tools (get_symbol_source,
get_symbol, get_file_summary, get_editing_context, get_cfg) kept serving the old
body until the watcher caught up. Route each path to its owning per-repo indexer
and re-index it when IsTrackedStale reports drift, then restamp the recorded mtime
so a follow-up read in the same window does no redundant work. IsTrackedStale is
false for untracked/new/current files, so only a known-and-changed file is touched
-- the accurate per-repo staleness check is what makes this safe, replacing the
earlier cross-repo mass re-index that raced the live read surface.
When an overlay session is active, read_file serves the editor's in-buffer text
over the on-disk file, previously with no signal -- so an agent could not tell a
live buffer view from disk content. Stamp served_from:"overlay" plus an omission
note when the bytes came from an overlay. A drifted overlay is still rejected
upstream by the overlay view guard (read_file errors rather than serving a stale
buffer); this only annotates the live-buffer case.
@zzet
zzet merged commit 6659e93 into main Jun 27, 2026
10 checks passed
@zzet
zzet deleted the fix/read-family-staleness-self-heal branch June 28, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant