# Memory

> How Fermix stores, extracts, and recalls conversation history and long-term facts across three layers: SQLite, ETS caches, and prompt-memory markdown files.

Memory is how Fermix remembers your conversations and long-term facts about you between turns and across restarts. It operates in three layers: a durable SQLite database (`memory.db`, a single-file local database) that is the authoritative store, fast in-memory ETS caches (ETS is the BEAM runtime's built-in in-memory table store) for quick access during a turn, and derived markdown files that are loaded into the system prompt. The markdown files are just copies; SQLite is the source of truth.

## Storage layers

| Layer | What it holds |
|---|---|
| SQLite (`~/.fermix/memory.db`) | Messages, memories with FTS search (full-text search, so you can find memories by keyword), scheduled jobs, job runs, memory sources, versioned resources. Migrations are applied at startup. |
| ETS: ConversationStore | Hot cache of recent messages per conversation key. Write-through to SQLite with up to 3 write attempts (failed writes are retried with a short delay, then logged). Unbounded by default (`max_conversation_history = :infinity`, operator-configurable to a positive integer); token-based auto-compaction, not a message count, is the real bound on history size. |
| ETS: Store | Read-through cache for individual memory rows (scoped facts). |
| Prompt files | Per-agent markdown files (`~/.fermix/memory/<agent_id>/USER.md`, `MEMORY.md`). Loaded by `PromptComposer` as system messages ahead of conversation history. |

## SQLite schema

### messages

Conversation history with a full-text search mirror (`messages_fts`) maintained by SQLite triggers on insert, update, and delete.

```
messages(
  id, agent_id, owner_id, channel, chat_id, thread_scope,
  sender, role, kind, content, metadata_json, created_at
)
index on (agent_id, channel, chat_id, thread_scope, created_at, id)
```

### memories

Scoped key-value facts with confidence, category, and promotion metadata. A unique constraint on `(agent_id, owner_id, scope_type, scope_id, key)` enforces deduplication at the database level.

```
memories(
  id, agent_id, owner_id, scope_type, scope_id, category,
  key, value, confidence, promote_target,
  source_id, source_type, source_name, source_description,
  session_id, run_id, source_message_id,
  archived_at, archived_by, archive_reason, created_at, updated_at
)
unique (agent_id, owner_id, scope_type, scope_id, key)
```

`promote_target` determines which prompt file (`user_md` or `memory_md`) a memory row is rendered into. Outdated rows are archived rather than deleted (`archived_at`, `archived_by`, `archive_reason`); `fermix memory restore ID` un-archives a row.

For `resources` and `resource_revisions`, see [resource versioning](/docs/resource-versioning). For `scheduled_jobs` and `job_runs`, see [scheduled jobs](/docs/scheduled-jobs).

## Memory categories

Memory categories determine where facts are stored in the prompt and who may write them. The current taxonomy uses six categories on a general-assistant spine: four about the user and two about the work.

| Category | Prompt target | Trust gate |
|---|---|---|
| `identity` | USER.md | Any trust |
| `preference` | USER.md | Any trust |
| `interest` | USER.md | Any trust |
| `goal` | USER.md | Any trust |
| `context` | MEMORY.md | Any trust |
| `directive` | MEMORY.md | Operator only (guests cannot promote) |

`directive` rows represent standing behavioral rules set by the operator. Guests cannot write them. The memory reviewer prompt also enforces that the agent's own refusals and one-shot answers are not stored as `directive` or `context` rows.

Whether a row actually renders into a prompt file also depends on its scope: conversation-scoped `context` rows still render into MEMORY.md, conversation-scoped `directive` rows never do, and job-scoped rows never render into either file.

Each prompt file is bounded by per-section row caps (USER.md: Identity 6, Preferences 6, Interests 6, Goals 5; MEMORY.md: Context 12, Working Rules 8) and a per-value character limit (200 characters). The most recently updated rows are kept when a section exceeds its cap. Token budgets per file are 1500 tokens for USER.md and 2000 tokens for MEMORY.md.

## Scope resolution

Every memory row has a `scope_type` and a `scope_id` that control which conversations and agents can read it back.

| Scope type | scope_id source |
|---|---|
| `owner` | The human's `owner_id` |
| `agent` | The Fermix `agent_id` |
| `conversation` | Derived from `{channel, chat_id, thread_scope}` |
| `job` | The scheduled job's `memory_source_id` |

Scope is determined by who writes the row: the background reviewer stores user-profile facts (`identity`, `preference`, `interest`, `goal`) at `owner` scope and working knowledge (`context`, `directive`) at `agent` scope; the `memory_store` tool writes at `conversation` scope; scheduled job runs write at `job` scope.

## Extraction pipeline

Fact extraction happens through the **time-gated background reviewer**, dispatched after the reply is delivered (in `TurnRunner.commit/4`) so it never sits on the turn's critical path. A failed review does not affect the turn that triggered it. There is no separate per-turn extractor — the reviewer is started every turn but only *runs* when its interval is due.

1. After a successful agent turn, the main agent calls `Memory.Reviewer.start_background/1`.
2. The reviewer is time-gated: it runs only if the review interval (`review_interval_hours`, default 24) has elapsed since this conversation was last reviewed — or it has never been reviewed; otherwise it skips (`:under_interval`). A recent failure backs it off further (`review_failure_backoff_ms`, default 5 minutes). `fermix memory review --now` forces a run past the interval and failure-backoff gates; it still requires new messages since the last review, and an atomic claim ensures only one review per conversation runs at a time.
3. When it runs, it reads the user-sent messages added since the last review (up to `review_max_messages`, default 40, packed to `review_input_token_budget`, default 4000 tokens) plus the current memory state.
4. It makes one bounded model call — on the same provider route the agent's turns use (the primary/fallback chain), tagged `agent: "memory_reviewer"` in traces — with a prompt to consolidate memory. The model returns strict operations (`add`, `replace`, `archive`) or the literal "Nothing to save."; when a message refines or contradicts an existing row, the latest statement wins — the row is replaced or archived by id instead of a duplicate being added.
5. Operations pass through admission (`Memory.Admission`): trust gating (guest-sourced `directive` operations are refused), category validation, and category-to-scope mapping. The database-level unique key prevents duplicate rows.
6. Admitted facts are written to SQLite, then `Memory.PromptFiles.rebuild/4` re-renders the promoted rows into `USER.md` and `MEMORY.md` using an atomic temp-file-and-rename to avoid partial reads.

The reviewer uses the centralized buffered LLM timeout policy (`:llm_buffered`) rather than a bespoke per-feature timeout. The four review knobs (`review_interval_hours`, `review_max_messages`, `review_input_token_budget`, `review_failure_backoff_ms`) live under `[fermix_core.memory]` in `config.toml`; the review interval is also settable through setup — see [configuration](/docs/configuration).

## Memory review

A background memory reviewer ("dreaming") runs on a configurable interval (default 24 hours, set in hours through the setup wizard or web setup; setting it to 0 disables background review — `--now` still works). It reads recent user-sent messages and the current memory state, then applies updates: replacing outdated rows, archiving stale ones, and generalizing overly-specific facts. The reviewer is instructed to write each value as a declarative fact about the user or their work, not a self-instruction, and to update-in-place rather than append duplicates.

You can trigger a review immediately or restore an archived memory row from the CLI:

```bash
fermix memory review --now                         # all conversations with new messages
fermix memory review --now --conversation telegram:12345
fermix memory restore ID                           # un-archive a memory row
```

`review --now` also accepts `--agent ID`, `--owner ID`, and `--json`. Restoring a row rebuilds the prompt files so the fact reappears in the system prompt immediately.

## Compaction

Compaction is how Fermix keeps a long conversation from outgrowing the model's context window: it replaces older messages with a short summary so the conversation can continue. Auto-compaction is handled by `Memory.Compactor`. When a conversation's provider-reported context-token count (the peak prompt tokens from the turn) crosses a configurable threshold of the model's context window, Fermix summarizes older messages into a single checkpoint and replaces the stored history. The default compaction budget is 0.5 times the context window. The default trigger threshold is 0.85 (configurable via `[fermix_core.compaction]`).

```toml
[fermix_core.compaction]
enabled = true
threshold = 0.85
reasoning_effort = "medium"
```

`reasoning_effort` sets the thinking effort of the summarization call itself (default `medium`); summaries are mechanical, so they deliberately do not inherit the agent's own — possibly much higher — reasoning-effort setting.

Compaction can fire both before a turn (a preflight check against the previous turn's recorded peak context-token count) and after delivery. When the provider-reported context-token count reaches `threshold * model_context_window`, it compacts; a cold or first turn with no prior measurement skips the preflight check cleanly and the post-delivery pass catches it. Failures are recorded with a 60-second backoff so the next turn does not retry immediately. When a preflight compaction succeeds, Fermix sends a short notice to the chat: "Trimmed older conversation history to stay within the context window."

You can also trigger compaction manually from any channel (operator-only, like all state-changing chat commands):

```
/compact
```

`/compact` summarizes the current window and keeps the summary in history, using the same primary/fallback provider chain as auto-compaction. `/new` or `/clear` clears only the conversation window; long-term memory, resource revisions, and scheduled jobs are preserved.

For the prompt composition details that govern where `USER.md` and `MEMORY.md` appear in the system prompt, see [prompt and compaction](/docs/prompt-and-compaction).

## Memory tools

The agent has three tools for direct memory access.

| Tool | Description |
|---|---|
| `memory_store` | Store a small key-value fact for the current conversation (arguments: `key`, `value`). Rows are written at `conversation` scope and are not rendered into the prompt files. |
| `memory_recall` | Key lookup or FTS5 search over memories and/or history. Accepts `scope` (`current`, `owner`, `all`) and `source` (`memories`, `history`, `all`); defaults to `current` scope and `memories` source. Scheduled job runs are restricted to their own job-scoped memories and cannot search conversation history. |
| `memory_sources_list` | List visible memory sources, including scheduled job sources |

Memory content loaded into the system prompt (the USER.md and MEMORY.md sections) is fenced in a `<memory-context>` block and labeled as informational background data, not new user input.

## Prompt files

`USER.md` holds `identity`, `preference`, `interest`, and `goal` rows. `MEMORY.md` holds `context` and `directive` rows. Both files live at `~/.fermix/memory/<agent_id>/`. They are rebuilt automatically after each extraction cycle.

The files are derived artifacts. Rolling back a prompt file restores the rendered content, but future extraction rebuilds will overwrite it if the underlying promoted memory rows are unchanged. To inspect or roll back resource revisions, see [resource versioning](/docs/resource-versioning).

```bash
mix fermix.resource.list
mix fermix.resource.history user_md --limit 10
mix fermix.resource.show user_md 3
mix fermix.resource.diff user_md 2 3
mix fermix.resource.rollback user_md 2
```

## Realtime voice transcripts

If transcript persistence is enabled (`persist_transcripts = true` in the realtime config; off by default), final spoken user and assistant transcript text is stored in conversation history as message rows of kind `voice_turn`, under a dedicated `realtime` conversation per device, with `source_type = "realtime"` in their metadata. Persisted voice turns feed the same background memory reviewer as chat messages. See [realtime voice](/docs/realtime-voice).
