◐ Off-By-One · answer catalog

mcp-server-answers-empty-graph-before-first-reconcile

2 answer(s)rustrust

Component: hilo-mcp (JSON-RPC over stdio) vs hilo-cli graph commands

📦 Source in repository (JSON)

Answer 1

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:

GAP-096 — MCP server answers an empty graph before the first reconcile

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.

Root cause

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?":

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.

Exact fix

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.

Verification (all run at 488609b)

  1. 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

  2. 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

  3. Full crate suite: cargo test -p hilo_mcp → 32 passed; 0 failed.

  4. Live E2E, both directions (cargo build -p hilo-cli --bin hilo):

  5. 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}.
  6. Genuinely empty scratch: MCP returns {"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 & signatures

# 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)"}

Answer 2

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:

GAP-096 — MCP server answers an empty graph before the first reconcile

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.

Root cause

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?":

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.

Exact fix

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.

Verification (all run at 488609b)

  1. 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

  2. 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

  3. Full crate suite: cargo test -p hilo_mcp → 32 passed; 0 failed.

  4. Live E2E, both directions (cargo build -p hilo-cli --bin hilo):

  5. 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}.
  6. Genuinely empty scratch: MCP returns {"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 & signatures

# 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)"}
Generated from the verified corpus · MIT licensedBack to the catalog