This is the technical companion to Crux in real sessions: 90% correct, 47% cheaper per answer. It covers the session-shaped methodology, the complete numbers, and the part we care most about: the token blowup the benchmark exposed on sympy, and the Crux 0.6.2 redesign it forced.
Method
- Sessions, not questions. Each run gives the agent six code-navigation questions in one continuous conversation — definition location, callers, reference files — drawn from the same verified question bank as the single-question benchmark. Fixed costs amortize; context grows the way real sessions do.
- Corpus: django (2,572 files, 344k lines) and sympy (1,555 files, 770k lines), pinned commits. Four sessions per repo per arm.
- Arms: vanilla (grep only) and Crux (index registered, one routing sentence in the prompt). Same agent (Codex CLI,
gpt-5.6-sol,xhigh), metered through a local passthrough proxy for identical token accounting. Compression off — this write-up isolates the index; the full-stack measurement is separate. - Scoring: definitions match on file plus line (±2); set answers score F1 after normalization; “correct” means F1 = 1.0.
- Binaries: every number below is the shipped Crux 0.6.2 unless explicitly labeled “before.”
Headline result
| Arm | Correct | Total tokens | Tokens per correct | Turns/session | Index calls |
|---|---|---|---|---|---|
| django vanilla | 16/24 (67%) | 669,175 | 41,823 | 10–12 | — |
| django Crux | 23/24 (96%) | 675,746 | 29,380 | 13 | 4 |
| sympy vanilla | 12/24 (50%) | 993,865 | 82,822 | 13–16 | — |
| sympy Crux | 20/24 (83%) | 684,953 | 34,248 | 13–14 | 4 |
Suite totals: Crux 43/48 (90%) at 1,360,699 tokens; vanilla 28/48 (58%) at 1,663,040. That is 18% fewer raw tokens and 47% less per correct answer — on sympy alone, 59% less.
Note the shape, not just the totals: every Crux session on both corpora ran 13–14 turns with exactly 4 index calls. The variance collapsed along with the cost.
The blowup this benchmark exposed
The first sympy session runs looked nothing like that table. Crux-arm sessions ran 29–38 turns, made 7–10 index calls, and burned 474k–649k tokens each — 2,247,104 total for 16/24. Accuracy still beat grep by 17 points; cost was 2.3× worse. Three compounding causes, all invisible on django:
1. Uncapped ambiguity dumps. A bare name like solve matches dozens of definitions in sympy. scip_map returned the entire candidate list — no cap, no ranking, no reference data — and closed with “specify the qualified name.” Every ambiguous name triggered a 3–4 call drill-down loop: map → candidate dump → find → map again with a qualified name.
2. Truncation that re-bills. With 400 references and a display limit of 20, the truncation tail said “raise ref_limit.” The obedient agent re-issued the call at the maximum, re-transmitting the 20 lines it already had plus 180 more — each line carrying a source snippet read from disk. There was no way to request the rest; only everything, again.
3. Alphabetical reference ordering. Truncation kept whatever sorted first by path, so a cut list was an arbitrary sample. Agents learned to distrust it and page.
The 0.6.2 redesign
Five changes to the result surface, none to the index itself:
- Ambiguity is ranked, capped at 10, grouped by file — and when one candidate clearly dominates (exact display-name match, uniquely top-ranked), Crux auto-resolves it and returns the normal answer block with
other candidates: Nnoted. The common case collapsed from four calls to one. - Grouped references above 30 sites. Per-line source snippets give way to
file.py: 12, 40, 91— the same locations at a fraction of the tokens. Snippets remain for small result sets, where they carry real signal. - Real pagination. Every truncated section says
… 50 more (pass offset=20). Offsets thread through find, map, references, and outline — which previously had a hard 38-line cap with no parameter at all, silently pushing agents to read whole files. - Relevance ordering. References sort definition-file first, then same top-level package, then the rest — so a truncated list is the useful prefix rather than the alphabetical accident.
- A global line cap (250 lines) bounds the worst case across a multi-name call, and the compact
callers:/files:summary lines are emitted only when they are complete, never as a truncated list that reads as complete.
Measured on a real sympy symbol with 70 references: the default response went from ~70 snippet lines plus duplicate summaries to 30 compact lines. The complete answer is one ref_limit=200 call away at 84 lines.
One hardening change shipped alongside, bought with a full wasted bench run: a crashed indexer had left a 91-byte stub index that answered a plausible-looking “no symbol named X” to every query, and two sessions measured garbage before we caught it. Crux 0.6.2 treats a zero-document index as an explicit error — “index is empty (0 documents), likely a crashed indexer; run scip_index to rebuild” — for every query tool, and crux check warns on it. Related operational note: SCIP indexes are content-addressed to the commit, so a healthy index built in one checkout can be copied to another checkout of the same commit — which is also how we recovered when scip-python itself crashed on sympy under two different Node majors.
Every session
sympy, Crux 0.6.2 (before-fix range for comparison: 474k–649k tokens, 29–38 turns):
| Session | Correct | Tokens | Turns | Index calls |
|---|---|---|---|---|
| 0 | 4/6 | 167,072 | 13 | 4 |
| 1 | 5/6 | 185,529 | 14 | 4 |
| 2 | 5/6 | 162,659 | 13 | 4 |
| 3 | 6/6 | 169,693 | 13 | 4 |
django, Crux 0.6.2:
| Session | Correct | Tokens | Turns | Index calls |
|---|---|---|---|---|
| 0 | 5/6 | 168,559 | 13 | 4 |
| 1 | 6/6 | 173,746 | 13 | 4 |
| 2 | 6/6 | 167,867 | 13 | 4 |
| 3 | 6/6 | 165,574 | 13 | 4 |
Vanilla baselines:
| Corpus / session | Correct | Tokens | Turns |
|---|---|---|---|
| django 0–3 | 4/6, 4/6, 5/6, 3/6 | 163,054 / 148,855 / 195,617 / 161,649 | 10–12 |
| sympy 0–3 | 2/6, 2/6, 4/6, 4/6 | 261,441 / 231,307 / 280,230 / 220,887 | 13–16 |
Amortization
In matched sessions, the per-question premium over vanilla melts as context builds: +29% (question 1), +32% (2), +7% (3), +5% (4), +6% (5), +0.3% (6). The first two questions pay for MCP setup and index warm-up; from question three the index answers at grep prices.
Honest limits
Four sessions per cell, one run each; the 3.3× before/after token gap on sympy is far outside observed between-run variance, single-digit accuracy deltas are not. One model configuration; Python-only corpus; six-question sessions of navigation questions — not edit tasks. Scoring does not bill the downstream cost of acting on a wrong answer, which favors the cheap-but-wrong baseline.
Takeaway
The index was never the expensive part — the conversation about the index’s output was. Result shaping is not cosmetics for an agent-facing tool: “raise the limit” versus “pass offset=20” is the difference between 2.3× grep and 41% under it on the same repo, same questions, same index. Crux 0.6.2 is out today, open source, MIT: github.com/pedr0v/crux.