Skip to content

Fix the search hang, stop leaking database processes, and show references - #24

Merged
Phoenixrr2113 merged 2 commits into
mainfrom
fix/query-hang-process-lifecycle-and-references
Aug 20, 2026
Merged

Phoenixrr2113 merged 2 commits into
mainfrom
fix/query-hang-process-lifecycle-and-references

Conversation

@Phoenixrr2113

Copy link
Copy Markdown
Owner

Three defects that would each have hit a real user, plus the dashboard work that came out of testing them.

Search could hang outright on a large codebase

enrichedSearchV2 computes a dependencyDepth per hit, asking for a shortest path from an entry point with OPTIONAL MATCH over a *1..6 pattern. That form is cheap when a path exists and ruinous when one does not, because proving absence means enumerating the symbol's entire six-hop neighbourhood.

zod has a symbol built to make that expensive. _parse is implemented on every schema type, so the name resolves to 38 nodes carrying 1406 inbound and 2340 outbound CALLS edges, and no entry point reaches any of them within six hops. That one name in a batch of 60 ran past 120s while every other symbol answered in under a millisecond. Because FalkorDB serves one query at a time, every later stage of the same search queued behind it, which is why the symptom read as "hangs at first query" rather than "one enrichment is slow".

A plain MATCH yields no row for unreachable symbols, which is exactly what the caller already treats as "depth unknown", so the answers are unchanged:

after before
400 zod symbols 0.4s 240s
symbols exceeding 20s 0 12
answers agreeing 388 of 388 the old query could finish baseline

The optional enrichment queries now also carry an explicit timeout, so no decoration can stall a search again.

The embedded database was outliving its process

Connecting removes the wrapper's SIGINT/SIGTERM handlers so they cannot stop Redis before our client disconnects, but nothing replaced them, and a process killed by a signal never runs its exit handlers. Ten orphaned redis-server processes accumulated from a single day of local use, all reparented to init, all still holding the same data directory, so their stale snapshots competed with the live one. Any MCP client stopping the server would leak one per session. Shutdown is now owned deliberately: open drivers are tracked and closed in the right order before the signal is re-raised.

A real database could not start

The startup budget was a fixed 10s. A 52MB snapshot takes 18.7s to load, so the database failed to open and reported Is falkordblite installed?, pointing at entirely the wrong cause. The budget now scales with the snapshot, CODEGRAPH_DB_STARTUP_TIMEOUT_MS overrides it, and the hint distinguishes a timeout from a missing package.

The source endpoint read any path it was given

/api/source called readFile on whatever absolute path the caller named. It now serves only files inside an indexed project, resolves symlinks before the containment check so a planted link cannot escape, and separates missing from forbidden.

Dashboard

Mostly found while testing the above.

  • Selecting a symbol shows where it is used. New /api/graph/references returns the callers, type users, subclasses, implementers and renderers of a declaration. The panel lists them grouped by other-files and same-file with the edge kind and a click through; the canvas marks the ones it has loaded. Callers in unloaded files have no node to highlight, which is why the panel carries the full set.
  • Stats counted part of the graph while being named for all of it, reporting 235 nodes where there were 635 and Commit: 0 where there were 200, because the query asked for seven labels while declaring thirteen.
  • The graph did not fit on load. The canvas was built before its resizable panel had a size, so cytoscape could not fit and left zoom at 1, putting roughly half of a 235-node graph off screen. Tree and Ring had the fit-before-animate problem already fixed for Force, and the zoom floor sat above what fitting a tree needs.
  • A failed index reported as a green success. Indexing a nonexistent path showed 0 files, 0 symbols; the API returned 200 with parsed: true and the dialog ignored success. Durations also showed milliseconds labelled as seconds (3161.0s for a 3.2s index).
  • Projects reported indexedAt: null, because the property is lastParsed.

Verification

pnpm turbo run typecheck 20/20 and pnpm turbo run test 32/32 (1266 tests). pnpm release:check passes end to end, with all four MCP tools and the dashboard smoke-verified. Embedded driver integration 49/49.

New cover: 7 reference tests, 15 source-access tests, 8 startup-budget tests, and two regression tests confirmed to fail against the old code (the depth test times out after 20s; the shutdown test catches the orphaned server).

The remote-FalkorDB integration suite fails locally because port 6379 is a plain redis with no graph module. That is environmental and predates this change; CI runs it against a real FalkorDB service.

Known limitation found on the way, not fixed here

A method call through a typed receiver, such as ops.touchEntity(...), produces no CALLS edge. Verified by indexing core and graph together: the calling file is indexed and the declaration exists, but the edge does not. Cross-file calls do resolve in general (79 in that slice, 453 in zod), so this is specifically receiver-typed method calls. Until that is handled, "where is this used" under-reports on code written that way, which is most TypeScript.

