Skip to content

fix(graph): invalidate cached context after atomic file replacement - #577

Draft
Frankie-Xu wants to merge 1 commit into
trailhq:mainfrom
Frankie-Xu:fix/graph-cache-file-identity-20261009
Draft

Frankie-Xu wants to merge 1 commit into
trailhq:mainfrom
Frankie-Xu:fix/graph-cache-file-identity-20261009

Conversation

@Frankie-Xu

@Frankie-Xu Frankie-Xu commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

When a long-lived process has cached the graph or ask sidecar, an external atomic replacement with the same file size and mtime leaves the old parsed value cached. Repairing malformed JSON by such a replacement can also keep returning cached null. Both uncached production readers already see the replacement bytes in the regressions.

Extend the existing file stamp with device, inode and ctime. The next load observes the replacement, while subsequent unchanged loads retain the shared parsed object. Public loader interfaces, null semantics, per-path isolation and explicit invalidation remain unchanged. This fixes stale Agent context after file replacement; it does not provide a content-hash guarantee for in-place writes whose entire observed stamp is unchanged.

Validation: the exact complete source closure on main fe30ead39d5e6f0c921018d364da2bdbc9d4b3ad is unchanged from the independently reviewed baseline. The observed baseline has 6 passes and 4 causal failures, and the same observed regression with the fix has 10/10 passes. The final upstream regression has 10/10 on both the original candidate and the actual main-based isolated checkout (macOS arm64, Node26.5.0, one worker). Tests use real production writers/readers and filesystem rename, including same-mtime/size replacement, malformed JSON repair, warm reuse, missing/removal/recreation, invalidation and root isolation. Linux/Windows replay entry points are prepared but have not run. No local full-suite, MCP end-to-end or performance result is claimed in this local validation.

This is independent of #561 root routing and #399 ingestion. A read-only file-list review of all119 public open PRs at collection time found no change to the loader or this new test; this does not rule out unpublished work. Developed with AI assistance; patch, causal evidence and independent review are preserved locally.

Candidate commit: 3e3885ea430d77015bc676059ed63359b6cb7771; its two source/test files are the exact reviewed and locally validated patch.

Hosted validation and shared CI blocker (2026-10-09):

The Ubuntu test job passed on this PR source: 1366 passed, 0 failed, 1 skipped. All 10 tests in the changed regression file appear as passing in the hosted log. Node 24 WASM regression also passed. This is hosted Ubuntu evidence; the separate finite platform replay entry has not run, and no local Windows or extra Git-version matrix is claimed.

CodeQL analyze fails before scanning because initialization uses4.37.8 while analysis uses4.38.1 (Loaded a configuration file for version ...). The existing independent #562 tracks that workflow pin mismatch. No workflow patch, rerun or unrelated CI change is included here.

Hosted Windows validation (2026-10-09): the Windows test job also passed: 1361 passed, 0 failed, 6 skipped. All 10 cases in this PR's changed regression file match passing TAP entries on Windows, including the actual filesystem/Git assertions. This adds hosted platform evidence; the separate baseline/candidate replay entry and extra Git-version matrix remain unexecuted.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

🌱 graft blast radius

1 area changed → 7 areas can be affected. 11 dependent symbols, depth 2.
Tests: no test reaches Graph Caching.
Tag: @anirudhkumar-nanonets — 8 of 8 areas · @shhdwi — 6 of 8 areas

flowchart TB
  A0(("CLI Initialization<br/>2 symbols"))
  A1(("Corpus Loading<br/>2 symbols"))
  A2(("Pull Request Review<br/>2 symbols"))
  A3(("Workspace Graph Traversal<br/>2 symbols"))
  A4(("MCP Tool Execution<br/>1 symbol"))
  AX(("2 smaller areas<br/>2 symbols"))
  classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
  class A0,A1,A2,A3,A4 reached;
  classDef tail fill:#EEF2F3,stroke:#9AA4A9,stroke-width:1px,color:#3A4247;
  class AX tail;
