> ## Documentation Index
> Fetch the complete documentation index at: https://checkfu.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Memory

> Give agents durable, versioned, governed knowledge that outlives a Session.

A **MemoryStore** is durable knowledge an agent can read and write across Sessions. It is a versioned file tree with an ownership boundary, an approval path, and an audit trail, not a vector index bolted onto a chat log.

## Two surfaces on one store

This is the first thing to get straight, because the same store ID serves both:

|             | Document surface           | Versioned surface                                                                   |
| ----------- | -------------------------- | ----------------------------------------------------------------------------------- |
| Endpoints   | `/documents`, `/document`  | revisions, changes, diff, export, fork, search, proposals, working-refs, redactions |
| Model       | Path-addressed, no history | Immutable revisions and named refs                                                  |
| Concurrency | `If-Match` on content hash | `expected_base_revision`                                                            |

Document Memory has `alpha` / `hosted` source posture; confirm its executable state in the live [capability status](/getting-started/status). The versioned surface has a qualified durable Postgres adapter for production and local development, while the in-memory adapter remains its test and conformance oracle. Hosted alpha still leaves `MEMORY_VERSIONED_SURFACE` disabled until an operator-approved target completes the D7 enablement sequence; versioned operations and Dream creation currently fail closed there with `503 memory.backend_unavailable`. Existing Dream metadata remains readable for recovery and audit. The distinction matters because the surfaces answer different questions: reach for the document surface when you just need a file at a path, and the versioned surface when you need history, review, or a per-Session branch.

## The model

<CardGroup cols={2}>
  <Card title="Store" icon="database">
    One ownership, history, and visibility boundary. Its owner decides lifecycle and the default PermissionAssignments minted at creation, never who may access it. Access is always PermissionAssignments.
  </Card>

  <Card title="Document" icon="file">
    Content at a normalized absolute path. Capped at 100 KB each.
  </Card>

  <Card title="Revision" icon="camera">
    An immutable snapshot of the store's **whole tree**, not one document's version. Content-hashed so every storage backend produces byte-identical values.
  </Card>

  <Card title="Ref" icon="bookmark">
    A named pointer at an exact revision. Refs are the only mutable part; they advance by compare-and-swap, never last-write-wins.
  </Card>
</CardGroup>

`canonical` is the shared truth. A Session gets its own **working ref** so it can write without disturbing anyone else, and a **checkpoint** seals that Session's accumulated changes at a turn boundary: at most one revision per changed store per turn. A turn that changed nothing produces nothing.

Ownership is one of `workspace`, `principal`, `agent_definition`, `session`, or `surface_scope`. At creation, every owner kind except `workspace` mints one PermissionAssignment giving the owner read and write. A Workspace-owned store mints none, so access has to be granted deliberately.

`POST /v1/memory-stores/{id}/fork` creates a new store from one exact source revision. You choose the target name, owner, and optional description; Checkfu copies that immutable tree, proves the target canonical `tree_hash` matches, and then the two stores evolve independently. The endpoint requires an `Idempotency-Key`: an exact retry returns the same fork, while reusing the key with different input is a conflict. Forking retained content is unavailable in a zero-data-retention Workspace and remains behind the same versioned-surface rollout fence.

### Portable exports

Two export routes exist, and they are different shapes rather than two sizes of the same one.
`GET /v1/memory-stores/{id}/export` returns the whole store as a single complete aggregate and
is kept unchanged for the dated v1 response, so a large store arrives as one unbounded body.
`GET /v1/memory-stores/{id}/export-pages` is the additive bounded route and the one to reach
for: a normalized, resumable graph stream of `revision`, `path_fence`, `document_version`,
`revision_entry`, and `ref` items in canonical order. Read a page, then pass its `next_page`
value back as the `page` query parameter until `next_page` is null. That cursor is
authenticated and opaque, and it pins every page to one Workspace, source store, container
generation, scope, retention instant, and the earliest included body expiry. A concurrent
mutation or an elapsed expiry returns `memory.conflict` instead of mixing snapshots or
extending custody. A page carries at most 200 items (ask for fewer with `limit`) and 1 MiB of
canonical item payload bytes; the manifest and response envelope sit outside that byte count,
and an importer should recompute both the count and the canonical ordering rather than trust
the page's declaration.

Structural history survives erasure: every revision/path pair remains a `revision_entry`,
while its referenced document version carries either an available body with content and its
immutable expiry ceiling or an explicit erased body. Erasure replaces raw provenance with its
structural digest, retaining tree hashes and document counts without containing or
reconstructing shredded bytes or source identifiers.

## How canonical actually moves

Canonical advances differently depending on who owns the store, and that is what keeps shared memory safe:

