-
Notifications
You must be signed in to change notification settings - Fork 4
CLI Companion Concepts Memory
Part of how the companion works, under
ed companion in the CLI reference. This page
explains what "memory" means here, where every byte lives, and why the design
is append-only. No machine-learning background is assumed.
When the companion "remembers" something, no model is being trained and nothing is being memorised by the language model. Memory is ordinary data in an ordinary database: rows in Postgres and files on disk. The intelligence only shows up later, at read time, when a language model is handed a small selection of those rows and asked to answer with them. This is the single most useful mental model for the whole system: storage is dumb and permanent, intelligence is rented and momentary.
That split has practical consequences:
- You can inspect everything. Every remembered fact is a row you can query and a file you can open. Nothing is hidden inside model weights.
- Deleting or exporting memory is a database operation, not a retraining:
ed companion export,import,eraseandwipeare those operations. - Swapping the language model (the "reasoner") changes how well the companion talks about your memory, but never changes the memory itself.
Memory is organised as a ladder. Each rung is derived from the one below it, and each rung adds a layer of interpretation:
| Rung | Table | What it is | Made by |
|---|---|---|---|
| 1 | sources |
A unique file you gave it, identified by fingerprint | Ingestion |
| 2 | episodes |
One memory event: the readable body of that file, with a time | Ingestion |
| 3 | chunks |
Small searchable pieces of an episode, each with an embedding | Indexing |
| 4 |
claims, observations
|
Things you asserted, and things the record observed | Learning passes and connectors |
| 5 | beliefs |
Durable conclusions about how you work | Reflection |
Walk one note up the ladder. Say you drop goa-trip.md containing a journal
entry. Ingestion fingerprints the text, stores the original file, and creates
one sources row and one episodes row (rungs 1 and 2). Indexing splits the
body into a few chunks and computes an embedding for each (rung 3), which
is what makes the note findable when you later ask "what did I want to build
after Goa?". That night, the claim extractor may pull out "I want to build an
offline-first journal" as a claims row (rung 4). After a few similar notes,
reflection may form the beliefs row "wants long unstructured mornings
before screen time" (rung 5). The original words are never altered by any of
this; higher rungs only ever point back down with ids.
Chat and ask retrieve rung 3, then can also add matching beliefs and
observations from rungs 4 and 5 to their evidence. The Mind tab and
ed companion beliefs, claims and observations expose the derived rungs.
The two bottom rungs look similar but answer different questions.
A source answers "have I seen this exact content before?". It stores the content fingerprint (a SHA-256 hash, explained below), the path of the preserved original in the vault, and the byte count. The fingerprint column is unique, which is the whole deduplication mechanism.
An episode answers "what happened, and when?". It stores the readable
text (body_original), a kind, a title, the language, and two timestamps:
occurred_at, when the content happened in
your life, and ingested_at, when the companion received it. Search,
chunking, claims and reflection all operate on episodes; they never go back
to the raw file.
Direct file ingestion uses md, pdf, voice, image and video kinds.
Other paths add kinds such as standup and inquiry, and import preserves
the kind stored in the bundle.
Today the relationship is one-to-one, but keeping them separate means one source could later produce several episodes (say, one per journal heading) without changing the model.
Sources and episodes are append-only. Derived conclusions can change status or be superseded as new evidence arrives:
- Episodes are immutable. There is no update endpoint and no update SQL.
- If you edit a note on disk and drop it again, its text hashes differently, so it becomes a brand-new source and episode. The old version stays.
- Beliefs are never deleted by the companion itself. When it changes its mind,
the old belief is marked
supersededand linked to its replacement, so you can see what it used to think and when. - Conversations can be deleted (
ed companion forget), because chat history is your convenience data, not the memory of record.
Append-only is a rule for the companion, not a cage for you. The memory is
yours, so deletion is always yours to order: ed companion erase removes one
episode and everything derived from it, and ed companion wipe empties the
whole store. ed companion export writes everything into a bundle that
ed companion import restores, so leaving, backing up, or moving machines
never needs database surgery.
Why build it this way? Because the companion's job is to remember what you wrote then, not what you later wished you had written. A diary you can silently rewrite is not evidence of anything. Append-only also makes the engineering safer: immutable rows cannot be corrupted by a half-finished update, and "has this episode been indexed?" has a trivially correct answer (does it have chunks yet?) precisely because episode bodies never change.
A hash function takes any input, a byte or a gigabyte, and produces a short fixed-size number, here 256 bits written as 64 hex characters. The same input always produces the same output, and any change to the input, even one character, produces a completely different output. Finding two different inputs with the same output is computationally out of reach, so in practice the hash is a unique fingerprint of the content. The companion hashes the text of Markdown files and the raw bytes of PDF, audio, image and video files, and treats "same fingerprint" as "same memory".
Beside the database sits the vault, a plain directory that keeps the exact original bytes of everything ever ingested. Its layout is derived from the fingerprint, a scheme called content-addressed storage:
/vault/objects/<first two hex chars>/<full sha256>/<original filename>
The two-character prefix just spreads files across subdirectories so no
single directory grows huge. Because the path is the fingerprint, writing is
naturally idempotent: if the path already exists, the content is already
there, byte for byte, and the write is skipped. Nothing in the vault is
overwritten. User-directed erase and wipe operations remove the
corresponding originals.
The vault matters for two reasons. First, honesty: the episode body for a PDF,
voice memo, image or video is an extraction, transcription or caption, in
other words a lossy
copy, and the vault keeps the ground truth it came from. Second, playback:
GET /v1/episodes/{id}/media streams the vault file back out, which is how
the app shows the original PDF, photo or video and plays your actual recording
rather than only showing derived text.
The base backend runs as five containers, and its durable runtime state sits in named volumes on that machine. GPU profiles can add a reranker service and cache volume.
| What | Where | Volume |
|---|---|---|
| Every table (episodes, chunks, beliefs, ...) | Postgres 18 with the pgvector extension | companion-pg |
| Original files | The vault directory | companion-vault |
| The embedding model | Ollama's model cache | companion-ollama |
| The speech-to-text model | whisper.cpp's model dir | companion-whisper |
The client Mac stores the app's endpoint, the CLI's deployment and stack
configuration, a small upload outbox, and any stack secrets saved in Keychain.
The remembered episodes, observations, beliefs and conversations remain on
the deployed backend. A reasoning key set through ed companion reason lives
in the backend settings table, and the server returns only a masked hint.
Two footnotes about the container stack. Redis is present and health-checked but nothing uses it yet; it is capacity for future queues, not a load-bearing part. And there is no SQLite and no scattering of state across files: one server owns all of it, which is what lets the app, the CLI and remote machines all see the same memory through one HTTP API.
Postgres is a battle-tested relational database: data lives in tables with typed columns, and you query it with SQL. pgvector is an extension that adds a vector column type, so a row can carry a list of numbers (an embedding, explained properly in chunks, embeddings and search) and be queried by "which rows are nearest to this vector?". Using one database for both ordinary rows and vector search lets indexing commit all of one episode's chunks in a transaction. If embedding fails, that episode keeps zero chunks and remains pending for a later pass.
- Ingestion: the exact path from a dropped file to an episode, for all five media.
- Chunks, embeddings and search: how text becomes numbers and how nearest-neighbour search works.
- Asking and chatting: how memory turns into grounded answers with citations.
- The learning loop: claims, observations, corroboration and beliefs.
Auto-generated from docs/, edit the docs in the repo, not the wiki.
CLI reference
Companion
- Deploy
- Concepts
- Concepts Memory
- Concepts Ingestion
- Concepts Search
- Concepts Chat
- Concepts Learning
- Concepts Brain
- Concepts Friend
- Hosts
- Stack
- Status
- Doctor
- Search
- Index
- Ingest
- Episodes
- Sync
- Observations
- Reflect
- Beliefs
- Ask
- Extract
- Claims
- Corroborate
- Runs
- Chat
- Conversations
- Forget
- Export
- Import
- Erase
- Wipe
- Episode
- Nightly
- Reason
- Personas
- Council
- Lenses
- Core
- Why
- Hypotheses
- Predictions
- Commitments
- Discrepancies
- Calibration
- Inquire
- Entities
- Eval
- Standup
- Machines
- Baselines
- Connectors
- Facts
- Correct
- Weekly
- Db
Guides