To find out which tools your coding agent actually calls — and whether those calls return anything — CGraph records every operation served by its daemon and flushes the totals to a durable ledger, one JSONL line per daemon lifetime. cgraph stats rolls that ledger up into per-op counts, approximate latency percentiles, and the number that matters most: the zero-hit rate, the fraction of queries that returned nothing.
You wired eight MCP tools into Claude Code. Which ones does the agent reach for when nobody is watching? Anecdote says "it queries a lot." The ledger says exactly what it did.
What gets recorded, and what deliberately doesn't
The daemon times every request it dispatches — query, path, explain, impact, context, plus the session-memory ops remember and recall — with a monotonic clock, and keeps three things in memory:
- Lifetime totals per op: count and total milliseconds, plus a per-op latency histogram binned into pinned, log2-spaced buckets (1ms, 2ms, 4ms … 1024ms, and an overflow bucket).
- A rolling window of the last 256 operations with their individual latencies, so "how is it doing right now" has a real p50 rather than a lifetime mean.
- Usefulness counters: queries that matched nothing (
query_zero_hits), context requests whose focal node didn't resolve, and recalls that found no checkpoint.
One exclusion is load-bearing. A read that arrives while the graph is still (re)building is counted as not_ready and kept out of the op counts and zero-hit tallies. Without that, every rebuild would smear fake misses into the stats — a query against an empty half-built snapshot isn't a retrieval failure, it's bad timing. The zero-hit rate is only honest because "not ready" is not a miss.
One ledger line per daemon lifetime
In-memory stats die with the process, and CGraph's daemon is designed to idle-shutdown. So on teardown — the same block that persists the graph — the daemon appends one line to cgraph-out/op-stats-ledger.jsonl:
{
"schema_version": 1,
"boot": "2026-06-15T09:36:00Z",
"shutdown": "2026-06-15T11:20:12Z",
"uptime_seconds": 6252.0,
"query_zero_hits": 7,
"ops": {
"query": {"count": 42, "total_ms": 380.5, "hist_ms": [0,3,9,18,8,3,1,0,0,0,0,0]},
"context": {"count": 9, "total_ms": 540.0, "hist_ms": [0,0,0,1,2,3,2,1,0,0,0,0]}
}
}
The details are chosen for durability over ceremony. The flush is gated on at least one substantive op having been served, so a probe spawn that only answered status writes nothing. The wall clock is read exactly once, at flush — the live daemon stays purely monotonic, and the boot timestamp is derived backward from uptime. And the append is best-effort: it never blocks, throws, or fails the shutdown. On the read side, cgraph stats parses the ledger tolerantly — a torn trailing line from a crash mid-append is skipped, every well-formed line is kept.
Reading it: cgraph stats
cgraph stats --root /path/to/project --since 7d
--since takes all (default), today, an ISO-8601 timestamp, or a relative window like 24h or 7d. Here is the real output against CGraph's own repository — the graph the project's agents use while developing the tool:
DURABLE (ledger: .../cgraph-out/op-stats-ledger.jsonl, since 1970-01-01T00:00:00Z) 13 lifetime(s)
query count 32 zero-hit 0% p50 ~6ms p90 ~8ms
context count 74 p50 ~18ms p90 ~31ms
impact count 1 p50 ~6ms p90 ~8ms
(counts and mean are exact; p50/p90 are histogram approximations)
LIVE (this daemon, since boot)
query count 0 zero-hit 0% recent p50 97ms
The honesty note in the output is part of the design. Counts and means come straight from summed ledger fields, so they're exact. The percentiles are read off merged histograms by linear interpolation inside a bucket, so they're labeled approximate — and if the window ever mixes ledger schema versions, the rollup says so and refuses to merge incomparable histograms instead of silently summing them.
What the numbers actually tell you
Three readings fall out of even this small sample:
- Context dominates. 74 of 107 substantive ops were
context— the budgeted context tool that packs a focal node's neighborhood under a token budget. Agents don't want search results; they want the assembled working set. That's where tuning effort should go. - Zero-hit rate is a retrieval-quality dial. A 0% zero-hit rate says every query the agent issued resolved to something in the graph. If yours reads 15%, your agents are guessing at names the graph doesn't have — a signal to look at what they're asking, not just how fast you answer.
- Latency claims become checkable. We've written that impact analysis answers in ~10ms on a warm graph. The ledger is how that stays true in your repo, not just in our benchmark:
p50 ~6msabove is measured from real agent traffic, not a synthetic run.
Try it on your own traffic
If a CGraph daemon is already serving your repo over MCP, the ledger is accumulating now — the flush has been part of the daemon's teardown since mid-June. Let your agent work for a day, then ask:
cgraph stats --root . --since 24h
If the op mix surprises you — heavy explain, zero impact, a double-digit zero-hit rate — that's the point. It's the difference between believing your agent uses the graph and knowing what it asked.