Fix the search hang, stop leaking database processes, and show references - #24
Merged
Phoenixrr2113 merged 2 commits intoAug 20, 2026
Merged
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
🤖 Augment PR SummarySummary: This PR hardens graph search, embedded database lifecycle, source access, and dashboard graph exploration. Changes:
Technical Notes: Reference results are capped and report truncation; source access resolves symlinks before evaluating containment. 🤖 Was this summary useful? React with 👍 or 👎 |
…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
deleted the
fix/query-hang-process-lifecycle-and-references
branch
August 20, 2026 16:30
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
enrichedSearchV2computes adependencyDepthper hit, asking for a shortest path from an entry point withOPTIONAL MATCHover a*1..6pattern. 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.
_parseis implemented on every schema type, so the name resolves to 38 nodes carrying 1406 inbound and 2340 outboundCALLSedges, 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
MATCHyields no row for unreachable symbols, which is exactly what the caller already treats as "depth unknown", so the answers are unchanged: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
exithandlers. Ten orphanedredis-serverprocesses 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_MSoverrides it, and the hint distinguishes a timeout from a missing package.The source endpoint read any path it was given
/api/sourcecalledreadFileon 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.
/api/graph/referencesreturns 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.Commit: 0where there were 200, because the query asked for seven labels while declaring thirteen.0 files, 0 symbols; the API returned 200 withparsed: trueand the dialog ignoredsuccess. Durations also showed milliseconds labelled as seconds (3161.0sfor a 3.2s index).indexedAt: null, because the property islastParsed.Verification
pnpm turbo run typecheck20/20 andpnpm turbo run test32/32 (1266 tests).pnpm release:checkpasses 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 noCALLSedge. 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