---
title: "Container quickstart"
url: "https://nsta1.github.io/Orleans.Lattice/docs/lattice.api.mcp.repocontext/container.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/docs/lattice.api.mcp.repocontext/container.md"
package: "Orleans.Lattice.Api.Mcp.RepoContext"
status: "unreleased"
documents: "Orleans.Lattice 9.9.0 (release line 9.9)"
built: "2026-10-04"
all-pages: "https://nsta1.github.io/Orleans.Lattice/llms.txt"
bundle: "https://nsta1.github.io/Orleans.Lattice/docs/lattice.api.mcp.repocontext/llms-full.txt"
---
# Container quickstart

Part of the [Api.Mcp.RepoContext documentation](README.md).

The module ships as a single, restart-durable container image - "codebase memory in a box". The container's only application listener is the MCP endpoint (plus HTTP health probes, a Prometheus `/metrics` scrape endpoint, and under the `azure` profile the scaling-signal endpoint); no gRPC facade and no Explorer UI are exposed. All durable state lives on a host mount, so context survives a restart, a recreate, and an image upgrade.

The runnable sample is [`samples/RepoContextContainer`](../../samples/RepoContextContainer/README.md); this page summarises how it is wired.

## Contents

- [Topology](#topology): The container exposes a single application listener (the MCP endpoint, plus HTTP health probes) and reads the code it indexes from a read-only workspace mount, so it can never mutate that code.
- [The durability loop](#the-durability-loop): The end-to-end guarantee the sample demonstrates.
- [Durability profiles](#durability-profiles): The host selects a durability profile from the `LATTICE_DURABILITY` environment variable.
- [Data root and fail-fast](#data-root-and-fail-fast): All durable local state - the file WAL directory and, in the `local` profile, the SQLite database - lives under `LATTICE_DATA_ROOT` (default `/data`), which must be a bind mount or named volume.
- [Configuration](container/configuration.md): The host is configured entirely by environment variables.
- [Registering repositories at runtime](#registering-repositories-at-runtime): The container mounts a broad parent directory read-only at `LATTICE_WORKSPACE_ROOT` (default `/workspace`) and lets the MCP client decide which repositories under it to index - no repository path is baked into the container's configuration.
- [Index source strategies](container/index-source-strategies.md): Where a repository's content comes from is a per-repository choice between two strategies.
- [Background reconcile and change detection](#background-reconcile-and-change-detection): Once a repository is onboarded, its self-index grain keeps it converged without any client call.
- [Agent-memory backup and recovery](container/agent-memory-backup-and-recovery.md): Agent memory is the one tree in this container that **cannot be rebuilt from anything**. The structural, symbol, content, and vector trees are all derived from the workspace: delete them and a re-onboard reproduces them exactly.
- [Health probing](container/health-probing.md): The runtime image is distroless and shell-less, so probing is HTTP-only - there is no shell-exec healthcheck.
- [Metrics scraping](container/metrics-scraping.md): `GET /metrics` serves a Prometheus text exposition (`text/plain; version=0.0.4`) on the same listener as MCP and the health probes, so a scraper needs no second port and no sidecar.
- [Graceful shutdown](container/graceful-shutdown.md): On `SIGTERM` (a `docker stop` or `restart`) the host flips readiness to not-ready first, then drains: the silo deactivates and the WAL commit-log flushes buffered records before exit, so an in-flight write is durable after restart.

## Topology

```mermaid
flowchart LR
    agent["AI coding agent<br/>(MCP client)"]

    subgraph container["repocontext container"]
        mcp["MCP listener :8080<br/>+ /health/live, /health/ready, /health/backup, /health/silo<br/>+ /metrics"]
        silo["Orleans single silo<br/>Lattice CRDT B+ trees<br/>(structural, symbol, content, memory, vector)"]
        mcp --> silo
    end

    embed["embedding companion<br/>(separate container)"]
    workspace[("/workspace<br/>read-only mount")]
    data[("LATTICE_DATA_ROOT (/data)<br/>file WAL + SQLite")]

    agent -->|"tools/list, tools/call"| mcp
    silo -->|"embed over HTTP"| embed
    silo -->|"walk + digest (read-only)"| workspace
    silo -->|"WAL + grain state"| data
```

The container exposes a single application listener (the MCP endpoint, plus HTTP health probes) and reads the code it indexes from a read-only workspace mount, so it can never mutate that code. The `local` profile keeps both the WAL and the relational store under `LATTICE_DATA_ROOT`; the `postgres` and `azure` profiles move the relational store (and, for `azure`, the WAL) to an external service, leaving the same listener and workspace wiring unchanged. The embedding companion is optional for search but not for readiness: the embedding provider is always bound, so with no reachable companion (an unset `LATTICE_EMBEDDING_ENDPOINT` points it at `http://localhost:9000` inside the container) search degrades to the keyword path, and once a repository is registered `/health/ready` holds 503 - see [Health probing](container/health-probing.md). Besides the `repocontext_*` tools, the same MCP endpoint serves the tree-administration tool group (`lattice_treeadmin_*`) with its mutating tree-lifecycle verbs enabled and its mutating schema-control verbs disabled, so whole-tree operator verbs such as the orphaned-leaf audit and repair are reachable in-process (issue #3287); see [MCP tools](../lattice.api.mcp/tools.md).

## The durability loop

The end-to-end guarantee the sample demonstrates:

**start -> add a repo under the mounted workspace -> recall -> restart -> context is still present.**

State is replayed from the WAL and the relational store on the mounted volume after a restart, so an agent's onboarded structural model and its remembered notes are all still there.

## Durability profiles

The host selects a durability profile from the `LATTICE_DURABILITY` environment variable:

| Profile | Grain storage + reminders | WAL | Use |
|---|---|---|---|
| `local` (default) | Single SQLite file under the data root | File-backed WAL under the data root | Zero external services - a laptop or a single box. |
| `postgres` | PostgreSQL | File-backed WAL | A durable relational store you already run. |
| `azure` | Azure Table Storage | Azure Table WAL | A cloud deployment; also enables the scaling signal endpoint. |

Every profile applies finite per-tree tombstone compaction to the churn trees (structural, symbol, content, cross-reference, session, memory, the vector membership and metadata projection trees, the approximate-index tree, and the vector-coverage digest), so re-write, re-embed, and forget tombstones are reaped rather than accumulating. The write-once, content-addressed vector-payload tree is excluded because it never deletes in place.

SQLite grain storage and reminders derive their busy-retry window from the resolved Orleans `SiloMessagingOptions.ResponseTimeout`: half the budget, rounded down to whole seconds (15 seconds for the default 30-second request budget). This leaves headroom for a held write lock to surface as `SQLITE_BUSY` before the enclosing request times out; it is not an end-to-end deadline guarantee when a request also queues or performs several storage operations. Budgets below two seconds are rejected because a zero command timeout means unlimited retries. Very large budgets are capped at SQLite's signed 32-bit millisecond limit. Startup schema initialization uses the same derivation with the Orleans default budget and applies the matching `busy_timeout` PRAGMA; runtime providers use the resolved budget through their connection-string command timeout. Every lock failure on the grain store is attributed to the grain and write convoy that suffered it; see [SQLite grain-storage lock attribution](container/graceful-shutdown.md#sqlite-grain-storage-lock-attribution).

## Data root and fail-fast

All durable local state - the file WAL directory and, in the `local` profile, the SQLite database - lives under `LATTICE_DATA_ROOT` (default `/data`), which must be a bind mount or named volume. The host fails fast at startup if the file WAL directory (when the file WAL is selected) or the directory holding the SQLite database (when a SQLite store is selected) cannot be created or is not writable by its non-root UID. Both sit under that root unless `LATTICE_WAL_DIR` or `LATTICE_SQLITE_PATH` moves them, so a misconfigured mount - read-only, or owned by another UID - surfaces immediately instead of silently losing durability; a profile that selects neither, such as the `azure` defaults, leaves the guard nothing to probe. A missing directory is created rather than refused, so the guard cannot tell a real mount from a directory inside the container.

That includes the **agent memory tree**, which sits under the same root as the rebuildable index and cannot be moved off it - so a gesture that destroys the data volume, `docker compose down -v` above all, destroys authored memory alongside a code index that would have rebuilt itself in minutes. `repocontext_reset_index` exists for the index case and loses nothing. See [Memory durability](memory-durability.md) for why the two cannot be split across volumes, and for the opt-in memory archive that makes the destructive gesture survivable.

## Registering repositories at runtime

The container mounts a broad parent directory read-only at `LATTICE_WORKSPACE_ROOT` (default `/workspace`) and lets the MCP client decide which repositories under it to index - no repository path is baked into the container's configuration. The client drives this with these tools:

- `repocontext_add_repo` - registers a repository under the workspace and starts ingesting it (walk, digest, reconcile). This is the workspace-mode onboarding tool; it supersedes `repocontext_bootstrap`, which is not exposed in the container. Supply `path` (for example `/workspace/my-repo`); omit `repoId` to derive it from the final path segment. By default it honours the repository's `.gitignore` files (pass `respectGitignore=false` to index untracked files too) and drops files that look binary (pass `excludeBinary=false` to ingest blobs too); `includeGlobs` and `excludeGlobs` narrow the walk further. Ingestion runs asynchronously off the request thread and returns a `Running` snapshot at once, so poll `repocontext_index_status` for the same `repoId` to follow it to completion; a dropped client stream never aborts the run, and an interrupted one resumes after a restart. Re-adding the same repository is idempotent - only changed files are updated and deleted ones pruned.
- `repocontext_index_status` - reports a repository's indexing progress (lifecycle status, current phase, file, chunk, symbol, and content-projection counters, the reset's tree and entry counters, the cumulative index-run count `attempt` (run starts - every reconcile and back-fill adds one - not retries), timing, any failure reason, and a live `pacing` reading while the run is `Running`), so an agent can watch an `add_repo` pass complete or diagnose a failure. A repository that was never onboarded reports `status=None`.
- `repocontext_list_repos` - lists every registered repository with its last-ingested marker, recorded file count, and `embeddedVectorCount` (the count of sources whose embedding has landed, derived from a walk of the durable vector-membership tree, so a restart re-derives it rather than losing it; sources include files, captured symbols, and embedded memory entries, so the count can exceed the file count once symbols or memory are embedded), so an agent can discover what is queryable and how far semantic coverage has progressed before recalling, scanning, or searching. Counting exactly means walking the whole membership tree, so the listing never does it inline: it serves the last completed walk, omits the field until one completes (which is not the same answer as `0`), and sets `embeddedVectorCountPending` while a refresh is outstanding - which it will be for most of an active ingest, since every membership write supersedes the previous figure. Each row also reports `indexedRoot`, the absolute path the repository was indexed from, because a repository id does not imply a root - it defaults to the final segment of that path but may be supplied explicitly, and in a worktree the base repository is what is indexed. Without it, a correct index and a wrongly-rooted one are indistinguishable from outside (#2617).
- `repocontext_remove_repo` - forgets every record for a repository (structural nodes, symbols, content projection, memory, and vectors). The working tree on disk is never touched.
- `repocontext_reset_index` - drops a repository's code index and every derived plane but keeps its durable memory and its place in the listing, so a wedged or stale index is repaired without losing notes; a following `repocontext_add_repo` rebuilds it. The sweep is bound to the host rather than to the call, so a call that times out or drops does not cancel it: it reports itself through `repocontext_index_status` as `Running` in phase `Resetting` (with the tree and entry counters above advancing), then `Completed` or `Failed`. See [Tools](tools.md#workspace-mode-tools).

Every path passed to `repocontext_add_repo` is resolved to its real on-disk location - defeating both `..` traversal and symlink escape - and must sit inside `LATTICE_WORKSPACE_ROOT`; a path outside it is refused. Mounting the workspace read-only means the container can never mutate the code it indexes.

A repository configured to be sourced from a git remote is not registered this way at all: it is declared in configuration, onboards itself, and is refused by `repocontext_add_repo` so a mounted path can never shadow the configured remote. See [Index source strategies](container/index-source-strategies.md).

## Background reconcile and change detection

Once a repository is onboarded, its self-index grain keeps it converged without any client call. On the first tick after `LATTICE_RECONCILE_INTERVAL_SECONDS`, plus up to `LATTICE_RECONCILE_JITTER_SECONDS` of random jitter, has passed since the previous reconcile, it re-drives an idempotent reconcile that walks the tree, diffs it against the stored structural records, and applies exactly the delta - so files added, edited, and deleted on disk are picked up automatically. The reconcile is single-flight and each tick is a fresh grain turn, so re-driving on completion polls for the previous run rather than recursing; a short `LATTICE_RECONCILE_INTERVAL_SECONDS` therefore makes it near-continuous, bounded only by the tick.

To keep that cheap on a large tree, the background reconcile uses **directory-modification-time pruning**: a directory whose modification time is unchanged since the previous walk carries its known files forward without re-stating them, while every subdirectory is still descended so a nested structural change is never missed. Adding, renaming, or deleting a file bumps its directory's modification time, so those changes defeat pruning and are caught on the next reconcile. An in-place content edit that leaves the directory's modification time untouched is invisible to pruning, so it is caught by the periodic full sweep instead: every `LATTICE_FULL_WALK_INTERVAL_SECONDS` a reconcile ignores the prune cache and stats every file. That deadline is enforced by **counting reconcile passes**, not by reading a clock. The distinction matters because the reconcile is single-flight: the real gap between two walks is the larger of the configured spacing and the previous pass's own duration, so on a repository whose pass runs longer than its spacing a wall-clock deadline is already past on arrival every single time, forcing a full walk on every pass and leaving the prune cache written but never read. Counting passes holds the bound however long a pass takes. The interval is converted once, by dividing it by the widest scheduled spacing - `LATTICE_RECONCILE_INTERVAL_SECONDS` plus `LATTICE_RECONCILE_JITTER_SECONDS` - rounding up, and clamping to at least one pass; the shipped defaults give 3 passes, so 2 reconciles in every 3 prune. Setting the interval at or below one reconcile spacing clamps it to a single pass, which reproduces the old "full walk every time" behaviour deliberately rather than by accident. Worst-case detection latency for a pure in-place content edit is therefore that many reconciles, which is the configured interval or longer in wall clock. The first walk after a process start is always a full one, so a restart re-establishes an exact baseline.

The same pass counting spaces out the **embedding gap scan**. Beyond structural convergence, a pass also re-probes files it decided were unchanged, looking for one whose structural record is committed but whose vector never landed. That probe used to cost two membership reads per indexed source, which once a repository was converged made it by far the most expensive thing a pass did while reliably finding nothing. It now reads the per-page vector-coverage digest instead - a fixed 257 rows whatever the corpus size - and the membership probe survives only as the fallback for a digest that has not been built yet. It runs every `LATTICE_EMBEDDING_GAP_SCAN_INTERVAL_SECONDS`, likewise counted in passes. Two safeguards mean the spacing costs no healing latency: a repository that has never yet been observed gap-free is probed on every pass until it is - unless its last scan could not measure coverage at all (a failed or gate-pruned membership probe, or embed work deferred under saturation), in which case it falls back to the periodic cadence rather than escalating on an unknown (issue #3340) - and the self-index grain's continuous out-of-band paged gap sweep - which is already incremental and bounded - forces an immediate in-pass scan on the very next reconcile the moment it finds one, rather than waiting for the cadence.

Pruning is applied only to this background reconcile. An explicit `repocontext_add_repo` onboarding (or re-onboarding) always runs a full, exact walk, so an agent that re-adds a repository observes the current on-disk state immediately rather than within the full-walk bound.

Everything in this section describes the mounted-workspace strategy. A git-sourced repository never walks a directory and never prunes by modification time: its loop is the fetch-and-diff cycle in [Index source strategies](container/index-source-strategies.md), where the change set - deletes included - comes from the commit itself.
