Component: hilo-mcp (JSON-RPC over stdio) vs hilo-cli graph commands
I found the repository (gethilo/hilo), reproduced the bug on the pre-fix tree, applied/confirmed the fix, and verified it live. The full solution is written to ~/solution.md:
Component: hilo-mcp (JSON-RPC over stdio) vs hilo-cli graph commands
Version: master 488609b (hilo 0.3.1-dev)
Symptom: vfs_graph_stats → {"total_edges":0,"total_files":0} on a corpus whose .vfs/graph/graph.db cache is absent but whose sibling .vfs/graph/edges.jsonl holds the full edge set; hilo graph stats on the same corpus reports the real counts.
GraphDB::open already does a read-through reconcile: if the DuckDB cache is absent/behind and a sibling edges.jsonl exists, it replays the JSONL in (budgeted + resumable, GAP-094). The bug is the gate deciding whether open is ever reached.
The CLI and MCP used different predicates for "does graph data exist?":
hilo-cli/src/commands/graph.rs::resolve_graph_db_path returns the DB path if graph.db or edges.jsonl exists, then calls GraphDB::open → reconcile runs → real counts.!Path::new(GRAPH_DB_PATH).exists(). With the cache absent it returned zeros immediately and never called GraphDB::open, so the reconcile never fired — a silent-wrong-answer class bug.Fix shape: make the short-circuit key identical to the CLI's — "cache exists OR sibling edges.jsonl exists" — so open (and its reconcile) runs before the tool answers. Neither present ⇒ zeros/empty is still correct.
In hilo-mcp/src/tools/mod.rs, add a helper and replace the predicate in all six handlers:
const GRAPH_DB_PATH: &str = ".vfs/graph/graph.db";
/// True when the DuckDB cache exists OR a sibling edges.jsonl does.
/// When the cache is absent but edges.jsonl exists, GraphDB::open
/// reconciles it into a fresh cache (GAP-096).
fn graph_data_present() -> bool {
const EDGES_JSONL_PATH: &str = ".vfs/graph/edges.jsonl";
Path::new(GRAPH_DB_PATH).exists() || Path::new(EDGES_JSONL_PATH).exists()
}
- if !Path::new(GRAPH_DB_PATH).exists() {
+ if !graph_data_present() {
return Ok(serde_json::json!({ /* zeros / empty */ }));
}
...
let db = hilo_graph::GraphDB::open(GRAPH_DB_PATH)?; // now reached; reconciles
Call sites (all six so the bug can't move to a sibling tool):
graph_stats (vfs_graph_stats), graph_untested, graph_module, graph_understand, graph_search, rule_check.
488609b)Regression test added by the fix — writes edges.jsonl with 2 edges, asserts graph.db absent, calls vfs_graph_stats, asserts total_edges == 2 and graph.db materialized:
cargo test -p hilo_mcp --test mcp_test test_graph_stats_reconciles_edges_jsonl_when_db_absent
→ test ... ok
Bug reproduced on the parent (git checkout e56ef28 -- hilo-mcp/src/tools/mod.rs, keep the new test):
assertion failed: ... {"total_edges":0,"total_files":0} left: 0 right: 2
test ... FAILED
Full crate suite: cargo test -p hilo_mcp → 32 passed; 0 failed.
Live E2E, both directions (cargo build -p hilo-cli --bin hilo):
edges.jsonl-only scratch, fresh (no graph.db): CLI Total edges: 2 distinct / 2 raw; MCP serve --mcp returns "total_edges":2,"total_files":2 and materializes graph.db. Pre-fix the same request returned {"total_edges":0,"total_files":0}.{"total_edges":0,"total_files":0} and creates no cache.| Corpus state | CLI | MCP (fixed) | MCP (pre-fix) |
|---|---|---|---|
edges.jsonl only |
2 | 2 + graph.db |
0 (bug) |
| neither file | empty | 0, no cache | 0 |
Why it's correct: the fix doesn't reimplement reconcile in MCP; it removes the gate that blocked the existing budgeted/resumable GraphDB::open reconcile, keeping one source of truth identical to the CLI. The request-path budget (reconcile_budget_ms) still bounds the first call and the checkpoint lets the next one resume.
# Evidence - Problem class: mcp-server-answers-empty-graph-before-first-reconcile - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-23T00:12:42.199Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A resident MCP server answered an empty graph (total_edges=0) on a corpus whose graph.db cache was absent while a sibling edges.jsonl held the full edge set; the one-shot CLI on the same corpus reconciled and answered the real counts. Root cause: the MCP tool handlers gated their zero-results short-circuit on !Path::new(GRAPH_DB_PATH).exists() and never reached GraphDB::open, whose read-through reconcile replays edges.jsonl into a fresh cache (budgeted + resumable). Fix: a graph_data_present() helper that returns true when the DB OR the sibling edges.jsonl exists; the short-circuit fires only when neither exists. Applied to all six affected tools (stats/untested/module/understand/search/rule_check). Live E2E verified both directions: serve --mcp on an edges.jsonl-only scratch corpus returned total_edges=2 matching the CLI, and a genuinely empty scratch still returned the all-zeros object. Regression test: write edges.jsonl with 2 edges, call vfs_graph_stats, assert total_edges==2 and graph.db materialized.", "environment": "Hilo (Rust workspace, 11 crates), hilo-mcp JSON-RPC server over stdio, hilo-graph DuckDB graph cache + git-tracked edges.jsonl, host with ~/.cargo toolchain", "language": "rust", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "mcp-server-answers-empty-graph-before-first-reconcile", "provider": "openrouter", "solved_at": "2026-09-23T00:12:42.204Z", "version": "master 488609b (hilo 0.3.1-dev)"}I found the repository (gethilo/hilo), reproduced the bug on the pre-fix tree, applied/confirmed the fix, and verified it live. The full solution is written to ~/solution.md:
Component: hilo-mcp (JSON-RPC over stdio) vs hilo-cli graph commands
Version: master 488609b (hilo 0.3.1-dev)
Symptom: vfs_graph_stats → {"total_edges":0,"total_files":0} on a corpus whose .vfs/graph/graph.db cache is absent but whose sibling .vfs/graph/edges.jsonl holds the full edge set; hilo graph stats on the same corpus reports the real counts.
GraphDB::open already does a read-through reconcile: if the DuckDB cache is absent/behind and a sibling edges.jsonl exists, it replays the JSONL in (budgeted + resumable, GAP-094). The bug is the gate deciding whether open is ever reached.
The CLI and MCP used different predicates for "does graph data exist?":
hilo-cli/src/commands/graph.rs::resolve_graph_db_path returns the DB path if graph.db or edges.jsonl exists, then calls GraphDB::open → reconcile runs → real counts.!Path::new(GRAPH_DB_PATH).exists(). With the cache absent it returned zeros immediately and never called GraphDB::open, so the reconcile never fired — a silent-wrong-answer class bug.Fix shape: make the short-circuit key identical to the CLI's — "cache exists OR sibling edges.jsonl exists" — so open (and its reconcile) runs before the tool answers. Neither present ⇒ zeros/empty is still correct.
In hilo-mcp/src/tools/mod.rs, add a helper and replace the predicate in all six handlers:
const GRAPH_DB_PATH: &str = ".vfs/graph/graph.db";
/// True when the DuckDB cache exists OR a sibling edges.jsonl does.
/// When the cache is absent but edges.jsonl exists, GraphDB::open
/// reconciles it into a fresh cache (GAP-096).
fn graph_data_present() -> bool {
const EDGES_JSONL_PATH: &str = ".vfs/graph/edges.jsonl";
Path::new(GRAPH_DB_PATH).exists() || Path::new(EDGES_JSONL_PATH).exists()
}
- if !Path::new(GRAPH_DB_PATH).exists() {
+ if !graph_data_present() {
return Ok(serde_json::json!({ /* zeros / empty */ }));
}
...
let db = hilo_graph::GraphDB::open(GRAPH_DB_PATH)?; // now reached; reconciles
Call sites (all six so the bug can't move to a sibling tool):
graph_stats (vfs_graph_stats), graph_untested, graph_module, graph_understand, graph_search, rule_check.
488609b)Regression test added by the fix — writes edges.jsonl with 2 edges, asserts graph.db absent, calls vfs_graph_stats, asserts total_edges == 2 and graph.db materialized:
cargo test -p hilo_mcp --test mcp_test test_graph_stats_reconciles_edges_jsonl_when_db_absent
→ test ... ok
Bug reproduced on the parent (git checkout e56ef28 -- hilo-mcp/src/tools/mod.rs, keep the new test):
assertion failed: ... {"total_edges":0,"total_files":0} left: 0 right: 2
test ... FAILED
Full crate suite: cargo test -p hilo_mcp → 32 passed; 0 failed.
Live E2E, both directions (cargo build -p hilo-cli --bin hilo):
edges.jsonl-only scratch, fresh (no graph.db): CLI Total edges: 2 distinct / 2 raw; MCP serve --mcp returns "total_edges":2,"total_files":2 and materializes graph.db. Pre-fix the same request returned {"total_edges":0,"total_files":0}.{"total_edges":0,"total_files":0} and creates no cache.| Corpus state | CLI | MCP (fixed) | MCP (pre-fix) |
|---|---|---|---|
edges.jsonl only |
2 | 2 + graph.db |
0 (bug) |
| neither file | empty | 0, no cache | 0 |
Why it's correct: the fix doesn't reimplement reconcile in MCP; it removes the gate that blocked the existing budgeted/resumable GraphDB::open reconcile, keeping one source of truth identical to the CLI. The request-path budget (reconcile_budget_ms) still bounds the first call and the checkpoint lets the next one resume.
# Evidence - Problem class: mcp-server-answers-empty-graph-before-first-reconcile - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-23T00:12:42.199Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A resident MCP server answered an empty graph (total_edges=0) on a corpus whose graph.db cache was absent while a sibling edges.jsonl held the full edge set; the one-shot CLI on the same corpus reconciled and answered the real counts. Root cause: the MCP tool handlers gated their zero-results short-circuit on !Path::new(GRAPH_DB_PATH).exists() and never reached GraphDB::open, whose read-through reconcile replays edges.jsonl into a fresh cache (budgeted + resumable). Fix: a graph_data_present() helper that returns true when the DB OR the sibling edges.jsonl exists; the short-circuit fires only when neither exists. Applied to all six affected tools (stats/untested/module/understand/search/rule_check). Live E2E verified both directions: serve --mcp on an edges.jsonl-only scratch corpus returned total_edges=2 matching the CLI, and a genuinely empty scratch still returned the all-zeros object. Regression test: write edges.jsonl with 2 edges, call vfs_graph_stats, assert total_edges==2 and graph.db materialized.", "environment": "Hilo (Rust workspace, 11 crates), hilo-mcp JSON-RPC server over stdio, hilo-graph DuckDB graph cache + git-tracked edges.jsonl, host with ~/.cargo toolchain", "language": "rust", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "mcp-server-answers-empty-graph-before-first-reconcile", "provider": "openrouter", "solved_at": "2026-09-23T00:12:42.204Z", "version": "master 488609b (hilo 0.3.1-dev)"}