cd /news/developer-tools/how-we-cut-repo-wide-symbol-indexing… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-104973] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=↑ positive

How we cut repo-wide symbol indexing for LLM agents from 30s to 98ms

LiuHe, a code-operation toolchain designed for LLM agents, has cut repo-wide symbol indexing from tens of seconds to 98ms. The project, built by developer wulun811, uses a Rust tree-sitter parse daemon, SQLite symbol index, and transactional journal-backed writes to address the unique constraints of agents that lack hands, eyes, and memory. The toolkit includes features like atomic edits, undo journals, and executable error recovery actions to prevent silent data loss.

read6 min views1 publishedAug 20, 2026

If your coding agent has ever stalled for tens of seconds on "what's in this repo?" β€” or burned hundreds of tokens re-reading a file after a failed edit β€” this is the story of why that happens and how we fixed it.

TL;DR β€” we rebuilt code tooling for agents that have no hands, no eyes, and no memory:

repo_map

in kill -9

β€” no more silently lost workLiuHe (https://github.com/wulun811/LiuHe) is a code-operation toolchain designed for LLMs rather than humans: Node orchestration, a Rust tree-sitter parse daemon, a SQLite symbol index, and transactional journal-backed writes. MIT, v0.4.6, zero-build deploy (no cargo, no npm install

).

This post is the architecture story: what was slow, what we changed, and the numbers we measured while doing it.

Six families, all deterministic, all reproducible from benchmarks/

:

read_symbol

(version-anchored), symbol_search

, code_search

, repo_map

(98ms, paginated skeleton), reindex

, dep_graph

, references

impact_analysis

, call_chain

, trace_symbol

(constant tracing), inspect

, sweep_dead_code

, config_drift

edit_batch

(4-level tolerant matching), edit_transaction

(atomic + undo journal), rename_symbol

, git_worktree

, edit_sandbox

, edit_collision_guard

, diff_facts

code_review

, security_review

, code_quality

, style_sniffer

, guard_patterns

, naming_consistency

, dependency_gatekeeper

, fix_imports

, mock_sync

β€” zero LLM callstest_bridge

, find_tests

, verify_pipeline

, debug_runner

, tsc_check

, patch_parser

, spec_gen

health

(self-healing), gc

, feedback

If you've watched an agent burn thousands of tokens re-reading a file because a sed

didn't match, or lose work to a silently botched write β€” these are the failure modes this toolkit exists for. Everything below is about making those tools fast, safe, and cheap.

Our first version answered "what symbols exist in this repo?" by walking the tree and parsing every file on every request. On a 347-file project that was tens of seconds. On a real Ansible repo of 1,482 files it was worse β€” and agents ask for repo maps constantly (every tool-call needs file β†’ symbol β†’ reference context).

The fix came in three layers:

Result: full index of 1,482 files in 9.7s (153 files/s); repo map afterwards: 98ms.

The original tree-sitter binding inside Node had two failure modes: a parse exception killed the entire MCP process (on average every 2–4 hours of use), and GC s + per-node JS↔C crossings made batch indexing stall.

The Rust daemon fixes both:

catch_unwind

PARSE_PANIC

error code; the MCP server keeps running. Users forgive slow, never dead.worker_threads

startup costs.Human tools assume you have hands, eyes, and memory. An LLM has none. Three compensations:

edit_transaction

is all-or-nothing; every write produces an undo journal. We tested kill -9

mid-write: the half-written transaction rolls back, source files untouched.suggestion

next_action

β€” an executable recovery call the model reissues verbatim instead of guessing.workspace_dir

; writes are version-anchored (optimistic concurrency), so even if the model forgets the version it read, the write On the "silent corruption" point: while building with a default agent tool stack, one overwrite write silently lost 400+ lines β€” surfaced ~40 turns later, by luck. We stopped betting on "models will get better" and moved the safety into the tool layer.

Errors aren't a dead end β€” they're an interface. Every failure carries a stable code, a human-readable suggestion, and a next_action

that is executable, not advice:

{ "error": { "code": "VERSION_CONFLICT", "message": "base_version mismatch: FILE_CHANGED", "suggestion": "Re-read the file and regenerate the batch.", "next_action": { "tool": "read_symbol", "params": { "locator": { "file_path": "src/api.js" } } } } }

The model doesn't parse the suggestion and decide what to do β€” it reissues next_action

verbatim and recovers. Successful calls carry a next_step

the same way. Errors become signposts with navigation instead of dead ends.

edit_batch

