Skip to content

Daemon and index

github-actions[bot] edited this page Oct 13, 2026 · 7 revisions

How CodeLoupe answers: one small background process that keeps a per-file fact index in SQLite and composes answers from it when a question arrives. This page is the model in the head; the numbers are measurements (see Benchmarks).

One daemon per machine

  • Started on demand, by the first CLI call, codeloupe start, the Claude Code plugin's session hook or the desktop app. A second start finds the running daemon and does nothing; starts on one home go one at a time (<home>/daemon.lock), so two CLIs that find no daemon cannot damage each other's token or databases. codeloupe stop ends it and writes <home>/stopped so that the desktop app does not start it again; codeloupe start removes the marker. stop refuses while jobs run (--force ends them).
  • Stateless MCP over HTTP (Streamable HTTP) on 127.0.0.1:47391, plus the same tools on the CLI. A restart of the daemon does not break an agent window: its next call simply reaches the new process. Only requests with the x-codeloupe header, no Origin and a local Host are served, and everything that acts for the user also needs the token of <home>/daemon.token (Configuration). daemon.json (pid, port, start time) names the daemon; the CLI and the app refuse an answer from a process it does not name, and hook.sh and the CLI send the token only to a daemon that proves it holds it.
  • Idle costs nothing: no file watchers and no timers that wake it. A worktree is checked when a query arrives, a tracker is polled only while tool calls arrive.
  • Small by design: builds of an index run one at a time in a short-lived child JVM at low priority, so the daemon itself stays small. Heavy work never blocks a read.
Measured Value
Daemon, two repositories indexed about 210 MB resident
Peak while indexing a 319-file repository 517 MB
CPU while idle, 30 s 0 ms
Warm call 10-11 ms; 26 ms text search; about 750 ms changes of a branch
First index of a repository 7.3 s (319 Kotlin files), 2.7 s (457 files of CodeLoupe)
Index on disk 62 MB for both repositories
Idle daemon of a small bundle 94-109 MB (Packaging and releasing)
CLI call against a running daemon about 0.2 s

Budgets that make /status warn (p95Ms, queueWaitMs, rssMb, busyRate) are configurable (Configuration).

What the index holds

The index holds the facts of one file at a time and no global graph: per file its imports, its declarations (kind, name, container, parameters, types, modifiers, annotations, supertypes, the line ranges of declaration, body and KDoc) and its references (name, position, kind, the text of the receiver, the enclosing declaration). Relations across files (who uses, who calls, subtypes) are composed when a question is asked, so changing a file means parsing that file again, nothing more. Modules and their project dependencies come from settings.gradle(.kts) and pom.xml.

Kotlin and Java files are parsed by the Kotlin compiler's own parsers (syntax only, no classpath), which is why there is no IDE and no build needed. The result is a SQLite database per repository base and one per worktree overlay, in the daemon's home (Worktrees and overlays).

How references are resolved

Usages are resolved without an IDE or compiler: the scopes, imports and aliases a file sees, the receiver's type where syntax tells it (declared types, Type(…), what a call returns, collection elements in lambdas), and overloads by argument count. Unsure hits are marked, never dropped. One heuristic: on a receiver of unknown type, a name the index declares only once (and no library declares, judging by the core API and the files' imports) is taken as exact. A name that matches several unrelated declarations must be qualified (Type.member).

A hit is exact (only the target is possible) or candidate (it may be the target: a dispatch through a supertype, a smart cast, a constructor argument); references that resolve to other declarations are counted and listed with all=true. usages therefore answers a superset of rg -w over code; precision of exact is checked against a manually verified oracle in the tests (Development).

The parse worker

The daemon does not parse in its own process: edited files (an overlay refresh, a small base sync, task_code) go to a parse worker, a small child JVM (index.ParseWorker, one JSON line in, one out) that the daemon starts on the first file to parse and that exits after parseWorkerIdleSeconds (default 300) without one, or when the daemon ends. The compiler's parser is some 20 MB of classes, symbols and code plus the heap its syntax trees fill; held in the daemon it would be resident for as long as the daemon lives. While the worker runs it holds about 100 MB of its own; a worker that cannot start or dies is replaced once, and then the daemon parses itself (slower on memory, never wrong). A worker starts from the install's AOT cache (<home>/aot/…worker.aot, made with the CLI and daemon caches, see Getting started): start to first answer ~150 ms instead of ~800, so changes after 40 edited files with a worker that has to start again takes ~0.5 s instead of ~1 s. daemon.log says for every worker how long it took to start and whether it had the cache. With the worker the daemon's heap limit is 256 MB, with parseWorkerIdleSeconds 0 it is 320. The limit is not the footprint (the serial collector commits what the live set needs); 64 MB ran out on a desktop with ~20 worktrees, three trackers and 5 000 transcripts (CL-238). /status reports heapLiveMb (live after the last collections), heapLivePeakMb and heapLimitMb.

A daemon or worker that runs out of heap ends (-XX:+ExitOnOutOfMemoryError) instead of collecting forever with its port bound. A daemon whose process is alive while its port gives no answer is wedged: codeloupe status says so (exit code 3), a CLI call fails naming it instead of starting a second daemon, and codeloupe start (which the desktop app runs) or codeloupe stop ends that process first. Only the process that wrote daemon.json counts: it must have started just before the startedAt written there.

What it keeps in its home

In <home> What
config.json the settings (Configuration)
daemon.json, daemon.log, calls.jsonl the running daemon's pid, port and version; its log; tool, latency and size of every call
base and overlay databases, aot/ the indexes (Worktrees and overlays); the JVM AOT caches of the CLI, the daemon and its parse worker
jobs.db, jobs/, events.db jobs, their logs and the event log (Jobs and events)
know.db the knowledge store, created on first use (Knowledge store)
trackers/ tracker mirrors (Trackers)
secrets/ the encrypted store and its audit (Environment and secrets)
transcripts.db the ingested agent runs (Metrics and savings)
writes.jsonl the journal of edit writes
ports.json, releases.json, reconcile-state.json, reconcile.jsonl workspaces (Workspaces and Docker cleanup)
accounts.json Claude Code and YouTrack accounts (Desktop app)

Only the daemon writes the indexes; a new base goes to a new file and is switched in atomically.

Clone this wiki locally