The Daemon

The LIP daemon is a persistent background process that maintains the query graph, watches files, and answers queries. Everything else — the LSP bridge, MCP server, CLI queries — connects to it.


Starting

lip daemon --socket /tmp/lip.sock
lip daemon \
  --socket /tmp/lip.sock \
  --journal ~/.local/share/lip/journal.lip

The daemon logs to stderr. Set LIP_LOG=info for startup details, LIP_LOG=debug for verbose query tracing.


Persistence — the WAL journal

The daemon writes all mutations (file upserts, deletes, annotations) to a write-ahead log (WAL journal) before applying them. On startup it replays the journal to restore the full graph state.

This means:

  • The graph is immediately warm on restart — no re-indexing.
  • Annotations and Tier 2 upgrades survive restarts.
  • Crash recovery is automatic.

The journal grows incrementally. On startup the daemon compacts it by replaying only the most recent state per file.

Default location: ~/.local/share/lip/journal.lip

To reset the graph entirely:

rm ~/.local/share/lip/journal.lip
lip daemon  # starts cold

File watcher

The daemon uses the OS-native filesystem watcher (FSEvents on macOS, inotify on Linux). It watches each tracked file individually — not the entire directory tree.

How it works:

  1. A file is registered when the daemon first receives a delta for it.
  2. When the OS delivers a change event, the daemon reads the file and compares its content hash to the stored version.
  3. If the hash is unchanged (e.g. a write that didn’t modify content), nothing happens.
  4. If the hash changed, the daemon re-indexes just that file and updates the blast-radius graph.

Path canonicalization: On macOS, /tmp is a symlink to /private/tmp. The watcher canonicalizes all paths so events match their registered entries correctly.


Confidence tiers and background verification

The daemon applies updates in two stages:

Tier 1 (immediate): Tree-sitter indexing runs synchronously on every file change. Results are available within milliseconds.

Tier 2 (background): If a Tier 2 analyzer is configured, the daemon queues a verification job for the blast radius of the change. When complete, Tier 1 symbols are upgraded to Tier 2 confidence (score 51–90).

Tier 2 is wired for Rust (rust-analyzer), TypeScript (typescript-language-server), Python (pyright-langserver / pylsp), and Dart (dart language-server). All four backends are lazy-initialized and degrade gracefully if the binary is not in PATH.


Protocol

The daemon listens on a Unix socket and speaks a length-prefixed JSON protocol:

[4 bytes: big-endian message length][JSON body]

Each connection handles one request/response pair. The protocol is synchronous per connection; clients that need concurrent queries open multiple connections.

Message types (client → daemon):

TypeDescription
ManifestSend repo root + Merkle hash on connection
DeltaUpsert or delete a file (carries source text)
QueryDefinitionFind definition at (uri, line, col)
QueryReferencesFind all references to a symbol URI
QueryHoverHover info at (uri, line, col)
QueryBlastRadiusBlast radius for a symbol URI
QueryWorkspaceSymbolsSearch symbols by name
QueryDocumentSymbolsList symbols in a file
QueryDeadSymbolsFind unreferenced symbols
AnnotationGet/Set/ListPersistent symbol annotations
EmbeddingBatchCompute and cache file embedding vectors
QueryNearestTop-K nearest files by cosine similarity
QueryNearestByTextTop-K nearest files by text query
QueryIndexStatusDaemon health and embedding coverage
QueryFileStatusPer-file index and embedding status
BatchQueryMultiple queries in one round-trip

Acknowledgment: Every Delta receives a DeltaAck { seq, accepted } response, eliminating the fire-and-forget drift that LSP is known for.

BatchQuery

BatchQuery { queries: [...] } executes N sub-queries under a single db lock acquisition and returns one result per query in order:

{
  "type": "batch_query",
  "queries": [
    { "type": "query_blast_radius", "symbol_uri": "lip://local/src/auth.rs#AuthService" },
    { "type": "query_references",   "symbol_uri": "lip://local/src/auth.rs#AuthService", "limit": 50 },
    { "type": "annotation_get",     "symbol_uri": "lip://local/src/auth.rs#AuthService", "key": "lip:fragile" }
  ]
}

Response:

{
  "type": "batch_result",
  "results": [
    { "ok": { "type": "blast_radius_result" }, "error": null },
    { "ok": { "type": "references_result"   }, "error": null },
    { "ok": { "type": "annotation_value"    }, "error": null }
  ]
}

Manifest, Delta, and nested BatchQuery entries return an error result for that slot without aborting the batch.


Running as a system service

launchd (macOS)~/Library/LaunchAgents/dev.lip.daemon.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>       <string>dev.lip.daemon</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/you/.cargo/bin/lip</string>
    <string>daemon</string>
    <string>--socket</string><string>/tmp/lip.sock</string>
  </array>
  <key>RunAtLoad</key>   <true/>
  <key>KeepAlive</key>   <true/>
  <key>StandardErrorPath</key>  <string>/tmp/lip-daemon.log</string>
</dict>
</plist>
launchctl load ~/Library/LaunchAgents/dev.lip.daemon.plist

systemd (Linux)~/.config/systemd/user/lip-daemon.service:

[Unit]
Description=LIP daemon

[Service]
ExecStart=%h/.cargo/bin/lip daemon --socket /tmp/lip.sock
Restart=on-failure
StandardError=journal

[Install]
WantedBy=default.target
systemctl --user enable --now lip-daemon

Disabling the file watcher

If you prefer to push file changes manually (e.g. in CI or editor plugins that send deltas themselves):

lip daemon --no-watch

In this mode the daemon only updates when it receives an explicit Delta message via the socket.