# Architecture

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

Section: Guides  
Canonical: https://gno.sh/docs/architecture  
Markdown: https://gno.sh/docs/architecture.md

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

- **Bun** — TypeScript-first runtime and global package installation
- **SQLite + sqlite-vec** — BM25 FTS plus vector similarity via a SQLite extension. One database file per GNO installation.
- **node-llama-cpp** — local LLM inference for embedding, reranking, and answer generation. GGUF model format. Models run in a helper process that GNO starts and stops; each GNO process has its own.

## Storage layout

- `~/.config/gno/` — config files (index.yml, presets)
- `~/.local/share/gno/` — SQLite database, model cache, asset cache
- `~/.local/share/gno/write-receipts/` — private per-index request ledger for [retry-safe writes](https://gno.sh/docs/retry-safe-writes#storage)
- `~/.cache/gno/` — temporary artifacts, rerank scratchpad

## Surfaces

Every GNO surface speaks to the same shared retrieval core:

- **CLI** — the `gno` executable installed by the Bun package
- **Web UI** — `gno serve` launches a Bun HTTP server with the browser workspace and the REST API
- **SDK** — package-root importable client, same core under the hood
- **REST API** — exposed by `gno serve`, 35+ endpoints
- **MCP server** — stdio transport, read/write tools, graph navigation, and resources for any MCP-compatible client
- **Desktop** — a native window wrapping the web workspace

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](https://gno.sh/docs/api#host-paths).

## 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:

- **Source hash** — the original file bytes. Changes when the file changes at all.
- **Mirror hash** — the canonical normalized form. Changes when the extracted text changes, which can happen from a converter upgrade even if the source is untouched.
- **Passage hash** — the exact bytes of one span. Changes only when that specific passage moves.

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](https://gno.sh/docs/troubleshooting#backend-switch)). 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:

- **Authentication** — who is calling. Non-loopback daemon access requires an explicit bearer-token file plus exact Host and Origin allowlists.
- **Write authorization** — whether mutation tools exist at all. Read-only is the default everywhere; writes need an explicit opt-in and authentication alone never grants them.
- **Egress** — whether content may cross a transport boundary. Owned by the collection, evaluated before transfer, and not overridable by either check above.
- **Source availability** — whether source content may be materialized during indexing (`any` default or opt-in `local`). Distinct from egress. Local mode supports macOS File Provider folders for Google Drive (My Drive and individual Shared drives), iCloud Drive, and OneDrive (SharePoint libraries mounted directly under the shared libraries folder). It refuses unsupported layouts and has no Windows or Linux cloud-folder support.

## 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                 │
└─────────────────────────────────────────────────┘
```

## Related

- [How search works](https://gno.sh/docs/how-search-works)
- [Context Capsules](https://gno.sh/docs/context-capsules)
- [Collection egress policies](https://gno.sh/docs/collection-egress)
- [Source availability](https://gno.sh/docs/configuration#source-availability)
- [Privacy model](https://gno.sh/features/privacy-first)

## 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.](https://gno.sh/docs/typed-metadata)
