Grounded Docs
v0.2.0 · alpha Download Changelog GitHub
>_ docs / concepts / The cabinet & lanes
§ 02 Concepts

The cabinet & lanes

Grounded keeps memory in four explicit lanes over one durable home. Nothing is inferred into existence: your agents write facts, log sessions, ingest docs, and set vision as they work — recall and briefs only ever retrieve, rank, and cite what's there.

// The durable home

The cabinet #

The cabinet is the folder that holds your data — ~/Grounded/Cabinet by default (%USERPROFILE%\Grounded\Cabinet on Windows). On the default adapter it holds a single SQLite database that carries every lane plus its vector and full-text indexes. The index is inside the cabinet; you can copy, diff, and back it up with ordinary tools. Settings live apart, in ~/.grounded, which holds only configuration, models and runtime.

~/Grounded/Cabinet/grounded.dbfacts · sessions · docs · vision
~/Grounded/Cabinet/exports/portable
~/Grounded/Cabinet/backups/snapshots
~/.grounded/config.tomlsettings
~/.grounded/install.jsondeploy manifest

A custom --home folder is self-contained: its data lives in cabinet/ inside it. An older install with data in ~/.grounded/cabinet keeps using it until you run grounded migrate-cabinet.

Switch config.toml to the Postgres adapter and the same lanes live in a grounded schema instead — the behaviour is identical. See self-hosting.

// What actually reached the agent

Delivery accounting #

Every list-shaped response — facts, sessions, docs, vision, recall, and impact — comes back as {data, meta}, never a bare array. meta is a DeliveryMeta: {returned, available, truncated, limit, bySource?}. available is a real computed count of what matched the query — never data.length — so GET /facts?limit=8 can answer {returned: 8, available: 24, truncated: true} instead of quietly handing back eight rules and losing the other sixteen.

Most memory systems report what they stored. Grounded reports what actually reached the agent. A truncation signal at the retrieval response — not a log line, not a debug flag, the response itself — is what lets a caller tell "this is everything" from "this is page one" every time, without a second round trip.

mem0, Graphiti/Zep, Letta/MemGPT, Honcho, cognee, and Basic Memory all report what they stored. None of them surface a truncation signal at the retrieval response — a caller discovers that sixteen facts were silently dropped only after the agent acts on an incomplete set.

// Lane 01

Facts #

Durable rules and operator truths — the things an agent must follow. A fact has a scope (e.g. global, project:grounded, agent:codex), an optional category and detail, a pinned flag, and an importance from 0 to 1.

Facts stay explicit: your agents write them as they work and edit them in place by id — only the fields passed change, and a fact re-embeds only when its text does.

// Lane 02

Sessions #

The chronological work log — what happened, recently, per project. A session carries a summary, optional details, and metadata: project, agent, machine, tags, and a source (manual, hook, import). Recent sessions carry momentum into every brief and lose weight over time by a recency half-life.

// Lane 03

Docs #

The markdown and notes you ingest. On ingest, Grounded walks your paths (indexing .md .markdown .mdx .txt), applies .groundignore, strips <private>…</private> regions, and splits each file into ~1200-character chunks (150 overlap). Each chunk is hashed, so re-ingesting only touches what changed — a body-unchanged chunk whose tags did change gets a cheap tag-only rewrite instead of a re-embed. A doc chunk has a status of active, archived, or missing.

Every doc also carries a scope — a lane, e.g. global (the default) or administration — set per ingest batch via docsIngest(paths, {scope}). Lanes let you keep corpora that shouldn't mix (engineering docs vs. household admin, say) in one cabinet without one polluting the other's recall results. Browsing is unfiltered: GET /docs and ground_docs_ingest's list-side counterpart return every lane unless you ask for one. Retrieval is filtered by default: recall and brief only read the global lane unless you pass scopes / docScopes explicitly — and that list is the complete read set, not an addition to the default, so asking for ["administration"] alone drops your global docs out of the results. Want both? Pass ["global","administration"].

