Skip to content

Cut trail's peak memory on big repos by a third - #737

Open
anirudhkumar-nanonets wants to merge 1 commit into
mainfrom
claude/trail-memory
Open

anirudhkumar-nanonets wants to merge 1 commit into
mainfrom
claude/trail-memory

Conversation

@anirudhkumar-nanonets

@anirudhkumar-nanonets anirudhkumar-nanonets commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Issue: In a Claude Code session on assign, trail's processes used up to 4.2 GB at once, enough to make a laptop hang while trail syncs.
Why: The MCP server rebuilt the code map inside itself after edits and kept about 2 GB for the rest of the session. Every query also copied the whole search index, and one non-ASCII character made each map file twice as big in memory.
Fix: Rebuilds run in a short-lived process that gives its memory back when it ends, queries keep only the words they look up, and the map files are written as plain ASCII. One turn's peak drops to 2.9 GB, with the same answers.

Moves: active users, people with session_summary in the last 7 days (PostHog db 11, posthog_event_full_view): 7,214 on 2026-10-29, read 13:10 IST (996 of them from the trail CLI).

Second PR for the reported hang, after #736 (lock takeover, which can't happen on the reporter's 0.26.0). This one is the memory, and every path it changes is also in 0.26.0.

Measured: one turn on assign, median of 3

The MCP server answers a query, trail ask runs as the prompt hook runs it, the agent edits a file and queries again, then trail build runs as the end-of-turn sync does. Copy of assign (88k symbols: 94 MB map, 70 MB search index, 255 MB extract cache), 12-core Mac, 24 GB. Before is origin/main (19865ff4).

