# Persistence adapters (/brain/docs/adapters/persistence)

Persistence adapters implement two small Go interfaces in `internal/persistence`: `Objects` for immutable checkpoint blocks and manifests, and `Metadata` for optional cross-instance session discovery (see [Go interfaces](/brain/docs/adapters/interfaces)).

## Authoritative local database [#authoritative-local-database]

`store.Metadata` is separate from the eventual `persistence.Metadata` index. Its
`Create`, `Get`, `Save`, `List`, `Scan`, and `Import` operations must preserve durable
writes and tenant ownership. `List` and host-only `Scan` page by ascending ID.
`Save` cannot create a missing record; `Import` cannot transfer a session to another
tenant. All operations honor context cancellation.

`store.SQLite` is the built-in implementation. `store.New(root, metadata)` combines
an adapter with local session files and takes ownership of its lifecycle. It
prepares native directories before creating metadata; a database failure propagates
to the caller. Run `internal/store/metadata_test.go` against each implementation,
plus the restart/recovery journeys. A remote SQL database alone does not provide
session placement, distributed ownership, or remote native files.

## Built-in implementations [#built-in-implementations]

| Interface | Purpose | Built-in implementations |
| --- | --- | --- |
| `persistence.Objects` | Stores 64 KiB immutable file blocks, bounded tail objects, checkpoint manifests, and per-session `latest` pointers. | `FileObjects` (`filesystem`), `S3Objects` (`s3` and `r2` in `internal/persistence/objects.go`), `GCSObjects` (`gcs` in `internal/persistence/gcs.go`). |
| `persistence.Metadata` | Maintains an eventually consistent tenant-scoped index of sessions and their latest checkpoint IDs. | `Postgres` (`postgres_env` in `internal/persistence/metadata.go`). |

## `Objects` interface [#objects-interface]

`Objects` provides `Put(ctx, key, bytes, immutable)` and `Get(ctx, key)`:

- **Immutable writes:** When `immutable` is `true`, `Put` must never overwrite an existing object with different content (`FileObjects` uses hard links; `S3Objects` sends `If-None-Match: "*"`; `GCSObjects` sets `DoesNotExist: true`). If the key already exists from an earlier upload attempt, returning `nil` is safe because block and manifest keys are generation- and checkpoint-scoped.
- **Error distinction:** Missing objects on `Get` must wrap `os.ErrNotExist` so `persistence.Archive` can distinguish missing checkpoints (`404 not_found`) from transient network or storage failures (`500 brain_error`).
- **Scoping and bounds:** Implementations must validate keys with `validKey` (rejecting empty keys, leading slashes, backslashes, or path traversal), prepend the configured `prefix`, and cap individual object sizes at `2 MiB` (`MaxObjectBytes = 2 << 20`).

### How `Archive` publishes and restores checkpoints [#how-archive-publishes-and-restores-checkpoints]

`persistence.Archive` (`internal/persistence/archive.go`) coordinates incremental uploads of `events.jsonl` and append-only `native/` files (up to 128 files per checkpoint):

<Steps>
  <Step title="Split into 64 KiB immutable blocks">
    Complete `64 KiB` (`BlockBytes = 64 << 10`) blocks are uploaded once under `sessions/<id>/blocks/<generation>/<index>` (`immutable: true`). Subsequent checkpoints reuse the same `generation` ID and upload only newly appended full blocks plus the bounded remainder tail under `sessions/<id>/tails/<checkpoint_id>/<generation>` (`immutable: true`).
  </Step>
  <Step title="Write the immutable checkpoint manifest">
    Only after every referenced block and tail object is stored does `Archive.Publish` write `sessions/<id>/checkpoints/<checkpoint_id>` (`immutable: true`). A failed upload can leave unreachable blocks, never a partially readable checkpoint.
  </Step>
  <Step title="Update `latest` and optional metadata">
    `Archive.Publish` updates the mutable pointer `sessions/<id>/latest` (`immutable: false`), updates `Metadata.Put` if `postgres_env` is configured, and saves `uploaded.json` in the local session directory.
  </Step>
</Steps>

## `Metadata` interface [#metadata-interface]

`Metadata` provides `Put(ctx, session, checkpointID)`, `Get(ctx, tenant, id)`, `List(ctx, tenant, after, limit)`, and `Close()`:

- `Get` and `List` are strictly scoped by `tenant`, and `List` returns sessions ordered by `id` ascending (`id > after`, `limit` `1`–`1000`).
- `Put` upserts `(id, tenant, data, checkpoint)` and rejects cross-tenant collisions with `403 forbidden` (`metadata tenant mismatch`).
- External metadata is an eventually consistent discovery index updated after checkpoint publication; it is never used for locking or leader election.

## Testing an adapter [#testing-an-adapter]

Every storage adapter should pass the contract tests in `internal/persistence/`:

- Immutable block writes (`immutable: true`) and idempotent retries when a block or manifest already exists
- Interrupted manifest publication (unreachable blocks must never expose a half-written checkpoint)
- Missing object detection (`errors.Is(err, os.ErrNotExist)`) and tenant/path isolation
- Restoring into an empty local cache and verifying the next Pi model turn preserves conversation history