Separate from the lane, every doc also carries a project — derived at ingest from the corpus/<project>/ path segment, and indexed in its own right, mirroring sessions.project. It is nullable with no default: a path with no corpus/ segment stays null rather than claiming a membership it can't prove, and nothing is ever guessed from a filename. The two axes answer different questions — scope is which corpus may read this, project is which work it belongs to. Re-ingesting an existing cabinet backfills it: the retag path fills project on files whose bodies never changed, so nothing re-embeds to gain a column.

Scoping is routing, not a security boundary. A self-hosted cabinet has one shared API token; scope and agent are values the caller declares on each request, not something the server authenticates. Anyone who can call the API can read or write any lane.

impact (see the API) is the one operation that crosses this lane boundary on purpose. Recall drops out-of-lane docs entirely; impact returns them citation-only — path, citation, and scope survive, title/snippet come back null, and inScope: false says why. That's still routing, not enforcement: the caller learns a dependency exists without its content crossing the boundary, and declaring the lane in scopes reveals it. Lane gating applies to docs only — facts and sessions are never laned, so impact always reports them inScope: true, scope: "global".

Orphaned rows — files that were deleted or moved on disk — aren't caught by ingest. Reconcile them with docsPrune() (POST /docs/prune, ground_docs_prune): marks stale rows missing by default, or deletes them with {remove:true}. This is distinct from an ingest's own removed count, which only tracks stale chunk indexes inside a file that shrank on re-chunk — it says nothing about files gone from disk.

// Lane 04

Vision #

What the work is for. Vision is one active narrative-markdown record per scope — a global vision and a project:<name> vision. Like facts, setting it edits the one active record for that scope in place. Unlike the other lanes, vision is excluded from recall — it isn't embedded or indexed. It is always injected, verbatim, into the brief, so every agent shares your direction, not just your history.

// Everything is cited

Typed IDs & citations #

Every retrievable item has a typed ID — fact:2, session:274, doc:1091 — and no result ever comes back without one. Recall returns compact cards; each card names its source and how it matched. Pass a typed ID to GET /get/<typedId> for the full record, traced back to its row and, for docs, its file path. Nothing is anonymous, nothing is unrecoverable.

// Context before work

Briefs #

A brief is the scoped startup summary an agent reads before it acts. It assembles, in order: a startup note, your vision (global + project), the most recent sessions, the facts that apply to this agent and project, and related docs. Fact scope is derived from the request — global plus agent:<a> plus project:<p> — so an agent sees the rules meant for it and not another project's. Related docs come from the global lane unless docScopes says otherwise (see docs above). Ask for it as markdown or JSON — the brief's list-shaped members carry the same delivery envelope as everything else.

Facts are the scarcest thing in a brief: they share one token reserve. Pinned is a delivery guarantee, not a rank boost — the entire pinned set survives the reserve, so a fact you pinned is a fact the agent read.

Past the full-text budget, the rest don't vanish. They fall to an index tier: a one-line compressed form — topicKey — detail (fact:NN) — rendered within its own small budget. A fact's detail is its trigger clause, so the line still tells an agent when the fact matters and hands it an id to fetch in full; only what overflows the index budget too is genuinely dropped. The counts stay honest either way: meta.indexed counts index lines and is never folded into returned, their ids come back in indexedItems, and droppedItems keeps its one meaning — absent from the brief.

Optional per-category floors guarantee a minimum number of slots to specific categories (a commit-rule or a convention, say) before the reserve is spent, so a rare-but-critical category can't be crowded out entirely by a larger one. Floors only reorder facts within their existing ranking — they never filter anything out; with no floors configured, a brief behaves exactly as it always has.

The session lines carry a date only, rendered in UTC unless you pass an IANA timezone. That default is worth a thought if you are not on UTC: a session logged at 23:53 local, west of it, renders as the next day — and a brief exists to tell an agent what happened yesterday. Pass your zone and the dates land where you expect; the header then names it, so a non-UTC date is never ambiguous. It is display only — stored instants and the JSON createdAt are always UTC.

>_ POST /brief format: markdown
note
=== STARTUP CONTEXT ===
1
vision
=== VISION (global · project:Y) ===
2
work
=== MOST RECENT WORK (newest first) ===
3
facts
=== DYNAMIC FACTS (curated · scope: …) ===
4
docs
=== RELATED DOCS ===
5