Before After
All trail processes at once 4,311 MB 2,957 MB
MCP server 3,704 MB 1,519 MB
MCP server after the turn 1,228 MB 619 MB
End-of-turn build 2,455 MB 2,029 MB
Rebuild after an edit inside the MCP server 1,744 MB, own process, exits
trail ask (prompt hook's child) 1,099 MB 933 MB
Map read in each prompt / post-edit hook 591 MB, 212 ms 483 MB, 149 ms
MCP call: clean / after an edit 1.95 s / 7.7 s 2.1 s / 6.5 s

Same answers: trail ask --json is byte-identical on 8 queries, ask-index.json is byte-identical, and wiring.json and the extract cache read back the same values.

What changed

  • src/graph/refresh.ts + new src/graph/refresh-run.ts: ensureFreshGraph's drift rebuild runs in a child process when the compiled script ships next to it (from source, i.e. the tests, it still builds in process). The query waits for it as it waited for the in-process build. A signal that ends the query kills the child before releasing the lock.
  • src/ask/ask.ts: a sidecar doc keeps only the query's terms and the plural forms hasTerm checks, plus its body length. --in keeps whole bags, since its idf comes from them.
  • src/graph/write.ts (asciiJson), src/graph/extract-cache.ts: non-ASCII written as \uXXXX. One such character made V8 hold the whole file as a two-byte string: ~100 MB more per map read, ~300 MB per extract-cache read. The extract cache is also written one file's entry at a time instead of as one 255 MB string.
  • src/ask/index-file.ts: the search index is written entry by entry, with the same bytes.
  • Tests: the rebuild child (success, and a failure that leaves the old graph and releases the lock), and ASCII round-trips for both files. A learning for the next session.

Kept away from src/cli.ts, src/telemetry/contract.ts, src/claude/format.ts, src/notes/notes.ts and #736's files.

Tests

npm test with a fresh scratch HOME: 4129 pass, 9 skipped, 1 fail. The one failure is upkeep-hooks "a repo graft set up, on a machine without trail", which fails on clean main the same way.

trail check flags 2 conflicts with the 27 Oct learning that ruled out "building synchronously inside the query". That learning is about the first build of a missing map, which is unchanged (still detached via sync-run). The drift rebuild was already awaited by the query before this PR; it now runs in a child, and a hook timeout still ends it and frees the lock, as before.

Follow-ups

  1. A build still peaks around 2 GB: it reads the whole extract cache (every file's code) to re-parse one file. Split it per file.
  2. The prompt and post-edit hooks still parse the whole map (483 MB) for a scope hint and a list of callers. Give them a small file of their own.
  3. The search index is still ~430 MB once parsed (5.8M [word, count] pairs); a word-to-documents format would be about a tenth.
  4. A peak-memory bucket on build_completed and query.
  5. Ask the reporter for laptop memory and how many sessions were open; then move them to a release with this and Find why trail sync crashes a laptop, and fix it #736.

🤖 Generated with Claude Code

@trailhq-graft

trailhq-graft Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🌱 graft blast radius

2 areas changed → 4 areas can be affected. 7 dependent symbols, depth 2.
Tests: Ask Index Generation has tests the diff did not touch; 1 area updated its tests.
Tag: @shhdwi — 5 of 6 areas · @qoole — Graph Building · 贾旭卿 — Graph Persistence

flowchart TB
  A0(("CLI Graph Refresh<br/>3 symbols"))
  A1(("MCP Server Integration<br/>2 symbols"))
  A2(("Graph Building<br/>1 symbol"))
  A3(("Pull Request Review<br/>1 symbol"))
  classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
  class A0,A1,A2,A3 reached;
Loading
Can be affected Symbols Nearest hop Reached from
CLI Graph Refresh 3 src/cli.ts:L253-L263 refreshBefore — calls, depth 1 Ask Index Generation, Graph Persistence
MCP Server Integration 2 src/mcp/tools.ts:L219-L247 callTool — calls, depth 1 Graph Persistence
Graph Building 1 src/graph/build.ts:L212-L479 buildGraph — calls, depth 1 Ask Index Generation, Graph Persistence
Pull Request Review 1 src/app/review.ts:L45-L99 reviewPullRequest — calls, depth 2 Ask Index Generation, Graph Persistence
Who knows this code — 3 people across 6 areas
Area Who knows it
Graph Persistence · changed @shhdwi — 3 commits, last yesterday · 贾旭卿 — 1 commit, last 2mo ago
Ask Index Generation · changed @shhdwi — 18 commits, last 7d ago
CLI Graph Refresh · affected @shhdwi — 52 commits, last yesterday
MCP Server Integration · affected @shhdwi — 21 commits, last 2d ago
Graph Building · affected @shhdwi — 7 commits, last yesterday · @qoole — 3 commits, last 3mo ago
Pull Request Review · affected only you — nobody else has touched these files

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 7 dependent symbols, grouped by area

CLI Graph Refresh — 3 symbols in 2 files

  • src/cli.ts:L253-L263 — refreshBefore (calls, depth 1)
    260: : await ensureFreshGraph(root, { contextDir: globalDir, disabled });
  • src/cli.ts:L1-L2845 — cli.ts (calls, depth 2)
    35: import { ensureFreshChildren, ensureFreshGraph, refreshNote } from "./graph/refresh.js";
  • src/engine.ts:L95-L105 — graph (calls, depth 2)

MCP Server Integration — 2 symbols in 2 files

  • src/mcp/tools.ts:L219-L247 — callTool (calls, depth 1)
    230: // this agent). ~3ms when nothing moved; a structural, $0 rebuild when it did.
  • src/mcp/server.ts:L100-L254 — startMcpServer (calls, depth 2)
    229: // Async because a tool call may rebuild the graph first (see

Graph Building — 1 symbol in 1 file

  • src/graph/build.ts:L212-L479 — buildGraph (calls, depth 1)
    292: // rebuild itself looks at. Reading is ~0.05ms/file against the ~4.6ms parse

Pull Request Review — 1 symbol in 1 file

  • src/app/review.ts:L45-L99 — reviewPullRequest (calls, depth 2)
Test signal per changed area — 1 ✓ · 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 Persistence — 3 of 9 reached · 2 test files changed here: test/extract-cache-write.test.ts, test/graph-refresh.test.ts
    • not reached: writeAll, writeCacheAtomic, main, onSignal, rebuild, asciiJson
  • ⚠ Ask Index Generation — 1 of 5 reached · 2 test files reach it, none changed here
    • not reached: bagLength, bag, lexical, writeAll
42 test suites also reference this code

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

  • test/ask-index.test.ts
  • test/ask.test.ts
  • test/claude-prompt-private.test.ts
  • test/container-extract.test.ts
  • test/context-only-dir.test.ts
  • test/context.test.ts
  • test/covers.test.ts
  • test/depth-parity.ts
  • test/generic-extract.test.ts
  • test/graph-follow-submodules.test.ts
  • test/graph-go.test.ts
  • test/graph-incremental.test.ts
  • test/graph-invariants.test.ts
  • test/graph-java.test.ts
  • test/graph-languages.test.ts
  • test/graph-load.test.ts
  • test/graph-native-fallback.test.ts
  • test/graph-php.test.ts
  • test/graph-posix-paths.test.ts
  • test/graph-python.test.ts
  • …22 more

⚠️ 1 changed file not in the graph (.trail/learnings/2026-10-29-a-query-time-rebuild-in-the-anirudhkumar.md) — no parser claims the extension, or the index predates the file.

graft blast · origin/main...HEAD · depth 2 · 9 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 29, 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