# `SuperPi.Memory`
[🔗](https://github.com/oleksandr/super-pi-beam/blob/v0.1.0/lib/super_pi/memory.ex#L1)

Durable notes that outlive a conversation: the store behind `SuperPi.Plugins.Memory`.

Notes live in the `memories` table, not in a durable document and not in artifacts. A document
rewrites the whole corpus as one base row at every checkpoint and pins it in ETS, and artifacts
have no search and die with their conversation. A note belongs to a scope `{scope_kind, scope_id}`:
`{"owner", principal}` by default or `{"global", ""}`. It is not forked and not rewound, so a note
that a branch writes is visible to its parent. Read-only subagents cannot write one, because
`memory_write` carries the `state_write` effect and plan mode blocks it.

## Writing

`write/3` validates, then inserts or updates in one `Storage.commit/1` (the index triggers run in
the same transaction). Limits and rules:

- Title and body are stored byte for byte. They must be valid UTF-8 without NUL (PostgreSQL
  cannot store NUL) and the body is capped at `:max_body_bytes` (32 000 by default).
- An explicit `slug` upserts. A slug derived from the title never overwrites: a taken one gets `-2`, `-3`.
- `pinned` is refused for compaction provenance and for a note with `source.url`, unless the
  caller passes `allow_pin_untrusted: true`. A pinned body goes into the system prompt, so
  anything from the open internet needs an explicit opt-in. An update takes `pinned` from the call
  (default `false`), so rewriting a pinned note unpins it.
- Provenance is not model-settable: `web` when `source.url` is set, else `agent`. Code passes
  `:provenance` (`user`, `compaction`, `import`).
- The result lists similar live notes with a score, which keeps the store from becoming a junk
  drawer. The score is the Jaccard similarity of title and topic words, which does not depend on
  the search backend.

## Reading and linking

`search/3` is lexical (`SuperPi.Memory.Search`). `read/2` returns a note with its links.
`link/5` adds a `related`, `derived_from` or `supersedes` edge. `supersedes` marks the older note
superseded, refuses a cycle and refuses a note that already has a superseder.

Configuration (`config :super_pi, :memory, [...]`): `:max_body_bytes`, `:index_token_budget`,
`:pinned_body_bytes`, `:half_life_days`, `:recency_weight`, `:pinned_bonus`, `:duplicate_threshold`,
`:search` (backend module), `:harvest_model`, `:harvest_max_facts`, `:harvest_llm`.

# `scope`

```elixir
@type scope() :: {String.t(), String.t()}
```

# `config`

```elixir
@spec config(atom()) :: term()
```

A configuration value, with the module default.

# `fingerprint`

```elixir
@spec fingerprint(scope()) :: String.t()
```

`{count, max updated_seq}` of the scope: cheap proof that the index text is still current.

# `format_error`

```elixir
@spec format_error(term()) :: String.t()
```

Human-readable message for an error from `write/3` or `link/5`.

# `get`

```elixir
@spec get(scope(), String.t()) :: SuperPi.Durable.Schemas.Memory.t() | nil
```

The note with `slug` in the scope, or `nil`.

# `link`

```elixir
@spec link(scope(), String.t(), String.t(), String.t(), keyword()) ::
  {:ok,
   %{
     from: SuperPi.Durable.Schemas.Memory.t(),
     to: SuperPi.Durable.Schemas.Memory.t(),
     rel: String.t()
   }}
  | {:error, term()}
```

Adds a `rel` edge from the note `from_slug` to `to_slug`. `supersedes` marks `to` as replaced by
`from`. Idempotent for an existing edge.

# `normalize_topics`

```elixir
@spec normalize_topics(term()) :: String.t()
```

Normalizes topics to a space-separated string of lowercase words. Anything outside letters,
digits, `.`, `_` and `-` becomes `-`. At most 8 topics of 40 characters.

# `provenances`

# `read`

Reads a note with its outgoing links, its backlinks and the slug of its superseder.
Returns `{:ok, %{memory, links: [{rel, note}], backlinks: [{rel, note}], superseded_by: slug | nil}}`
or `{:error, :not_found}`.

# `rels`

# `scope`

```elixir
@spec scope(integer() | nil, keyword()) :: scope()
```

The scope of a conversation: its owner's notes by default, or the shared `global` scope.

# `scoped`

The query of every note in a scope.

# `search`

Searches the scope. See `SuperPi.Memory.Search.run/3`.

# `slugify`

```elixir
@spec slugify(String.t()) :: String.t()
```

Derives a slug from a title: ASCII words joined by `-`, or a short hash when no ASCII word is left.

# `snippet`

```elixir
@spec snippet(String.t(), [String.t()], pos_integer()) :: String.t()
```

A short window of `body` around the first query word, or the start of the body. Whitespace is collapsed.

# `topic_list`

The topics of a note as a list.

# `write`

```elixir
@spec write(scope(), map(), keyword()) :: {:ok, map()} | {:error, term()}
```

Writes a note. `attrs` has string keys: `"title"`, `"body"`, and optionally `"slug"`, `"topics"`
(list of strings), `"pinned"`, `"confidence"` (0..1) and `"source"` (map with `url`, `artifact`, `note`).

Options: `:conversation_id` (origin), `:provenance`, `:allow_pin_untrusted`.

Returns `{:ok, %{memory: note, created: boolean, similar: [%{slug, title, score}]}}` or `{:error, reason}`
(see `format_error/1`).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
