Architecture

Bun, SQLite, local GGUF models via node-llama-cpp, and one retrieval core shared by CLI, Web UI, SDK, REST, and MCP.

GNO is a TypeScript CLI package executed by Bun, with a SQLite-backed index, local model inference via node-llama-cpp, and a shared retrieval core that every surface (CLI, Web UI, SDK, REST API, MCP) plugs into.

Runtime

Storage layout

Surfaces

Every GNO surface speaks to the same shared retrieval core:

Host absolute paths stay with callers on the owner's machine: the CLI, the SDK, stdio MCP, and the same-host Web UI. A remote REST caller and every Streamable HTTP MCP caller get the gno:// URI and collection-relative path instead. See Host paths and remote callers.

Data flow

  1. Ingestion — the file walker reads sources, parsers extract text and frontmatter, export adapters split containers into logical records, and the chunker splits documents into retrievable units. Code files get structural first-pass chunking for TypeScript, JavaScript, Python, Go, and Rust.
  2. Mirroring — every source is normalized into a canonical mirror: LF line endings, NFC text, stable line numbering. All coordinates in the system address the mirror, not the original bytes, which is what makes “lines 40 to 48” mean the same thing for a Markdown file and a converted PDF.
  3. Embedding — chunk text is encoded to dense vectors by the active embedding model, prefixed with the document title so the vector carries context beyond the chunk itself.
  4. Indexing — chunks land in SQLite FTS5 and sqlite-vec tables; documents, links, tags, and typed edges land in relational tables. Change detection is SHA-256 per file. Unchanged active content can reuse storage; identical bytes restored at an inactive path still trigger reactivation and its journal event.
  5. Retrieval — the query enters the pipeline, hits BM25 and vectors, optionally expands one hop through the graph, merges by reciprocal rank fusion, reranks with a cross-encoder, and returns.
  6. Compilation or answer — a Capsule request adds facet derivation, coverage-driven selection, and global budgeting on top of retrieval. ask sends the selected evidence to the generation model with a citation-preserving prompt, and --verify adds a claim-classification pass against the closed Capsule.

Sync also updates links from other collections that point at changed files. After an interrupted sync, an upgrade, or a change to collection or link rules, the next sync does a full pass.

Identity and hashing

Three hashes exist for every piece of evidence, and the distinction runs through the whole system:

Keeping them separate is what lets verification say “the file changed but your cited paragraph did not”. Document IDs are derived from the source hash, so identity is content-addressed rather than path-addressed.

Shared canonical chunks do not imply interchangeable embeddings. Proven vector reuse requires the complete formatted input, including title context, and vector identity (model weights, formatter, dimensions, context size, truncation policy) to match. A runtime change such as backend, Bun version, or thread count keeps the partition when a measured sample reproduces the stored vectors (see Switching backend or Bun version). Different titles can require distinct vectors for the same body. Restoring unchanged input preserves proven vectors; changed input requires new embeddings.

Re-embedding for a new model runs in the background and resumes if interrupted. GNO switches to the new vectors only once every chunk is covered.

The resident runtime

gno serve and gno daemon are two front ends for one resident runtime: a single exclusive owner per data directory, holding the store, the watcher, the job queue, the model lifecycle, and a Streamable HTTP MCP gateway at 127.0.0.1:3000/mcp.

Several MCP clients share the store and matching adapter while keeping isolated sessions, cancellation, and configuration snapshots. Model files, downloads, and native initialization are deferred until inference. Warm reuse lasts within the five-minute default idle grace; the next request after that reloads. Background embedding does not keep idle generation or reranking models loaded. Metadata polling does not renew the grace. A second process against the same data directory is rejected.

On shutdown, GNO stops accepting work, gives running requests and jobs about five seconds to finish and five more to cancel, then stops the model process. Completed checkpoints are saved and unfinished embeddings stay pending for the next run. If GNO cannot confirm the model process stopped, shutdown reports an error.

Background embedding works in small batches and yields to searches and other interactive requests between batches, so they stay responsive while a long embedding job runs. Chunks that fail stay pending. A batch that runs past the inference timeout fails on its own; later batches still embed. A backlog that already exists when gno serve or gno daemon starts gets its first pass about 30 seconds after startup. These limits are not configurable.

Cancelling a request stops delivery of its result. Model work already running keeps its capacity until it finishes; a stuck cancelled operation gets five seconds before GNO restarts the model process. Failed requests are not replayed automatically; send them again. These limits do not apply to HTTP inference endpoints.

Cross-cutting gates

Three checks sit between a request and an effect, and they are independent by construction:

Ports and adapters

The core defines ports — converter, store, embedding, rerank, generation — and adapters implement them. That is why an OpenAI-compatible HTTP endpoint can substitute for a local GGUF per model role without the retrieval core knowing, and why a per-collection embedding override is a configuration change rather than a code path.

┌─────────────────────────────────────────────────┐
│      CLI / MCP / Web UI / REST / SDK / Desktop   │
├─────────────────────────────────────────────────┤
│  Ports: Converter, Store, Embedding, Rerank, Gen │
├─────────────────────────────────────────────────┤
│  Adapters: SQLite, FTS5, sqlite-vec, llama.cpp,  │
│            OpenAI-compatible HTTP                │
├─────────────────────────────────────────────────┤
│  Core: Identity, Mirrors, Chunking, Retrieval,   │
│        Capsules, Journal, Egress                 │
└─────────────────────────────────────────────────┘

Typed metadata filters

GNO applies typed metadata filters inside the index before candidate limits, so large indexes are not filtered in memory. Filters narrow results and never grant access outside your scope.

Read the metadata guide: examples, operators, missing fields, limits, and recovery.