History and provenance
Every write to memory makes a new version; nothing is edited in place. Versions are kept in an append-only mirror, so a fact's history survives background merges. Each row records who wrote it and how it entered memory, derived rows record their sources, and a person can attest that a row is true. How the version mirror works is in memory architecture.

Operations
| Operation | Role | Answers |
|---|---|---|
memory.history |
viewer | Every version of a key, newest first, including tombstoned and redacted versions. |
memory.provenance |
viewer | Where the live row came from: source trace, writer, source class, the conversation it was extracted from. |
memory.evidence |
viewer | The source turns a fact was extracted from, with the supporting span and how each link was made. |
memory.backlinks |
viewer | What points at a row: versions, digests, typed links in and out, evidence count, entity neighbours. |
memory.lineage |
viewer | The promotion trail of a shared memory: who promoted it, from which rows, through which review. |
memory.verify |
writer | Attest that a row is true as the signed-in person or service account, or revoke the attestation. |
memory.redact |
admin | Scrub the value of one historical version, keeping the row and its audit entry. |
namespace.rollback |
admin | Return every memory head in a namespace to its version at a point in time. |
namespace.snapshot |
admin | Export a namespace's curated state as a JSON payload. |
namespace.restore |
viewer | Import a snapshot into a fresh namespace, or an existing one the caller administers; never a collection. |
namespace.snapshot and namespace.restore are not MCP tools. Field lists are in the
operations reference under memory and
namespace.
Versions: memory.history
loom.memory holds current state and is read with FINAL, so a merge keeps only the
newest version of each row. loom.memory_versions is an append-only mirror filled by a
materialized view on every insert; it is the history that memory.history,
memory.redact and namespace.rollback read.
A history row carries version (the write's nanosecond epoch), written_at, value,
confidence, pinned, expires_at, tombstoned_at, redacted_at,
redaction_reason, content_sha256, agent, actor and source_class. created_at
is the row's first write and repeats on every version. agent="*" spans every writer of
the key.
from loom import Agent
async with Agent(org="acme", namespace="support", agent="auditor") as agent:
for v in await agent.memory.history("customer/4812/plan", agent_id="*"):
print(v["written_at"], v["actor"], v["source_class"], v["value"])
A tombstone is a version too, so a delete shows in history and memory.restore can
revive the key within 14 days. For point-in-time reads of current state, use as_of on
memory.get, memory.search and memory.ask
(validity time).
Who wrote it: actor and source class
The server stamps actor (human:<subject> or process:<subject>) and source_class
on every row from the token and the operation. The source_class values are in
memory concepts. A request may only downgrade its class to
external, imported or derived.
memory.provenance returns both, plus:
source_traceand asource_kindderived from it (conversation,extracted,observed,consolidated,app,manual,other);- the
source_resource_idthe writer cited; - the conversation
thread_idandthread_exists(false when the namespace does not record transcripts, or the transcript is gone); - the authored links (
memory.link) touching the row.
Search hits carry source_class and verified, which the optional trust term
(LOOM_MEMORY_SEARCH_TRUST_WEIGHT) reads.
Evidence: which turns a fact came from
Extraction links each fact to its turns in loom.memory_belief_sources.
memory.evidence(memory_key) returns those links with the turn's text (up to 600
characters), role, time, the supporting span, and how the link was made:
scope |
support_type |
Meaning |
|---|---|---|
cited |
extracted_from |
The extractor named the supporting turn. |
lexical |
lexical_support |
The server matched the fact to its best turn. |
window |
window |
Nothing matched; the whole extraction window is linked. |
derived |
derived_from |
Consolidation lineage: the item is the source memory's text. |
The response counts links by scope, so a check can tell a cited fact from a loosely tied
one. For a conversation's exact wording, memory.recall searches the stored turns
(extraction).
Lineage
memory.consolidateand period digests write onederived_fromlink per source intoloom.memory_belief_sources;memory.evidencereturns them asderiveditems.- A session digest lists the keys it compressed (
derived_fromon asummaryhit). memory.lineage(scope="shared") walks a shared memory's promotion: the event, who promoted it, the supporting rows, the learnings candidate it came from when one matches (match_confidenceexactorinferred), and the audit events.mechanismisreview,import,autoorunknown.memory.backlinksis the inverse: what points at a row.
memory.delete retires a consolidated summary whose sources are all gone and downgrades
one with surviving sources to conflicted.
Attestation: memory.verify
pinned and confidence do not say who checked a fact. memory.verify records that: the
actor from the caller's token and the content hash they attested. Address the row by
memory_id, or by namespace and memory_key with the writer identity. It needs writer
on the row's namespace.
memory.get and memory.get_by_id return verification, derived from the stored hash
and the row's current one:
verification |
Meaning |
|---|---|
human |
A person attested and the content has not changed since. |
process |
A service account attested and the content has not changed since. |
stale |
Someone attested, and the content has changed since. |
none |
Never attested, or revoked. |
- An edit does not clear an attestation; it makes it
staleuntil someone re-verifies. revoke=trueclears it. Re-verifying an unchanged row by the same actor is a no-op (changed=false).- The
human:prefix comes from the token, so an agent cannot mark its own write human. The body'sactoris read only in dev mode without auth. - Snapshots and restores carry the attestation columns; a restore never strips
verified_by.
Scrubbing history: memory.redact
memory.redact(memory_id, version) scrubs one historical version's text everywhere it is
copied, and every profile version a model built from the memory. It stamps redacted_at
and redaction_reason, and keeps the rows and their audit entries. Use it for a secret or
personal detail that leaked into an earlier version.
| Where | What happens |
|---|---|
loom.memory, loom.memory_versions |
value becomes "[REDACTED]"; value_json, evidence, entities and concepts are cleared. |
loom.memory_embeddings, loom.memory_chunks |
Rows whose excerpt or chunk hash is not the live head's are deleted; the live head's rows stay. |
| The user's profile versions built from the memory | Every version whose trace names the memory is scrubbed the same way, the live head included, with its key's derived rows. |
Out of scope: tags, files, content_sha256, loom.memory_evidence (owned by erasure),
the lineage tables (ids and hashes only), loom.shared and the entity graph.
- It refuses the memory's live head (
refused_head=true): delete or rewrite the key first. A scrubbed live profile is left out of renders, packs and profile search until the worker rebuilds it. - Both lock modes refuse it, on the memory's own key and on any profile key it reaches.
- It deletes cached
memory.askandcode.askanswers and precedents that quoted the key (cache_rows_deleted,precedent_rows_deleted). For a profile key it reaches, the deletion is org-wide: every user's profile has the same key, so it clears every cached answer and precedent in the org whose window held that key, in every namespace. - The namespace is checked after the fetch; a refusal answers
redacted=false, not 403. - A version that does not exist answers
redacted=falseand scrubs nothing, no profile version included. - It runs a synchronous
ALTER TABLE … UPDATEon the two memory tables and a lightweightDELETEon derived rows: an administrative call.
The profile reach and its limits are in security. To remove a person's data everywhere, use erasure.
Rolling a namespace back: namespace.rollback
namespace.rollback(as_of) re-inserts, for each key, the highest version at or before
as_of as the new head, and tombstones keys created after it. The rollback is itself new
versions, so it can be undone the same way.
- Dry run by default, reporting
reinstate,removeandsample;dry_run=falseapplies it. memory_keys(up to 500) restricts it;max_keys(default 5,000) bounds one call andtruncatedsays to call again.- The
embedding_workerre-embeds reinstated heads. - An
as_ofolder than the namespace's history, or in the future, is refused.
curl -sX POST http://localhost:7777/v1/namespace.rollback \
-H "Authorization: Bearer $LOOM_API_KEY" -H 'Content-Type: application/json' \
-d '{"org":"dev","namespace":"local","as_of":"<YYYY-MM-DD>T09:00:00Z"}' \
| jq '{reinstate, remove, truncated, sample}'
Snapshots: namespace.snapshot and namespace.restore
namespace.snapshot exports a namespace's curated state as JSON: every memory row with
version, content_sha256, actor, source_class, validity and attestation columns,
the per-type configuration, the grants, and up to include_audit_tail_n recent audit
rows (default 1,000). include_embeddings is false by default, so the target re-embeds
under its own model.
namespace.restore(target_namespace, payload) writes the payload back as version rows.
It is idempotent on snapshot_id (a rerun answers restored=false). It restores into a
fresh namespace (the caller becomes admin; refused for a name with no row that already
holds data or grants, unless the caller already administers it) or one the caller administers;
rebind_agent reassigns every row's writer. Restored rows are imported.
A snapshot is not a deployment backup; see backup and recovery.
The audit log
Every write that creates or changes memory leaves one hash-only row in loom.audit_log
(key, id, content hash, actor, source class, verdict; never the value). Hourly
checkpoints chain the log. See audit.
Limits
- Redaction of a merged-away version is best effort in
loom.memory; the mirror is scrubbed either way. - History is only as complete as the mirror. A column added to
loom.memorymust be added toloom.memory_versionsand its view in the same migration. - Validity is valid-time only.
knowledge_timereplayscreated_atagainst the retirement (invalid_at) and deletion (tombstoned_at) stamps; there is no separate transaction-time table. thread_exists=falseis not an error.- A rollback does not restore hard-deleted keys (
skipped_no_head).