: tolerant matching, paranoid writing LLMs generate old_string

anchors with mistakes humans rarely make: collapsed double spaces, truncated line ends, curly quotes where the code has straight ones. A bare no_match

sends the model off to re-read the whole file β€” thousands of tokens per failed match.

Matching degrades in four stages: exact β†’ trailing-whitespace-stripped β†’ edit-distance candidates (similarity β‰₯ 0.5) β†’ diagnostics (whitespace visualized, 17 Unicode confusable pairs listed). A typical failure reads: "candidate at line 42, similarity 0.87 β€” you used curly quotes, the code has straight ones." Usually one retry fixes it.

The write side is paranoid: symlink guards, unique temp file names, TOCTOU check between match and commit, rename retries for Windows AV file locks, post-write syntax check (node --check

/ py_compile

/ JSON.parse

).

security_review

, code_review

, sweep_dead_code

are pure regex/AST β€” zero LLM calls. Same input, same output; CI-safe and auditable.

We state the boundary explicitly: these tools do not cover control flow, data flow, or cross-module semantics. Zero findings β‰  safe; a high score β‰  healthy. A deterministic pattern scanner that admits its scope beats a "comprehensive security" claim every time.

30+ rounds of "LiuHe reviews LiuHe" β€” every bug found becomes a regression test. Real fixes from those rounds: a scope filter scanning outside its target directory, dead-code false positives on registration patterns, constant tracing missing read sites, SQL parameterization cleanup. Assertions grew every round: 2,013 JS + 92 Rust, full chain green.

LiuHe (ε…­εˆ, "six harmonies") names the six design constraints applied to every tool in the toolkit:

The three compensations earlier (hands / eyes / memory) are the user-facing summary; these six are the per-tool checklist behind them. The AST layer that enforces them is called Malong.

All under a real docker --memory=512m

cgroup:

Metric Value
repo_map
98ms (was tens of seconds)
Full index 1,482 files in 9.7s
Concurrency 128 concurrent / 256 in-flight, zero OOM
Peak RSS
134MB (~26% of limit)
Throughput ~588 calls/s (60–600Γ— realistic agent demand)
Hot-file storm 32-way read/write mix, zero torn writes, integrity_check PASS, 95/95 conflicts rejected as FILE_LOCKED
Token savings ↓65.3% (7,673 β†’ 2,662 est. on same task)

Honest boundary: throughput doesn't scale with concurrency β€” better-sqlite3

's synchronous queries serialize on the Node event loop. We evaluated worker_threads

and decided the risk wasn't worth the gain. 588 calls/s is already overkill.

Where the token savings come from: tiered tool-description compression (44 tools β‰ˆ 1.33k tokens β€” core tools keep full descriptions, low-frequency ones shrink to ≀70 chars, verbose ones ≀230, with the detail deferred to next_step

hints), incremental returns with explicit pagination instead of dumping everything, and batch endpoints (read_symbols

, write_symbols

) that cut round-trips. Same task: 7,673 β†’ 2,662 estimated tokens (↓65.3%) and 6 calls β†’ 3 (↓50%).

All benchmarks are reproducible from benchmarks/

and tests/

in the repo (concurrency correctness: tests/test-mvp-concurrency.js

).

We also shipped first-day support for DeepSeek Harness (dsh web) β€” one line to register, all 44 tools exposed as malong__*

with workspace_dir

auto-filled from the conversation's workspace:

dsh plugin --profile web add @jieai/dsh-malong-bridge

Full guide in the repo: malong/dsh/DSH-INTEGRATION.md

.

Zero-build deploy (no cargo, no npm install

):

git clone https://github.com/wulun811/LiuHe liuhe && cd liuhe/malong
mkdir -p ~/.local/bin
tar -xzf ../releases/malong-liuhe-0.4.6-linux-x86_64.tar.gz
cp malong-parse/target/release/malong-parse ~/.local/bin
malong-parse &   # start the parse daemon
node --max-old-space-size=512 --expose-gc mcp-server.js --workspace /path/to/project

If better-sqlite3

is unavailable, it falls back to vendored sql.js

WASM β€” no install, no compile, no network.

Skepticism welcome β€” all numbers above are self-measured and reproducible. If this resonates with an agent failure you've had, try it, break it, and tell us where we're wrong β€” the repo is wulun811/LiuHe.

── more in #developer-tools 4 stories Β· sorted by recency
── more on @liuhe 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/how-we-cut-repo-wide…] indexed:0 read:6min 2026-08-20 Β· β€”