SmartMemory
Guides

Auditable Memory

Auditable Memory

SmartMemory can answer the question every agent eventually gets asked: "how do you know that?"

Three surfaces work together. As-of recall returns what the system believed at a past instant. Explain returns the full provenance of one memory in a single call. Verify proves the history has not been rewritten.

As-of recall

Every search can travel transaction time. Pass as_of_date and results reflect the belief the system held at that instant, not the belief it holds now. Pass include_superseded to keep replaced items visible.

from smartmemory_client import SmartMemoryClient

client = SmartMemoryClient(base_url="...", team_id="...", api_key="...")

# What did we believe on July 1st?
results = client.search(
    "database migration plan",
    as_of_date="2026-07-01T00:00:00+00:00",
)

# Include items that have since been replaced
results = client.search("auth approach", include_superseded=True)

The JavaScript SDK mirrors this with asOfDate and includeSuperseded. The MCP memory_search tool accepts the same parameters, so agents can time travel directly.

An as-of query never widens the tenant boundary. Time travel changes when, never who.

When a result cannot be resolved

Sometimes a memory's version history cannot be resolved back to the date you asked about. The honest answer matters more here than a tidy one, because a result that quietly carries today's content into a historical answer is indistinguishable from a real one.

So every result of an as-of search carries as_of_resolution. resolved means a version current at that date supplied the content. no_chain means the memory was never superseded and already existed then. unresolved means resolution failed and you are looking at present-day content, which you should not read as a past belief. The response also carries an as_of_diagnostics block counting the unresolved results and naming them.

If you would rather have no answer than a partial one, pass as_of_strict and the request fails with 422 instead. That verdict is judged on the results actually returned, so a result that ranking dropped from your page does not fail the request.

Two cases refuse to answer at all rather than label anything. A date that cannot be parsed returns 400, and an unavailable version resolver returns 503. Both previously returned an ordinary present-day search with nothing marked, which looked like a successful answer to a question you did not ask.

Read unresolved_count for what it is. Zero means no returned result was labelled unresolved. It does not certify the whole query, since a result dropped because its historical content could not be reconstructed never reaches the page to be counted.

Explain

explain composes the complete audit answer for one memory:

answer = client.explain("memory_id")

The response carries the item's identity and origin tier, every belief the system held for it over time (each with its transaction-time window, change reason, and evidence), supersession in both directions, the canonical memories it derives from, any decisions that cite it as evidence, and the result of live chain verification.

REST surface: GET /memory/{id}/explain. MCP tool: memory_explain. Both are available on every plan. An agent should always be able to show its work.

A chain_verified of null means there is nothing to verify yet, such as a memory that predates hash chaining or was never superseded. It is not a warning. A false comes with chain_break_at and a reason.

Tamper-evident version chains

Every superseded belief is written as a hash-chained version. Each version records a content hash over its exact stored bytes and a chain hash linking it to its predecessor. Rewriting, reordering, or deleting a covered version breaks the chain, and verification reports where and why.

Chains are per-memory and always on. They work the same in lite mode on SQLite as they do on the full server stack.

Verification recomputes everything from stored rows. It never trusts stored pointers. A chain that began after a legacy history verifies over its hashed suffix only and reports the count of uncovered legacy versions, so a positive result is always an honest one.

Workspace verification (Enterprise)

GET /memory/audit/verify checks a single item's chain, or the whole workspace against the latest checkpoint. A background sweep periodically snapshots every committed chain head into a Merkle tree and stores the root. Verification then answers with exactly one state: verified, stale, mismatch, witness_invalid, manifest_unavailable, or no_digest when no checkpoint exists yet.

Responses carry a trust tier. local_only means the history is consistent with a checkpoint stored in the same database, which an operator could in principle rewrite wholesale. witnessed means the checkpoint root was anchored externally through OpenTimestamps and the proof was validated against a real Bitcoin block at the moment you asked. Only the witnessed tier supports the strong claim.

Validation is deliberately strict. The proof has to commit to the exact checkpoint being verified, and that commitment covers the workspace the checkpoint belongs to, so a genuine proof cannot be swapped in from another document or lifted from another workspace. The Bitcoin block header behind the attestation has to hash to the block hash its source reported, carry valid proof of work, clear a difficulty floor, and be agreed on by two independent sources.

Be precise about what those checks buy. They put the cost of a forged header at days of the entire Bitcoin network, and they stop a single source from voting twice. They do not prove the header belongs to the real Bitcoin chain, because an isolated header carries no evidence of its own height or ancestry. Point the verifier at your own node with AUDIT_WITNESS_BITCOIN_RPC_URL and that gap closes. Rely on the default public explorers and you are trusting them to report the canonical chain, with the requirement that two of them agree before an answer is accepted.

Alongside the tier, responses carry a witness_validation block holding the verdict, a machine-readable reason, the block height, and the block time. Read the anchor time from there. The anchored_at value stored on the checkpoint is only the moment a background job noticed the confirmation, written by the same system that stores the history, so it is a convenience field rather than evidence. Treat the block time as approximate. A miner picks it and consensus constrains it only loosely, so it is a coarse bound meaning the root existed by around then. It never shows the root existed any earlier.

When validation cannot run at all, because no header source is reachable, the answer stays local_only and says why. An outage is never reported as tampering.

Operator switches (sweep and witness submission are off by default):

AUDIT_DIGEST_ENABLED=true       # checkpoint sweep
AUDIT_WITNESS_ENABLED=true      # external witness submission + confirmation

# Optional. Verification falls back to public explorers when these are unset.
AUDIT_WITNESS_BITCOIN_RPC_URL=  # your own bitcoind, sufficient on its own
AUDIT_WITNESS_HEADER_SOURCES=   # comma separated explorer APIs, two must agree
AUDIT_WITNESS_HEADER_TIMEOUT=5  # seconds per request

What this proves, and what it does not

The chain proves history integrity over its hashed coverage. Any rewrite of a covered version is detectable. It is not per-transaction author signing, it does not cover versions written before chaining began, and a private chain alone cannot defend against an operator who recomputes everything from scratch. That last case is what the external witness addresses: a recomputed history can no longer match a root that was anchored publicly before the rewrite.

One hole the witness does not close is rollback. An operator can restore the whole database to an older checkpoint that was genuinely witnessed, delete every later checkpoint, and verification will answer verified and witnessed, because every artifact involved is authentic and merely old. Staleness does not catch it either, since the timestamp the staleness check reads is stored in the same database. Closing that requires remembering the newest root somewhere the operator does not control, which is why a client that pins the last root and height it saw is meaningfully stronger than one that does not.

Memory stays fully editable. This is a store that is still allowed to learn. Beliefs update, supersede, and consolidate freely. The chain seals the logbook, not the mind.

On this page