Summary
While using query_graph to find the longest call chain in an indexed Go/TS repo, I hit three compounding Cypher-engine bugs that together make any path-length/cycle analysis return meaningless results — plus a server crash on a heavy expansion. Reported together because they were found in one session and reinforce each other; happy to split if you prefer.
Environment: codebase-memory-mcp 0.8.1, macOS arm64 (Darwin 25.2.0), single project indexed (~21k nodes, ~88k edges, 21,193 CALLS edges per get_graph_schema).
Bug 1 — Repeated node variables in a pattern are not unified
A pattern that reuses the same variable should only match when both endpoints are the same node. It doesn't — (a)-[:CALLS]->(a) matches every CALLS edge:
MATCH (a)-[:CALLS]->(a) RETURN count(*) -- 19814
MATCH (a)-[:CALLS]->(b) RETURN count(*) -- 19814 (identical)
Consequence: self-loop/cycle detection via variable reuse silently returns garbage (it looks like the graph has 19,814 self-loops). Same for RETURN a.qualified_name LIMIT 10 — it just lists sources of arbitrary edges, which is very misleading because the output is plausible.
(Side note: count(*) reports 19,814 while get_graph_schema reports 21,193 CALLS edges — possibly a separate discrepancy.)
Bug 2 — Variable-length expansion reuses the same edge (no edge-uniqueness)
Standard Cypher semantics require relationship uniqueness within a path. This engine allows the same edge to repeat, so a single self-loop edge generates "paths" of arbitrary length.
In my graph there is a node whose only outbound CALLS edge is a self-loop (see "Amplifier" below):
MATCH (a {name: 'NewRelayServer'})-[r:CALLS]->(b)
RETURN b.qualified_name, r.callee, r.confidence
-- exactly 1 row: b = a itself (same qualified_name), callee='mux.HandleFunc', confidence 0.50
With one outbound edge that is a self-loop, no directed path of length ≥ 2 can exist. Yet:
MATCH (a {name: 'NewRelayServer'})-[:CALLS*2..2]->(b) RETURN DISTINCT b.name -- matches (itself)
MATCH (a)-[:CALLS*40..40]->(b) RETURN a.name, b.name LIMIT 1 -- matches
MATCH (a)-[:CALLS*100..100]->(b) RETURN a.name, b.name LIMIT 1
-- matches: a = b = 'NewRelayServer' — the self-loop traversed 100 times
Consequence: "longest path" / reachability-depth queries are dominated by whichever node has a self-loop, and any walk that can reach a self-loop node can pad itself to the hop cap.
Bug 3 — Silent 100-hop cap on variable-length expansion
MATCH (a)-[:CALLS*100..100]->(b) RETURN a.name LIMIT 1 -- match
MATCH (a)-[:CALLS*101..101]->(b) RETURN a.name LIMIT 1 -- empty, "Query returned no results" hint
A cap is reasonable, but it's indistinguishable from a genuine "no such path" answer. Combined with Bug 2, the engine confidently reports the longest chain in the codebase is exactly 100 calls — a pure artifact. An explicit error/warning ("hop limit exceeded") would prevent wrong conclusions.
Crash — heavy variable-length expansion kills the server
This query crashed the server mid-session (MCP error -32000: Connection closed); the immediately following request on the restarted process answered "project not found or not indexed" until a client reconnect:
MATCH (a)-[:CALLS*100..100]->(b) RETURN a.name, b.name LIMIT 20
The same query with LIMIT 1 completes fine. Possibly the same missing execution-budget issue as #601, but this is a hard crash rather than a hang.
Amplifier (parsing/quality, related: #725, #763)
The self-loop edge that makes Bug 2 so visible is itself a resolver artifact: a call to Go stdlib mux.HandleFunc inside NewRelayServer was resolved by the callee_suffix strategy (confidence 0.50) to the enclosing function itself. The same suffix-matching family also flags dozens of thin frontend CRUD wrappers (get, list, create, calling e.g. res.json() / api.get(...)) as self_recursive: true. Strategy counts in this graph: suffix_match 5,163 + import_map_suffix 1,972 + callee_suffix 8 ≈ 34% of CALLS edges. Low-confidence guessed edges routinely creating self-loops is what turns Bug 2 from a theoretical issue into a practical one. Happy to file this separately if useful.
Workarounds I tried that the parser rejects (for context)
- Path variables:
MATCH p = (a)-[:CALLS*5..5]->(b) RETURN length(p) → expected token type 66, got 85 at pos 6
- Node inequality:
WHERE a <> b / WHERE a.qualified_name < b.qualified_name → expected value
- Relationship inline property map:
()-[r:CALLS {strategy: 'suffix_match'}]->() → parse error
- Boolean in node inline map:
{self_recursive: true} → parse error (works via WHERE f.self_recursive = true)
So there is currently no expressible way to ask for simple (node-unique) paths or to filter out low-confidence edges in a variable-length match.
Expected
- Repeated node variables in a pattern unify to the same node (standard Cypher semantics).
- Variable-length expansion enforces relationship uniqueness per path.
- Hitting the hop cap surfaces an explicit error/warning instead of an empty result.
- Heavy expansions degrade (timeout/budget) instead of crashing the process.
Summary
While using
query_graphto find the longest call chain in an indexed Go/TS repo, I hit three compounding Cypher-engine bugs that together make any path-length/cycle analysis return meaningless results — plus a server crash on a heavy expansion. Reported together because they were found in one session and reinforce each other; happy to split if you prefer.Environment: codebase-memory-mcp 0.8.1, macOS arm64 (Darwin 25.2.0), single project indexed (~21k nodes, ~88k edges, 21,193
CALLSedges perget_graph_schema).Bug 1 — Repeated node variables in a pattern are not unified
A pattern that reuses the same variable should only match when both endpoints are the same node. It doesn't —
(a)-[:CALLS]->(a)matches every CALLS edge:Consequence: self-loop/cycle detection via variable reuse silently returns garbage (it looks like the graph has 19,814 self-loops). Same for
RETURN a.qualified_name LIMIT 10— it just lists sources of arbitrary edges, which is very misleading because the output is plausible.(Side note:
count(*)reports 19,814 whileget_graph_schemareports 21,193 CALLS edges — possibly a separate discrepancy.)Bug 2 — Variable-length expansion reuses the same edge (no edge-uniqueness)
Standard Cypher semantics require relationship uniqueness within a path. This engine allows the same edge to repeat, so a single self-loop edge generates "paths" of arbitrary length.
In my graph there is a node whose only outbound CALLS edge is a self-loop (see "Amplifier" below):
With one outbound edge that is a self-loop, no directed path of length ≥ 2 can exist. Yet:
Consequence: "longest path" / reachability-depth queries are dominated by whichever node has a self-loop, and any walk that can reach a self-loop node can pad itself to the hop cap.
Bug 3 — Silent 100-hop cap on variable-length expansion
A cap is reasonable, but it's indistinguishable from a genuine "no such path" answer. Combined with Bug 2, the engine confidently reports the longest chain in the codebase is exactly 100 calls — a pure artifact. An explicit error/warning ("hop limit exceeded") would prevent wrong conclusions.
Crash — heavy variable-length expansion kills the server
This query crashed the server mid-session (
MCP error -32000: Connection closed); the immediately following request on the restarted process answered"project not found or not indexed"until a client reconnect:The same query with
LIMIT 1completes fine. Possibly the same missing execution-budget issue as #601, but this is a hard crash rather than a hang.Amplifier (parsing/quality, related: #725, #763)
The self-loop edge that makes Bug 2 so visible is itself a resolver artifact: a call to Go stdlib
mux.HandleFuncinsideNewRelayServerwas resolved by thecallee_suffixstrategy (confidence 0.50) to the enclosing function itself. The same suffix-matching family also flags dozens of thin frontend CRUD wrappers (get,list,create, calling e.g.res.json()/api.get(...)) asself_recursive: true. Strategy counts in this graph:suffix_match5,163 +import_map_suffix1,972 +callee_suffix8 ≈ 34% of CALLS edges. Low-confidence guessed edges routinely creating self-loops is what turns Bug 2 from a theoretical issue into a practical one. Happy to file this separately if useful.Workarounds I tried that the parser rejects (for context)
MATCH p = (a)-[:CALLS*5..5]->(b) RETURN length(p)→expected token type 66, got 85 at pos 6WHERE a <> b/WHERE a.qualified_name < b.qualified_name→expected value()-[r:CALLS {strategy: 'suffix_match'}]->()→ parse error{self_recursive: true}→ parse error (works viaWHERE f.self_recursive = true)So there is currently no expressible way to ask for simple (node-unique) paths or to filter out low-confidence edges in a variable-length match.
Expected