Memory and recall
The agent's memory is two channels with different jobs. History (turns and chunk summaries) keeps everything verbatim and drillable; memory items hold only distilled, stable facts. Recall composes both, plus the space's own content, through one surface.
Memory, history and context
| Layer | What it holds | Lifetime |
|---|---|---|
| Application data | tasks, documents, contacts, and other structured records | stored in the space |
| History | original conversation turns and summaries of older turns | stored on each chat's log object |
| Memory items | selected facts, preferences, decisions, and lessons | stored on the agent's brain object |
| Model context | instructions, selected history and memories, current message, tool results | assembled for a model call |
A stored record becomes model context only when the harness selects it; a fresh run has no Python state and rebuilds its context from these records. The stores and runnable queries are in Agent data.
Memory items
A memory item is a record in the agent_memory_items dataset on the brain object of the bao space. A fact about another space names that space in its context or tags.
Saving requires category (an open slug set such as preference, decision, lesson, or fact) and context (a one-line fact). Optional fields are body, confidence, importance, tags, entities, keywords, edges (typed links to other objects), and validFrom. User-stated facts outrank inferred ones.
Save policy, taught by the _memory skill (the write path enforces the required fields and value ranges):
| Save | Skip |
|---|---|
| stable preferences | greetings, meta-chatter |
| decisions with the why | restatements of what was just said |
| durable domain facts | anything already in history (it is drillable) |
| hard lessons, shipped outcomes | low-confidence speculation |
Budget is ~1–2 saves per turn. Episodes and session summaries never become memory items — that is the history channel's job.
Save a fact from a program
Guest code with the agent overlay configured. space is the space recall searches for context; the item itself always lands on the agent space's brain:
c = use("agent:any@v1")
r = use("agent:recall@v1").recall(c, space)
m = use("agent:memory@v1").memory(c) # no space: the brain is the bao space's
m.save_with_dedup({"category": "decision",
"context": "picked sqlite for the importer: simpler ops",
"confidence": 8}, recall=r)
# → {itemId, action} or {deduplicated: true, mergedInto: "<id>", action: "merge"}
Dedup on save
Every save first runs recall over scope agent, then a cheap classify-tier judge decides same fact? → merge (evolve the existing item), supersede (new item plus a supersedes edge) or create. A {deduplicated: true} reply is a success. Merges are humble: a machine-sourced candidate never overwrites stored text, never lowers confidence, and unions tags and edges. There is no similarity threshold — the judge decides.
Recall: one surface, three axes
recall@v1 is read-only and binds to one space; its memory source is always the bao space's brain:
- Semantic —
r.search(q, scopes=["agent", "history", "basic", "email"])over the search index;r.hydrate(hits)turns hits into full records in one read. - Temporal —
r.by_period(from, to)merges memory items (validFrom), turns (createdAt) and chunks overlapping the range. - Graph —
r.neighbors(id)walks forward relation properties plus the server's backlinks, types and properties named by xKey. - Drill-down — a chunk expands to its children (chunks or raw turns); a turn's
traceRefopens the run.
Every memory item auto-recall injects gets its accessCount bumped, and so does an item a deliberate dig actually uses (m.bump_access), so the store measures its own usefulness.
Auto-recall
Prompt guidance alone does not produce memory behaviour — an agent asked nicely saves and searches only when memory is the topic. So recall is structural: at the start of every turn autorecall@v1 runs recall.search(user_message) (index-backed, no model call; the query embedding comes from the server's configured embedder) and injects the top hits, budget-capped.
The injection is framed as a synthetic run_cell call whose code is the literal recall idiom, plus a digest-shaped result — evidence the model weighs and can discount as stale, not prompt truth. Memory hits render as distilled facts with provenance date and confidence; history hits as "related past discussion, agent_roi_injections.
History: turns and the chunk pyramid
Turns are append-only records on the chat's log child. rollup@v1 (an hourly cron) summarises complete batches of ten turns into level-1 chunks and ten level-1 chunks into a level-2 chunk — level-2 summaries are built from child summaries only.
{"seq": 12, "level": 1, "fromSeq": 40, "toSeq": 49,
"summary": "…", "periodStart": …, "periodEnd": …, "unitsCovered": 10}
Same-level chunks are contiguous and non-overlapping, and every summary keeps explicit pointers to what it covers, so drill-down never dead-ends. The boot window is token-budgeted over this pyramid: newest raw turns, then L1, then L2 — all history present at decreasing resolution, at roughly 11% record overhead.
Background jobs
All maintenance runs as standing triggers on the election-active device, each fire a traced run with an agent_runs summary; when the bird's-eye view is stale, a run says why. A stopped device runs nothing, but the trigger records and everything they produced stay in the space.
| Program | Schedule | State |
|---|---|---|
extraction@v1 |
every 15 minutes, over newly persisted turns | on — proposes only stable-fact shapes, through the dedup judge, provenance fromSeq, confidence capped at 6 |
rollup@v1 |
hourly | on — the chunk pyramid |
linkgen@v1 |
hourly | on — seed-search neighbours, classify-tier proposes typed links from the curated vocabulary (relates_to, caused_by, supersedes, decided_in, part_of, owned_by, discussed_in); never invents edge types |
evolution@v1 |
every 6 hours | ships disabled — refreshes context/tags from linked neighbours |
reflection@v1 |
daily | ships disabled — synthesises never-recalled clusters into insights, flags contradictions |
decay@v1 |
daily | ships disabled — salience halves per idle half-life, never deletes |
Disabled jobs ship their mechanism and activate one at a time behind an eval; scoring fields are recorded before they are consumed.
What leaves the device
Dedup, extraction and summarisation are model calls: whatever provider serves the configured tier reads their inputs. The recall index is a second path — the server's default index.embedder: auto embeds online with a local fallback, local keeps it on the device once the model is present (Embedders).
Why it matters. The memory is a dataset in your encrypted space, not a vendor's RAG store. You can query it (guest code resolves the key
agent_memory_items; raw HTTP uses the discovered<typeId>_agent_memory_itemsstorage collection), subscribe to it, audit what the extractor saved and from which turn, and delete an item with a normal record delete. See Agent data for the fields and Runs and monitoring for a maintenance job's run.