* A **personal** store (principal- or session-owned) advances canonical directly. When such a store is mounted `read_write`, the agent's file changes seal into its durable documents at turn settlement (see [Writeback at settlement](#writeback-at-settlement)).
* A **shared** store (environment-, agent-definition-, or surface-scope-owned) advances canonical **only through an approved Proposal**. A direct canonical write returns `403 memory.read_only`, and a shared store never opens a `read_write` mount in the first place: the clamp is structural, so a stray `write` PermissionAssignment cannot turn a sandbox into a live writer. There is therefore no shared writeback to defer. A settlement capture naming a shared store matches no admitted writable mount and is refused outright, mutating nothing and producing no skip event.

An agent may only write its own Session's working ref. Writing another Session's ref is refused.

## Writeback at settlement

When a personal store is mounted `read_write`, the agent works its memory as an on-disk file tree, and at turn settlement Checkfu seals what changed: the runner diffs its live sandbox tree against the frozen staged manifest and the Session seals a bounded, idempotent pass into the store's **durable document surface** (`putDocument` / `removeDocument`, content-addressed per document). One `memory.change_checkpointed` records the changed paths and the `base`/`new` tree digests; one `memory.accessed` records the read. The seal fails closed: on any write error, an unreadable store, or ZDR denial it writes nothing partial. It is idempotent on replay. Because a personal store advances directly, a second Session the same principal opens reads back what the first one wrote. (The versioned revision graph of revisions, diffs, and working refs remains locally implemented but `preview` / `rollout_fenced` behind `MEMORY_VERSIONED_SURFACE`; the settlement seal advances the independently modeled durable document surface, so the digests above are content addresses, not graph revision IDs. See [capability status](/getting-started/status).)

Writeback needs the execution provider to enumerate the mounted tree, because a delete is inferred from a path that is no longer there — a partial listing would read as deletions. Checkfu therefore enumerates through a **platform-owned** reader, never a command run inside the sandbox image, whose binaries the agent controls. A provider that cannot enumerate does not silently drop the edits: a writable memory mount is refused at admission with `runtime.harness_incompatible`, so the Run fails visibly rather than settling a wrong revision. A `read_only` memory mount needs no enumeration and is unaffected.

The seal is also **single-store**: one turn may checkpoint at most one MemoryStore. A store's aggregate, its whole document tree, and its replay receipt commit in one durable transaction, and there is no cross-store coordinator that could make two independently owned stores one transaction. So a capture naming two stores is refused before any mutation rather than committing a prefix, and a terminal turn that changed two writable stores cannot settle. Admit one writeback-capable store per Run and mount the rest `read_only`.

### Proposals

A Proposal freezes its `base_revision`, `head_revision`, and diff hash at creation. Approving it compare-and-swaps canonical **from that frozen base**, so a proposal can never silently rebase onto work it was not reviewed against.

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
%%{init: {"theme":"neutral","themeVariables":{"fontFamily":"ui-sans-serif, system-ui, sans-serif","primaryColor":"#F5F5F4","primaryBorderColor":"#A8A29E","primaryTextColor":"#282828","lineColor":"#78716C","secondaryColor":"#FAFAF9","tertiaryColor":"#FFFFFF"}}}%%
stateDiagram-v2
    [*] --> open
    open --> approved: canonical advanced from the frozen base
    open --> rejected: reviewer declined
    open --> superseded: base moved before approval
    approved --> [*]
    rejected --> [*]
    superseded --> [*]