🤖 Generated with Claude Code

…nces

Three defects that would each have hit a real user, plus the dashboard work
that came out of testing them.

Search could hang outright on a large codebase. The dependency-depth
enrichment asked for a shortest path with OPTIONAL MATCH over a six-hop
pattern, which is cheap when a path exists and ruinous when one does not:
proving absence means enumerating the symbol's whole neighbourhood. On zod,
`_parse` resolves to 38 nodes carrying 1406 inbound and 2340 outbound call
edges with no entry point reaching them, and that one name in a batch of 60
ran past 120s. Because the database serves one query at a time, the rest of
the search queued behind it, so it looked like a hang rather than a slow
field. A plain MATCH gives identical answers and makes absence free: across
400 zod symbols the batch went from 240s to 0.4s, agreeing on all 388 the old
query could finish. The optional enrichment queries now also carry a timeout,
so no decoration can stall a search again.

The embedded database was outliving its process. Connecting removes the
wrapper's signal handlers so they cannot stop the server before our client
disconnects, but nothing replaced them, and a process killed by a signal never
runs its exit handlers. Ten orphaned servers accumulated in a single day of
local use, every one still holding the same data directory, their stale
snapshots competing with the live one. Shutdown is now owned deliberately.

A real database could not start at all. The startup budget was a fixed 10s and
a 52MB snapshot takes 18.7s to load, so the database failed to open and blamed
a missing package. The budget now follows the snapshot size, can be overridden,
and the message says what actually went wrong.

The source endpoint would read any absolute path a caller named. It now serves
only files inside an indexed project, resolves symlinks before deciding, and
tells missing apart from forbidden.

Dashboard, mostly found while testing the above:
- Selecting a symbol shows where it is used, in the panel and on the canvas.
  Callers in unloaded files have no node to highlight, so the panel lists the
  full set and the canvas marks what it has.
- Stats counted part of the graph while being named for all of it, reporting
  235 nodes out of 635 and no commits where there were 200.
- The graph did not fit on load, because the panel had no size yet when the
  canvas was built. Tree and Ring had the same fit-before-animate problem
  already fixed for Force, and the zoom floor sat above what fitting a tree
  needs.
- A failed index reported as a green success, and durations showed
  milliseconds labelled as seconds.
- Projects reported indexedAt as null because the property is lastParsed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
v0-landing-page-build Skipped Skipped Aug 20, 2026 4:22pm

@augmentcode

augmentcode Bot commented Aug 20, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: This PR hardens graph search, embedded database lifecycle, source access, and dashboard graph exploration.

Changes:

  • Replaces the dependency-depth optional path match with a bounded plain match and applies timeouts to enrichment queries.
  • Adds regression coverage for pathological hub-symbol depth searches.
  • Tracks embedded FalkorDBLite drivers across process signals and scales startup timeout to snapshot size.
  • Improves embedded connection hints when startup times out.
  • Restricts the source endpoint through realpath-based root containment checks.
  • Returns parse failures as failures and corrects project metadata and total graph statistics.
  • Adds a symbol-reference graph query, API route, dashboard panel, and canvas highlighting.
  • Improves graph fitting, layout behavior, parse duration display, and project status UX.

Technical Notes: Reference results are capped and report truncation; source access resolves symlinks before evaluating containment.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread packages/api/src/routes/source.ts Outdated
Comment thread packages/dashboard/src/lib/references.ts Outdated
Comment thread packages/graph/src/drivers/falkordblite.ts
…nect race

Three findings from review, all real.

The source endpoint drew its roots from the configured active projects as well
as the graph. Configuring a project only schedules indexing, so an active root
can be a directory the graph knows nothing about, which is wider than the
indexed-project boundary the endpoint claims. Roots now come from the graph
alone, and a graph we cannot read denies everything rather than falling back to
something broader.

A reference was identified by path and name, but a file can hold several
symbols of the same name and only one of them is the end of the relationship.
The line is now part of the identity, so the canvas marks the declaration that
is actually referenced rather than all of its namesakes.

Two connects running at once could leave the process with no shutdown handler
at all. The second connect snapshots the signal listeners before the first has
installed ours, so its cleanup removes the handler the first just added, and
the install is skipped because one was already recorded. Our own handler is now
never treated as something the wrapper added, and installing repairs a handler
that went missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Phoenixrr2113
Phoenixrr2113 merged commit f32f114 into main Aug 20, 2026
12 checks passed
@Phoenixrr2113
Phoenixrr2113 deleted the fix/query-hang-process-lifecycle-and-references branch August 20, 2026 16:30

This branch was previously deployed

1 inactive deployment
Preview — 8abde988 Deployed Aug 20, 2026 by vercel[bot]
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