Loading
Can be affected Symbols Nearest hop Reached from
CLI Initialization 2 src/cli.ts:L1-L1939 cli.ts — calls, depth 2 Graph Caching
Corpus Loading 2 src/ask/ask.ts:L162-L187 loadCorpus — calls, depth 2 Graph Caching
Pull Request Review 2 src/app/brain-build.ts:L251-L358 readRepository — calls, depth 2 Graph Caching
Workspace Graph Traversal 2 src/graph/traverse-cli.ts:L166-L242 runCallersCommand — calls, depth 2 Graph Caching
MCP Tool Execution 1 src/mcp/tools.ts:L247-L329 callSingleTool — calls, depth 2 Graph Caching
Blast Search 1 src/blast/blast-cli.ts:L63-L124 runBlastCommand — calls, depth 2 Graph Caching
1 smaller area 1 Grep Search see below
Who knows this code — 2 people across 8 areas
Area Who knows it
Graph Caching · changed @shhdwi — 2 commits, last 3mo ago · @anirudhkumar-nanonets — 1 commit, last 2mo ago
CLI Initialization · affected @anirudhkumar-nanonets — 52 commits, last 10d ago · @shhdwi — 23 commits, last 2mo ago
Corpus Loading · affected @shhdwi — 16 commits, last 2mo ago · @anirudhkumar-nanonets — 12 commits, last 14d ago
Pull Request Review · affected @anirudhkumar-nanonets — 9 commits, last 29d ago
Workspace Graph Traversal · affected @shhdwi — 10 commits, last 2mo ago · @anirudhkumar-nanonets — 5 commits, last 2mo ago
MCP Tool Execution · affected @shhdwi — 14 commits, last 2mo ago · @anirudhkumar-nanonets — 7 commits, last 1mo ago
Blast Search · affected @anirudhkumar-nanonets — 7 commits, last 1mo ago
Grep Search · affected @anirudhkumar-nanonets — 4 commits, last 2mo ago · @shhdwi — 3 commits, last 2mo ago

Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no @ has no GitHub handle in its commit email — tag them by hand, or add a .mailmap entry. A suggestion from history, not a CODEOWNERS rule.

All 11 dependent symbols, grouped by area

CLI Initialization — 2 symbols in 1 file

  • src/cli.ts:L1-L1939 — cli.ts (calls, depth 2)
    4: * map, init. Git is the sync: commit graft/ and a clone has the graph. A
  • src/cli.ts:L1012-L1229 — runInitCommand (calls, depth 2)
    1096: // graph, so each child's own `buildGraphIfMissing` then finds one and no-ops.

Corpus Loading — 2 symbols in 1 file

  • src/ask/ask.ts:L162-L187 — loadCorpus (calls, depth 2)
    186: return { concepts, graph: loadGraphCached(outDir), askIndex: loadAskIndexCached(outDir) };
  • src/ask/ask.ts:L1438-L1468 — skeleton (calls, depth 2)
    1440: const graph = loadGraphCached(outDir);

Pull Request Review — 2 symbols in 2 files

  • src/app/brain-build.ts:L251-L358 — readRepository (calls, depth 2)
    258: // four things in a trench coat — the clone, the graph build, the
  • src/app/review.ts:L45-L99 — reviewPullRequest (calls, depth 2)
    58: const graph = loadGraphCached(contextDir);

Workspace Graph Traversal — 2 symbols in 2 files

  • src/graph/traverse-cli.ts:L166-L242 — runCallersCommand (calls, depth 2)
    169: const graph = loadGraphCached(contextDir);
  • src/graph/workspace.ts:L145-L156 — loadWorkspaceGraphs (calls, depth 2)
    151: const graph = loadGraphCached(contextDirFor(join(root, child)));

MCP Tool Execution — 1 symbol in 1 file

  • src/mcp/tools.ts:L247-L329 — callSingleTool (calls, depth 2)

Blast Search — 1 symbol in 1 file

  • src/blast/blast-cli.ts:L63-L124 — runBlastCommand (calls, depth 2)
    69: const graph = loadGraphCached(contextDir);

Grep Search — 1 symbol in 1 file

  • src/search/grep-cli.ts:L87-L119 — runGrepCommand (calls, depth 2)
    90: const graph = loadGraphCached(contextDir);
Test signal per changed area — 1 ✗

Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.

  • ✗ Graph Caching — 0 of 2 reached · no test file reaches it
    • not reached: statOf, loadCached
3 test suites also reference this code

3 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.

  • test/graph-load.test.ts
  • test/graph-scopes.test.ts
  • test/regression-single-repo.test.ts

graft blast · origin/main...HEAD · depth 2 · 2 changed files

Open the interactive graph → — click an area to see its dependent symbols at file:line.

github-actions Bot added a commit that referenced this pull request Oct 9, 2026

This branch has not been deployed

No deployments
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