Schemas
A schema types a scope. It declares entity types, their attributes, each attribute’s value type (string, text,
enum, number, money, date, timestamp, bool, or ref to another entity) and cardinality (one, the
default, where a newer value replaces the older; or many, where values accumulate).
- A
refmay be markedpart_of(a meeting is part of its deal): passage search treats passages about the part as about the whole. - A
manyattribute may be marked"list": truewhen its values are held at once (the tools a customer uses). guidancelines tell agents how the domain’s questions map onto the types; they appear inquery_memory’s description.metricsname storedcountqueries an agent can run by name.- Three ship built in:
sales,engineering,corpus(schemas/). Any JSON file in the same shape works.
ref) apply in place. A change that would read stored facts differently
is breaking: on main it needs a higher version and --breaking.
Writes to a typed scope are checked: the entity id names its type (deal:42), the attribute is declared (unless
the schema’s mode is open), and the value parses ($120,000 is stored as 120000).
Extraction
A model reads episodes into facts. Which episodes:- What the schema’s
extractsection names (goal-oriented extraction). Each episode is read with the schema’s types and its episode type’s catalog; each fact cites the unit it comes from. - Starter documents (a repo’s
CLAUDE.mdand rules), always. - Everything else, goal-blind, only where turned on:
set_settings {extract: true}for an org or team, orAMPUP_MEMORY_EXTRACT_GOAL_BLIND=1for the whole server. Onserve --localuse the variable.
sourcestakesprefixes, entity types the episodes areabout, andfrom/totimes."sources": {}is every episode.facts_per_episodecaps one episode’s facts."outcomes": true(or a list of source prefixes) also asks for what the text reports happened, dated."style": "statements"writes each fact as one self-contained sentence under one subject, its predicate a kind (decision,commitment,plan,fix, …).- Each episode is read once per schema version.
ampup-memory backfill <scope>reads the history with the schema as it stands (--dry-runestimates tokens and dollars withAMPUP_MEMORY_EXTRACT_PRICE;--max-episodes,--max-usd). ampup-memory extract [scope] [--limit N] [--dry-run] [--branch b] [--local DIR]runs once.--dry-runprints what it would add and end, one JSON line per episode: audit it before turning extraction on over real data.
/metrics
(memstore_extraction_refused_facts_total{reason}) and in GET /doctor.
Entity names are normalised: BillingExport, billing-export and the billing export are all billing export.
Typed queries
Queries are JSON, the same overPOST /query, MCP query_memory and ampup-memory query <scope> <user> <json>:
find:entities(the default),facts,count(withgroup_byandmetricssuch as{"sum": "amount"}),changes(what started or ended in a window),passages, orschema.whereclauses all hold:{"attr", <op>: v}witheq ne in gt gte lt lte between exists missing contains older_than within;{"rel", "to": query};{"rel_from", "from": query}(two hops at most);{"any": [...]};{"not": clause}.id,match({"name": "acme"}resolves names and aliases),text,select(owner.namefollows a reference),order,include: ["evidence"],limit(at most 500) andcursor.{"metric": name}runs one of the schema’s named metrics.time:now(the default),all,{"as_of": T}or{"between": [T1, T2]}.- A cursor reads the memory its first page read (branch heads, time, index versions), so pages never repeat or skip a row when writes land between them.
- A mistake comes back as a reason:
type deal has no attribute "colour" (attributes: …).
Passages
- Bodies are cut into chunks of about 1,500 bytes (
AMPUP_MEMORY_CHUNK_BYTES,AMPUP_MEMORY_CHUNK_OVERLAP150) at paragraph or sentence boundaries. - Each episode ranks by its best chunk, so one long transcript can’t fill the page. A page spreads over kinds of
source (
"diverse": falseturns that off). Newer sources win near-ties ("recency": falseturns that off). {"find": "passages", "id": [...]}reads up to ten passages or episodes by id.group_bya type rolls hits up to the entities they are about.
How fact search ranks
- Keyword: BM25F over the fact’s value (weight 1), subject (0.6), quote (0.5), predicate and date (0.2).
- Meaning: cosine similarity of embeddings.
- The two are min-max normalised and combined 0.6 keyword / 0.4 meaning (reciprocal-rank fusion when a list has fewer than 3 hits). Age doesn’t scale scores.
- Time: a query that names a time (
Q3 2026,last year,2026-03-02) also takes facts stated in that window as candidates and ranks them first. - A search takes the best 300 rows of each list, so its first hits are the same at every
k. AMPUP_MEMORY_RANK_<knob>overrides each weight for experiments.
Supersession, closures and stale values
-
Under
nowandas_of T, a one-valued attribute’s current version is the newest one the reader can see. This is decided per reader: a newer version from an episode they can’t see doesn’t hide the older one. - A closure ends a value with no successor; it applies only to readers who can see its evidence.
-
A schema can say where a current value may no longer be true:
With
freshness: trueon an org or team (orAMPUP_MEMORY_FRESHNESS=1), reads mark a value a newer fact restates (later on <date>: "…") and a plan whose date passed while still pending.AMPUP_MEMORY_FRESHNESS_MARKS(later,overdue,stale) picks the kinds. Marks are derived at read time; nothing is written. Off by default.
Aggregation and briefs
extract.aggregates summarises extracted facts per subject: each element scored (none, identified, partial,
confirmed) with the facts behind it, items given a status (open, done, …), stale facts and passed plans
flagged.
- A subject is summarised again after
everynew episodes about it, or aftermax_wait_minutes.servechecks hourly (AMPUP_MEMORY_AGGREGATE_SECS).ampup-memory aggregate [scope] [--now] [--subject s] [--dry-run]runs it now. - One model call per batch (
AMPUP_MEMORY_AGGREGATE_MODEL, else the extraction model); every answer cites fact ids. - A row is readable by whoever may read every episode of its batch.
get_brief,POST /brief {subject, refresh?}andampup-memory brief <scope> <user> <subject>read where a subject stands in one answer of at most 4,000 tokens.
Dreaming (opt-in)
A dream pass catches what write-time reconcile misses: an older value backfilled after a newer one, the same attribute under two predicate names, two facts saying the same thing, one thing under several names.- It writes only new rows: closures, links between duplicate facts,
_aliasfacts. Nothing is deleted. - It runs per shared audience (
*,group:,role:), widest first, so the model never sees another audience’s facts. A person’s own memory isn’t dreamed over. - Code checks every decision: a list-valued or role predicate is never ended, a record of an event is never ended.
- It runs only on scopes
AMPUP_MEMORY_DREAM_ONnames, once a day (AMPUP_MEMORY_DREAM_SECS) when at leastAMPUP_MEMORY_DREAM_FACTS(200) new facts wait, withinAMPUP_MEMORY_DREAM_CALLS(100) model calls. ampup-memory dream [scope] [--dry-run] [--calls N] [--local DIR]runs a pass now.- Undo:
ampup-memory retract-facts <scope> --extractor dream:<model>@s2takes back everything a dreamer wrote.
Indexes
Each scope has a derived index atAMPUP_MEMORY_INDEX_URI (a path, gs://… or s3://…; default
~/.cache/memstore/index, or <dir>/index under serve --local). It holds the open facts, the closed (superseded)
facts and the passages, as Lance datasets. Rows are trusted only for data files still in the table’s current
snapshot, so a deleted row is never served.
Embeddings
Unset:
openai with AMPUP_MEMORY_EMBED_KEY/_KEY_FILE, else openrouter with OPENROUTER_API_KEY, else openai
with OPENAI_API_KEY, else local. cargo run and cargo test pin local (.cargo/config.toml).
AMPUP_MEMORY_EMBED_MODEL and AMPUP_MEMORY_EMBED_DIMS override the model. ampup-memory embed <text> prints which backend
is active.
AMPUP_MEMORY_EMBED_CACHE (a directory, gs://, s3://, file:// or off) keeps every vector by the SHA-256 of
its text, so the same text is embedded once wherever it is indexed. serve --local keeps it in
<dir>/embeddings; other processes keep none unless it is set.
Loading a corpus
ampup-memory load <dir> loads episodes*.jsonl, facts*.jsonl and closures*.jsonl (facts stand in for
extraction). It is idempotent: an episode or fact already committed is skipped. --workspace, --branch, --fresh
and --local apply. ampup-memory gen <seed> <episodes> <dir> writes a synthetic corpus with gold questions, and
ampup-memory eval <dir> answers them.