The problem worth solving
AI coding agents are good at solving small problems and bad at situating them. Ask Claude Code to "rename getUserSession and update every caller" in a 50,000-line codebase, and the answer depends on whether the agent can see the call graph or has to grep for it.
Most tools fix this with cloud-synced code intelligence. Sourcegraph, Cody, Cursor's index, Continue's RAG. They all work, and they all impose the same trade-off: your code goes to a service, an account, and a continuous indexing job.
I wanted code intelligence without that trade-off, so I built it as a local SQLite file with a single trigger and no background work. This post is a write-up of the design choices that made it possible.
What "local-first project knowledge graph" actually means
In Event Horizon v3, every workspace gets a graph stored at <workspace>/.eh/graph.db. The graph holds:
- Functions, classes, interfaces, methods (nodes)
- Calls, imports, extends, implements (edges)
- Markdown documentation as nodes linked to source files
- Code-comment rationale (
// WHY:, TODO, FIXME, JSDoc/TSDoc, Python docstrings, C# XML doc) attached to the function or class they describe - Agent activity as graph data: every completed task creates an
agent_activitynode withtouched/authorededges to the files it modified - Shared knowledge entries as graph nodes with
referencesedges to the code they mention
The graph is built and refreshed only by user-invoked skills, never by background processes. /eh:optimize-context builds or rebuilds it on demand. /eh:orchestrate and /eh:work-on-plan refresh it automatically when they finish, using the list of files their workers touched. There is no autoscan. There is no file watcher. Activation does not touch the disk. Every refresh is the consequence of a skill the user explicitly ran.
The architectural choices that make this work
Tree-sitter WASM, five languages, no native build
Code structure extraction runs through tree-sitter compiled to WebAssembly. Adds about 3 MB of grammars to the VSIX, no node-gyp, no platform-specific binaries. The shipped grammars cover TypeScript, JavaScript, TSX, PHP, Python, and C#. PHP traits and enums are first-class. Python decorators, docstrings, and # TODO / # FIXME / # WHY rationale comments land in the graph. C# records, structs, enums, and XML doc comments land too.
SHA256-based incremental skip
Every file's content hash is stored alongside the graph nodes it produced. On rebuild, files whose hash hasn't changed since the last build are skipped entirely. A re-run of /eh:optimize-context on a clean tree is close to free.
Vendor and minified file skipping
The scanner refuses to index vendor/, __pycache__/, .venv/, bin/, obj/, target/, *.min.js, *.bundle.js, *.designer.cs, and a handful of similar patterns. There is also a "first non-empty line longer than 1000 characters" check that catches inline-bundled vendor scripts that don't follow naming conventions. This drops graph node count by 50 to 80 percent on Laravel, Symfony, and .NET projects, where the vendor/ and bin/ folders are usually larger than the actual source.
Provenance on every inferred edge
I haven't seen this in any other open-source code-intelligence tool. Every edge in the graph carries:
- A provenance tag: EXTRACTED (deterministic from AST), INFERRED (heuristic), AMBIGUOUS (multiple resolutions possible)
- A confidence score (0 to 1)
When an agent queries the graph and reads a result, it can decide how much to trust an edge. An EXTRACTED 0.99 callee is reliable. An AMBIGUOUS 0.4 callee is a hint. The agent can act on the hint or ask for more context.
Shrink-guard
There's a small but practical guard in the extractor: if a rebuild would delete more than 50 percent of a file's prior nodes, the rebuild is rejected. This protects against extractor regressions, silently shrinking the graph during an upgrade.
What runs, when
The full lifecycle of the graph in Event Horizon v3 is small enough to fit in two paragraphs.
You open VS Code, the extension activates, and nothing happens on disk. You ask Claude Code (or OpenCode, or Copilot, or Cursor, all four are supported) to run /eh:optimize-context for a task. The skill builds or refreshes the graph, hands the agent the relevant slice of nodes and edges, and the agent uses the slice as context. When the agent finishes the task and emits task.complete, an agent_activity node is added with touched edges to every file it modified. When you run /eh:orchestrate or /eh:work-on-plan to coordinate multiple workers, the orchestration tracks every file its workers touched, and refreshes the graph against that list automatically before reporting its summary. No need to re-run /eh:optimize-context after every plan; the graph reflects reality as soon as the orchestrator finishes.
No background jobs. No autoscan. No telemetry. No outbound LLM calls from Event Horizon itself; agents that opt into LLM-based concept extraction (eh_extract_concepts) spend their own tokens.
Querying the graph
Five MCP tools wrap the graph for agent use:
-
eh_query_graphdoes search, callers, callees, neighbors, shortest path, explain, and recent activity. -
eh_extract_conceptsruns an opt-in LLM extraction pass when the agent wants higher-level concepts on top of the AST. -
eh_build_graphtriggers a manual rebuild from the agent side. -
eh_curate_contextselects a task-aware slice of the graph that fits within a token budget. -
eh_rescan_filestakes a path list and re-extracts only those files, runs the resolution pass once, and returns a scan summary. This is what powers the orchestrate-end auto-refresh, and it is also available to any agent that needs a targeted refresh after writing files.
eh_curate_context is the one that pays for everything else. It is the difference between an agent asking "show me everything related to authentication" and getting a 200,000-token dump, versus asking the same question and getting a 4,000-token slice that names the right functions, the right callers, and the relevant rationale comments.
Visualization
Like every other graph tool, this one has a canvas. Unlike most of them, the canvas is in a VS Code webview, not a browser tab on a remote service. The Knowledge tab renders rounded-square nodes (color-coded by type), straight edges, soft cyan glow halos on a dark blueprint grid background. Force-directed initial layout. Click a node to open a 320 px detail drawer with callers, callees, references, rationale, recent agent activity, and a "Reveal in editor" button that jumps to the source file. Pan with mouse drag, zoom with wheel.
The webview hydrates on connect, so reopening the panel shows the existing graph immediately. It re-fetches automatically whenever a build or refresh finishes, whether triggered by /eh:optimize-context or by an orchestration ending.
The reasoning behind "no autoscan"
I want to be honest about why the graph builds only when you ask. Background indexing is the normal pattern. JetBrains' Indexer, VS Code's reference indexes, and Sourcegraph's batch jobs all run continuously. The trade-off is that you pay for activity you didn't request: CPU cycles, disk writes, sometimes telemetry.
For this tool, the cost-benefit is different. The graph isn't there to power autocomplete; it is there to give an AI agent context for a specific task. The cadence of "build the graph" is the same as the cadence of "I am starting a non-trivial task". That is a few times a day, not a thousand times a day. Coupling the build to the slash command means:
- Predictable resource use: zero CPU until you ask.
- The graph reflects an explicit moment in time: the moment you decided to start a task. No drift between what the agent saw and what the codebase looked like five minutes later.
- One graph. One rebuild. One file at
<workspace>/.eh/graph.db. Easy to reason about the state.
What this design gives up
I want to be honest about the trade-offs:
- No real-time updates. The graph reflects the moment of the last build or refresh. The orchestrate-end auto-refresh covers the most common drift case (a long-running plan that touched many files), but a single agent editing files outside an orchestration still sees a stale view until the next explicit rebuild.
- No cross-machine sharing. The graph file lives on your laptop. Teams that want a shared code-intelligence backend need a server. There is no way around that.
- Tied to tree-sitter coverage. Languages without a tree-sitter grammar in the shipped set (Go, Java, Ruby, Rust) are not yet in the graph. The dispatcher is per-language, so adding grammar is a few hundred lines, but it has to be done per language.
These are real limits. For a solo developer running 3 to 5 AI agents on their own machine, none of them dominate. For a 50-person engineering org, several do.
The takeaway
Most code-intelligence tools assume "constant background work" is the price of context. For a single developer giving an AI agent context to do a task, it isn't. A SQLite file, a one-shot extractor, and a slash command cover the actual use case. Activation doesn't touch the disk. The graph builds only when you ask. The agent gets a curated slice via MCP. Nothing leaves the laptop unless an agent you opted into makes its own LLM call.
If that architectural stance interests you, Event Horizon is open source and on the VS Code Marketplace. v3 ships the graph and the orchestrate-end auto-refresh that keeps it current as your agents work, without ever installing a file watcher. Star the repo if you want to follow the next pieces (more languages, smarter slicing, agent-driven graph mutations).
Try it: Install from the VS Code Marketplace, or Open VSX for Cursor, VSCodium, and Windsurf.
Source: github.com/HeytalePazguato/event-horizon (MIT)
Top comments (0)