```

You respond with `approve` or `reject`. `superseded` is not something a reviewer chooses: it is what an approval becomes when canonical moved on beneath it. Handle it as "re-propose against the new base," not as a rejection.

The stored `status` values (`open`, `approved`, `rejected`, `superseded`) differ from the imperative verbs you send (`approve`, `reject`). An empty proposal is unrepresentable: base and head must differ.

## Mounting memory into a Session

A Session declares its Memory mounts at creation, requesting `read_only` or `read_write` plus optional agent-facing instructions. Admission then clamps the request against the acting Principal's PermissionAssignments:

| Requested    | PermissionAssignment | Effective                                                  |
| ------------ | -------------------- | ---------------------------------------------------------- |
| either       | no read              | **denied**: the mount fails and the Session does not start |
| `read_write` | read + write         | `read_write`                                               |
| `read_write` | read only            | `read_only`: degraded, but visibly                         |
| `read_only`  | read                 | `read_only`                                                |

Two structural write caps then apply on top of the PermissionAssignment clamp:

* A shared-owner store (Workspace, AgentDefinition, or SurfaceScope) never resolves to `read_write`, however the PermissionAssignments read. Shared canonical state advances through governed proposals, not a live sandbox writer.
* A principal-owned personal store is `read_only` if explicitly mounted into a shared public/private place. A personal/DM place is not shared, so the store remains `read_write`-eligible there.

Composition is stricter than that second mount clamp: the standard shared-place mount set excludes personal memory entirely from public and private places. It is included only for a personal/DM place or a direct non-place Session. These owner policy, place policy, and PermissionAssignment checks are independent; a mis-minted `write` PermissionAssignment cannot bypass any of them.

The degradation is recorded, not silent: `memory.mount_resolved` carries both the `requested` and the `effective` access, and each Run repeats the effective access in `session.mounts_resolved`. An archived store stays readable through an existing read-only mount but rejects a read-write plan.

## Retrieval

Search is lexical, deterministic, and revision-pinned. Omitting `revision` resolves canonical **once** and then searches that exact revision, never a moving target, so results within one search are internally consistent.

Reads are recorded as `memory.accessed`, batched once per store per turn. Under zero-data-retention the record downgrades from listing document paths to an aggregate count before it is persisted.

<Note>
  Memory events carry IDs, paths, hashes, and counts, never document content. The event log tells you what was touched, not what it said.
</Note>

## Erasure

`POST /v1/memory-stores/{id}/redactions` takes a list of paths and erases them across every surface the platform owns for that store, including the search index. It reports counts only (revisions, paths, and hashes affected) and never returns erased content. Redaction requires an administrator key and is itself recorded as a revision, so erasure appears in history rather than quietly rewriting it.

Deleting a store fences writers first, erases its complete versioned container and projections, and only then records the terminal tombstone. A delayed fork or change-set retry cannot recreate the store or replay paths from a stale receipt.

## Curated memory (Dreams)

A **Dream** is a batch job that distills what agents learned across many Sessions into a clean MemoryStore. One input store and 1–100 Sessions go in; one **new** Workspace-owned output store comes out. The inputs are never mutated: curation is copy-on-write.

<Note>
  Dreams are `preview` / `rollout_fenced`; see [capability status](/getting-started/status). The example below runs against local development with Postgres and `MEMORY_VERSIONED_SURFACE=true`; the in-memory adapter is only the test and conformance oracle. Hosted requests still fail closed until an operator-approved rollout enables the versioned Memory prerequisite.
</Note>

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --request POST http://localhost:8787/v1/dreams \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-29" \
    --header "Content-Type: application/json" \
    --data '{
      "memory_store": "memstore_0123456789abcdef0123456789abcdef",
      "session_ids": ["sess_1111…", "sess_2222…"],
      "instructions": "Merge duplicate facts; keep the most recent policy numbers.",
      "review": true
    }'
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  // Dreams are local-development only today, so point the client at your worker.
  const checkfu = new Checkfu({ baseUrl: "http://localhost:8787" })

  const dream = await checkfu.dreams.create({
    memory_store: "memstore_0123456789abcdef0123456789abcdef",
    session_ids: ["sess_1111…", "sess_2222…"],
    instructions: "Merge duplicate facts; keep the most recent policy numbers.",
    review: true,
  })
  ```
</CodeGroup>

The job seeds the output store from the input's current documents, then runs one curation pass over the selected Sessions' event logs and lands the result as a [proposal](#proposals):

* Omit `model_routing_profile_key` and curation is a deterministic dedupe pass: no inference, zero usage.
* Supply `model_routing_profile_key` and one schema-constrained model call, steered by `instructions`, does the curation.
* `review: true` leaves the proposal `open` for a human decision instead of auto-approving it (safe to auto-approve by default, because the fresh output store has no other writers).

`status` moves `pending → running → completed` (or `failed`/`canceled`), and a completed Dream names its `output_memory_store`. The create call returns as soon as the Dream is `pending`: execution runs on the platform's own work queue, so you poll (or cancel) a Dream that is genuinely still going rather than one that already finished inside your request. Cancel therefore lands mid-pipeline on a `pending` or `running` Dream, and a Dream can never strand `pending` with nothing to execute it — a failed enqueue settles the row `failed` at create time. To run curation on a schedule, point a schedule-triggered [Automation](/concepts/automations) at a `dream` target; each firing curates only the Sessions it has not seen before.

<Warning>
  Dreams run on the rollout-fenced versioned Memory surface. A zero-data-retention Workspace cannot dream: creation is rejected with `policy.denied`. A deterministic Dream makes no model call and reports zero usage; a model-curated Dream records its exact provider token usage on the Dream, including when curation fails after the tokens were spent.
</Warning>

## One trap worth knowing

Memory uses **three different concurrency mechanisms**, and picking the wrong one is the most common integration mistake:

| Operation                   | Precondition              | Carried as                                                                 |
| --------------------------- | ------------------------- | -------------------------------------------------------------------------- |
| Put or delete a document    | Expected content hash     | `If-Match` header                                                          |
| Patch or archive the store  | Expected resource version | `expected_version` in the body                                             |
| Apply changes or checkpoint | Expected base revision    | `expected_base_revision` in the body, plus per-operation `expected_sha256` |

All three fail with `409 memory.conflict` when the precondition misses. None of them fall back to last-write-wins.

## Next steps

<CardGroup cols={2}>
  <Card title="Sessions and runs" icon="timeline" href="/concepts/sessions-and-runs">
    How mounts resolve at admission and per Run.
  </Card>

  <Card title="Tenancy and governance" icon="shield" href="/concepts/tenancy-and-governance">
    The PermissionAssignments that decide what a mount is clamped to.
  </Card>
</CardGroup>
