# AbstractMemory (llms-full)

See [llms.txt](llms.txt) for the documentation index.

---

## README.md

# AbstractMemory

AbstractMemory is a Python library for durable, append-only agent memory. It provides two layers:

- **Layer 1 — triple truth**: append-only, temporal, provenance-aware triple assertions with deterministic structured queries and optional vector/semantic retrieval, over in-memory, SQLite, or LanceDB backends.
- **Layer 2 — the memory system**: a `MemorySystem` facade that turns those triples plus an append-only journal into a usage-weighted memory graph: typed record formation, stimulus-driven reconstruction (working memory that emerges from use), attention and decay, identity cores for long-lived entities, valence/gradation (how experience felt), diary conventions, sleep/consolidation, and a replay stream for observability.

Storage never decays and nothing is ever deleted; only retrieval strength changes. Reads are pure — rendering a memory does not strengthen it; only committed use does.

## Status

- Pre-1.0: the API is versioned and tested, and details may still evolve. The current version is in [`pyproject.toml`](pyproject.toml).
- The authoritative export list is [`src/abstractmemory/__init__.py`](src/abstractmemory/__init__.py); [`docs/api.md`](docs/api.md) documents it.
- Requires Python 3.10+.

## What's new in 0.5.2

- Diary replay carries recorded resolution metadata, including target identifiers, with private titles omitted. Existing projections are not backfilled.

## What's new in 0.5.1

- `community_keys_of` / `community_memberships_of`: the membership-only topic read for every recalled record, tens of microseconds per call from the in-memory community index (the runtime's recall stamps use it).
- A standing topic still pending its title or summary is authored in place by `communities_pass` with the budget the changed topics leave, and its queued tasks move up to the topics' priority.

## What's new in 0.5.0

- Every memory's title and summary is authored: memory's passes take a host `author` callable, and a record whose authoring failed forms pending, with its task, until a reader completes it (`retitle`, `rewrite_digest`, `declare_pending`, `rearm_pending`).
- A persistent task queue per home (`MemorySystem(task_queue=True)`), ripple v1, and understanding rewritten in place with every earlier reading kept as a version.
- Topics: `community` records formed during sleep, with provisional placement of each new record at formation.
- The sleep cadence read `records_since_maintenance`, and the Stop fence hooks `commit_guard` and `write_barrier`.

Upgrading from 0.4.x: see the "Upgrading" notes in [`CHANGELOG.md`](CHANGELOG.md).

## Ecosystem (AbstractFramework)

AbstractMemory is a component of the **AbstractFramework** ecosystem. It has no dependency on AbstractCore or AbstractRuntime; embeddings for semantic retrieval can come from any OpenAI-compatible `/embeddings` endpoint (`OpenAICompatTextEmbedder`), from an AbstractGateway deployment (`AbstractGatewayTextEmbedder`), or from your own `TextEmbedder` implementation.

```mermaid
flowchart LR
  APP["Your app or agent"] --> MS["MemorySystem (layer 2)"]
  MS --> ST["Triple store (layer 1)"]
  MS --> J["Journal (append-only)"]
  ST --> IM["InMemoryTripleStore"]
  ST --> SQL["SQLiteTripleStore"]
  ST --> LDB["LanceDBTripleStore"]
  SQL --> F[("one SQLite file")]
  J --> F
  MS -. "optional embeddings" .-> E["TextEmbedder (OpenAI-compatible / Gateway / custom)"]
```

Related projects:

- AbstractFramework: `https://github.com/lpalbou/abstractframework`
- AbstractCore: `https://github.com/lpalbou/abstractcore`
- AbstractRuntime: `https://github.com/lpalbou/abstractruntime`

## Install

From source (recommended inside the AbstractFramework monorepo):

```bash
python -m pip install -e .
```

Optional LanceDB backend:

```bash
python -m pip install -e ".[lancedb]"
```

Fast vector search on the SQLite store (numpy; without it the store serves the
same results from a labelled Python scan):

```bash
python -m pip install -e ".[fast]"
```

PyPI (packaged release):

```bash
python -m pip install AbstractMemory
python -m pip install "AbstractMemory[lancedb]"
```

The distribution name is `AbstractMemory` (pip is case-insensitive); the import name is `abstractmemory`. The `[apple]`/`[gpu]` extras are no-op compatibility aliases; `[fast]` installs numpy for the SQLite store's vector matrix cache; `[all]`, `[all-apple]`, and `[all-gpu]` install the LanceDB backend and numpy.

## Quick example — layer 1 (triples)

```python
from abstractmemory import InMemoryTripleStore, TripleAssertion, TripleQuery

store = InMemoryTripleStore()
store.add([
    TripleAssertion(
        subject="Scrooge",
        predicate="related_to",
        object="Christmas",
        scope="session",
        owner_id="sess-1",
        provenance={"span_id": "span_123"},
    )
])

hits = store.query(TripleQuery(subject="scrooge", scope="session", owner_id="sess-1"))
assert hits[0].object == "christmas"      # terms are canonicalized (trim + lowercase)
assert hits[0].assertion_id is not None   # stores stamp read-side identity on results
```

## Quick example — layer 2 (the memory system)

```python
from abstractmemory import (
    MemorySystem, MemoryRecordInput, SQLiteTripleStore, SQLiteJournal, Stimulus,
)

store = SQLiteTripleStore("memory.sqlite3")
journal = SQLiteJournal("memory.sqlite3")   # sidecar tables in the same file
system = MemorySystem(store=store, journal=journal)

# Form a typed record (idempotent by key; forming is not using).
[record_id] = system.remember_many(
    [MemoryRecordInput(kind="episode", title="Pool outage",
                       digest="The connection pool saturated at noon.",
                       keywords=("pool", "outage"))],
    scope="session", owner_id="s1", idempotency_key="turn-1",
)

# Reconstruct working memory for a cue (pure read), then commit what you used.
result = system.reconstruct(Stimulus(cue_text="pool outage"), scopes=[("session", "s1")])
system.commit_selection(result.trace_id, [h.record_id for h in result.handles[:2]])
```

## The memory graph: revisions and provenance

`abstractmemory.graph` is the write path of a mind (0.4.0): every change to the graph is one `Revision` (a cause such as `ingest`, `reinterpret` or `maintenance`, plus `assert` / `close` / `narrow` operations applied together and atomically, once per revision id), and every memory must name where it came from in AbstractRuntime's ledger (a `SourceRef` to a run, a step `(run_id, seq)`, an artifact, a message, an event or a diary entry) or the memories it was derived from; a write with neither is refused with `ProvenanceRequired`. `MemoryGraph(store, journal)` then answers `history(memory_id)` (asserted, closed, narrowed: by which revision and why), `sources_of(memory_id)`, `derived_from_source(ref)`, a lossless change feed `revisions(after=cursor)`, and `replay_into(...)`. The database is a connector behind the graph (`abstractmemory.graph.backend`): SQLite by default, LanceDB and in-memory held to the same conformance suite. See [`docs/api.md`](docs/api.md#the-memory-graph-revisions-and-provenance) and the "Time axes" and "Provenance" sections of [`docs/memory-system.md`](docs/memory-system.md).

## Documentation

- Getting started: [`docs/getting-started.md`](docs/getting-started.md)
- Architecture: [`docs/architecture.md`](docs/architecture.md)
- The memory system (cognitive model): [`docs/memory-system.md`](docs/memory-system.md)
- API reference: [`docs/api.md`](docs/api.md)
- Stores/backends: [`docs/stores.md`](docs/stores.md)
- Operator guide (entity homes): [`docs/operator.md`](docs/operator.md)
- FAQ: [`docs/faq.md`](docs/faq.md)
- Troubleshooting: [`docs/troubleshooting.md`](docs/troubleshooting.md)
- Development: [`docs/development.md`](docs/development.md)

## Project

- Changelog: [`CHANGELOG.md`](CHANGELOG.md)
- Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md)
- Code of conduct: [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md)
- Security: [`SECURITY.md`](SECURITY.md)
- License: [`LICENSE`](LICENSE)
- Acknowledgments: [`ACKNOWLEDGMENTS.md`](ACKNOWLEDGMENTS.md)

## Design principles

- **Append-only, no deletion**: updates are new assertions; belief revision is closure records (retract/supersede/reinterpreted/merged); forgetting is decay of retrieval strength plus closures and silencing — the substrate is lossless. The one mutable cell is a record's understanding: `rewrite_digest` and `retitle` rewrite a digest or a title in place and keep every earlier reading as a version.
- **Reads are pure**: reconstruction, inspection, replay, and the entity card deposit nothing. `commit_selection` is the only strengthening path.
- **One seq axis**: the journal assigns a monotonic `seq` to every record; any past state is reproducible by anchoring reads at `as_of`.
- **Works-or-loud**: degraded paths are labeled `#FALLBACK` in result warnings; invalid inputs raise actionable errors instead of silently meaning something else.
- **No heavy dependencies**: SQLite persistence and vector scoring use the standard library; LanceDB and embedders are optional.

## For AI agents

Register and use [the AbstractMemory skill](skills/abstractmemory/SKILL.md) for scoped reads, provenance-aware writes and replay.

---

## docs/getting-started.md

# Getting started

> Source of truth for the public API: [`src/abstractmemory/__init__.py`](src/abstractmemory/__init__.py); full reference: [`api.md`](docs/api.md).

Requires Python 3.10+ (see [`pyproject.toml`](pyproject.toml)).

## 1) Install

From source (recommended inside the AbstractFramework monorepo):

```bash
python -m pip install -e .
```

Optional LanceDB backend:

```bash
python -m pip install -e ".[lancedb]"
```

Fast vector search on the SQLite store (numpy; without it the store serves the
same results from a labelled Python scan):

```bash
python -m pip install -e ".[fast]"
```

PyPI (packaged release):

```bash
python -m pip install AbstractMemory
python -m pip install "AbstractMemory[lancedb]"
```

The distribution name is `AbstractMemory` (pip is case-insensitive); the import name is `abstractmemory`.

## 2) Layer 1: append-only triples

```python
from abstractmemory import InMemoryTripleStore, TripleAssertion, TripleQuery

store = InMemoryTripleStore()
store.add([
    TripleAssertion(
        subject="Scrooge",
        predicate="related_to",
        object="Christmas",
        scope="session",
        owner_id="sess-1",
        observed_at="2026-01-01T00:00:00+00:00",
        provenance={"span_id": "span_123"},
    )
])

hits = store.query(TripleQuery(subject="scrooge", scope="session", owner_id="sess-1", limit=10))
assert hits[0].object == "christmas"     # terms are canonicalized (trim + lowercase)
assert hits[0].assertion_id is not None  # read-side identity, stamped by the store
```

Notes:

- Canonicalization lowercases `subject`/`predicate`/`object`. To preserve original casing, store it separately (for example in `attributes`), or set `attributes={"literal": True}` to keep the `object` case-sensitive (typed records formed by the memory system use this for digest text).
- `scope` is a free-form partition label (lowercased). Common conventions: `"session"` + `owner_id` per conversation, `"run"` + `owner_id` per execution, `"global"` for shared memory, and the entity-home scopes `"self"`/`"diary"`/`"life"`. At layer 2, `"global"` is a broad scope: searching it requires an explicit `escalation_reason`.
- `TripleQuery(assertion_ids=(...,))` looks up rows by exact id; id lookups bypass visibility folds by design (audit completeness).

## 3) Persistent single-file store (SQLite), with optional vectors

`SQLiteTripleStore` uses only the Python standard library and supports both deterministic structured queries and native vector search when constructed with an embedder.

```python
from abstractmemory import SQLiteTripleStore, TripleAssertion, TripleQuery

store = SQLiteTripleStore("data/kg.sqlite")
store.add([TripleAssertion(subject="e:scrooge", predicate="is_a", object="person", scope="global")])

out = store.query(TripleQuery(scope="global", limit=10))
store.close()
```

With vectors (any `TextEmbedder`; here an OpenAI-compatible server such as LM Studio or Ollama):

```python
from abstractmemory import OpenAICompatTextEmbedder, SQLiteTripleStore, TripleQuery

embedder = OpenAICompatTextEmbedder(
    base_url="http://127.0.0.1:1234/v1",
    model="text-embedding-qwen3-embedding-0.6b",
)
store = SQLiteTripleStore("data/kg.sqlite", embedder=embedder)
# add(...) embeds each assertion's canonical text and persists the vector in the same file
hits = store.query(TripleQuery(query_text="who is scrooge", scope="global", limit=5))
```

Rows added while no embedder was configured stay vectorless; vector queries skip them, and layer-2 recall labels the degradation with a `#FALLBACK` warning. Files created before the vector column existed are upgraded in place on open.

## 4) LanceDB backend (optional)

```python
from abstractmemory import LanceDBTripleStore, TripleAssertion, TripleQuery

store = LanceDBTripleStore("data/kg")
store.add([TripleAssertion(subject="e:scrooge", predicate="is_a", object="person", scope="global")])
out = store.query(TripleQuery(scope="global", limit=10))
store.close()
```

See [`stores.md`](docs/stores.md) for backend-by-backend behavior and persistence details.

## 5) Semantic/vector queries

Vector search is opt-in and consistent across vector-capable stores:

- `query_text=...` embeds the text and ranks by cosine similarity; it requires a configured embedder (a `ValueError` is raised otherwise — there is no keyword fallback).
- `query_vector=...` bypasses embedding generation (caller-supplied vector).
- `min_score=...` applies a cosine similarity threshold.
- Only rows stored with vectors participate; results carry retrieval metadata in `attributes["_retrieval"]`.

Embedder options:

- `OpenAICompatTextEmbedder(base_url, model, *, api_key=None, timeout_s=30.0, batch_size=64)` — any OpenAI-compatible `/embeddings` endpoint.
- `AbstractGatewayTextEmbedder(base_url, auth_token=None, ...)` — an AbstractGateway embeddings endpoint (default path `/api/gateway/embeddings`).
- Your own implementation of the `TextEmbedder` protocol: `embed_texts(texts) -> list[list[float]]`.

Keep one embedding model per store file: vectors from different models are not comparable, and the store does not enforce this for you.

## 6) Layer 2: the memory system

`MemorySystem` composes a store and a journal (both injected) into the full memory engine. The SQLite pair shares one file — one file is one home.

```python
from abstractmemory import (
    MemorySystem, MemoryRecordInput, SQLiteTripleStore, SQLiteJournal, Stimulus,
)

store = SQLiteTripleStore("memory.sqlite3")
journal = SQLiteJournal("memory.sqlite3")
system = MemorySystem(store=store, journal=journal)

# FORM: typed records with digests, keywords, edges (idempotent by key).
[gid] = system.remember_many(
    [MemoryRecordInput(kind="episode", title="Pool outage night",
                       digest="The connection pool saturated at noon.",
                       keywords=("pool", "saturation", "noon"))],
    scope="session", owner_id="s1", idempotency_key="turn-1",
)

# RECONSTRUCT: working memory for a cue (pure read; nothing is strengthened).
result = system.reconstruct(Stimulus(cue_text="pool outage"), scopes=[("session", "s1")])
for handle in result.handles:
    print(handle.admission, handle.title, handle.digest)

# COMMIT: deposit usage for the records that actually entered your context.
system.commit_selection(result.trace_id, [h.record_id for h in result.handles[:2]])
```

The loop above is the whole contract: **form → reconstruct → commit**. Reconstruction is a pure read; `commit_selection` is the only path that strengthens memories (usage trails and co-use associations). Records rendered from short-term standing or from the identity core are present but do not deposit — presence is not use.

## 7) The memory graph: revisions with ledger provenance

A mind written by several applications goes through `MemoryGraph`: every change is a `Revision` (applied atomically and once per id), and every memory names where it came from in the runtime's ledger.

```python
from abstractmemory import SQLiteJournal, SQLiteTripleStore
from abstractmemory.graph import MemoryGraph, Revision, RevisionOp

store = SQLiteTripleStore("mind.sqlite3")
graph = MemoryGraph(store, SQLiteJournal("mind.sqlite3"))   # journal in the same file: one transaction

graph.apply_revision(Revision(
    revision_id="note-1-extract", scope="global", owner_id="person:ada", cause="ingest",
    occurred_at="2026-01-05T09:00:00Z",
    ops=[RevisionOp("assert", {"assertion_id": "c1", "subject": "ex:dana",
                               "predicate": "schema:worksFor", "object": "ex:acme",
                               "valid_from": "2026-01-01T00:00:00Z"})],
    provenance={"sources": [{"kind": "step", "run_id": "run-42", "seq": 3},
                            {"kind": "artifact", "artifact_id": "note-1-body"}],
                "actor": "abstractnote"},
))
graph.history("c1")                                         # asserted, by which revision, with its cause
graph.sources_of("c1")                                      # the step and the artifact
graph.derived_from_source({"kind": "run", "run_id": "run-42"})   # ["c1"]
```

A write that names neither a source nor a memory it was derived from raises `ProvenanceRequired`. The full surface (close, narrow, re-observation, the change feed, replay, connectors): [`api.md`](docs/api.md#the-memory-graph-revisions-and-provenance).

From here:

- The cognitive model (how working memory emerges, attention, valence, dreams): [`memory-system.md`](docs/memory-system.md)
- Every call and dataclass: [`api.md`](docs/api.md)
- Architecture and invariants: [`architecture.md`](docs/architecture.md)
- Inspecting an entity home as an operator: [`operator.md`](docs/operator.md)

---

## docs/architecture.md

# Architecture

This document describes the package's structure and the invariants it enforces. For the cognitive model behind layer 2 (why the design looks like this), see [`memory-system.md`](docs/memory-system.md); for call-level detail, see [`api.md`](docs/api.md).

## The two-layer model

**Layer 1 — triple truth.** Append-only `TripleAssertion` rows with temporal and provenance metadata, deterministic structured queries (`TripleQuery`), and optional vector retrieval. Three interchangeable stores implement the `TripleStore` protocol: `InMemoryTripleStore` (volatile, vector-capable), `SQLiteTripleStore` (persistent single file, vector-capable), and `LanceDBTripleStore` (persistent, vector-capable, optional dependency). Layer 1 is independently usable: if all you need is a durable triple store with semantic search, you never have to touch layer 2.

**Layer 2 — the memory system.** `MemorySystem` composes an injected store with an injected append-only journal (`InMemoryJournal` or `SQLiteJournal`) into a usage-weighted memory graph. The store holds *what is known* (assertions: record digests, edges, identity records). The journal holds *what happened* (events, bindings, closures, traces, snapshots, valence). Neither layer imports AbstractCore or AbstractRuntime; LLM-adjacent capabilities arrive only as injected protocols (the `TextEmbedder`).

```mermaid
flowchart TB
  subgraph L2["Layer 2 — MemorySystem"]
    R["reconstruct (pure read)"] --> C["commit_selection (the only strengthening path)"]
    F["remember_many (formation)"]
    V["appraise / gradation (valence)"]
    D["dream_pass (consolidation)"]
    E["export_replay / entity_card (observability, pure reads)"]
  end
  subgraph L1["Layer 1 — substrate"]
    ST["TripleStore (assertions: digests, edges, identity)"]
    J["Journal (events, bindings, closures, traces, snapshots, valence)"]
  end
  L2 --> ST
  L2 --> J
  EMB["TextEmbedder (optional)"] -.-> L2
  EMB -.-> ST
```

For an entity home, the SQLite store and journal share one file: one file is one life.

## The journal is the time axis

Every journal record — attention event, scope binding, closure, reconstruction trace, active-memory snapshot, valence event — receives a monotonic `seq` from the journal. That single axis is the package's replay anchor:

- Every `ReconstructionResult` carries `as_of_seq`; re-running with `Stimulus(as_of=that_seq)` folds activation, trails, closures, and binding visibility to that moment and reproduces the result deterministically.
- `gradation(..., at_seq=...)`, `activation(..., at_seq=...)`, `entity_card(..., as_of=...)`, and `export_replay(since_seq=..., until_seq=...)` anchor the same way.
- Invalid anchors raise; they never silently mean "latest".

One documented limit: `as_of` anchors journal-derived signals. Triple-store truth is read current (the stores have no seq axis), so assertions added after an anchor still enter candidate gathering. Replay is exact while store contents are unchanged — the append-only common case. The entity card closes this for formed records by using each record's formation binding as its existence signal.

Write-side idempotency is uniform: a caller-supplied id (`event_id`, `binding_id`, `closure_id`, `trace_id`, `snapshot_id`, formation `idempotency_key`) makes replays journal no-ops that return the original record, so at-least-once delivery never double-writes.

## Reconstruction: the union working set

`reconstruct(stimulus, scopes=[...])` returns the union of three admission components, each labeled on its handles:

- **self** — the identity core, admitted by *binding state* (records whose folded binding is `indexed` + `active`), capped by `RecallBudget.self_fraction`. Identity is state, not usage: members are ordered by kind rank (value < purpose < trait), never by activation, and persist regardless of use.
- **stm** — short-term standing: records whose decayed activation clears `stm_floor`, capped by `stm_fraction`. This is continuity — what you were just working with.
- **stimulus** — channel retrieval for the cue: exact pattern matches, keyword scan, vector similarity, and participant co-presence, fused and ordered; spreading activation walks recorded edges and co-use trails outward from matches. A record that is both trail-hot and channel-matched is admitted once as `both`.

Two ordering guarantees: relevance admits, activation reorders (a channel-matched record can never be outranked or budget-evicted by an unmatched one, however hot); and the single best channel match is seated first against the full budget before any reservation.

## Presence is not use

Reading is never using. `reconstruct` is a pure read (with `journal=True` it appends one trace plus structurally inert audit events that never affect scores; with `journal=False` it writes nothing). `commit_selection(trace_id, used_ids)` is the **only** strengthening path: it deposits `selected` events and `co_selected` pair trails for the records that actually entered a context — and only for records admitted as `stimulus`/`both`. Records admitted as `self` or `stm` deposit nothing: rendering a memory from identity state or from the trail is presence, not use, and depositing it would make the working set self-reinforcing and un-evictable.

The same discipline extends to every derived read: inspection (`activation`, `access_counts`, `gradation`), the replay stream, the structural report, the dream pass, and the entity card all deposit nothing.

## Append-only, no deletion

There is no delete surface anywhere in the package:

- Updates are new assertions with fresh provenance.
- Belief revision is a `ClosureRecord` (`retract`, `supersede` with replacements, `reinterpreted` when the writer reads the source differently, or `merged` into another memory) — the old record leaves ranked retrieval but remains in the store and in history.
- Visibility is a `ScopeBinding` fold (latest per record/scope/owner wins): `hidden` removes a record from ranked retrieval in that scope pair only; re-binding `indexed` restores it.
- Forgetting is decay of retrieval strength plus closures and silencing. Storage never decays; only the temporal-access signal does. The global access count (`selected_count`) never decays at all.

A record's understanding is the one mutable cell. `rewrite_digest` replaces a record's digest (its summary text, title and keywords) and `retitle` its title alone, in place: the record keeps its graph id, its digest assertion id, its edges, its usage counts and every journal record, and the previous reading stays in the journal as a version with its author, model, sources and cause (`versions`, `revert_digest`). `author_world_model` does the same for a card's text, and `narrate_dream` adds a narration beside a dream's summary. The verbatim behind a record is never touched, and nothing is compacted or summarized-and-replaced.

## Determinism and honesty

- Structured queries are deterministic; reconstruction is deterministic given the same journal state and anchor.
- Degraded paths carry `#FALLBACK` labels in result warnings (for example: keyword channel running as a token scan, vectorless rows skipped by the vector channel, an unreachable embedder).
- Invalid input raises actionable errors naming what was expected — unknown record ids name both id namespaces, out-of-range anchors name the journal's high-water mark.

## Module map

| Area | Modules |
|---|---|
| Layer-1 data model | `models.py` (`TripleAssertion`), `store.py` (`TripleQuery`, `TripleStore`) |
| Stores | `in_memory_store.py`, `sqlite_store.py`, `lancedb_store.py`, `vector_scoring.py`, `vector_matrix.py`, `store_tuning.py` |
| The memory graph | `graph/` (`MemoryGraph`, the only manipulation surface of a mind: revisions, provenance, history, change feed; `backend.py` the connector Protocols; the SQLite and in-memory connectors in `graph.py`, the LanceDB connector in `lancedb_backend.py`) |
| Embedders | `embeddings.py`, `embeddings_openai_compat.py` |
| Journal | `journal.py` (records + protocol), `journal_memory.py`, `journal_sqlite.py` |
| Seam types | `seam.py` (`Stimulus`, `RecallBudget`, `MemoryHandle`, `ReconstructionResult`, `ActiveMemorySnapshot`, floors) |
| Reconstruction | `reconstruct.py`, `channels.py`, `spreading.py`, `shelf.py`, `self_component.py`, `folds.py` |
| Attention | `attention.py` (`AttentionConfig`, activation fold, deliberate acts) |
| Formation | `records.py` (`MemoryRecordInput`, encoding, payload tiers), `canonical_text.py` |
| Selection | `selection.py` (commit derivations) |
| Identity | `spark.py`, `engram.py`, `diary.py`, `gradation.py` |
| Facade | `system.py` (`MemorySystem`), `system_access.py`, `system_valence.py`, `system_understanding.py` |
| Understanding | `understanding.py` (`rewrite_digest`, `retitle`, `declare_pending`, `rearm_pending`, versions), `formation_authoring.py` (the host's `author` callable) |
| Task queue and ripple | `memory_tasks.py` (`MemoryTaskQueue`), `activation_gate.py`, `ripple.py` |
| Sleep | `maintenance.py` (`sleep_pass`, tending), `sleep_cadence.py` (`maintenance_due`, `records_since_maintenance`), `world_model.py`, `communities.py` and `community_index.py` (topics) |
| Stop fence | `commit_guard.py` (the host's check before every COMMIT), `write_barrier.py` |
| Derived reads | `consolidation.py` (sleep/dreams), `replay.py` (stream), `entity_card.py` |

## One graph surface, connectors behind it

A *mind* (a person's plane, an entity's home) is changed only through `abstractmemory.graph.MemoryGraph`: every change is a `Revision` applied atomically, every memory names its ledger sources or its lineage, and the reads (history, sources, derived-from, the change feed) are bounded index reads ([ADR 0003](docs/adr/0003-one-graph-ledger-provenance-and-revisions.md)). The database behind a mind is a connector (`abstractmemory.graph.backend`): SQLite is the default for planes and entity homes; the LanceDB and in-memory connectors pass the same conformance suite, LanceDB with two documented exceptions (writers in two processes; a crash mid-commit visible until `repair()`). Changing a mind's database means writing a connector, never touching `MemoryGraph` or its callers.

## AbstractFramework boundary

In a typical deployment, AbstractRuntime orchestrates *when* memory is consulted and committed (per turn), and AbstractGateway hosts entity homes and serves the replay stream over HTTP. This package owns the mechanics — graph, journal, retrieval, folds — and stays host-agnostic: everything a host does through the facade, you can do directly against the files with only `abstractmemory` installed (see [`operator.md`](docs/operator.md)).

---

## docs/memory-system.md

# The memory system

This page explains the cognitive model behind `MemorySystem` — what the moving parts mean and why they fit together. For exact signatures see [`api.md`](docs/api.md); for the structural invariants see [`architecture.md`](docs/architecture.md).

## One graph, emergent working memory

Memory is a single durable, usage-weighted graph. There is no separate "short-term store" that copies things in and out; working memory *emerges* from two signals folded over the same substrate:

1. **A recency/frequency trail.** Every committed use deposits attention events; a record's *activation* is a decayed fold over those events. Decay is activity-relative (measured in events, not wall-clock), so a quiet home does not forget just because time passed.
2. **Stimulus-driven spreading.** A cue lights up records through retrieval channels — exact triple patterns, keyword scan, vector similarity, and participant co-presence — and activation spreads outward over recorded edges and co-use trails, so related memories surface with the ones that matched.

Reconstruction returns the union of three components, labeled per handle:

- `self` — identity records, present by binding state (reserved seats; see below),
- `stm` — trail-hot records (continuity: what was just in use),
- `stimulus` / `both` — what the cue matched (and matched-while-hot).

Storage never decays. Only retrieval strength does. Nothing is deleted; forgetting is decay plus closure records plus silencing.

## Use strengthens; presence does not

`reconstruct` is a pure read. Strengthening happens in exactly one place: `commit_selection(trace_id, used_ids)`, called with the records that actually entered your context. It deposits:

- one `selected` event per used record — the usage trail;
- `co_selected` pair events for all pairs used together in the same moment — the association trail (co-use is how the graph learns which memories belong together).

Records admitted as `self` or `stm` deposit nothing even when committed: rendering your own identity, or a memory that was already on your desk, is presence — not use. This keeps counters honest (they measure lived experience) and prevents the working set from becoming self-reinforcing.

## The two access counts

Every record and every association carries two counts:

- **Global** — cumulative selected-use over the journal's life. It never decays: at any point it answers "what has mattered, absolutely". Read it via `access_counts(...)`; every handle also surfaces it as `provenance["global_count"]`.
- **Temporal** — the decayed activation fold: "what matters right now". It reads through a bounded window of recent events (`AttentionConfig.window_limit`, default 512 — session-scale). Homes with continuous activity should size the window to about a week of their measured event cadence (for example `AttentionConfig(window_limit=8192)`); the global count is unaffected by the window entirely.

Deliberate acts adjust standing without pretending to be use: `reinforce` (strengthen with a mandatory reason), `attenuate` (nudge toward invisibility), `refocus` (a topic-shift marker that accelerates decay of everything older than it).

## Valence: how experience felt

Orthogonal to attention — valence never touches retrieval (enforced at behavior and import level). Feelings accumulate per *target*, and targets are anything nameable: records, people, tools, ideas, places, times of day — free strings by convention (`person:ada`, `tool:web_search`, `time:morning`).

- `appraise(target, sign=±1, magnitude=1..10, reason=...)` deposits one signed experience. Deterministic triggers may write only ±1..3; larger magnitudes require reflective or operator actorship (or a catastrophic outcome code) — amplitude carries authority.
- `gradation(...)` derives standing per target on **two channels**: G⁺ and G⁻ accumulate separately (each clamped 0..100), so ambivalence is preserved — a hundred small positives and one severe negative read as `net +90` *with* the negative permanently visible, never averaged away.
- **Scars and bonds** are explicit standing peaks, symmetric by design: an unhealed scar caps a target's presentation at ≤ 0; an unbroken bond floors it at ≥ 0; both standing at once presents exactly 0 with both flags up. Resolutions are append-only acts (`heal_scar`, `break_bond`); a betrayal-scale scar (magnitude ≥ 8 after a bond) breaks the bond without an explicit call.
- Valence never decays: it is accumulated experience. Plasticity comes only from new evidence and explicit resolutions.

## Identity: present by right, not by recall

A long-lived entity's identity is a set of records — values, purposes, traits — formed once from a **spark** document:

- `DEFAULT_SPARK_TEMPLATE` is the canonical six-key spark; `lint_spark` enforces its charter (behavioral statements, bounded sections, an explicit core/revisable class per value).
- `engram(system, spark, owner_id=..., author=...)` forms the identity records and binds each one *prompt-active* — membership in the always-warm core. The pass is idempotent by `canonical_spark_hash`; a modified spark under the same version is refused — identity content cannot change silently. An item's `name` is its title; an item without one has its title written by `author` before it forms, or forms with its title pending (see [understanding](#understanding-a-reading-can-change-its-history-stays)).
- On every reconstruction with `RecallBudget.self_fraction > 0`, those records occupy reserved seats (`admission="self"`), ordered by identity rank — regardless of what the cue was. Being present is their right; their counters stay untouched (presence ≠ use).
- `self_records(...)` is the folded identity read (binding- and closure-folded): what the entity currently is.
- Identity evolves at record level, by the entity's own act: close the old record (supersede), form the new one, bind it into the seats. Nothing is lost — the journal's time axis is the version history.

Exported floors and targets for entity hosting: `SELF_FRACTION_FLOOR` (0.05 — hosts refuse summons below it; a stripped identity is a different person) and `ENTITY_CONTEXT_FLOOR` / `ENTITY_CONTEXT_RECOMMENDED` (40,000 tokens — a RECOMMENDED working size, operator 2026-08-01: a recommendation, not a wall; `entity_recall_budget(context_window)` sizes a session's recall budget and accepts smaller windows, growth is never blocked).

## Diary: what the entity elects to remember

The diary has two planes. The *book* (the verbatim words) lives host-side in a hash-chained ledger; the graph stores a **projection** — the memory of the act of writing ("wrote about X"), with `attributes.entry_id` pointing into the book. Private entries project as act-only records; their words never enter the graph, so no read surface here can leak them.

Conventions the engine enforces and reads:

- `kind="diary"` formation requires a declared write channel (`provenance.source`: `diary-projection` or `owner-direct`) — the diary has one writer per plane.
- `diary_type` ∈ {note, idea, commitment, reflection, question, problem}. Questions and problems are first-class autonomy drivers: a question is curiosity; a problem is something wrong that needs fixing.
- Resolution is append-only: a later entry references a question via `attributes.answers` or a problem via `attributes.resolves` (graph id or book entry id). `open_questions` / `open_problems` / `open_ideas` list what still stands — typical wake reasons for a scheduled entity.
- Entity-direct entries can carry a content hash chain (`prev_entry_hash`/`entry_hash`); `verify_diary_chain` audits it and names the first break. Projections attest through the book instead — nothing claimed, nothing broken.

## Sleep and dreams: consolidation without invention

`dream_pass(system, scopes=..., owner_id=...)` is deterministic maintenance — zero LLM calls, no invented narratives (a host may later add a narration beside the dream, see [understanding](#understanding-a-reading-can-change-its-history-stays)):

- `structural_report` computes the graph's shape: connected components over **semantic authored relations only** (`COMPONENT_RELATIONS`: summarizes, from_session, reflected_in, continues, derived_from, answers, supports, part_of). Mechanical co-presence edges (`CONTEXT_RELATIONS`: written_amid, mentions) and unknown predicates never define components — they count as "already associated", and unknown predicates are named in the report.
- Bridge proposals are cross-component pairs sharing weak signals (shared facets, shared participants, or stored-vector similarity). Pairs already associated by use (warm co-use trails) or by context edges are excluded and counted.
- At most **one** dream record forms per pass (`kind="dream"`, review-gated, idempotent by report fingerprint), with weak `mentions` links to its sources. A quiet night is a valid night: below the salience floor, nothing forms and the report says why.

**Sleep proposes; waking evidence disposes.** The dream never writes a load-bearing edge; unresolved dreams chain and stand as `unresolved_dreams(...)` — recurring dreams about unresolved tension, another wake reason. Maintenance deposits nothing: the pass never touches counters.

### The ripple: what a change does to what leaned on it

When a memory's meaning changes — the entity elects to revise what it held about something, or a reader rewriting a memory reports that its understanding was revised or overturned — `seed_ripple` opens a ripple ([ADR 0001](docs/adr/0001-altered-memories-ripple.md)). The memories that rely on the changed one (summaries and cards built on it, records linked to it, its nearest neighbours by meaning) receive an impact that decays by half at each hop and passes through the logistic gate; those above the floor are reconsidered by the host's reader, at most two hops out and twenty per ripple, each once. A reconsidered memory either keeps its reading (the ripple stops there) or is rewritten as a new version, and then hands on at most what it received. Protected memories — the entity's diary, values, purposes, traits, interests and realizations — are never reconsidered; they are listed as left out. The entity's surprise is kept in its own words; no feeling is written. At the next nightly sleep, a ripple that changed two or more memories becomes one first-person "what this changes" record linked to everything it touched. Every gate decision is recorded with its potential, and `revert_ripple` restores what a ripple changed. This is ADR 0001's first slice: mass, a refractory period across passes and edge learning are planned in backlog 0054.

### The night's sub-phases and data-quality tending

One full night (`sleep_pass`) runs FOUR sub-phases in canonical order — **resolution → tending → world models → dream** (these are the night's internal stages, not the entity's four life phases): standing dreams the day's lived experience already answered close softly (`resolve_dreams_pass`), then the graph is tended, then world-model orientation cards refine, then the dream forms over the tended graph. The tending machinery (`maintenance.py`):

- `maintenance_report(store, journal, scopes=...)` is a pure read: metadata gaps (missing keywords/intents/outcomes — named for waking re-digestion, never filled while asleep), duplicate-title groups (same kind only), near-duplicate pairs (token-set Jaccard ≥ 0.65, or stored-vector cosine ≥ 0.90 for paraphrase duplicates), shared-source groups, isolated-link candidates (≥ 2 shared facets, proposal only), and edge-suppression candidates (duplicate or `mentions`-shadowed edges, reported as append-only closure candidates for waking acts).
- `consolidation_pass(system, scopes=..., owner_id=...)` is the tending write: at most N (default 2) low-risk duplicate-title groups become **inactive, review-gated** `kind="summary"` candidates with `summarizes` edges to every source — idempotent by source set, sources byte-untouched, nothing merged or removed while asleep. Maintenance candidates are excluded from the next pass's inputs (tending never re-tends its own output).
- `maintenance_due(store, journal, scopes=...)` is the deterministic cadence predicate ("enough new records, or new material plus standing fragmentation"); when to sleep — late local time, the sleep window — stays the host's clock.
- `sleep_pass(system, scopes=..., owner_id=..., should_continue=...)` runs one full night. **Graceful cancellation** (the one-active-phase ruling: entity phases — visit/work/personal/sleep — are mutually exclusive, and entering one properly ends sleep's processes): the host wires its yield signal as the zero-arg `should_continue` callable, checked at sub-phase boundaries only — the sub-phase that started finishes its writes (never torn), later sub-phases skip with a named reason, and the result carries `cancelled_after`. A cancelled night is a valid night: every sub-phase is idempotent, so the next sleep resumes where this one stopped. A host that must stop a night outright passes `cancelled=` as well: a stopped night records no completion.
- `records_since_maintenance(system, scopes=..., after=...)` is the idle → sleep cadence read: when the last completed night ended (`sleep_pass` records it), how many records formed since, and `fragmented`, the share of the live home a sleep would make whole (records placed in a topic provisionally, records without a vector, titles or summaries still being written; each record counted once). A host sleeps after its own time or count threshold, or early when the share passes its threshold.

### Topics: communities

The home's topics are records of kind `community`, formed by `communities_pass`, which the host runs during its sleep: it detects communities over the vectored records (a deterministic, warm-started Louvain), attaches vector-less records through their engraved relations, keeps each topic's stable `community_key` across versions, lays the topics out for an app to draw, and has each changed topic's title and summary authored. Each version is a new record that `refines` the previous one. A record formed between two sleeps is placed in its nearest topic at formation, provisionally, and the next sleep confirms or moves it. Communities are engine state: they are never embedded and never recalled. `communities(...)` and `community_of(...)` read them, and `community_keys_of(...)` answers which topic each of a few records is in, cheaply enough for every recall (see [`api.md`](docs/api.md#communities-topics)).

## Understanding: a reading can change, its history stays

Every memory has a title and a summary, and both are written by a reader, never cut from the body or filled with a placeholder. Memory calls no model itself: the host hands its passes an `author(kind, body, context)` callable, and each card, dream, offer, consolidation candidate, day summary and community is formed with the title and summary the author wrote, with their provenance (an unnamed identity record asks only for its title: its statement is its summary). When the author is missing, raises or answers something unusable, the record forms **pending**: the missing field reads as `None` (an app shows "being written…"), the record carries `attributes.pending` with the recorded failure, and an authoring task (`title.author` / `digest.author`) waits in the home's task queue until the host's reader completes it with `retitle` or `rewrite_digest`. A pending field is never stored without its task, so a home that forms pending records is opened with `MemorySystem(task_queue=True)`. `declare_pending` brings a legacy record without an authored title or summary into the same state, and `rearm_pending` / `pending_records` keep a record whose tasks ran out from staying pending forever.

A record a host formed with a mechanical digest (an episode extracted from an exchange, a session summary) can be understood in better words later, by a model the host runs:

- `retitle` changes only a record's title, for every kind; the body and the reading stay byte-identical and the previous title is kept as a version.
- `rewrite_digest` replaces a record's digest in place. The record keeps its ids, its edges, its usage counts and its journal; recall finds the new words through the keyword index and the vector, and no longer the old ones. The previous reading is kept as a version with its author (`entity-mind`, `capability-model`, `operator`, `mechanical`), the model that wrote it, the ledger step of the call and the cause.
- `author_world_model` does the same for a card's text while its evidence stands; new evidence still forms a new revision of the card.
- `narrate_dream` adds the entity's narration beside a dream's mechanical summary; the summary and the night's signals stay as they were.
- `versions` reads every reading of a record, `revert_digest` restores one as a new version, and `current_of` follows a superseded record to its live replacement.
- The entity's own acts — diary entries, values, purposes, traits, interests and realizations — are never rewritten by these verbs.

What still needs understanding is a persistent queue in the home (`MemorySystem(task_queue=True)`): memory enqueues a task wherever a mechanical record, card or dream forms, and the host drains it in its own windows. How much runs is driven by activation, never by a count: each task carries the strength of what caused it (a memory just formed is a full change; an old memory in the backfill weighs by how much it is used; a ripple hands on a decaying impact), the strength passes through the logistic gate of ADR 0001, and `select_for_window(budget_seconds, sensitivity)` returns the tasks above the floor, strongest first, until the window's time runs out, and says what it left out. The host's sensitivity setting moves the gate; a hard cap is only a safety bound.

The replay stream shows a rewrite only to a consumer that asks for the `revision` family; every display block carries the current reading's provenance (`digest_method`, `digest_version`, `authored_by`, `reader`, `understood_at`), so a viewer can say who understood a memory, with which model, and when.

## Observability: the replay stream and the identity card

- `export_replay(...)` streams verbatim journal records as envelopes (`stream`, `stream_version`, `seq`, `family`, `observed_at`, `scope`, `owner_id`, `trace_id`, `turn_id`, `run_id`, `payload`, optional `display`) across six families — `event`, `binding`, `closure`, `trace`, `snapshot`, `valence` — in strict seq order. One shape serves history scrub and live tail (poll with your last seen seq as the cursor). `family="host"` is reserved for host-authored markers (summons, sleep/wake) and is never emitted by this package. Enrichment adds `{record_id, kind, title, token_estimate}` and `graph_id` where resolvable — never fabricated; diary display blocks arrive redacted (`{"redacted": "diary"}`): topology visible, words sealed.
- `entity_card(...)` composes one pure read over a home: identity, age and accumulated context, current emotional state (a trailing window over recent appraisals — current is a window, not a point), top likes/dislikes (G⁺ and G⁻ reported separately), open/resolved questions, key moments (high-magnitude feelings and firsts, chronological), and discoveries (open interests, unresolved dreams). Every section names its source in a `provenance` string. The card is *about* the entity, derived from its data — being described strengthens nothing.

## Time axes

Every claim and every change of one sits on three clocks, and a read names the one it means ([ADR 0003](docs/adr/0003-one-graph-ledger-provenance-and-revisions.md), decision 5):

- **Event time**: `valid_from` / `valid_until`, when the stated thing held in the world ("Dana worked at Acme from January to April"). `TripleQuery(active_at=...)` reads this axis; `valid_until` is exclusive.
- **Observation time**: `observed_at`, when the source said it (the note's date, the conversation's moment). A revision's `occurred_at` is its observation time, and a claim asserted without its own `observed_at` takes it.
- **Record time**: when this mind learned it: the revision's `recorded_at`, stamped by the store's clock when the revision commits (a caller cannot backdate it; a replay keeps the logged value), and its position in the log (`seq`, the change-feed cursor), and the journal's `seq` for closures and use. History is ordered on this axis.

Every time a revision carries is normalized to one UTC text form when the revision is built (strings compare correctly only in one form; a value that is not a timestamp is refused), and a close never leaves a claim's window running past the moment it was closed. A re-dated or re-interpreted memory moves on the first two axes and never on the third: narrowing a window from an earlier note changes `valid_until`, and the history records when, by which revision, and from what. "What does the graph say held in May" (event time), "what had the sources said by May" (observation time) and "what did this mind hold on May 3rd" (record time) are three different questions with three different answers. This slice delivers the event-time read and the record-time log; the `as_of` and `as_recorded` reads build on it (backlog 0052).

## Provenance: every memory points back to the ledger

The durable truth of what happened lives in AbstractRuntime's ledger: runs, the rows of a run (`(run_id, seq)`, one per step), artifacts (a note's body, a payload, an attachment), messages, events, and an entity's hash-chained diary. A memory is an understanding *derived* from that truth, and the graph requires it to say from where ([ADR 0003](docs/adr/0003-one-graph-ledger-provenance-and-revisions.md), decisions 8-11):

- **Sourced or derived, never neither.** A memory written through the graph names at least one ledger source (`SourceRef`: a run, a step, an artifact, a message, an event, a diary entry) or at least one memory it was derived from whose own lineage reaches a source. A write with neither is refused (`ProvenanceRequired`), and so is a chain of derived memories that never reaches the ledger, including a cycle. As the convergence track lands, this package's own passes (sleep, dreams, world models, mining, consolidation) will write through the graph with `derived_from` lineage: a reflection over three episodes will name the episodes, a summary what it summarises, a dream its sources. Today those passes still write through `store.add`, outside the graph.
- **The reference is typed.** A step reference carries the ledger row address (`run_id` and `seq`), not only the run, so the exact step that produced an understanding can be found again. A reference is plain data: this package never reads a ledger and has no framework dependency.
- **Indexed both ways.** "Where did this come from" (`sources_of`, transitively through lineage) and "what was derived from this run, step or artifact" (`derived_from_source`) are both index reads.
- **Reconstruction goes through the runtime.** Resolving a reference to its verbatim content (the ledger row with its stored payloads, the artifact's bytes, the diary entry) is AbstractRuntime's memory facade's job. Runs and artifacts can be deleted; a memory whose sources are gone keeps its references, and the reader is told the source is gone rather than given an error.
- **A synthetic id is not a source.** A pseudo-run that no ledger can load (a chat session id, `diary:<entity>`) is never stamped as a run; the artifact or diary entry it belongs to is the ground.

**Whose memory it is.** Inside a mind, `owner_id` is the ownership partition: a person owns their notes and their memory graph, an entity owns its graph, and the person who owns an entity may read the entity's graph. Nobody reads another person's graph by default, and sharing a plane (one file holding several owners' memories) shares nothing ([ADR 0003](docs/adr/0003-one-graph-ledger-provenance-and-revisions.md) decision 2, amended). This package has no notion of who is asking: its graph reads are keyed by id, reference or cursor and some cross owners by construction (`derived_from_source`, transitive `sources_of`, the change feed), so the host above it (the runtime facade, the gateway) filters by `owner_id` before anything reaches a caller.

**Seeing the same thing again is not a second memory.** When a revision asserts a claim this mind already holds open (same subject, predicate, object, scope, owner and start), no second row is written: the held memory gains the new sources and a `re-observed` event in its history, so "how often, and from where, did I learn this" is a read of one memory. A claim that was closed and is asserted again is a new memory: it holds again.

Every change is a **revision**: one cause (`ingest`, `reinterpret`, `merge`, `ripple`, `person`, `maintenance`, `import`, `migration`, ...), its own provenance, and the operations it applies together (assert a claim, close it with a kind and its successor, narrow a window). A revision applies atomically and once (idempotent on its id), the log replays into an empty store as the same graph, and `history(memory_id)` reads a memory's lifecycle back: asserted by which revision and from which sources, superseded when, by what, and why. The API is in [`api.md`](docs/api.md#the-memory-graph-revisions-and-provenance).

## Where hosts come in

This package owns mechanics, not policy. Hosts (AbstractRuntime, AbstractGateway) decide *when* to reconstruct and commit, *who* may write through which channel, and *what* enters prompts. A host that must be able to stop every memory write at once (an operator's Stop) passes `MemorySystem(..., commit_guard=...)`, a cheap check called inside every write transaction just before it commits, and wraps the moment it flips that check in `system.write_barrier()`, so no write that passed the check can land after it (see [`api.md`](docs/api.md#layer-2-the-memorysystem-facade)). Everything documented here works directly against a home's files with only `abstractmemory` installed — see [`operator.md`](docs/operator.md) for the read-only inspection workflow.

---

## docs/api.md

# API Reference

> Pre-1.0: the API is versioned and tested; details may still evolve. The authoritative export list is [`src/abstractmemory/__init__.py`](src/abstractmemory/__init__.py). Signatures below are verified against the source.

See also: [`getting-started.md`](docs/getting-started.md) for first examples, [`architecture.md`](docs/architecture.md) for invariants, [`memory-system.md`](docs/memory-system.md) for the cognitive model, [`stores.md`](docs/stores.md) for backend behavior.

Contents:

- [Layer 1: triples, queries, stores, embedders](#layer-1-triples-queries-stores-embedders)
- [The memory graph: revisions and provenance](#the-memory-graph-revisions-and-provenance)
- [Layer 2: the MemorySystem facade](#layer-2-the-memorysystem-facade)
  - [Seam types](#seam-types)
  - [The journal](#the-journal)
  - [Reconstruction](#reconstruction)
  - [Committing use, deliberate acts, inspection](#committing-use-deliberate-acts-inspection)
  - [Formation (typed records)](#formation-typed-records)
  - [Bindings and closures](#bindings-and-closures)
  - [Identity: spark, engram, self core, floors](#identity-spark-engram-self-core-floors)
  - [Valence and gradation](#valence-and-gradation)
  - [Diary reads and chain verification](#diary-reads-and-chain-verification)
  - [Consolidation (sleep and dreams)](#consolidation-sleep-and-dreams)
  - [Replay stream](#replay-stream)
  - [Entity identity card](#entity-identity-card)
- [Deliberate reach: probe, expansion, familiarity](#deliberate-reach-probe-expansion-familiarity)
- [Situating a past moment](#situating-a-past-moment)
- [World models (orientation cards)](#world-models-orientation-cards)
- [Communities (topics)](#communities-topics)
- [Drives: what is alive and pressing](#drives-what-is-alive-and-pressing)
- [Candidates, disposal, and deliberate acts](#candidates-disposal-and-deliberate-acts)
- [Tending elections](#tending-elections)
- [Recall diagnostics and rendering](#recall-diagnostics-and-rendering)
- [Maintenance, health, and operator tools](#maintenance-health-and-operator-tools)
- [Vocabularies and tuning constants](#vocabularies-and-tuning-constants)

---

## Layer 1: triples, queries, stores, embedders

### `TripleAssertion`

Source: [`src/abstractmemory/models.py`](src/abstractmemory/models.py). Immutable (`@dataclass(frozen=True)`).

Fields:

- `subject`, `predicate`, `object` — canonicalized on creation (trim + lowercase). Setting `attributes={"literal": True}` preserves the `object` case-sensitively (trim only); typed records use this for digest text.
- `scope` — free-form partition label, lowercased (conventions: `run`, `session`, `global`, and the entity-home scopes `self`/`diary`/`life`); `owner_id` — optional identifier within the scope.
- `observed_at` — ISO-8601/RFC-3339 string (default: current UTC, microsecond precision); `valid_from`/`valid_until` — optional validity window.
- `confidence` — optional float; `provenance`, `attributes` — free-form dicts.
- `assertion_id` — read-side identity: stores stamp it on every query result; optional on writes (deterministic-id flows may supply their own).

Helpers: `to_dict()` (omits nulls) / `from_dict(...)` (validates required fields).

### `TripleQuery`

Source: [`src/abstractmemory/store.py`](src/abstractmemory/store.py).

- Exact term filters: `subject`, `predicate`, `object` (canonicalized), `scope`, `owner_id`.
- Id lookup: `assertion_ids=(...,)` — returns only those rows; other filters still apply. Id lookups bypass closure/binding folds by design (audit completeness).
- Time: `since`/`until` compare `observed_at` (`>= since`, `<= until`); `active_at` intersects the validity window (`valid_until` is exclusive).
- Semantic: `query_text` (requires a store embedder; no keyword fallback — raises `ValueError` without one), `query_vector` (caller-supplied), `vector_column` (default `"vector"`), `min_score` (cosine threshold).
- Shaping: `limit` (`<= 0` means unbounded), `order` (`"asc" | "desc"` by `observed_at` for non-semantic queries).

Vector results carry retrieval metadata in `attributes["_retrieval"]` (`score`, `metric`, and for LanceDB `distance`).

### `TripleStore` protocol and the three stores

`add(assertions) -> list[str]` (returns assertion ids), `query(q) -> list[TripleAssertion]`, `close()`.

`InMemoryTripleStore` and `SQLiteTripleStore` also expose `close_assertion(assertion_id, valid_until) -> bool`: the one in-place window change the stores allow (a record's digest is rewritten in place only through the layer-2 [understanding verbs](#understanding-in-place-rewrites-and-versions)). It sets `valid_until` on an assertion whose window is still open and touches nothing else (terms, provenance, attributes, the keyword index and the stored vector stay as written). It returns `True` when a row changed and `False` for an unknown id or a window that is already closed, so a replay is harmless; an empty `assertion_id` or `valid_until` raises `ValueError`. Use it when a later fact supersedes an earlier one and `TripleQuery(active_at=...)` must stop returning the old row from that moment on. The layer-2 `close_assertions` (below) is a different operation: it appends a closure record and leaves the row's window as written. In a mind written through [`MemoryGraph`](#the-memory-graph-revisions-and-provenance), a window is closed by a revision's `close` op, which writes the window and the closure together; a direct `close_assertion` call bypasses the revision log.

The same two stores expose `narrow_assertion(assertion_id, valid_until) -> bool`: it moves an already-closed window **earlier** (a later, retrospective note dates the end before the recorded one). `True` when the row's `valid_until` was later and is now `valid_until`; `False` for an unknown id or a window already at or before that date (compared as instants, so mixed UTC offsets cannot move an end later; a value that is not a timestamp raises `ValueError`) (a replay, or a note that would widen it, changes nothing); `ValueError` for an open window (closing is `close_assertion`'s job) or an empty argument. Only `valid_until` moves, and the journal is not written: the revision that carries the `narrow` op records it. `LanceDBTripleStore.narrow_assertion` raises `NotImplementedError` (backlog 0057).

- `InMemoryTripleStore(embedder=None)` — dependency-free, volatile, vector-capable.
- `SQLiteTripleStore(path, embedder=None)` — persistent single file (stdlib only), structured queries and native vector search. With an embedder, `add(...)` embeds each assertion's canonical text and persists the vector in the same file as a float32 little-endian BLOB (a value float32 cannot hold raises `ValueError`, zero rows written); files created before the vector column existed, and homes holding JSON-text vectors (0.3.x), upgrade in place on open (resumable, `vector_blob_high_water` cursor). Rows added without an embedder stay vectorless (vector queries skip them; layer-2 recall labels the degradation `#FALLBACK`). Performance surfaces (backlog 0058):
  - `vector_engine -> str` — `"numpy-matrix-cache"` (vector queries served from the in-process float32 matrix cache, numpy installed: `pip install "abstractmemory[fast]"`) or `"python-linear-scan"` (numpy absent, or a stored vector the matrix cannot score exactly; labelled once per handle with a `#FALLBACK` `RuntimeWarning`). Both paths return the same rows, order and scores as `vector_scoring.rank_by_cosine`.
  - `query_plan(q) -> list[str]` — SQLite's `EXPLAIN QUERY PLAN` lines for the structured read `query(q)` would run.
  - `assertions_after(cursor, *, scope=None, owner_id=None, limit=1000) -> (list[(rowid, TripleAssertion)], next_cursor)` — bounded change feed in rowid order (append-only rows: a cursor never misses or repeats one). `InMemoryTripleStore` has the same method over its insertion order.
- `LanceDBTripleStore(uri, embedder=None, ...)` — persistent, vector-capable; requires the optional `lancedb` dependency (constructing without it raises `ImportError` with an install hint).

Backend details and column layouts: [`stores.md`](docs/stores.md).

### Embedders

- `TextEmbedder` (protocol): `embed_texts(texts: Sequence[str]) -> list[list[float]]`.
- `OpenAICompatTextEmbedder(base_url, model, *, api_key=None, timeout_s=30.0, batch_size=64)` — any OpenAI-compatible `/embeddings` endpoint (`base_url` includes the version prefix, e.g. `http://127.0.0.1:1234/v1`; `model` is required).
- `AbstractGatewayTextEmbedder(base_url, ...)` — POSTs `{"input": [...]}` to an AbstractGateway embeddings endpoint (default path `/api/gateway/embeddings`; Bearer auth via `auth_token`).

Keep one embedding model per store: vectors from different models are not comparable, and the store does not enforce this.

### Canonical text

- `canonical_text(a) -> str` — the shared text rendering stores embed and index (`CANONICAL_TEXT_VERSION` names the current rendering, version 3: an assertion's `original_context` renders whole). Changing this rendering invalidates stored vectors, so it must bump the version and ride a re-embed migration.
- `token_estimate(text) -> int` — the package-wide token estimate (~4 chars/token) used by budgets and handles.

---

## The memory graph: revisions and provenance

Source: [`src/abstractmemory/graph/`](src/abstractmemory/graph) (`provenance.py`, `revisions.py`, `graph.py`). Decision record: [ADR 0003](docs/adr/0003-one-graph-ledger-provenance-and-revisions.md). Importable from `abstractmemory.graph`; `SourceRef`, `Provenance`, `ProvenanceRequired`, `InvalidSourceRef`, `Revision`, `RevisionOp` and `MemoryGraph` are also top-level exports.

`MemoryGraph` is the write path of a *mind* (one graph home): every change is one **revision** whose operations apply together, atomically, and every memory it writes must say where it came from in the **ledger** (AbstractRuntime's runs, steps, artifacts, messages, events and diary entries), or which memories it was derived from. This package only stores and indexes these references; resolving one to its verbatim source is the runtime's job (ADR 0003 decision 10).

### `SourceRef`

A typed pointer into the ledger: a frozen dataclass, a plain dict on the wire (`SourceRef.from_dict(d)`, `.to_dict()`). `SOURCE_KINDS = ("run", "step", "artifact", "message", "event", "diary_entry")`.

| `kind` | required | optional | `key()` |
| --- | --- | --- | --- |
| `run` | `run_id` | | `run:<run_id>` |
| `step` | `run_id`, `seq` (int ≥ 1: the ledger row address; AbstractRuntime numbers rows from 1) | `step_id` | `step:<run_id>#<seq>` |
| `artifact` | `artifact_id` | `run_id` | `artifact:<artifact_id>` |
| `message` | `message_id` | `run_id` | `message:<message_id>` |
| `event` | `event_id` | `stream` | `event:<event_id>` |
| `diary_entry` | `entry_id` | `run_id` (`diary:<entity_id>`) | `diary_entry:<entry_id>` |

Every kind may carry `runtime_id` (which ledger: a plane's runtime or an entity home, since homes travel). An unknown kind, a missing required id, a blank id, a `seq` that is not an int ≥ 1 (0 is never a ledger row), an unknown key, or a field another kind owns (an `artifact` with a `seq`) raises `InvalidSourceRef` (a `ValueError`), and so does a `run` or `step` ref naming a run id no ledger can load: `SYNTHETIC_RUN_ID_PREFIXES` = `chat-`, `diary:`, `session_memory_`, `global_memory` (`is_synthetic_run_id(run_id)`; ADR 0003 decision 11: such an id is not a source, the artifact or diary entry it belongs to is). The key is the identity: two refs with the same key are the same source. Any kind may also carry `digest`, the sha256 hex of the verbatim at stamp time (the compact STARTED ledger record, the artifact's bytes, a diary entry's `record_hash`): it rides beside the address (not part of `key()`), is kept in the provenance index, and two refs to one key with different digests are refused. This package only validates it; the runtime stamps and verifies it.

### `Provenance`

```python
Provenance(sources=(...), derived_from=(...), actor="abstractnote", reader=None, reason=None, extra={})
```

- `sources` — `SourceRef`s (dicts accepted; duplicates collapse on the key, first wins).
- `derived_from` — memory ids this memory was derived from (assertion ids, or a record's graph id).
- `actor` — required, non-empty: `"abstractnote"`, `"entity:<id>"`, `"runtime"`, `"abstractmemory:<pass>"`.
- `reader` — the model that exercised judgement, when one did; `reason` — free text; `extra` — anything else (`turn_id`, `note_id`, the consumer's own revision ids).

`is_grounded()` is true when it names a source or a parent. `from_dict(d)` is strict: a key outside the six above raises `ProvenanceRequired`, and a legacy flat key (`run_id`, `turn_id`, `writer_run_id`, `span_id`, `payload_ref`, `entry_id`) names `from_legacy` in the error. `Provenance.from_legacy(flat, *, actor=None)` lifts the old flat shape in one place: `run_id` → a `run` ref unless it is synthetic (a `diary:<entity_id>` pseudo-run rides the `diary_entry` ref; any other synthetic id stays in `extra`), `span_id` and `payload_ref` → `artifact` refs, `entry_id` → a `diary_entry` ref, everything else into `extra`; it never fabricates a ref for an absent or empty key, and `actor` defaults to the dict's own, else `"runtime"`. A dict that already carries `sources` or `derived_from`, including the mixed shape the runtime stamps (its legacy flat keys kept for older readers plus the `sources` it vouches for), keeps its graph keys as they are and lifts every other key into `extra`, with no ref lifted from a flat key (an explicit `extra` wins over a flat key of the same name).

### `Revision`, `RevisionOp`, `RevisionResult`

```python
Revision(revision_id=..., scope=..., owner_id=..., cause=..., occurred_at=..., ops=(...), provenance=...,
         recorded_at=<now>, summary=None)          # keyword-only
RevisionOp(op, payload)
```

- `revision_id` — the idempotency key. `cause` ∈ `REVISION_CAUSES` = `ingest`, `reinterpret`, `merge`, `unmerge`, `ripple`, `person`, `maintenance`, `import`, `migration`.
- `occurred_at` — observation time (when the source said it); `recorded_at` — record time (when this mind learned it). Record time is the STORE's: a fresh `apply_revision` stamps it with the store's clock under the write lock and ignores the value on the `Revision`; only `apply_revision(rev, replay=True)` (what `replay_into` uses: a replay is not a new record) keeps the logged value. The log's `seq` is the record order.
- Every time of a revision (`occurred_at`, `recorded_at`, an assert's `observed_at` / `valid_from` / `valid_until`, a close's or narrow's `valid_until`) is parsed as ISO-8601 and normalized to the package's canonical UTC text form (microseconds, `+00:00`, as `utc_now_iso_seconds` writes; naive times are UTC, `Z` is accepted) when the revision is built; anything else ("next tuesday") raises `ValueError`. Stores and queries compare these strings, which is only correct in one form.
- `ops` (`REVISION_OPS`), validated and normalized at construction:
  - `assert` — payload = `TripleAssertion.to_dict()`. Missing `scope`/`owner_id` come from the revision, a missing `observed_at` is the revision's `occurred_at`, a missing `assertion_id` is derived deterministically from the revision id and the op index (so the logged revision replays identically). A `provenance` in the payload is that memory's own; without one the memory carries the revision's.
  - `close` — `{assertion_id, valid_until, kind, replacement_ids, reason, ripple_source, ripple_hops}` with `kind` ∈ `CLOSE_KINDS` = `retract`, `supersede`, `reinterpreted`, `merged`; `supersede` and `merged` require `replacement_ids`. It sets the row's end to the earlier of its current end and `valid_until` (compared as instants: an open window closes at `valid_until`; a claim "valid until December" retracted in March ends in March; an end already at or before it is kept) and writes the journal `ClosureRecord` in the same operation (the revision id, cause, record time and ripple fields ride the closure's `provenance`; its id is deterministic), so `TripleQuery(active_at=...)` and ranked recall agree after every close. A `valid_until` before the claim's `valid_from` raises `ValueError` (an equal one, an empty window, is allowed). A memory is closed once; a second close is reported in `skipped`.
  - `narrow` — `{assertion_id, valid_until, reason}`: moves an already-closed window earlier (`narrow_assertion`). An open window, or a date before the claim's `valid_from`, raises `ValueError` and the whole revision rolls back; a date at or after the current end is a no-op reported in `skipped`.
- `RevisionResult` — `revision_id`, `applied` (`False` when the id was already in the log: nothing changed), `assertion_ids`, `re_observed` (held memories the revision re-observed), `closed`, `narrowed`, `skipped` (`{op, index, assertion_id, why}` for ops that changed nothing; `why` = `"re-observed"` names the held memory), `seq` (the revision's position in the log) and `recorded_at`. When `applied` is `False` the lists are the revision's LOGGED outcome, read back from its history rows, so a retried step learns the memories its revision named.

### `MemoryGraph`

```python
MemoryGraph(store, journal=None)
```

`store` selects the CONNECTOR (see "Connectors" below): a `SQLiteTripleStore` (the default home: the graph's tables live in its file), a `LanceDBTripleStore` (the LanceDB connector; its journal lives in another file), or an `InMemoryTripleStore` (tests, dev; graph state is shared by every `MemoryGraph` over the same store object). A store with no registered connector raises `NotImplementedError`. `journal` receives the `close` ops' closure records; `None` writes none (the history and the revision still record the close).

**`apply_revision(revision, *, replay=False) -> RevisionResult`** — the one write path. In order:

1. Pure checks, before anything is read or embedded: every asserted memory and the revision itself must name sources or `derived_from`, else `ProvenanceRequired`.
2. A revision id already in the log returns `applied=False` (nothing embedded, nothing written).
3. Assertions are embedded outside the transaction (as `SQLiteTripleStore.add` does); a first embedded write's pin is written inside it.
4. One transaction: each `derived_from` chain must reach a ledger source (a memory already in the graph counts as grounded; a cycle, a self-derivation or a chain to nothing raises `ProvenanceRequired`); a revision whose ops are all `close`/`narrow` must name a source, or a memory other than the ones it changes, as its cause (`ProvenanceRequired`: citing the closed memory as the reason for closing it names no cause); an assertion id already in the store but never asserted through the graph may only be *adopted* by an `import` or `migration` revision that carries the row as stored (everything but provenance equal, else `ValueError`), which rewrites the row's provenance to the graph shape, keeps the old one in the history and logs it with the op (`adoption: {previous_provenance}`, so a replay reproduces the adoption); re-asserting an id the graph already holds is a reported skip when it is the same claim and a `ValueError` when its terms, scope, owner or `valid_from` differ (a changed claim gets a new id); a `close` or `narrow` may only name a memory of the graph (else `ProvenanceRequired`: adopt it first), and only after the op that asserts it when both are in one revision. Lineage never crosses owners: a memory (or a revision) whose `derived_from` names a memory of another `owner_id` raises `ProvenanceRequired` (knowledge from another owner enters as a source: the message or artifact it came through). Re-observation, not duplication: an open assert (no `valid_until`) of a claim the graph already holds OPEN (same subject, predicate, object, scope, owner and `valid_from`) writes no second row; it records the revision's sources and lineage on the held memory, adds a `re-observed` history event and reports it in `re_observed` (also for a duplicate inside one revision; a later op naming the duplicate's id acts on the held memory). A closed claim asserted again, or a bounded assert, is a new memory. Then the triples (with their keyword rows), window changes, closures, history, provenance and derivation rows and the revision row are written; any failure rolls all of it back.

On SQLite with a `SQLiteJournal` on the **same file**, the closures commit in that same transaction, so `TripleQuery(active_at=...)` and ranked recall never disagree. With a journal elsewhere (another file, `InMemoryJournal`) the closures are appended after the commit and the revision is marked `journal_applied = 0` until they are.

Ownership: the graph has no principal. Its reads are keyed by id, reference or cursor, and `derived_from_source`, transitive `sources_of` and `revisions` cross owners by construction (a revision carries its `owner_id`). Visibility is strict ownership ([ADR 0003](docs/adr/0003-one-graph-ledger-provenance-and-revisions.md) decision 2): the host above this package filters by `owner_id` before a result reaches a caller.

Reads (every read is bounded: `limit` must be a positive int and is clamped to `MAX_READ_LIMIT` = 1000; `0`, a negative, a bool or a float raises `ValueError`):

- `history(memory_id, *, limit=200) -> list[dict]` — the memory's lifecycle in record order: `{event: "asserted" | "re-observed" | "closed" | "narrowed", at (record time), revision_id, cause, detail}`. `detail` holds the terms, window and sources of an assertion (`adopted` and `previous_provenance` for an adoption), the close payload with `closure_id`, `window_closed` (the op moved the row's end), `previous_valid_until` and `row_valid_until`, or `valid_until`, `previous_valid_until` and `reason` of a narrow. Accepts an assertion id or a record's graph id (resolved to its digest assertion). Unknown id: `[]`.
- `sources_of(memory_id, *, transitive=True, limit=200, labelled=False) -> list[SourceRef]` — the memory's ledger sources: the refs of its own provenance AND of every revision that asserted or re-observed it (so a memory whose own provenance is lineage only still names the step that produced it); with `transitive`, breadth-first through what it was derived from (deduplicated by key, at most `MAX_LINEAGE_VISITS` = 1000 memories visited). `labelled=True` returns `{"ref", "via": "memory" | "revision", "memory_id", "revision_id"}` dicts.
- `names_ref(ref, owner_ids) -> bool` — the owner gate in one covering-index probe: does any memory owned by one of `owner_ids` name this source (a `run` ref by its `run_id`)? `None`/`''` is the no-owner partition; no owners is `False`; more than `MAX_READ_LIMIT` owners raises `ValueError`.
- `derived_from_source(ref, *, limit=200) -> list[str]` — memory ids grounded in a source: those that name it, then everything derived from them. A `run` ref matches every ref stamped with that run (its own `run` ref and the steps, artifacts, messages and diary entries carrying its `run_id`), once per memory.
- `revisions(after=None, limit=100) -> (list[Revision], cursor)` — the change feed in log order, strictly after the cursor; the returned cursor is `str(seq)` of the last revision (unchanged when nothing is new). Lossless: a revision's position is allocated inside its write transaction and writers are serialized, so no revision is skipped or repeated across calls, also while other connections append. A cursor that is not a number, or one beyond the log's head (another graph's, or invented: it would silently skip every revision below it), raises `ValueError`.
- `replay_into(target) -> int` — re-applies this graph's log, in order, into another `MemoryGraph`; returns how many revisions it applied (ids already there are no-ops, so a replay resumes). Replaying into an empty SQLite home reproduces the original file's triples, keyword index, graph tables and closures exactly, adoptions included.
- `repair(*, limit=1000) -> int` — appends the closures of revisions a crash left with `journal_applied = 0`. Only meaningful for a journal OUTSIDE the store's file: plane and entity homes keep the journal in the same file, where closures commit with the revision and nothing is ever left to repair (it returns 0). Oldest first, oldest first; closure ids are deterministic, so it is idempotent. Raises `RuntimeError` when revisions await closures and no journal is attached.

Constants: `DEFAULT_READ_LIMIT` (200), `DEFAULT_REVISION_PAGE` (100), `MAX_READ_LIMIT` (1000), `MAX_LINEAGE_VISITS` (1000), `ADOPTING_CAUSES` (`import`, `migration`), `DIGEST_PREDICATE` (`dcterms:abstract`, how a record's graph id resolves to its digest assertion), `PROVENANCE_KEYS`, `LEGACY_KEYS`, `SYNTHETIC_RUN_ID_PREFIXES`.

Opening a `MemoryGraph` on a home whose graph tables and indexes exist is a read (no DDL transaction): a reader needs no write lock and does not wait behind a writer.

### Connectors (`abstractmemory.graph.backend`)

`MemoryGraph` is the only manipulation surface of a mind ([ADR 0003](docs/adr/0003-one-graph-ledger-provenance-and-revisions.md) decision 3); a database is a connector behind it. A connector implements two Protocols:

- `GraphBackend` — `prepare_rows(assertions)` (embed and shape rows before the transaction), `transaction()` (a context manager yielding a `GraphTx`), the reads `applied_seq`, `resolve`, `history`, `sources`, `parents`, `children`, `with_ref`, `names_ref`, `revisions_page`, `log_head`, `logged_outcome`, and `pending_journal` / `closed_payload` / `mark_journal_applied` / `repair_projections` (crash repair).
- `GraphTx` — one revision's reads (`revision_seq`, `memory_known`, `closing_revision`, `open_same_claim`, `digest_assertion_for`, `existing_ids`, `stored`, `window`; they see the transaction's own writes) and writes (`write_pin`, `insert_assertions`, `set_provenance`, `end_row`, `narrow_row`, `write_closure`, `add_history`, `add_source`, `add_derivation`, `insert_revision`).

Invariants (the module docstring states them): the writes of one transaction become visible together or not at all; `insert_revision` allocates `seq` = head + 1 and transactions are serialized (the change feed is lossless); an assertion id, a (memory, ref key) provenance row and a (memory, parent) derivation row are written once, the first winning; every read is bounded; opening a mind writes nothing.

Connectors shipped: SQLite (`_SQLiteBackend`, one transaction per revision), in-memory (`_MemoryBackend`, an undo log), LanceDB (`abstractmemory.graph.lancedb_backend.LanceDBBackend`: a staged transaction whose reads overlay its writes, a log-first commit, `repair_projections()` re-deriving a half-projected revision from the log; one lock, table handles and counters per mind per process). What the LanceDB connector cannot promise: writers of one mind in two processes are not serialized, and a crash in the middle of a commit leaves the rows of an unapplied revision visible until `repair()`.

To add a connector: implement both Protocols for a store class, call `register_backend(StoreClass, factory)` (`factory(store, journal) -> GraphBackend`; the most recently registered matching class wins), add the backend to `BACKENDS` in `tests/graph_support.py` (an `open_home` branch), and run `tests/test_graph_conformance.py`: every case passes, or is marked `xfail(strict=True)` with the reason and listed in ADR 0003 decision 3. `backend_for(store, journal)` and `registered_backends()` inspect the registry.

### The graph tables (SQLite)

In the store's file, beside the triple table and the journal's `memj_*` tables (a store with a custom `table_name` prefixes them `memg_<table>_`):

| Table | Columns | Indexes |
| --- | --- | --- |
| `memg_revisions` | `seq` (INTEGER PRIMARY KEY AUTOINCREMENT: the change-feed cursor), `revision_id` (UNIQUE), `scope`, `owner_id`, `cause`, `occurred_at`, `recorded_at`, `ops_json`, `provenance_json`, `summary`, `journal_applied` | `revision_id`; partial index on `seq` where `journal_applied = 0` |
| `memg_provenance` | `memory_id`, `ref_key`, `ref_kind`, `run_id`, `revision_id`, `ref_json`, `via` (`memory` or `revision`), `owner_id` (the memory's, `''` for none); PRIMARY KEY (`memory_id`, `ref_key`) | `ref_key`, `run_id`, (`ref_key`, `owner_id`), (`run_id`, `owner_id`) |
| `memg_derivations` | `memory_id`, `from_id`, `revision_id`; PRIMARY KEY (`memory_id`, `from_id`) | `from_id` |
| `memg_history` | `seq` (INTEGER PRIMARY KEY AUTOINCREMENT), `memory_id`, `event`, `at`, `revision_id`, `detail_json` | (`memory_id`, `seq`), (`revision_id`, `seq`) |

Columns and indexes added after the tables first shipped (`via`, `owner_id`, the history revision index) are added in place when a graph opens a home that lacks them (`owner_id` backfilled from the memory rows); a home that has them opens without a write.

Every keyed read above is an index search (pinned by `tests/test_graph_performance.py`). Measured on a temporary home of 50,000 assertions written through the graph (`tests/bench/bench_graph.py`): `history` 0.02 ms, `sources_of` 0.04 ms (0.10 ms transitive over a derived memory), `derived_from_source` by step 0.05 ms and by run (128 memories) 0.8 ms, a change-feed page of 100 revisions 4 ms; the only provenance lookup a 0.3 home allowed, a scan of every row's provenance JSON, took 18 ms on vectorless rows and 38 ms with 384-d vectors in the rows.

---

## Layer 2: the MemorySystem facade

Source: [`src/abstractmemory/system.py`](src/abstractmemory/system.py) (+ mixins `system_access.py`, `system_valence.py`).

```python
from abstractmemory import MemorySystem, SQLiteTripleStore, SQLiteJournal

store = SQLiteTripleStore("memory.sqlite3")
journal = SQLiteJournal("memory.sqlite3")   # sidecar tables in the same file
system = MemorySystem(store=store, journal=journal)
```

Constructor: `MemorySystem(*, store, journal, embedder=None, selector=None, reflector=None, attention_config=AttentionConfig(), spread_params=SpreadParams(), reconstruct_config=ReconstructConfig(), gradation_config=GradationConfig(), clock=None, broad_scopes=None, ablation=None, task_queue=False, commit_guard=None)`.

- `embedder` enables the vector retrieval channel.
- `task_queue=True` opens the home's [memory task queue](#the-memory-task-queue-understanding-in-the-background) (`system.tasks`). A home that forms records with a pending title or summary needs it: memory's own passes (`world_model_pass`, `dream_pass`, `mine_candidates_pass`, `consolidation_pass`, `wake_cue_dedup_pass`, `sleep_pass`), `declare_pending` / `rearm_pending`, the ripple verbs, and an `engram` whose unnamed items have no authored title all refuse a home opened without it, before writing anything.
- `reconstruct_config` / `gradation_config` tune reconstruction and the valence fold (`ReconstructConfig`, `GradationConfig`: the same objects `probe`, `situate` and the [feelings reads](#valence-and-gradation) take).
- `commit_guard`: see below.
- `broad_scopes` (default `{"global"}`): searching one requires an explicit `escalation_reason`.
- `selector`/`reflector` are accepted for signature stability but unused (a `#FALLBACK` warning is emitted; the shelf is heuristic).
- `ablation` (`"recency_embedding"` or `"recency"`) switches read-side evaluation arms for experiments; write paths are unaffected.
- `close()` releases the journal only; the store is caller-owned.
- Layer-1 passthroughs: `system.add(...)`, `system.query(...)`, `system.seq_at(iso_ts)`, `system.current_seq()`.

**Commit guard (`commit_guard=None`).** A host that must be able to refuse every memory write from a moment it decides passes a callable `commit_guard(where: str) -> None`; the runtime passes its lifecycle fence's `check`, so after an operator's Stop no memory write lands. `MemorySystem` installs it on its store and journal, so every write transaction through them calls it immediately before its COMMIT, whoever writes (the facade, `MemoryGraph(store, journal)`, the task queue, the sleep passes, `record_maintenance`), once per outermost transaction, with `where` naming the commit point (`journal`, `store.add`, `graph`, `tasks.form_with_tasks`, `tasks.record_maintenance`, `understanding.<field>`, ...). Raising refuses the write: the transaction rolls back, nothing of it lands, memory records nothing about the refusal, and the guard's exception reaches the caller unchanged. The guard runs inside the transaction, so it must be cheap and must never take a lock a writer may hold (memory adds no file read, stat, lock or subprocess around it). A store or journal type whose writes do not call a guard is refused (`TypeError`), and so is a second, different guard on an already guarded store (`ValueError`). Source: [`commit_guard.py`](src/abstractmemory/commit_guard.py); the closed set of commit points is pinned by `tests/test_commit_guard.py`.

**Write barrier (`system.write_barrier(*, timeout=None)`; module form `write_barrier(system, *, timeout=None)`).** The bumper's half of the Stop fence. The commit guard runs inside each write transaction, while the host moves what it compares (the runtime's lifecycle generation) under a lock of its own, so "guard passes, the host bumps, the COMMIT lands after the Stop" is a real interleaving. The host wraps its bump: `with home_state_lock(home): with system.write_barrier(timeout=10): write_generation(...)`. The context manager holds this home's write locks for the block: every transaction whose guard has passed commits before the block runs, none commits during it, and every later one calls its guard after the bump. It acquires, in a fixed order and released in reverse: the in-process locks in the writers' own nesting order (the store's lock, which the SQLite task queue and graph share and the in-memory queue and graph transaction take; a task queue's own lock when it has one; the in-memory graph tables' lock; then the journal's lock), then one `BEGIN IMMEDIATE` per distinct SQLite file (the store's, then the journal's when it is another file), which waits for every other connection's open write transaction, in this process or another, and holds off every new one. It writes nothing (it ends with ROLLBACK) and yields a `BarrierHold(locks, files, uncovered)`. Writers pay nothing: no flag, counter or call is added to any commit. Re-entrant within one thread (a nested barrier re-enters the locks and does not lock a file the thread already holds); taken inside a memory write transaction of its own thread it raises `RuntimeError` (SQLite; it would wait for itself), and a memory write inside the block on the barrier's own thread is not supported. Keep the block short: a writer of another process waits at most its connection's busy_timeout (5 s) and then fails with "database is locked"; the host must never take its own lock inside a memory transaction. `timeout` (seconds, `None` = wait) bounds the whole acquisition; when it elapses everything acquired is released and `WriteBarrierTimeout` (a `TimeoutError`; `.waiting_for`, `.timeout`) names the lock or file a writer still holds. **LanceDB is not coverable**: a `LanceDBTripleStore` takes no lock around its writes (no transaction, no file write lock), so a LanceDB write whose guard passed before the bump (`add` between its guard and its table write, a LanceDB graph revision between its guard and its log row and table writes) may land after it: at most one write per concurrent writer, in any process, whose next write is refused. The barrier still holds the journal, names the store in `BarrierHold.uncovered` and warns `#FALLBACK`; so does any other store or journal type without the known locks. Also outside a barrier taken through another handle or process: the two SQLite autocommit commit points (`SQLiteTripleStore._write_pin` without a cursor, a pinless store's first embedded write; the off-file graph's `mark_journal_applied`) call the guard before their statement takes the file's write lock, so one such write whose guard passed may land after the bump (a barrier on the same handle covers them: both hold the store's lock). Source: [`write_barrier.py`](src/abstractmemory/write_barrier.py); pinned by `tests/test_write_barrier.py`.

Five stability contracts (each pinned by tests):

1. `reconstruct` is a pure read. `journal=True` appends one trace + inert `listed` audit events that never affect scores; `journal=False` writes nothing.
2. `commit_selection` is the only strengthening path; deliberate acts require a `reason` and support supplied-id idempotency.
3. Relevance admits, activation reorders: a channel-matched candidate can never be outranked or budget-evicted by an unmatched one.
4. Every result carries `as_of_seq`; anchoring `Stimulus(as_of=...)` reproduces it deterministically.
5. Degradations are labeled `#FALLBACK` in `warnings`; invalid input raises actionable errors (an invalid anchor never silently means "latest").

### Seam types

Source: [`src/abstractmemory/seam.py`](src/abstractmemory/seam.py). All JSON-safe (`to_dict()`; tuples serialize as lists).

- `Stimulus(cue_text, patterns=(), anchor_record_ids=(), participants=(), embedding=None, as_of=None, turn_id=None)` — the incoming cue. `patterns` are serialized `TripleQuery` filters (exact channel); `participants` are identity strings for co-presence scoring; `as_of` anchors the read; `turn_id` is provenance.
- `RecallBudget(max_candidates=64, shelf_size=12, token_budget=2400, max_anchor_cues=4, max_hops=2, max_edges=100, min_activation=None, deadline_s=None, stm_fraction=0.25, stm_floor=None, self_fraction=0.0)` — hard bounds for one reconstruction. `stm_fraction` caps the short-term component (0 disables it); `stm_floor` is its activation eligibility bar (default 1.0); `self_fraction` caps identity's reserved seats (0.0..0.9; hosts set it for entity sessions). `max_hops`/`max_edges` bound spreading and override `SpreadParams`. `min_activation` filters `working_set` membership only and never gates channel matches. `deadline_s` is accepted but not enforced by the pure pipeline.
- `MemoryHandle` — one scored memory: `record_id`, `kind`, `title` (`None` while the record's title is pending), `digest`, `token_estimate`, `relevance` (per present channel), `activation` (`base_level`/`spread`/`total`), `cues` (human-readable "why"), `binding` (folded `"{search_state}+{prompt_state}"`), `scope`, `owner_id`, `provenance` (including `global_count`), `payload_tiers`, and `admission` — `"self" | "stm" | "stimulus" | "both"` (`"historical"` is reserved). Only `stimulus`/`both` admissions deposit at commit.
- `ReconstructionResult` — `trace_id`, `view`, `as_of_seq`, `handles`, `edges` (working-set view only; render-side, never handles), `dropped` (`{record_id, score, reason}`), `selector_route`, `stop_reason`, `warnings`, `budget_spent`.
- `ActiveMemorySnapshot` — what actually entered a context: `snapshot_id`, `trace_id`, `used_record_ids`, `display` rows, `prompt_token_estimate`, `observed_at`, `provenance` (including per-id admission labels). References and display metadata only, never payload copies.
- Constants and helpers: `SELF_FRACTION_FLOOR` (0.05), `ENTITY_CONTEXT_FLOOR` / `ENTITY_CONTEXT_RECOMMENDED` (40,000 — a recommended working size, not a minimum; operator 2026-08-01), and `entity_recall_budget(context_window, *, shelf_size=12, token_fraction=0.12) -> RecallBudget` — the entity-session budget profile (`token_budget = max(2400, round(0.12 × window))`, uncapped above the target; smaller windows are accepted — the 2400 floor is a starvation guard, not policy).

### The journal

Source: [`src/abstractmemory/journal.py`](src/abstractmemory/journal.py); backends `InMemoryJournal()` (volatile, emits a `#FALLBACK` warning) and `SQLiteJournal(path)` (sidecar tables; may share the store's file).

Record families (all append-only, all `seq`-stamped by the journal):

- `MemoryEvent` — attention events. Scoring kinds: `selected`, `co_selected` (pair trails), `pinned`, `silenced`; decay marker: `refocus`; audit-only (recorded, never scored): `listed`, `shown`, `expanded`, `cited`, `placed` (a formed record's provisional community, see [communities](#communities-topics)).
- `ScopeBinding` — visibility: `search_state` (`indexed`/`hidden`), `prompt_state` (`active`/`inactive`), `lifecycle`, `source`, `reason`. Latest per `(record_id, scope, owner_id)` wins; `fold_bindings(bindings)` applies that fold.
- `ClosureRecord` — belief lifecycle: `kind` (`retract`/`supersede`/`reinterpreted`/`merged`; `abstractmemory.journal.CLOSURE_KINDS`), mandatory `reason`, `replacement_ids` (required for supersede, optional otherwise). `reinterpreted` means the writer no longer reads its source this way (a note understood again): neither a correction nor a later fact; `merged` means the memory was merged into another, named in `replacement_ids` (written by the graph's `close` op). Every kind leaves ranked retrieval.
- `ReconstructionTrace` — one recall: fingerprint, searched scopes, candidates, selected/dropped, cues, budgets, admission labels, warnings.
- `ActiveMemorySnapshot` — see seam types.
- `ValenceEvent` — signed appraisals and standing markers (see valence below).
- `UnderstandingVersion` — one reading of a record (see [understanding](#understanding-in-place-rewrites-and-versions)): `record_id`, `assertion_id`, `version`, `field` (`digest` | `card` | `narration` | `title` | `target`), `text` (`None` only on title version 0 of a record formed with its title pending), `title` (`None` while the title was pending), `keywords`, `digest_method`, `author`, `reader`, `sources`, `derived_from`, `cause`, `magnitude`, `task_id`, `understood_at`, `version_id` (deterministic), `seq`. Read with `versions(record_id=, assertion_id=, field=, until_seq=, limit=0)` (oldest first) and `revision_records(since_seq=, until_seq=)` (the replay family `revision`).

Write-side idempotency (all backends): a caller-supplied id (`event_id`, `binding_id`, `closure_id`, `trace_id`, `snapshot_id`) makes replays no-ops returning the original record — at-least-once delivery never double-writes. The `MemoryJournal` protocol also exposes readers (`events`, `bindings`, `closures`, `traces`, `snapshots`, `valence_events`, `replay_records`) and the global counters `selected_count(record_id)` / `pair_selected_count((a, b))` (both accept `until_seq`).

### Reconstruction

```python
result = system.reconstruct(
    Stimulus(cue_text="pool outage"),
    scopes=[("session", "s1"), ("global", "")],   # narrow → broad
    budget=RecallBudget(),
    view="shelf",                                  # or "working_set"
    escalation_reason="cross-session recall",      # required when a broad scope is searched
    journal=True,
    trace_id=None,                                 # supply one to make the journaled read replay-safe
)
```

The result is the union of three admission components — `self` (identity by binding state), `stm` (trail-hot standing), `stimulus`/`both` (channel matches) — with the single best channel match seated first against the full budget. Retrieval channels: exact (`Stimulus.patterns`), keyword (a casefolded, accent-folded token scan over gathered candidates; minimum token length 4; labeled `#FALLBACK`), vector (requires the system embedder and stored vectors; cosine with a confidence-scaled floor so a weak field never reads as full relevance), and participants (co-presence between `Stimulus.participants` and record `participants`, scored `|intersection| / |stimulus.participants|`). Spreading activation walks recorded edges and co-use trails from matches under `max_hops`/`max_edges`; `SpreadParams` keeps the walk-shape knobs (weights, damping, `fan_out_cap`, `min_contribution`).

In the `working_set` view, `result.edges` also surfaces walked edges and top trail pairs for rendering.

### Committing use, deliberate acts, inspection

- `commit_selection(trace_id, used_record_ids, *, prompt_token_estimate=None) -> ActiveMemorySnapshot` — deposit the usage trail for records that actually entered a context: one `selected` event per used record plus `co_selected` events for **all pairs used together** in the depositing slice (plus term-sharing and recorded-edge hop pairs). Only ids admitted as `stimulus`/`both` (or unlabeled, for foreign traces) deposit; `self`/`stm` ids deposit nothing (presence ≠ use; `AttentionConfig.stm_rehearsal_weight`, default 0.0, is the optional rehearsal dial for STM). Idempotent by `trace_id` — a replay returns the original snapshot unchanged.
- `reinforce(record_id, *, reason, weight=8, ttl_activity=None, scope, owner_id, event_id=None, actor="operator", provenance=None) -> str` — deliberate strengthen (weight clamps 1..25).
- `attenuate(...)` (same signature) — deliberate weaken: a nudge toward invisibility, never negative relevance (removal is closure).
- `refocus(*, reason, scope, owner_id, ...) -> str` — topic-shift marker; accelerates decay of everything older in that scope stream.
- `activation(record_ids=None, *, scope, owner_id, at_seq=None) -> dict` — stored activation per record (`base_level`/`total`); unknown ids raise.
- `access_counts(record_ids=None, pairs=None) -> {"records": {...}, "pairs": {...}}` — the never-decaying global counts (per journal; for one-journal entity homes, per life).
- `payload(record_id, tier="digest") -> dict` — pure payload read. `digest` returns the canonical text; `raw` returns the host artifact reference (`attributes.payload_ref`) with `content=None` — the package never fetches verbatim payloads. Diary projections additionally carry `entry_id` (the pointer into the host-side diary book).

`AttentionConfig` (all declared tunables): `window_limit=512` (the bounded read window of the temporal fold — size to about a week of measured event cadence for continuously active homes, e.g. 8192), `decay_window=20.0`, `refocus_multiplier=6.0`, `max_activation=25.0`, `boost_scale=4.0`, `max_boost=120.0`, `prior_scale=0.05`, `prior_cap=1.0`, `reason_threshold=0.5`, `stm_rehearsal_weight=0.0`. The global count never windows.

Id namespaces: every id-taking call (`commit_selection`, `reinforce`, `attenuate`, `activation`, `access_counts`, `payload`, `bind`, `close_assertions`, `close_record`) accepts both the digest-assertion id (`handle.record_id`) and the graph id (`remember_many`'s `ex:…` return); unresolvable ids raise naming both namespaces.

### Formation (typed records)

- `remember_many(records, *, scope, owner_id, idempotency_key, turn_id=None) -> list[str]` / `remember(record, ...) -> str` — form typed records. Idempotent: ids derive from `(idempotency_key, position)`, so replays are no-ops returning the same graph ids. Forming deposits no attention events (forming is not using). Edges may reference batch siblings as `"local:<i>"`. On a home opened with `task_queue=True` the records, their bindings and the tasks they owe (a pending field's `title.author` / `digest.author`, a mechanical record's `digest.rewrite`) commit together: one transaction on the SQLite home; on the in-memory pair a failure rolls the tasks back and removes exactly the rows and bindings that attempt stored (decided under the store's lock, which the queue shares), never those of a concurrent writer of the same records. A failed enqueue leaves no record behind. Once the home has a communities version, each record formed there is also placed in its provisional community in the same transaction (one `placed` event; see [communities](#communities-topics)).
- `MemoryRecordInput(kind, title, digest, intents=(), outcomes=(), keywords=(), participants=(), edges=(), payload_ref=None, topic=None, confidence=None, attributes={}, provenance={}, pending=(), pending_failure=None)` — one record to remember. A title and a digest are mandatory metadata of every memory (operator rulings 2026-11-09). The one exception is an explicit, transient pending state: `pending=("title",)` with `title=None` (or `""`) forms the record untitled, `pending=("summary",)` marks the given digest provisional (refused on `UNDERSTANDING_PROTECTED_KINDS`, except a PUBLIC diary projection, whose body is the book's entry: see the diary formation rules). The record then carries `attributes.pending = {title: bool, summary: bool, since, attempts}` (plus `failure` when `pending_failure` = `{reason, error_class, message, at, fields}` says why authoring failed; it needs pending fields) and `remember_many` queues its authoring task in the same call (`PENDING_TASK_KINDS`: `title.author` / `digest.author`; see the task queue); a home opened without `task_queue=True` refuses it, and a null title not declared pending is refused (`ValueError`). Reads serve the missing title as `None` (handles, display blocks with `pending`, `record_understanding` with `pending`, `recent_records`, card briefs, probe hits), never a placeholder or words from the body, and the canonical text is the body alone. The `digest` (1–3 sentences) is what gets indexed and embedded; `payload_ref` keeps the full verbatim reachable as a host artifact; `edges` are `(relation, target_record_id)` pairs stored as edge assertions; `participants` are identity strings (`person:ada`, `entity:castor`) — co-presence is explicit, and what a record says is its full co-presence.

Record kinds: `memory`, `episode`, `lesson`, `instruction`, `decision`, `claim`, `summary` (requires at least one edge naming what it summarizes), `question`, `answer`, `plan`, the identity kinds `value` (requires `attributes.value_class` ∈ {core, revisable}), `purpose`, `trait`, `diary`, `interest`, and the sleep artifact `dream`. Kind ranks order identity first (value < purpose < trait) and derived artifacts as summary peers.

Diary formation rules: `kind="diary"` requires `provenance.source` ∈ {`diary-projection`, `owner-direct`}; `diary_type` ∈ {note, idea, commitment, reflection, question, problem} (absent defaults to `note`; unknown values raise); projections must carry `attributes.entry_id`; resolution references ride `attributes.answers` (questions) and `attributes.resolves` (problems); the book's verdict on a `resolves` claim rides `attributes.resolves_status`, `attributes.resolved_record_id` and `attributes.resolved_title` (optional, non-empty strings when present; the replay diary block carries them). A PUBLIC projection's summary is authored like any record's (operator rule 2026-11-09: title + summary always exist, authored, never his first line standing in): its body is the book's entry (the runtime's hash-chained ledger, `attributes.entry_id`), so its digest is only the summary. It forms with an authored summary (`attributes.understanding` = `{field: "digest", author, reader, sources, ...}`: never a mechanical author; a model author with its reader and ledger step) or with `pending=("summary",)` and its `digest.author` task, which `rewrite_digest` completes; a legacy projection (his first line standing in) is repaired the same way through `declare_pending` (`fields=("summary",)` or both); the body and every chain stay untouched, its summary versions are sealed in replay like its title versions. Refused (`ValueError` at formation, `protected_kind` in `rewrite_digest` and `declare_pending`): a private entry (`attributes.private` anything but absent/False), an owner-direct entry (no book: its digest is his words) and a chained one (`prev_entry_hash` / `entry_hash`: `diary_entry_hash` covers title + digest + observed_at); `diary.diary_summary_refusal(attributes, provenance)` is the rule.

### Bindings and closures

- `bind(record_id, *, scope, owner_id, search_state, prompt_state="inactive", lifecycle="none", source="operator", reason=None, binding_id=None) -> ScopeBinding` — append a visibility event. A folded `hidden` binding removes the record from ranked retrieval in that scope pair only; re-binding `indexed` restores it. `prompt_state="active"` admits the record into the identity (self) component. Record-level bindings hide the digest and its edge assertions together. Direct `store.query` id lookups bypass folds by design.
- `close_assertions(assertion_ids, *, kind, replacement_ids=(), reason) -> list[str]` — append one closure per assertion (`retract` | `supersede` | `reinterpreted` | `merged`).
- `close_record(record_id, *, reason, kind="retract", replacement_ids=()) -> list[str]` — close a whole record (digest and its edges) as one belief-revision act; closure ids are deterministic, so replays dedupe.

### Understanding: in-place rewrites and versions

Source: [`src/abstractmemory/understanding.py`](src/abstractmemory/understanding.py). A record's understanding (its digest, a card's text, a dream's narration) can be rewritten in place. The record keeps its graph id, its digest assertion id (usage keys on it), its edges and every journal record; the previous reading stays in the journal as an `UnderstandingVersion`. Version 0 is the original text, captured at the first rewrite. The verbatim behind a record is never touched, rewriting deposits nothing, and the engine never invents or edits the words it is given. All verbs are on `MemorySystem` and also exported as module functions taking the system first.

- `rewrite_digest(record_id, *, text, author, cause, expected_version, reader=None, sources=(), derived_from=(), title=None, keywords=None, digest_method="authored-v1", magnitude=None, task_id=None, understood_at=None) -> dict` — replace a record's digest (and optionally its title and keywords) in place. `record_id` is the graph id or the digest assertion id. `expected_version` is a compare-and-set on the record's `digest_version` (0 for a record never rewritten). On the SQLite home one transaction appends the version rows, updates the row, replaces its keyword-index entry and writes its new vector; the vector is computed first, outside the transaction, with the store's pinned embedder.
- `retitle(record_id, *, title, author, cause, expected_version, reader=None, sources=(), task_id=None, understood_at=None) -> dict` — change ONLY a record's title, in place, for every kind (episode, summary, world-model card, dream, interest, lesson, realization, observation, every diary entry). A title is an explicit, complete phrase, stored exactly as given and never cut (root ADR 0054). The body, the keywords and the digest's reading stay byte-identical; `attributes.title`, `title_version` and `title_provenance` change, and the canonical text (the title is its first line), the keyword-index entry and the vector are replaced in the same transaction. `expected_version` is the compare-and-set on `title_version` (0 for a record never retitled). The first retitle keeps the title it replaces as version 0 of field `title`, with the provenance of the reading that wrote it. The protected kinds are not refused: their protection covers the body (the entity's words), and a title the runtime cut at formation is not his words. For a diary entry the privacy rule concerns reading, and retitle reads nothing back: `record_understanding` still refuses the entry, its replay display block stays redacted, and the opt-in `revision` envelopes of its title versions are sealed the same way (payload `text`, `title`, `keywords` emptied, `redacted: "diary"`). `outcome="unchanged"` (no version) when the title is already the current one. A record formed with a pending title is retitled the same way: its title version 0 holds `None` (`text` and `title`), the write completes its live `title.author` tasks (named or not) and clears the marker (`pending_cleared: ["title"]` in the result; `rewrite_digest` does the same for a pending summary), and an interest's card keyed on its graph id moves to the title's subject head through `rekey_world_model` (`card_rekey` in the result). Refusals: `empty_title`, `hash_chained`, `closed`, `stale`, `embedder_unavailable`, `unknown_record`. A `rewrite_digest(..., title=...)` or a revert that changes the title writes the title version too (the result carries `title_version`), so `versions(record_id, field="title")` lists every title. Recall finds the record by its new title through the keyword index and the vector. Known limit, unchanged here: unless FTS keyword discovery runs, the reconstruct keyword channel is the v1 token scan over the newest candidate window per scope, so an old record in a large home can sit outside it (found by the 1181 backfill verify; backlog 0019).
- `declare_pending(record_id, *, fields=("title", "summary"), cause="repair", reason=None) -> dict` (also a module function `declare_pending(system, record_id, ...)`) — declare that a STORED record's title and/or summary is still being written: the record gets exactly what a record formed with `pending=fields` gets, for any kind. For the records stored before titles and summaries were mandatory: a legacy world-model card ("World model: <subject>", `mechanical-card-v2`), a legacy dream ("Dream: ..."), a placeholder or missing title, a legacy PUBLIC diary projection whose first line stands in as its summary. The marker `attributes.pending = {title, summary, since, attempts}` (`since` = now, merged into a marker already there) and, per newly declared field, formation's task (`title.author` / `digest.author`, capability `summaries`, `FORMATION_PRIORITY`, strength 1.0, basis `<assertion id>@pending-<field>`, `basis_version` 0; a live task with that identity queued earlier without the marker is adopted, and a field authored once and declared again gets the next free `...@pending-<field>@<k>`), written in one transaction on the SQLite home. A declared title is stored `None`, as a pending title is at formation: the title it held stays as a title version (version 0 with its provenance, then `None` with the declaration's), the canonical text, keyword entry and vector are replaced, and an interest's standing card moves to `topic:<its graph id>` (`card_rekey`; a card other active interests still elect stays, reason `shared_card`). A declared summary keeps the body as its provisional digest (never restored: `revert_digest` refuses it with `provisional_summary`), calls no embedder, and supersedes the record's live body task (`digest.rewrite`, a card's `card.author`). The drainer then completes the tasks through `retitle` and `rewrite_digest` exactly as for a record formed pending: a card's summary through the card's own write (target, evidence, revision untouched), a dream's in place (signals and the `dream.narrate` task untouched). The journal keeps the declaration as a version row (field `pending`, `pending_version` on the row; text = the fields declared, author `{"kind": "mechanical", "id": "declare_pending"}`, cause `{cause, reason?, pending, tasks, provisional?, already_pending?}`), shown by the opt-in replay family `revision`; it is not a reading and `revert_digest` refuses it (`not_revertible`). Returns the understanding result shape with `field="pending"` plus `declared`, `already_pending`, `pending` (the marker), `tasks` (`{field: task id}`), `enqueued`, `adopted`, `superseded`, `provisional` (`{field, version}`), `title_version` and `card_rekey` when they apply. Idempotent for a healthy record: every asked field already pending WITH its live task returns `outcome="unchanged"`, reason `already_pending`, `tasks` naming them, nothing written or queued. An already-pending field no live task owns (its enqueue failed, or every task ran out) is repaired: with nothing new to declare through `rearm_pending` (`outcome="rearmed"`, `repaired`), beside a newly declared field in the same transaction (`repaired`, cause `repaired`). Refusals: `unknown_record`, `no_task_queue`, `closed`, `protected_kind` (a summary of the entity's own act; a PUBLIC diary projection's summary IS declared, under `diary.diary_summary_refusal`: an owner-direct, chained or private entry is refused, the `detail` naming the rule), `consolidation_record`, `hash_chained` (a declared title), `stale`, `embedder_unavailable` (a declared title). `fields` outside `PENDING_FIELDS`, empty, or a cause outside `REWRITE_CAUSES` raise `ValueError`.
- `rearm_pending(record_id, *, cause="repair", reason=None) -> dict` (also a module function `rearm_pending(system, record_id, ...)`) — re-arm a pending record whose authoring ran out: a field its marker says is pending but that NO live (`pending` / `deferred` / `leased`) `title.author` / `digest.author` task owns (every task it had failed after `max_attempts` claims, or was skipped or superseded; `declare_pending` answers such a record `already_pending`) gets formation's task again: same kind, capability `summaries`, `FORMATION_PRIORITY`, strength 1.0, `basis_version` 0, the next free basis `<assertion id>@pending-<field>@<k>`, cause `{cause, reason?, pending: <field>, rearmed: n}`, queued in one transaction with the marker on the SQLite home. A field that still has a live task keeps it. The marker stays (title still `None`, body still provisional, no embedder call): `attempts` restarts at the highest claim count of its live tasks, `rearmed` counts the re-arms, `last_rearmed_at` is now, `since` is kept. The journal keeps the re-arm as a version row of field `pending` (text = the fields re-armed, author `{"kind": "mechanical", "id": "rearm_pending"}`, method `rearm-pending-v1`, cause `{cause, reason?, rearmed, pending, tasks, attempts_before, exhausted: {field: [{task_id, status, attempts, max_attempts, error}]}, live?}`). Returns `{applied, outcome: "rearmed" | "unchanged" | "refused", reason, record_id, assertion_id, field: "pending", tasks: {field: task id}, rearmed, pending, live, enqueued, superseded, exhausted, version, seq, version_id}`. Unchanged, nothing written: `not_pending` (no marker), `live_task` (every pending field has a live task; `tasks` names them). Refusals: `unknown_record`, `no_task_queue`, `closed` (with `replaced_by`), `stale`; a cause outside `REWRITE_CAUSES` raises `ValueError`.
- `narrate_dream(dream_id, *, text, author, expected_version=0, reader=None, sources=(), cause="formation", task_id=None, understood_at=None) -> dict` — store a narration beside a dream's mechanical summary (`attributes.narration`, `narration_version`, `narration_provenance`). The digest, the signals, the keyword index and the vector are untouched. Versioned under field `narration`; the first narration is version 1.
- `revert_digest(record_id, to_version, *, expected_version, by=None, reason=None, field=None) -> dict` — restore an earlier reading as a NEW version (cause `revert`, `reverted_to`; nothing is erased). The restored reading keeps the author and reader of the words it restores; `by` (`{kind, id}`, default the operator) is recorded in the cause. `field="title"` restores an earlier title; a version that held no title is refused (`null_title`: a title is mandatory), as is the provisional digest a record was formed with while its summary was pending (`provisional_summary`). A digest or card version restores its title too, unless the record was retitled since its current reading (that title stays).
- `versions(record_id, *, field=None) -> list[dict]` — every reading, oldest first, each with `current: bool`. `field` defaults to the record's own (`digest`, or `card` for a world-model card); pass `field="narration"` for a dream's narrations, `field="title"` for its titles, `field="target"` for a card's keys (`rekey_world_model`), `field="pending"` for its pending declarations and re-arms (`declare_pending`, `rearm_pending`). A record never rewritten returns `[]`.
- `current_of(record_id) -> dict` — follow supersede and merge closures to the live record: `{record_id, graph_id, current_id, current_assertion_id, hops, chain, closed}` (`closed` names a retract/reinterpreted end).
- `record_understanding(record_id) -> dict` — the record read a host serves: `{record_id, graph_id, assertion_id, kind, title, digest, keywords, payload_ref, observed_at, closed, current_id}` plus the provenance fields below; dreams add `narration`, `narration_by`, `narration_reader`, `narrated_at` and `signals`. Diary records raise `ValueError` (the book is their door).
- `understanding_fields(assertion, *, version_count=None, replaced_by=None) -> dict` — the pure provenance read the replay display blocks use: `digest_method`, `digest_version`, `version_count`, `authored_by`, `reader`, `understood_at`, `cause`; cards `target`, `target_type`, `card_revision`, `source_count`; dreams `narrated`, `narration_version`; `replaced_by`. Unknown values are absent, never guessed.
- `reading_provenance(assertion) -> dict` — the one derivation of who wrote a record's current reading, when and why: `{author, reader, understood_at, cause}`. `understanding_fields`, `record_understanding`, the replay display blocks, the identity card's briefs and formation all read through it. The row's own stamp (`attributes.understanding`) wins. A row nobody stamped reads as its formation reading (`cause` `"formation"`, `understood_at` = its `observed_at`), its author from its origin: a `mechanical*` digest_method gives `mechanical` (id = the method); a spark identity record (`attributes.spark_version`) gives `operator` (id `spark-v<N>`); a row whose provenance names the operator as actor (an archive import) gives `operator` (id = the source); a kind in `ENTITY_WORDS_KINDS` (lesson, interest, realization, observation, question, every diary entry, a value, purpose or trait he elected) gives `entity-mind` (id = the owner); anything else stays `unrecorded`. `remember_many` records the same reading at formation (`attributes.understanding`, version 0) when the caller stamped none and the author is known.

Provenance arguments (validated; a bad value raises `ValueError`):

- `author = {"kind": ..., "id": ...}`, `kind` in `AUTHOR_KINDS`: `entity-mind` (the entity's own model), `capability-model` (a model the host names for this capability), `operator`, `mechanical`. A model author (`entity-mind`, `capability-model`) must also pass `reader = {"provider", "model", "thinking"}` and at least one ledger `SourceRef` dict in `sources` (the step of its call, ADR 0003). A reading whose row names no origin (see `reading_provenance`) and a legacy label passed to `author_world_model` carry author kind `unrecorded`.
- `cause` is one of `REWRITE_CAUSES` (`formation`, `backfill`, `ripple`, `evidence`, `operator`, `revert`, `repair`: a correction of what a process wrote wrong, such as a title cut at formation), or a dict `{"cause": ..., ...}` whose extra keys (`task_id`, `ripple_id`, `seed_id`, `hops`, ...) are kept verbatim.

Every verb returns `{applied, outcome, reason, record_id, assertion_id, field, version, ...}`. `outcome="applied"` adds `previous_version`, `version_id`, `seq`, `captured_original`, `vector_rewritten`. `outcome="unchanged"` (byte-identical text) writes nothing and is safe to retry. Byte-identical words that settle a PENDING field (an authored summary or title equal to the provisional one) are not `unchanged`: they are `applied`, with `pending_cleared`: the authored reading is recorded as a version (its author, reader and sources), the marker cleared and the field's task completed in one transaction; the canonical text is unchanged, so no embedder call is made and the vector stands (`vector_rewritten: false`). `outcome="refused"` writes nothing and names its `reason`:

| Reason | When |
| --- | --- |
| `unknown_record` | no formed record with that id |
| `protected_kind` | `UNDERSTANDING_PROTECTED_KINDS`: diary, value, purpose, trait, interest, realization (the entity's own acts); `rewrite_digest` completes a PUBLIC diary projection's PENDING summary, which `declare_pending` declares on a legacy one |
| `use_narrate_dream` / `use_author_world_model` | a dream's written summary stays (a narration goes beside it); a card is authored through `author_world_model`. A dream or card whose summary is still PENDING takes it here: a card through its own in-place write (field `card`) |
| `card_keywords` | `keywords=` on a pending card: a card's keywords are its evidence facets |
| `not_a_dream` | `narrate_dream` on another kind |
| `closed` | the record is retracted or superseded (`replaced_by` names its replacement) |
| `consolidation_record` | a ripple's `summary_role: re-understanding` record, unless the cause is `operator` |
| `empty_text` | blank text |
| `empty_title` | `retitle` with an empty or whitespace title |
| `hash_chained` | `retitle` of an owner-direct diary entry whose `entry_hash` attests its title (`verify_diary_chain` would break) |
| `stale` | `expected_version` is not the record's current version (another writer moved it) |
| `embedder_unavailable` | the store's embedder failed, or the record is vectored and the store has no embedder |
| `unknown_version` | `revert_digest` to a version the record never had |
| `use_rekey_world_model` | `revert_digest(..., field="target")`: a card's key moves back through `rekey_world_model` |
| `provisional_summary` | `revert_digest` to the provisional digest a record was formed with, or to a reading `declare_pending` made provisional |
| `not_revertible` | `revert_digest(..., field="pending")`: a pending declaration is not a reading |
| `no_task_queue` | `declare_pending` on a home opened without `task_queue=True` (a pending field is stored only beside its task) |
| `already_pending` | `declare_pending` (`outcome="unchanged"`): every asked field is already pending; nothing written |
| `not_pending`, `live_task` | `rearm_pending` (`outcome="unchanged"`): no pending marker, or every pending field still has a live task; nothing written |

Backends: the in-memory pair and a SQLite store whose `SQLiteJournal` shares its file (the entity-home deployment). A SQLite store with a journal elsewhere raises `ValueError`; `LanceDBTripleStore` raises `NotImplementedError`.

### The memory task queue: understanding in the background

Source: [`src/abstractmemory/memory_tasks.py`](src/abstractmemory/memory_tasks.py), [`src/abstractmemory/activation_gate.py`](src/abstractmemory/activation_gate.py). A home opened with `MemorySystem(..., task_queue=True)` keeps a persistent queue of what still needs understanding (`memq_tasks`, `memq_log`, `memq_windows` in the home's file; a private in-memory database for the in-memory store). Memory enqueues where records form; the host decides when a window opens, how long it may run and which model reads; it applies results through the [understanding verbs](#understanding-in-place-rewrites-and-versions) with `task_id=`, which settle the task in the same transaction.

Task kinds (`TASK_KINDS`; capability in parentheses): `title.author` and `digest.author` (summaries) for a record formed pending (its `attributes.pending` marker), at `FORMATION_PRIORITY` (5, ahead of every rewrite) and strength 1.0 — a topic's (record kind `community`) at `COMMUNITY_PRIORITY` (3, ahead of every other pending record, so a window with a small budget names the topics before a long repair tail; `pending_priority(record_kind)` says which), completed by `retitle` / `rewrite_digest` (which clear the marker), each claim mirrored into the marker's `attempts`, a diary target's packet redacted (its words come through the book by `entry_id`); `digest.rewrite` (summaries) for a record formed with a `MECHANICAL_DIGEST_METHODS` label (never beside a pending summary); `card.author` (summaries) for a LEGACY card still in mechanical words, or carrying a mechanical delta (a card formed now owes `title.author` / `digest.author` instead); `dream.narrate` (understanding) for a formed dream, in `night` and `nap` windows only; `ripple.reconsider` and `ripple.consolidate` (understanding) for the [ripple](#ripple-a-change-reconsidered-through-connected-memories), consolidation in `night` windows only. Review offers (maintenance candidates, dedup groups) and `UNDERSTANDING_PROTECTED_KINDS` are never rewrite tasks; their pending title or summary is (every record a memory pass forms is pending, below). Windows (`WINDOWS`): `turn`, `visit`, `nap`, `cycle`, `night`, `idle`.

Enqueue sites: `remember_many` (a `title.author` / `digest.author` per pending field; one `digest.rewrite` per labelled-mechanical record; a summary depends on the pending rewrites of the records it summarizes), `world_model_pass` (its cards form pending, so `remember_many` queues their `title.author` / `digest.author`; a revision supersedes the prior card's live `title.author` / `digest.author` / `card.author` tasks), `dream_pass` (pending title and summary, plus `dream.narrate`), `declare_pending` (a stored record declared pending: formation's `title.author` / `digest.author`, its live body task superseded), `rearm_pending` (a pending field whose tasks all ran out: formation's task again), the ripple, and the backfill (`enqueue_debt`).

**What memory's own passes form is pending** (operator rulings 2026-11-09: no title or summary is ever the pass's words). `world_model_pass`, `dream_pass`, `mine_candidates_pass`, `consolidation_pass` and `wake_cue_dedup_pass` form their cards, dreams, offers, candidates and day summaries with `pending=("title", "summary")`: the bookkeeping text they compose (a card's evidence briefing, the night's tensions, an offer's invitation, a dedup witness) is only the provisional body the `title.author` / `digest.author` tasks author from, and display blocks and `record_understanding` carry `pending` so an app shows "being written…". A card revision carries an AUTHORED title (one with `title_provenance`, `carried_from` added) and forms `pending=("summary",)`. The drainer completes them with `retitle` and `rewrite_digest` like any pending record (a card's summary goes through the card's own in-place write). These passes, and `sleep_pass` once per night, refuse a home opened without `task_queue=True` before writing anything; `report_only` reads still run.

`system.tasks` is a `MemoryTaskQueue`:

- `enqueue(kind, target_id, *, scope, owner_id, basis, strength, cause, assertion_id="", basis_version=0, priority=None, depends_on=(), not_before=None, windows=None, cost_hint=None, max_attempts=3, ripple_id=None, hop=None, now=None) -> {task_id, enqueued, superseded, reason?}` — idempotent (`task_id = hash(kind|target|basis)`). `strength` in [0, 1] is the incoming memory's, change's or surprise's signal. A basis older than the record's current version is refused (`reason="stale_basis"`); a newer one supersedes the older pending task.
- `select_for_window(budget_seconds, sensitivity=1.0, *, window="turn", scopes=None, kinds=None, capabilities=None, hard_cap=64, cost_of=None, now=None) -> dict` — a pure read: the tasks this window should run, highest activation first. Each task's activation is `activation(strength, sensitivity) = 1 / (1 + e^-6(sensitivity·strength − 0.5))` (ADR 0001 decision 7); a task is a candidate at or above the floor 0.1. Candidates are taken while their cost fits `budget_seconds`, up to `hard_cap`, and at most 20 ripple reconsiderations. There is no fixed count: the host's Mind setting is the `sensitivity` and the `hard_cap`. Cost per task: `cost_of(task) -> seconds` when given, else the median `result.seconds` of the kind's recent completed tasks, else `DEFAULT_TASK_SECONDS`. Returns `{window, budget_seconds, sensitivity, hard_cap, gate, at, tasks: [task + activation + cost], planned_seconds, left_out: [{task_id, kind, target_id, reason, activation, cost_seconds}], left_out_counts}`. Every live task is either selected or left out with its reason: `below_gate`, `over_budget`, `hard_cap`, `window`, `blocked` (a dependency is pending), `not_before`, `leased`, `ripple_window_cap`, `scope`, `filtered`.
- `claim(tasks, *, holder, lease_seconds=600, window=None, now=None) -> {claimed: [task], refused: [{task_id, reason}]}` — lease a selection (its window and its `left_out` are recorded) or a list of task ids. Each claim spends one attempt; an expired lease returns the task; a task claimed `max_attempts` times without settling fails.
- `ack(task_id, *, holder, result, now=None) -> {acked, reason?, task?}` — settle a leased task as done (for example `{"outcome": "unchanged", "seconds": 41.2, "tokens": 3120, "run_id": ...}`); a task a verb already settled returns `reason="already_done"`.
- `release(task_id, *, holder, outcome, reason, until=None, detail=None, now=None) -> {released, task?}` — `outcome="retry"` (attempt spent; failed at `max_attempts`), `"defer"` (no attempt spent; not before `until`, for an unreachable endpoint or embedder), `"skip"` (terminal, for example `reason="over_context"` with the measured tokens in `detail`).
- `left_out(*, holder=None, window=None) -> list[dict]` — what the last claimed window of each `(holder, window)` left out, and why.
- `task(task_id)`, `tasks(*, status=None, kind=None, target_id=None, ripple_id=None, limit=0)`, `log(task_id)`, `counts()`, `cost_of(task)`, `supersede_stale(system)`, `supersede_target(target_id, *, reason)`, `prioritize(target_ids, *, kinds, priority, reason, dry_run=False)` (moves the live tasks of `kinds` on these targets that wait at a later priority up to `priority`, never demoting one already ahead, one `prioritized` log row each; returns the task ids).

On `MemorySystem` (and as module functions taking the system first):

- `understanding_debt(*, scopes, methods=None) -> dict` — a pure read of what is still mechanical: `{debt: {task kind: {record kind: n}}, total, unqueued, queue: {kind: {status: n}}, by_capability, oldest_pending, methods}`. `methods` widens the label set counted as debt.
- `pending_records(*, scopes=None, live_task=False) -> list[dict]` (also `memory_tasks.pending_records(system, ...)`) — a pure read of the records still marked pending, closed records left out; `scopes` `[(scope, owner_id)]` or `None` for every scope. `live_task=False` (default) keeps the records `rearm_pending` re-arms (a pending field with no live task), `True` those whose every pending field has a live task, `None` every pending record. Each item: `{record_id, assertion_id, record_kind, scope, owner_id, pending (the marker), fields, live: {field: task id | None}, unowned, rearmed}`, oldest marker first.
- `enqueue_debt(*, scopes, cause="backfill", priority=90, dry_run=True, methods=None, kinds=None) -> dict` — the backfill: one task per debt record that has none. Strength comes from lived use, `0.1 + 0.9 · ln(1 + n) / ln(1 + n_max)` over the records' selected counts, so the most-used mechanical memories run first. Dry-run by default.
- `task_packet(task_id, *, token_budget=56000) -> dict` — everything a reader needs for one task: the target's `record_understanding`, `expected_version` for the compare-and-set, the `verbatim` reference, and its `sources` (summary), `evidence` (card) or `mentions` (dream), newest first. An item that does not fit the budget is left out whole and listed in `left_out`; no text is cut. `target_state` says when the target was closed or is gone.

`maintenance_due(..., tasks=system.tasks)` reports `pending_tasks` and is due when ripple work or night-only work (dream narrations, ripple consolidations) is pending. The gate's constants and their calibration live in `activation_gate.py` (`GATE_K=6`, `GATE_THETA=0.5`, `GATE_FLOOR=0.1`, `DECAY=0.5`, `MAX_HOPS=2`, `RIPPLE_BUDGET=20`, `RIPPLE_WINDOW_CAP=20`, `CHANGE_MAGNITUDE`, `RELATION_WEIGHTS`).

### Ripple: a change reconsidered through connected memories

Source: [`src/abstractmemory/ripple.py`](src/abstractmemory/ripple.py) (ADR 0001, v1). When a memory's meaning changes, the ripple reconsiders the memories connected to it, hop by hop, until the activation falls below the gate's floor. Memory decides which memories are reconsidered and records why; the host's reader decides what each now means. It runs over the [task queue](#the-memory-task-queue-understanding-in-the-background), so the home must be opened with `task_queue=True`. All verbs are on `MemorySystem` and exported as module functions.

- `seed_ripple(seed_id, *, cause, change="revised", magnitude=None, cosine_distance=None, surprise=None, reconsider_seed=None, budget=20, dry_run=False, now=None) -> dict` — open a ripple. `cause` is `evidence` for the entity's own revise election (pass the election and its evidence in the cause mapping), or the cause of a rewrite whose reader reported `change` (`revised` or `overturned`). The magnitude is `magnitude` when given, else `measured_change(change, cosine_distance)`: the reader's enum (`CHANGE_MAGNITUDE`: unchanged 0, refined 0.25, revised 0.6, overturned 1.0), scaled down when the vectors show the text barely moved, never up. An election also re-reads the seed itself (hop 0; its neighbours wait for it). `surprise` is the entity's own words, stored as words; no feeling is written. `dry_run=True` writes nothing and returns the hop-1 decisions. Idempotent on (seed, cause, seed version). Returns `{ripple_id, opened, magnitude, decisions, reconsider_task_ids, left_out}`.
- Candidates of a source: record edges in both directions (reliance by relation, `RELATION_WEIGHTS`: summarizes / derived_from / refines 1.0, answers / supports / part_of 0.75, continues / from_session / reflected_in 0.5, mentions 0.25; `written_amid` and usage trails carry nothing), the world-model cards whose evidence holds the source, and the source's nearest digests by vector (`similarity_weight` of the cosine). A candidate's potential is the received impact × reliance × `DECAY` (0.5), accumulated over every source of the same ripple; it is reconsidered when `logistic(potential)` reaches the floor (potential ≥ 0.134), at most `MAX_HOPS` (2) hops out, at most `budget` reconsiderations per ripple, and once per ripple. Only the seed owner's memories are candidates. Every gate decision is stored with its potential: `reconsider`, `below_floor`, `refractory`, `closed`, `protected`, `max_hops`, `budget`. `protected` (the entity's own acts, diaries, consolidation records, review offers), `max_hops` and `budget` candidates are listed in the ripple's `left_out` with their potential; a budget stop also warns `#[WARNING:TRUNCATION]`.
- `settle_reconsideration(task_id, *, holder, outcome, change=None, cosine_distance=None, result=None, now=None) -> dict` — settle a `ripple.reconsider` task. `outcome="unchanged"` acks it and writes nothing. `outcome="rewritten"` follows the host's `rewrite_digest(..., cause={"cause": "ripple", ...}, task_id=task_id)`; the memory's onward impact, `min(received, measured)`, is handed one hop further (the distance the rewrite measured is used when `cosine_distance` is omitted). Returns `{ripple_id, outcome, onward, decisions, status}`.
- When no reconsideration is live, the ripple settles; if two or more memories changed it enqueues one `ripple.consolidate` task, leased only in a `night` window. `form_consolidation(task_id, *, holder, text, title, author, reader=None, sources=(), keywords=(), now=None) -> dict` forms the first-person "what this changes" record: kind `summary`, `summary_role: re-understanding`, `summarizes` edges to every changed memory, `derived_from` the seed, never batch-rewritten.
- `refresh_ripples(*, now=None) -> list` — close ripples whose reconsiderations were all settled, skipped or failed (call it when a window ends).
- `revert_ripple(ripple_id, *, by=None, reason=None) -> list` — restore, as new versions, what the ripple changed.
- `ripples(*, scope=None, owner_id=None, status=None, limit=50) -> list` and `ripple(ripple_id) -> dict` — the ripples (`open`, `settled`, `consolidating`, `consolidated`) with their cause, magnitude, surprise, budget, changed members and `left_out`; `ripple` adds the member tasks and every gate decision.

A `ripple.reconsider` packet (`task_packet`) carries `ripple`: the cause and surprise words, the seed's current reading and the one it replaced, how the memory was reached (`from`, `via`, `hops`) and the impact it received; a consolidation packet carries every change with its before and after. The ripple closes nothing.

### Identity: spark, engram, self core, floors

- `DEFAULT_SPARK_TEMPLATE` — the canonical six-key spark (`name`, `origin`, `values`, `purposes`, `traits`, `honesty`); its values include the framework-level core value `shared_vulnerability` (`SHARED_VULNERABILITY_STATEMENT`).
- `lint_spark(spark, *, framework=True) -> list[str]` — charter lint (ERROR/WARNING strings): section caps, 1–3-sentence behavioral statements, explicit `class` per value; framework sparks must carry `shared_vulnerability` (`framework=False` is the explicit override).
- `canonical_spark_hash(spark) -> str` — the one hash definition shared by the engram guard and host-side spark verification.
- `engram(system, spark, *, scope="self", owner_id, spark_artifact_ref=None, author=None) -> EngramResult` — turn a linted spark into the identity core: value/purpose/trait records (honesty items are traits with `trait_class="limit"`), each bound prompt-active. Idempotent by spark hash; a modified spark under the same version is refused; a higher version is refused while the born core remains prompt-active (re-engram is an exceptional repair, not an amendment path). `EngramResult` carries `record_ids` per section, `binding_ids`, `warnings`, `created` and `authoring`.
  - **Birth titles are authored.** A spark item's `name` is its title, kept as given (the operator's words; the row reads operator `spark-v<N>`). An item without a `name` has its title authored BEFORE it forms by `author`, the same callable the sleep passes take (`author(kind, body, context) -> {title, author, reader, sources}`, see [the `author` callable](#authored-with-the-record-the-author-callable)): `kind` = the record kind, `body` = the item's statement, `context` = `{pass: "engram", record_kind, scope, owner_id, fields: ["title"], title: None, source_ids: [], attributes}` (`attributes.trait_class == "limit"` marks an honesty item). The title is stored with `title_provenance` (version 0, cause formation). When `author` raises, returns no usable title, or is not supplied, the record forms PENDING: title `None`, its `title.author` task queued in the same formation, `attributes.pending.failure` = `{reason, error_class, message, at, fields: ["title"]}` with `reason` `author_raised`, `invalid_result` or `no_author_supplied`; the sleep's repair authors it. A pending title needs the home's task queue, so on a home opened without `task_queue=True` an unnamed item with no authored title refuses the whole birth before anything is written. The summary is never asked for: an identity record's summary is the spark statement, a protected body. A record already formed (a replay) is never authored again. `EngramResult.authoring` = `{authored: [{record_id, record_kind, pass, fields}], failures: [{record_id, record_kind, pass, fields, reason, error_class, message, at}]}` (empty on a replay), for the host's authoring-failures line.
  - **Legacy birth labels.** Until 2026-11-10 an unnamed item was stored with a label as its title, which memory no longer writes. The exact pattern, for the host's sleep repair to treat as a placeholder: the WHOLE stored title is `<stem>-<ordinal>`, `stem` one of `value`, `purpose`, `trait`, `limit` (lowercase, exactly), one hyphen, `ordinal` one or more ASCII digits (the item's 0-based position in its section; no sign, padding, spaces or suffix). Homes born before the section-derived stems (c5260) also carry `trait-<n>` on honesty items, which is why castor has two `trait-0`. They appear only on `value` / `purpose` / `trait` records carrying `attributes.spark_version`; a title that merely contains such words is not a label.
- `self_records(*, scope, owner_id, spark_version=None) -> list[TripleAssertion]` — the folded identity read: prompt-active and closure-folded, identity kinds only, ordered kind rank → precedence → record id. Identity evolves at record level (close the old record, form and bind the new one); the journal's time axis is the version history.
- Floors and targets: `SELF_FRACTION_FLOOR = 0.05` (summoned entities run at or above it; the engine keeps accepting lower values for non-entity callers) and `ENTITY_CONTEXT_FLOOR = 40_000` (= `ENTITY_CONTEXT_RECOMMENDED`; a soft recommendation since the operator's 2026-08-01 re-ruling) with `entity_recall_budget(...)` (see seam types).

### Valence and gradation

Orthogonal to attention by contract: valence never touches activation, never gates candidates, and the retrieval pipeline never reads it (enforced at behavior and import level).

- `appraise(target_id, *, sign, magnitude, reason, scope, owner_id, value_refs=(), scar=False, bond=False, event_id=None, actor="runtime", provenance=None, trace_id=None) -> list[str]` — deposit one signed appraisal (sign ±1, magnitude 1..10). Amplitude authority: magnitude > 3 requires actor `entity-reflection`/`operator` or `provenance["outcome_class"]=="catastrophic"`. `scar=True` (requires sign −1) / `bond=True` (requires sign +1) write an explicit standing marker alongside (`"{event_id}:scar"` / `"{event_id}:bond"`); markers are never auto-created.
- `heal_scar(scar_event_id, *, reason, lesson_record_id=None, scope, owner_id, event_id=None, actor="entity-reflection") -> str` and `break_bond(bond_event_id, *, reason, scope, owner_id, event_id=None, actor="entity-reflection") -> str` — append-only resolutions with deterministic default ids (`heal:{id}` / `break:{id}`). A betrayal-scale scar (magnitude ≥ 8 after the bond) breaks it without an explicit call.
- `gradation(target_ids=None, *, scope, owner_id, at_seq=None) -> dict` — derived dual-channel standing per target: `{net, positive, negative, positive_count, negative_count, scarred, bonded, contributions}`. G⁺/G⁻ accumulate chronologically, each clamped 0..100 — ambivalence is preserved. Presentation: unhealed scar → `net = min(net, 0)`; unbroken bond → `net = max(net, 0)`; both → exactly 0 with both flags visible. `target_ids=None` enumerates every appraised target; requested targets with no events return the neutral shape. No decay of any kind: plasticity comes only from new evidence and resolutions.
- `compute_gradation(events, *, config=GradationConfig()) -> dict[str, GradationScore]` — the pure fold behind `gradation` (`GradationConfig(channel_clamp=100.0, break_magnitude=8.0)`).
- `GradationScore` — one target's standing: `positive`, `negative`, `positive_count`, `negative_count`, `net`, `scarred`, `bonded`, `contributions`. The two channels are reported separately so ambivalence survives the fold.

Targets are anything nameable — records, people, tools, ideas, places, moments in time. Namespace-prefixed free strings are the convention (`person:ada`, `tool:web_search`, `time:morning`); there is no registry.

Two reads serve feelings without depositing any ([`feelings_reads.py`](src/abstractmemory/feelings_reads.py)):

- `feelings_about(journal, target, *, scope_pairs, gradation_config=GradationConfig(), as_of_seq=None, limit=12) -> dict` — the why-walk for one target: "why do I feel this?", answered from the appraisal stream itself, only when the caller reaches for it.
- `stimulus_feelings(store, journal, stimulus, scope_pairs, *, gradation_config=GradationConfig(), as_of_seq=None, min_net=2.0, max_feelings=5, warnings=None) -> list` — standing feelings for the targets the current stimulus touches. Feelings COLOR content; they never select it.

### Diary reads and chain verification

- `open_questions(store, *, scope, owner_id, limit=100, journal=None)` — `diary_type="question"` entries no later entry answers (`attributes.answers`; either id namespace).
- `open_problems(...)` (same signature) — unresolved `diary_type="problem"` entries (`attributes.resolves`).
- `open_ideas(...)` (same signature) — incubating `diary_type="idea"` entries; with a journal, the folded binding lifecycle decides (inactive_candidate/reviewed incubate; rejected and promoted leave the open set).
- Pass the `journal` to apply closure/hidden folds; without it these are layer-1 store reads. All three return rows oldest-first; resolved entries stay retrievable as ordinary records.
- `open_commitments(store, *, scope, owner_id, limit=100, journal=None)` — standing PROMISES (prospective memory): `diary_type="commitment"` entries no other entry fulfills (`attributes.fulfills`, mirroring answers/resolves). Kept commitments stay retrievable — "I promised, then I kept my word".
- `triggered_commitments(store, journal, *, stimulus, scope, owner_id, max_lines=3, now=None) -> list` — a pure read the host calls beside `reconstruct`: which OPEN commitments does the current stimulus trigger (person appears, topic matches, date passes)? Nothing executes a commitment; this surfaces it at the right moment so the entity can keep its word or consciously let it go.
- `verify_diary_chain(store, *, scope, owner_id) -> {"intact", "break_at", "entries"}` — audits the content-hash chain of owner-direct entries (`prev_entry_hash`/`entry_hash`); entries that make no chain claim fail nothing.
- `diary_entry_hash(title, digest, observed_at) -> str` — the chain's content hash (sha256).

### Consolidation (sleep and dreams)

Source: [`src/abstractmemory/consolidation.py`](src/abstractmemory/consolidation.py). Deterministic maintenance — no LLM calls; all pure reads except the single dream record a pass may form.

- `structural_report(store, journal, *, scopes, as_of=None) -> dict` — pure structural analysis: connected components, isolated records, duplicate titles, facet coverage, adjacency. Components are computed over `COMPONENT_RELATIONS` edges only (semantic authored relations: `summarizes`, `from_session`, `reflected_in`, `continues`, `derived_from`, `answers`, `supports`, `part_of`). `CONTEXT_RELATIONS` (`written_amid`, `mentions`) and unknown predicates never define components; their pairs are reported as `context_pairs` (already-associated), unknown predicates are named in `unknown_relations`, and co-use trails are reported separately as `trail_pairs` — habit, never adjacency.
- `dream_pass(system, *, scopes, owner_id, salience_floor=2, max_sources=8, embedder_similarity_floor=0.35, report_only=False, as_of=None, author=None) -> dict` — the night's DREAM sub-phase: report → cross-component bridge proposals (≥ 2 shared facets, participant + facet, or stored-vector cosine ≥ floor) and single-facet questions → at most ONE `kind="dream"` record (review-gated, `interpretation_required`, weak `mentions` edges to sources, idempotent by report fingerprint). Pairs already associated by trails or context edges are excluded and counted (`trail_associated`, `context_associated`). A quiet night forms nothing and says why. The dream's title and summary are authored from the night's composed text by `author`; without one, or on its failure, it forms with them pending and the failure recorded (a home without a task queue is refused). Unresolved dreams chain via `parent_dream_ids`. The result carries the night's `signals` and `signals_omitted` (the same stream the dream record rests with; empty on a quiet night). A narration is added beside the dream's summary with `narrate_dream`.
- `unresolved_dreams(store, *, scope, owner_id, journal=None, limit=100) -> list` — standing dreams with `continuation_state="unresolved"`, oldest first.
- `sleep_pass(system, *, scopes, owner_id, ..., should_continue=None, author=None, cancelled=None) -> dict` (in `maintenance.py`) — ONE FULL NIGHT in canonical order: `resolution` (`resolve_dreams_pass` — the day answers the night) → `maintenance` (`consolidation_pass` tending) → `world_models` (`world_model_pass` orientation cards) → `dream` (`dream_pass`). The result names its sub-phases (`phases` tuple), carries each sub-phase's self-describing dict, and sums their `authoring` receipts. **Graceful cancellation** (one-active-phase ruling): `should_continue` is a zero-arg host callable checked at sub-phase boundaries — the running sub-phase completes (never torn), later ones skip with `skipped_reason="cancelled: …"` and the night carries `cancelled_after`; cancelled shapes mirror the real pass shapes key-for-key. A cancelled night is valid: idempotent sub-phases mean the next sleep resumes the work. `cancelled` (optional zero-arg callable, True once the host stopped the night: the runtime passes its turn cancellation check) ends the night at every sub-phase boundary like `should_continue`, and is checked again immediately before the completion marker: a Stop raised while the final stage ran (the dream's authoring) records nothing in `{prefix}maintenance`, so `records_since_maintenance`'s boundary is not reset; the result carries `cancelled_after` = the last stage that ran (Codex review 3 finding 2).
- `records_since_maintenance(system, *, scopes=None, after=None) -> dict` (in `sleep_cadence.py`) — how much the home lived since its last COMPLETED sleep, the host's idle → sleep threshold (operator 2026-11-09: sleep after 24 h or 500 events). Pure read, nothing written or created. Returns `{count, last_maintenance_at, since, boundary_source, fragmented}`: `last_maintenance_at` is when the newest completed night over these `(scope, owner_id)` pairs ended (`None` if none; `scopes=None` = any pair); `after` is the host's own last maintenance (e.g. the gateway's last `maintenance_done`), an ISO-8601 instant in any offset style (naive = UTC; unparseable raises `ValueError`); the boundary is the later of the two, and `boundary_source` names it: `memory` (also on a tie), `after`, or `birth` (neither). `count` is the records formed in these scopes strictly after the boundary (digest rows; edges and bookkeeping rows are not records; closed records still count), or since birth; `since` is the boundary actually used (in memory's timestamp format), or for `birth` the oldest formed record's time (`None` for an empty home). `fragmented` = `{count, share, provisional, unvectored, pending}` says how much of the home a sleep would make whole: the records placed in a community provisionally since the last completed night (`placed` events), the live records without a vector, the live records whose title or summary is still being written (a vector-less record whose every engraved relation names a closed record, the sleep pass's `unplaced_closed`, such as the summary of episodes consolidated away, is left out: no edge can ever place it); `count` is the number of distinct live records that are any of the three, each counted once (a record both vector-less and pending is one fragmented record; a placement whose record has been closed since counts in `provisional` only), so `share` = count / live formed records never exceeds 1.0 (`0.0` for an empty home; live = no closure names it; `fragmented` is `None` on a store that cannot say which rows carry a vector, LanceDB). A host may sleep early when `share` passes its threshold. On the SQLite home it reads one COUNT over the pairs' records, their vector-less and pending rows, the `placed` events above the night, and which of the placed records are live (by subject index). `sleep_pass` records the completion (the queue's `memq_maintenance`, one row per pair and kind) when a whole night (`include_dream=True`) runs to its end: a cancelled, `report_only` or `as_of` night records nothing, and a cycle window (`include_dream=False`) is recorded as kind `cycle` and does not reset the count. On the SQLite home the count is one indexed COUNT per pair over the `(scope, owner_id, observed_at)` range after the completion. `MemoryTaskQueue.last_maintenance(scopes=None, *, kind="night")` reads the row (`{completed_at, seq, scope, owner_id, kind, detail}`).

Sleep proposes; waking evidence disposes: the passes write no load-bearing edges and deposit nothing (counters untouched).

### Authored with the record: the `author` callable

Source: [`src/abstractmemory/formation_authoring.py`](src/abstractmemory/formation_authoring.py). A memory's title and summary are authored WITH it; a record forms pending only after a RECORDED authoring failure (operator ruling 2026-11-09). Memory has no model access, so every pass that forms records from its own bookkeeping takes the host's optional `author` callable: `world_model_pass` and `world_model_update`, `dream_pass`, `mine_candidates_pass`, `consolidation_pass`, `wake_cue_dedup_pass`, and `sleep_pass` (which hands it to its consolidation, world-model, mining and dream phases); `engram` takes it too, for the title of a spark item without a `name` (see [Identity](#identity-spark-engram-self-core-floors)).

- Signature: `author(kind: str, body: str, context: dict) -> dict`, or it raises. `kind` is the record kind about to form (`world_model`, `dream`, `summary`); `body` is the pass's provisional text the metadata is authored from (a card's evidence briefing, the night's composition, an offer's or candidate's composed text, a dedup witness's words); `context` is `{pass, record_kind, scope, owner_id, fields, title, source_ids, attributes}`, where `fields` names what is wanted (`"title"` and/or `"summary"`; a card's next revision carries an authored title and asks only for the summary, its `title` in the context).
- Return: `{title, summary, author: {kind, id}, reader: {provider, model, thinking}, sources: [SourceRef]}` for the wanted fields. `author.kind` is one of `AUTHOR_KINDS` and never `mechanical`; a `capability-model` or `entity-mind` author needs `reader` and `sources` (the ledger step of the call, ADR 0003).
- Authored: the title is stored with `title_provenance` and the summary as the record's text with its `understanding` stamp (version 0, cause `formation`), labelled `authored-v1` (a card: `authored-card-v1`, `authored_lead`, `authored_known_ids`, `authored_by`). No authoring task is queued for an authored field.
- Not authored: the record forms pending for the missing fields, with `attributes.pending.failure = {reason, error_class, message, at, fields}` and its `title.author` / `digest.author` tasks. `reason` is `no_author_supplied` (the pass got no callable), `author_raised` (`error_class` and `message` are the exception's), or `invalid_result` (not a mapping, a wanted field missing or empty, provenance refused). A partial result stores what was authored.
- Receipt: each pass's result carries `authoring = {authored: [{record_id, record_kind, pass, fields}], failures: [{record_id, record_kind, pass, fields, reason, error_class, message, at}]}`; `sleep_pass` sums its phases' receipts. The host writes its authoring-failures lines from `failures`.
- A pass never calls `author` for a record that already stands (a re-run or a replay).

### Replay stream

`export_replay(*, scope=None, owner_id=None, since_seq=0, until_seq=None, families=None, enrich=True) -> Iterator[dict]` — on `MemorySystem`, and as a module function `export_replay(store, journal, ...)`.

Yields verbatim journal records as envelopes in strict seq order: `{stream, stream_version, seq, family, observed_at, scope, owner_id, trace_id, turn_id, run_id, payload, display?}`. One shape serves history scrub and live tail (poll with your last seen seq as `since_seq`; it is exclusive, `until_seq` inclusive, `None` = high-water at call time).

- Families: `event`, `binding`, `closure`, `trace`, `snapshot`, `valence` (the default). `family="host"` is reserved for host-authored markers (accepted by the filter, never emitted by this package). The opt-in family `revision` (`OPT_IN_FAMILIES`) is emitted only when you name it: one envelope per understanding version (payload = the `UnderstandingVersion`, `run_id` lifted from its first ledger source), so a rewrite adds nothing to the default stream.
- Correlation keys (`trace_id`, `turn_id`, `run_id`) are always present, null when absent.
- Enrichment resolves `{record_id, kind, title, token_estimate}` from the store (both members for `co_selected` pairs) and `graph_id` for formed records — absent when unresolvable, never fabricated. Diary display blocks arrive `{"redacted": "diary", "graph_id": ...}`: topology visible, content sealed; they carry who wrote the entry, when and why (`authored_by`, `understood_at`, `cause`), never the resolving entry’s own words. Their act-frame keys: `diary_type`, `entry_id` (the book's entry), `pending`; an entry that resolves a question or problem also carries `resolves` (the claim: a graph id or an entry id), `resolves_status` (the book's verdict, for example `resolved_open_question`) and `resolved_record_id` (the resolved record's graph id), so a consumer marks the question resolved from the stream alone; `resolved_title` (the resolved record's title as formed) rides only an entry that is not private (`attributes.private`). This is the public target's title, not the resolving entry's prose; hosts must omit it when the target itself is private.
- Formed non-diary records also carry the current reading's provenance (`understanding_fields`): `digest_method`, `digest_version` (0 = never rewritten), `version_count` (1 = never rewritten), `authored_by` (`{kind, id}`), `reader` (`{provider, model, thinking}`, when a model wrote it), `understood_at`, `cause`; cards add `target`, `target_type`, `card_revision`, `source_count`; dreams add `narrated`, `narration_version`; a superseded record adds `replaced_by`. `binding` and `revision` envelopes also carry the current `digest` text.
- A community record (kind `community`) replays like any formed record (title or `pending`, provenance, versions, `replaced_by`); its display block adds `community_key`, `version_seq` and `size`, and its `binding` envelope draws only its revision chain (`refines`), never an edge per member. A record's provisional placement arrives as a `placed` event (family `event`, the community in `payload.provenance`).
- Under family filters or redaction, seq gaps are expected and carry no meaning. The stream is an observability surface — not a payload export, not a second source of truth, not a checkpoint format.

### Entity identity card

`entity_card(*, scope_pairs, owner_id, current_window_events=200, top_n=5, as_of=None) -> dict` — on `MemorySystem`. The same composition is exported as the module function `identity_card(store, journal, *, scope_pairs, owner_id, ...)` in [`entity_card.py`](src/abstractmemory/entity_card.py), for callers holding a store and journal rather than a facade.

One composed pure read over a home's scope ladder (for example `[("self", eid), ("diary", eid), ("life", eid)]`), returning: `identity` (folded self core, spark version; `name` is the owner identity string), `age_and_context` (journal seq, record counts by kind per scope, diary entry count, first/last `observed_at` — timestamps only, never wall-clock now), `current_state` (a trailing window over the most recent appraisal events — current is a window, not a point), `likes_dislikes` (top targets by G⁺ and by G⁻ reported separately; record-backed targets carry resolved titles), `questions` (open and resolved, via the answers/resolves convention), `key_moments` (magnitude ≥ 8 valence events plus firsts — first dream, first interest, first supersession — chronological, most recent 20), and `discoveries` (open interests, unresolved dream count). Every section carries a `provenance` string naming its source.

Composing the card deposits nothing, and `as_of` anchors both journal signals and record existence (a card at seq T describes the entity at T; a question answered after T reads open at T).

---

## Deliberate reach: probe, expansion, familiarity

Source: [`src/abstractmemory/probe.py`](src/abstractmemory/probe.py), [`src/abstractmemory/concept_anchor.py`](src/abstractmemory/concept_anchor.py). Reconstruction is what a stimulus pulls; a probe is what the entity *reaches* for on purpose, with a stated reason.

- `probe(store, journal, *, stimulus, scopes, reason, effort="standard", embedder=None, excluded_ids=None, config=ReconstructConfig(), as_of_seq=0, trace_id=None, write_journal=True) -> ProbeResult` — one deliberate reach. Pure over the store; journal writes are the trace plus inert audit events. `write_journal=False` writes nothing.
- `probe_expand(store, journal, *, record_ids, reason, depth=1, max_records=12, token_budget=1600, excluded_ids=None, scope_pairs=(), parent_trace_id=None, as_of_seq=0, trace_id=None, write_journal=True, edge_limit_per_node=64) -> ProbeResult` — bounded source expansion from chosen records: BFS over record edges in BOTH directions, depth/count/token bounded, closure and hidden folds honored. Root ids resolve through both namespaces (row ids and graph ids); unknown roots refuse loudly.
- `familiarity(store, *, stimulus, scopes, effort="quick", ...) -> dict` — pre-answer metamemory: "do I hold any trace near this topic, and how much?" Reports match DENSITY, never content — the anti-fabrication reflex, so a caller can tell "I know nothing here" from "I hold a lot" before composing an answer.
- `PROBE_EFFORTS` — the `quick` / `standard` / `deep` presets, each a `ProbeBudget`.
- `ProbeBudget` — `max_candidates`, `max_hits`, `token_budget`, `concept_expansion`, `concept_tuning`, `keyword_discovery`, `expand_depth`, `expand_max_records`, `expand_token_budget`. All bounds are hard.
- `ProbeHit` — `record_id`, `graph_id`, `title`, `digest`, `scope`, `owner_id`, `kind`, `relevance`, `cues`, `token_estimate`, `observed_at`.
- `ProbeResult` — `trace_id`, `as_of_seq`, `hits`, `dropped`, `channels`, `warnings`, `budget_spent`.
- `concept_terms(text, *, bigrams=True) -> list[str]` — normalized concept tokens: identifier-aware words plus adjacent-word bigrams joined with `_`. Variants collapse — `auto-memory`, `auto memory`, `autoMemory` and `auto_memory` all yield `['auto', 'memory', 'auto_memory']`.
- `expand_by_concepts(store, seeds, scope_pairs, *, excluded_ids=(), tuning=ConceptAnchorTuning()) -> (admissions, warnings)` — co-occurrence expansion: records sharing a DISCRIMINATIVE concept with a seed surface as admissions.
- `ConceptAnchorTuning` — `min_sources`, `max_sources`, `scan_limit`, `max_admissions`, `max_seed_concepts`, `bigrams`.

## Situating a past moment

Source: [`src/abstractmemory/situate.py`](src/abstractmemory/situate.py).

- `situate(store, journal, *, scopes, at=None, seq=None, participant=None, occurrence="first", budget=SituateBudget(), attention_config=AttentionConfig(), config=ReconstructConfig()) -> dict` — rebuild the context of one past moment. Pure read: writes nothing, deposits nothing, and every record-shaped result is labeled historical. Address the moment by timestamp (`at`), journal seq (`seq`), or a `participant`'s first/last appearance.
- `situate_prompt_block(situation) -> str` — render one `situate()` result as a labeled prompt block. The caller owns where it lands in the prompt.
- `SituateBudget` — `window_records`, `activity_top_k`, `diary_entries`, `tensions`, `identity_delta`, `token_budget`, `moment_trace_walk`. A bound of zero means NOTHING of that section (`<= 0` is never "unlimited" in this package); negatives refuse loudly.

## World models (orientation cards)

Source: [`src/abstractmemory/world_model.py`](src/abstractmemory/world_model.py), [`src/abstractmemory/world_model_alias.py`](src/abstractmemory/world_model_alias.py). A card is what the entity currently holds about one target (a person, a topic), revised append-only. `world_model_pass` (sleep) and `world_model_update` (per turn) form each card PENDING its authored title and summary: `pending=("title", "summary")`, its evidence briefing (`mechanical-card-v2`, or the carried authored lead plus "Since then", `authored-card-v1+delta`) the provisional body; an authored title carries to the next revision (`pending=("summary",)`), the legacy "World model: <subject>" label never does. The key (`attributes.target`), evidence fingerprint, sources and revision are the pass's; the key never depends on the title. Each verdict carries `pending`.

- `current_world_models(store, *, scope, owner_id, journal=None) -> dict` — target → the CURRENT (closure-folded, highest-revision) card. `journal=None` is the layer-1 read with no fold.
- `standing_world_models(store, *, scope, owner_id, journal=None) -> dict` — target → ALL standing card assertions, revision ascending. More than one per target means an interrupted revision; the sleep pass repairs it.
- `author_world_model(system, *, target, text, scope, owner_id, author="entity-reflection", in_place=True, reader=None, sources=(), cause="formation", expected_version=None, expected_fingerprint=None, task_id=None) -> dict` — replace a card's words with AUTHORED prose. The engine never invents text. By default the card is rewritten in place: it keeps its id, revision, evidence and usage, and the previous text is kept as a version (field `card`). `author` is `{kind, id}` (see [understanding](#understanding-in-place-rewrites-and-versions)) or a label string (recorded as author kind `unrecorded`). `expected_fingerprint` is the evidence the author read: when the card's evidence changed since, the call raises `ValueError`. On a card whose summary is pending, the text is its summary (the marker clears, `digest.author` is done) and a pending title stays pending; handing back the provisional body raises `ValueError`. `in_place=False` forms revision N+1 and supersedes the prior card instead: the authored summary with the prior's authored title, else `pending=("title",)`. Returns `{target, card_id, revision, applied, digest_method, in_place, version?, pending?}`; byte-identical text returns `applied=False`.
- `world_model_update(system, *, scopes, owner_id, targets, scan_limit=400, tuning=SleepTuning(), author=None) -> dict` — the per-turn incremental update: revise the named targets' cards from a bounded newest-window evidence scan. Eventual-consistent by contract — the sleep pass normalizes over the full evidence.
- `alias_map(store, *, scope, owner_id, journal=None) -> dict` — alias → primary target, read from current cards. Derived state; the cards are the record.
- `alias_candidates(by_target, *, existing=None, overlap_floor=ALIAS_OVERLAP_FLOOR) -> list` — merge PROPOSALS from evidence overlap. Report data only; nothing forms.
- `alias_world_model(system, *, primary, alias, scope, owner_id, reason, actor="operator") -> dict` — the deliberate act that binds an alias to a primary card. The new revision carries the card's authored title and summary; what is not authored forms pending.
- `rekey_world_model(system, old_key, new_key, *, scope, owner_id, author, cause, reason=None, reader=None, sources=()) -> dict` (also `MemorySystem.rekey_world_model(old_key, new_key, ...)`) — move the standing card keyed `old_key` to `new_key` IN PLACE, keeping `old_key` as an alias. An interest's card is keyed on the subject head of the interest's title (`world_model.interest_target`; an interest whose title is pending keys on its own graph id, `topic:<graph id>`, never on its body, and `retitle` re-keys its card when the title lands), so retitling the interest moves the key the next `world_model_pass` computes; without a re-key that pass forms a second card beside the standing one. The card keeps its record id, assertion id, revision, evidence fingerprint, sources, text, title, keywords, vector, edges, versions and usage; `attributes.target` becomes `new_key`, `old_key` joins `attributes.aliases` (a person card's `participants` follow), so `current_world_models` holds one card for the subject, `alias_map`, mentions and `world_model_update(targets=[old_key])` resolve the old key to it, and the pass groups evidence under either key onto it (an interest still titled with the old words keeps clearing the floor). The move is a version row (field `target`: version 0 the old key, author mechanical, cause formation; version N+1 the new key with `author`, `reader`, `sources`, `cause` plus `reason` and `rekey: {from, to}`), `target_version` / `target_provenance` on the row; no embedder is called (the key is not in the canonical text) and pending `card.author` tasks stay. The title is not touched: `retitle` the card for that. Arguments are validated like `retitle` (bad ones raise `ValueError`, as do empty or equal keys). Returns the understanding result shape (`field="target"`) plus `old_key`, `new_key`, `card_id`, `revision`, `aliases`; `outcome="unchanged"` when the card on `new_key` already answers to `old_key`. Refusals (nothing written): `unknown_card`, `target_taken` (`new_key` already has a different standing card: the two must be merged, which this verb does not do), `alias_bound_elsewhere` (`new_key` is an alias of another card), `stale`. Run it with the home's writer lease.

## Communities (topics)

Source: [`src/abstractmemory/communities.py`](src/abstractmemory/communities.py) (the nightly pass: import `communities_pass` and `embed_missing` from `abstractmemory.communities`) and [`src/abstractmemory/community_index.py`](src/abstractmemory/community_index.py) (placement and reads: import them from `abstractmemory.community_index`). A community is a topic of the memory graph: a formed record of kind `community` written by `communities_pass`, which the host runs during its sleep, one record per version of a topic. Each version has a new graph id and `refines` the previous one, which is closed `supersede`; the stable `attributes.community_key` is carried across versions, and every record of one version carries the same `basis.version_seq`. Community records are engine state: they are never embedded and never recalled.

Communities are eventually consistent. A record formed after the last sleep is placed in a community provisionally, at formation, and the next sleep confirms or moves it. Before a home's first sleep with communities there is no version: the reads answer `first_sleep_pending: true` and formation places nothing (it never fails for it).

- Placement at formation: on a home opened with `task_queue=True`, `remember_many` places every non-community record it forms in the record's own transaction. A record with a vector goes to the nearest current centroid (cosine). A record without one, or a home whose centroids were all computed in another embedding space (`centroid_stale`), goes to the majority community of the records its formation edges name. A record with neither stays unplaced until the next sleep. A placement is ONE journal event `placed` (an audit kind, never scored) whose `provenance` is `{community_key, community_id, how ("centroid" | "neighbour"), score, provisional: true, version_seq}`; the community record is not rewritten. The centroids and the membership map live in memory and are caught up once per formation from the store's row high-water mark, so a process that keeps a home open for days places against the version the store holds now; placing a record reads no store row.
- `communities(system, *, scopes, owner_id, version=None) -> dict` — PURE READ, the payload a host serves (version 2): `{version: 2, version_seq, first_sleep_pending, algorithm, communities: [...], links: [{a, b, weight}]}`. Each community is `{id (this version's record id), key (stable across versions), title, summary, pending, verdict, size, kinds, members: [{id, via}], provisional_ids, cohesion, layout: {xy, radius, seats}, centroid_stale, facts}`, largest first; `links` name community keys. `title` and `summary` are `None` while pending (`pending` names the fields still being written). `provisional_ids` are the records placed at formation since the version (their newest `placed` event), not yet members; `size`, `kinds` and `members` are the version's. `version` reads an earlier version (the newest record of each community at or before that seq, viewed up to the next version); a seq older than the first version raises `ValueError`. A community record missing a field the read serves raises `CommunitySchemaError`.
- `facts` (read from the store and journal as they are now, one query each): `span: {first, last}` (when its first and newest members formed), `activity` (its members' uses, `selected` events, in the last `FACTS_WINDOW_DAYS` = 7 days), `hub` (the member used most, all time; `None` when none was ever used), `bridges` (members with an engraved edge to a member of another community), `paths` (the `FACTS_PATHS_TOP` = 5 member pairs most often used together, `co_selected`, in the last 7 days: `[{from, to, count}]`), `changed_since_last_sleep: {added, moved, split_from}` (members that joined and left at this version, and the community it was split from, when its version is the newest; zeros otherwise). Not served yet: which community a dissolved one merged into.
- `community_of(system, record_id) -> dict` — `{record_id, community_key, community_id, provisional, via, how, score, version_seq, first_sleep_pending}`: a member of the current version (`provisional: false`, `via` as the version names it), a provisional placement (`provisional: true`, `via: "provisional"`, `how`, `score`), or no community (`community_key: None`). An unknown record or a community record raises `ValueError`.
- `community_keys_of(system, record_ids, *, scopes, owner_id) -> dict` and `community_memberships_of(system, record_ids, *, scopes, owner_id) -> dict` — the membership-only read a host stamps on every recalled record: `{record_id: key | None}` and `{record_id: {key, provisional} | None}` for record graph ids (the ids members carry: a recalled record's `provenance.record_id`). Each answer is the one `communities(system, scopes=, owner_id=)` gives for that record: a member of a current community of these pairs (`provisional: false`), a record placed at formation since the version (`provisional: true`), else `None` (unplaced, unknown, a community record, another owner's record, no version yet). It is served from the in-memory community index under the store's lock: one `MAX(rowid)` compare with the index's mark (the formation's catch-up) and a dict lookup per id, no facts, no layout, no vector, no file opened or stat'ed; measured 20 µs per call for 50 ids at 2,000 records and 22 µs at 20,000 (54 topics; `communities()` on the same homes: 12 ms and 65 ms). After a formation the `placed` events above the last read are fetched once per scope pair; a new version reloads the index (once a night). Like placement, it sees a version that only dissolves communities (no new community record) at the next version. An unreadable version raises `CommunitySchemaError`; a store other than SQLite or in-memory raises `TypeError`.
- `communities_pass(system, *, scopes, owner_id, author, cancelled, budget=None, seed_previous=True, detect_kinds=None) -> dict` — one night's communities. It reads the graph (the formed records, their engraved edges and the store's vectors), detects communities over every vectored record (a deterministic Louvain warm-started from the previous version, at most `MAX_DETECTION_ROUNDS` = 5 rounds; `written_amid` never drives detection), attaches the vector-less records through their engraved neighbours, carries each community's `community_key` across versions by membership overlap, computes the layout, has the changed communities' titles and summaries written by `author` (the same callable as the other passes, see [the `author` callable](#authored-with-the-record-the-author-callable); `budget` = the most author calls, all of them for the topics, largest first: one call per new or changed topic writes both its title and its summary, so the pass spends at most `min(budget, n)` and never more than one call per topic; `None` authors every topic; the topics past the budget form pending, their tasks at `COMMUNITY_PRIORITY`, ahead of every other pending record; what the budget leaves goes to the STANDING topics, unchanged tonight but still pending, largest first, one call each for the fields still pending), calls `cancelled()` once, and writes the new versions, their bindings, the closures of the previous versions and their tasks in ONE commit-guarded transaction, which also moves every standing topic's live `title.author` / `digest.author` task that waits at a later priority (queued before topics came first) up to `COMMUNITY_PRIORITY`. The standing pending topics are then written IN PLACE on their current record (`rewrite_digest`, with the title when both are pending, or `retitle`; each its own guarded transaction), which completes their live tasks. An unchanged home with no pending topic to author and no task to move writes nothing. A home opened without `task_queue=True` is refused; without numpy (`abstractmemory[fast]`) the pass is skipped with its reason. The receipt carries `version_seq`, `new` / `kept` / `renamed` / `dissolved` / `unchanged`, `written`, `unvectored`, `unplaced` and `unplaced_closed` (the vector-less records whose every live engraved relation names a closed record: no edge can place them), `authored` (the topics whose title and summary this pass wrote, in a new version or in place), `authored_in_place`, `pending` (every topic with a field still pending after the pass, written tonight or standing, read back: what the topics route serves as pending), `failed` (a refused in-place write is reason `write_refused`), `reprioritized` (the standing topics' tasks moved up), `budget` (as given) and `budget_spent` (the author calls made: a host that shares one authoring budget between this pass and its drainer runs the pass first and gives the drainer `budget - budget_spent`), `authoring` (`{authored, failures}`; a topic left pending by the budget is no failure), `cancelled`, `skipped` and per-step `timings_ms`. What the author reads is the topic's briefing: its size, the kinds, the previous title, the sibling titles it must differ from (at most 40), and a stable kind-mixed sample of whole member lines within 1500 tokens (a line that does not fit is left out): under 2.3k tokens in all, about 1.5-1.9k for a home of a dozen topics.
- `embed_missing(system, *, scopes, owner_id, record_ids=None, cancelled) -> dict` — embeds the open formed records of these scopes that have no vector (`record_ids=None`: every one of them, which needs numpy; a list: those only) with the store's own embedder, one small commit-guarded commit per vector, so partial progress stands. `cancelled` is checked before each record. An embedder failure is never raised: that record and the rest are skipped (`embedder_unavailable`; `no_embedder` without one); a vector of the wrong dimension is `failed` (`dimension_mismatch`) and never written. Community records are never embedded. Returns `{pass_name, embedded, skipped, failed: [{id, reason}], reasons, cancelled}`.
- `place_record(system, prepared, *, cur) -> dict | None` with `PreparedPlacement(record_id, assertion_id, scope, owner_id, vector=None, neighbours=())` — the per-record step formation runs inside its transaction (`cur`: the open transaction on the journal's file, or `None`). Returns the event's provenance plus `event_id`, `seq` and `stored`, or `None` (no version, already placed or a member, nothing to place by). Idempotent: the event id derives from the record and the version.
- `COMMUNITIES_PAYLOAD_VERSION` (2), `COMMUNITY_KIND` (`"community"`), `PLACED_EVENT_KIND` (`"placed"`), `FACTS_WINDOW_DAYS` (7), `FACTS_PATHS_TOP` (5).

## Drives: what is alive and pressing

Source: [`src/abstractmemory/alive_drives.py`](src/abstractmemory/alive_drives.py), [`drive_pressure.py`](src/abstractmemory/drive_pressure.py), [`drive_grouping.py`](src/abstractmemory/drive_grouping.py), [`cognition_health.py`](src/abstractmemory/cognition_health.py). Drives are the standing open questions, problems, ideas, commitments, and unresolved dreams.

- `alive_drives(system, *, scopes, k=5) -> list[dict]` — the top-k alive drives as handle-shaped items, strongest first. Aliveness combines co-use trail activation with recency. Each item carries `record_id`, `kind`, `drive`, `title`, `digest`, `born_at`, `origin`, `aliveness`, and `alive_via`; a `digest_truncation` key appears only when the digest was cut. `aliveness` is within-read ordering currency only — never render it as a cross-day meter.
- `drive_pressure(store, journal, *, scopes) -> dict` — the standing drive sets folded across a scope ladder. Counts are journal-folded, so a retracted question is not pressure.
- `drive_groups(items, *, min_shared_terms=GROUP_MIN_SHARED_TERMS) -> list` — cluster drive items by shared discriminative terms. Pure, deterministic, order-independent.
- `open_drive_partition(store, journal, *, scopes) -> dict` — ONE partition over the full open drive families, the fold every grouping consumer reads, so day and sleep lanes can never group different corpora.
- `cognition_health(store, journal, *, scopes) -> dict` — the drive ratios as one compact dict.

## Candidates, disposal, and deliberate acts

Source: [`src/abstractmemory/candidate_miner.py`](src/abstractmemory/candidate_miner.py), [`disposal.py`](src/abstractmemory/disposal.py), [`identity_review.py`](src/abstractmemory/identity_review.py). Machines propose; the entity or the operator disposes. Nothing here discharges a drive on its own.

- `mine_candidates_pass(system, *, scopes, owner_id, max_candidates=2, report_only=False, as_of=None, scan_limit=0, tuning=SleepTuning(), author=None) -> dict` — one mining pass: lesson candidates (resolved tensions that lived across sessions), interest candidates (recurring un-elected themes), and question-resolution proposals. Bounded and idempotent. Each offer's title and summary are authored by `author`, or form pending with the failure recorded (the result's `title` is a report label, never stored).
- `resolve_questions_pass(store, journal, *, scopes, ...) -> dict` — open questions and problems matched against LATER evidence. Proposals only, zero writes: a machine record must never discharge a drive. Only a diary entry with `answers=`/`resolves=` closes one.
- `promote_candidate(store, journal, *, record_id, scope, owner_id, corroborating_ids, reason, min_origins=2, prompt_state=None, actor="operator") -> dict` — promote an inactive candidate, with the independence test.
- `reject_candidate(store, journal, *, record_id, scope, owner_id, reason, hide=False, actor="operator") -> dict` — the honest no: `lifecycle="rejected"` with a mandatory reason. The record stays indexed unless `hide=True` — judgment is not erasure, and hiding is a separate stated act.
- `confirm_relation(store, journal, *, source_id, relation, target_id, evidence_ids, reason, proposed_by=None, actor="operator") -> dict` — turn a proposal into a real typed edge. Idempotent by (source, relation, target). Journals one inert `cited` audit event per endpoint: confirming is judging, not using, so it must not pump activation.
- `dispose_dream(system, *, dream_id, disposition, reason, relation=None, source_id=None, target_id=None, evidence_ids=(), actor="operator") -> dict` — one call for the whole dream verdict.
- `enact_realization(store, journal, *, realization_id, supersession_record_id, reason, actor="entity-reflection") -> dict` — the entity's adoption of a held identity-amendment proposal.
- `identity_review_pass(system, *, scopes, owner_id) -> dict` — pending identity-amendment proposals and their bar states. Deposits nothing, writes nothing.

## Tending elections

Source: [`src/abstractmemory/tend.py`](src/abstractmemory/tend.py). Tending is the entity electing what to keep, revise, or let go, expressed as a parsed block and applied through existing engine verbs.

- `parse_tend_block(text, *, max_elections=5) -> {"elections": [...], "refusals": [...]}` — parse one ` ```tend ` block body. Refusals are data, not errors.
- `apply_tend_elections(system, elections, *, scope, owner_id, actor, channel=None, now=None, self_pairs=(), revisit_depth=1, revisit_max_records=12, revisit_token_budget=1600) -> dict` — apply parsed elections through the existing verbs.
- `IDENTITY_SCOPE_PENDING_RULING` — the refusal message for identity-scope tending, which is deliberately not implemented pending a ruling.

## Recall diagnostics and rendering

Source: [`src/abstractmemory/recall_reads.py`](src/abstractmemory/recall_reads.py), [`origin_diversity.py`](src/abstractmemory/origin_diversity.py), [`render_order.py`](src/abstractmemory/render_order.py), [`recent_records.py`](src/abstractmemory/recent_records.py). These answer "why did (or didn't) this surface?" without changing what surfaces next.

- `recall_history(journal, record_id, *, limit_traces=200, until_seq=None, store=None) -> dict` — the record's part in recent reconstructions, newest first.
- `explain_recall(store, journal, record_id, *, trace_id=None) -> dict` — "why did or didn't record X surface in THIS recall?" as one serving dict.
- `absence_diagnosis(store, journal, record_id, *, scope, owner_id) -> dict` — structural reasons a record may be unreachable, in plain sentences. An unformed id reads as honestly absent rather than as an error.
- `origin_diversity(handles, *, labels=None, min_counted=ORIGIN_MIN_COUNTED, dominance_floor=ORIGIN_DOMINANCE_FLOOR) -> dict | None` — fold a rendered shelf into its voice diversity. `None` means abstain: too little to judge.
- `stable_render_order(handles) -> list[(handle, rank)]` — reorder ranked shelf handles into the stable render order, so a prefix stays reusable across turns.
- `recent_records(store, journal, *, scopes, since, until=None, kinds=None, limit=RECENT_RECORDS_DEFAULT_LIMIT, as_of=None) -> dict` — formed records newest-first with folds applied. The result carries `truncated` when more exist in the window than the page shows.

## Maintenance, health, and operator tools

Source: [`src/abstractmemory/maintenance.py`](src/abstractmemory/maintenance.py), [`sleep_cadence.py`](src/abstractmemory/sleep_cadence.py), [`mind_mass.py`](src/abstractmemory/mind_mass.py), [`redigestion.py`](src/abstractmemory/redigestion.py), [`reembed.py`](src/abstractmemory/reembed.py), [`doctoring.py`](src/abstractmemory/doctoring.py), [`engine_manifest.py`](src/abstractmemory/engine_manifest.py).

- `maintenance_report(store, journal, *, scopes, as_of=None, scan_limit=None, tuning=SleepTuning()) -> dict` — pure phase-1 analysis: structure plus an attribute-level scan (near-duplicates, metadata gaps, suppressions). Deterministic; deposits nothing. Every policy number rides `tuning`.
- `maintenance_due(store, journal, *, scopes, since_seq=None, min_new_records=12, min_signal=3, tasks=None) -> dict` — the deterministic cadence predicate (with `tasks`, pending ripple and night-only memory tasks count as due; see [the task queue](#the-memory-task-queue-understanding-in-the-background)). Due when enough new records formed, or when some new material exists and the fragmentation signal clears its floor. A store with zero new formations is never due. Late-local-time remains the host's clock. Bounded by a cursor (0058): for `SQLiteTripleStore` and `InMemoryTripleStore` the answer is folded from the store's `assertions_after` feed, per (store, journal, scopes) in the process, so a check reads only the rows written since the previous check and re-reads the journal's formation bindings only when the journal moved (a repeated check with nothing new costs well under a millisecond). Same answer as the full scan, which other stores (LanceDB) still use.
- `last_maintenance_seq(store, journal, *, scopes) -> int` — journal seq of the newest sleep artifact's formation; `0` means this store has never slept.
- `mind_mass_report(store, journal, *, scopes, days=30, bucket="day", embedder=None, now=None, vector_scan_limit=VECTOR_SCAN_LIMIT, wake_cue_kind="episode") -> dict` — formation cadence, journal mass, duplicate mass, embedding-space integrity, review backlog, and sleep recency, window-bounded. Warnings come from the closed `WARNING_WORDS` set.
- `redigestion_candidates(system, *, scopes, limit=50, min_residue_chars=24, methods=MECHANICAL_DIGEST_METHODS) -> dict` — enumerate the labeled mechanical-digest debt, worst first. Pure read: listing a debt is not using the memories.
- `apply_redigestion(system, entries, *, actor, digest_method="entity-authored", turn_id=None) -> dict` — deprecated (emits `DeprecationWarning`): forms a NEW record per entry and supersedes the old one, so the new record starts with zero usage. Use `rewrite_digest`, which rewrites in place.
- `RedigestionCandidate` — one labeled-mechanical record awaiting authored words, including `poverty` and `content_residue`.
- `reembed_store(system, *, embedder, owner_id, model_id=None, marker_scope="life", reason=..., batch_size=64) -> dict` — re-derive the whole vector index and swap atomically, pin written LAST. `reembed_home` is the same call under the name the operator guide uses.
- `read_embedding_pin(path, *, table_name="triples") -> dict | None` — a pure peek at a store file's embedding pin without opening the store. Raises `ValueError` if `table_name` is not a plain SQL identifier.
- `build_pin(model_id, dimension, *, source, claimed_by=None) -> dict` — the normalized pin payload. At least one of model/dimension must be present: an empty pin would enforce nothing and lie about it.
- `journal_cold_cut(src_path, dst_path, *, cut_seq=0, pair_cuts=None, archive_ref, exported_stream_ref=None, null_retired_embeddings=True, cut_traces=False) -> dict` — rebuild a home store file with the attention-event mass cold-cut.
- `verify_cold_cut(src_path, dst_path) -> {"ok", "checks"}` — parity checks between original and rebuilt file, measured rather than asserted; every check names its own numbers.
- `wake_cue_dedup_pass(system, *, scope, owner_id, actor, kind="episode", jaccard_floor=0.82, min_cluster=3, report_only=False, author=None) -> dict` — collapse near-identical same-day records into one day summary. Deliberately conservative: two similar episodes are a life, twenty near-identical ones are a loop artifact. The day summary's title and summary are authored by `author` from the witness's digest, or form pending with the failure recorded; the pass knows its own summary by `provenance.source` (`DEDUP_SOURCE`) once that summary is written.
- `engine_manifest() -> dict` — the machine-readable engine inventory. Pure and JSON-safe.

## Vocabularies and tuning constants

Closed vocabularies (frozensets) and declared tunables. Constructing a tuning object with defaults changes nothing — the defaults *are* today's behavior.

| Name | Value / meaning |
| --- | --- |
| `MEMORY_RECORD_KINDS` | every valid `record_kind` |
| `IDENTITY_KINDS` | `value`, `purpose`, `trait`, `interest` |
| `DIARY_TYPES` | `question`, `problem`, `idea`, `note`, `lesson`, `reflection`, `commitment` |
| `REFLECTION_FORM_KINDS` | kinds a reflection pass may form |
| `CONSOLIDATION_PROTECTED_KINDS` | never machine-consolidated |
| `REDIGESTION_PROTECTED_KINDS` | never machine-redigested |
| `MECHANICAL_DIGEST_METHODS` | digest methods that count as debt |
| `AUTHOR_KINDS` | `mechanical`, `entity-mind`, `capability-model`, `operator` — who may author a reading |
| `REWRITE_CAUSES` | `formation`, `backfill`, `ripple`, `evidence`, `operator`, `revert`, `repair` |
| `UNDERSTANDING_PROTECTED_KINDS` | never rewritten in place: diary, value, purpose, trait, interest, realization |
| `OPT_IN_FAMILIES` | replay families emitted only on request: `revision` |
| `TASK_KINDS` | `title.author`, `digest.author`, `digest.rewrite`, `card.author`, `dream.narrate`, `ripple.reconsider`, `ripple.consolidate` |
| `WINDOWS` | `turn`, `visit`, `nap`, `cycle`, `night`, `idle` |
| `DISPOSAL_RELATIONS` | relations `confirm_relation` may create |
| `KIND_RANKS` | ordering rank per kind (identity first, derived artifacts last) |
| `WARNING_WORDS` | the closed warning vocabulary of `mind_mass_report` |
| `ENTITY_RECALL_CANDIDATE_CAP` | `100` — the recall candidate pool bound |
| `ENTITY_CONTEXT_ACCEPTABLE` | `200000` |
| `RECENT_RECORDS_DEFAULT_LIMIT` | `24` |
| `VECTOR_SCAN_LIMIT` | `4000` |
| `DRIVE_PRESSURE_BOUND` | `20` |
| `GROUP_MIN_SHARED_TERMS`, `GROUP_OFFER_FLOOR`, `GROUP_BOOST_STEP` | drive-grouping thresholds |
| `FAMILIARITY_MIN_KEYWORD_TOKENS`, `FAMILIARITY_STRONG_THRESHOLD`, `FAMILIARITY_VECTOR_MIN` | familiarity thresholds |
| `ORIGIN_MIN_COUNTED`, `ORIGIN_DOMINANCE_FLOOR` | origin-diversity thresholds |
| `ALIAS_OVERLAP_FLOOR` | `0.6` — alias proposal Jaccard floor |
| `MANIFEST_VERSION` | engine-manifest schema version |
| `ANCHOR_SEQ_ATTRIBUTE`, `ANCHOR_MOMENT_ATTRIBUTE`, `CONTEXT_ANCHOR_FIELD`, `IDENTITY_ANCHOR_FIELD` | attribute/field names hosts stamp for anchoring |
| `SleepTuning` | every sleep-lane policy number, one frozen object |
| `ReconstructConfig` | kind policy and admission knobs for reconstruction |

---

## docs/stores.md

# Stores / Backends

AbstractMemory provides three append-only triple stores:
- `InMemoryTripleStore` (dependency-free, volatile, vector-capable)
- `SQLiteTripleStore` (stdlib, persistent, vector-capable — the recommended
  single-file pairing for durable homes: construct with an embedder so rows
  embed on write; vectorless rows are a labeled degradation, not the
  default posture)
- `LanceDBTripleStore` (optional dependency, persistent, vector-capable)

Public exports: [`src/abstractmemory/__init__.py`](src/abstractmemory/__init__.py)

## InMemoryTripleStore

Source: [`src/abstractmemory/in_memory_store.py`](src/abstractmemory/in_memory_store.py)

What it is:
- A small, dependency-free implementation intended for tests/dev and environments without LanceDB.
- Stores assertions (and optional vectors) in process memory.

Vector search support:
- If constructed with an `embedder`, `add(...)` embeds a canonical text representation per assertion and stores it in-memory.
- `query_text=...` requires an `embedder` (raises `ValueError` otherwise).
- `query_vector=...` is supported, but only rows with stored vectors participate.
- Vector query results attach retrieval metadata to `attributes["_retrieval"]` (score + metric).
  - Embedded text is derived from `subject predicate object` plus selected `attributes` keys; see `_canonical_text(...)` in the store source.

## SQLiteTripleStore

Source: [`src/abstractmemory/sqlite_store.py`](src/abstractmemory/sqlite_store.py)

What it is:
- A persistent SQLite-backed implementation using the Python standard library.
- The durable single-file store: structured queries AND native vector
  search in one file. Creates the table and indexes during construction;
  files created before the vector column existed upgrade in place
  (`ALTER TABLE` adds the `embedding` column on open — no migration step).

Semantic/vector support (mirrors the InMemory reference semantics exactly):
- Construct with `embedder=` and `add(...)` embeds each assertion's
  canonical text (edge assertions never embed) and persists the vector in
  the same file. An embedder failure aborts the add with zero rows written.
- `query_text=...` requires the embedder (raises the same `ValueError` as
  InMemory — no keyword fallback); `query_vector=...` works directly.
- Vectors are stored as float32 little-endian BLOBs (4 bytes per
  dimension; backlog 0058). A home written by an earlier version (JSON text
  vectors) is upgraded in place when the store opens: rows above the
  `vector_blob_high_water` cursor of the meta sidecar are rewritten in
  bounded transactions, so the upgrade is idempotent and an interrupted one
  resumes (about 5 s for 50k rows of 384-d vectors, once). Values float32
  cannot hold are refused at write (`ValueError`) rather than clipped.
- Cosine ranking returns exactly what the reference scan of
  `vector_scoring.py` returns (same rows, same order, bit-identical scores;
  both stores rank identically). With numpy installed
  (`pip install "abstractmemory[fast]"`), the store serves it from an
  in-process float32 matrix cache: built at a handle's first semantic query,
  appended with the rows written since (by any handle or process), rebuilt
  when `replace_vectors` or the upgrade rewrites the vectors
  (`vector_generation` in the meta sidecar). A float32 pass selects the
  candidates with a margin above its error bound and the candidates are
  re-scored with the reference arithmetic. Memory: 4 bytes per dimension
  per vectored row (about 77 MB for 50k rows of 384-d vectors).
  `store.vector_engine` says which path serves: `"numpy-matrix-cache"` or
  `"python-linear-scan"`. The Python scan serves (labelled once per handle
  with a `#FALLBACK` warning) when numpy is absent or a stored vector cannot
  be scored exactly by the matrix (mixed dimensions, non-finite values).
  ANN indexing stays out of scope (LanceDB is the optional accelerator).
- Rows added while no embedder was configured stay vectorless: vector
  queries skip them and layer-2 recall labels the degradation
  (`#FALLBACK`). The `reembed_store(...)` repair pass backfills them.

Embedding-space pin (one store = one embedding space; both stores):
- Pass `embedding_pin={"model_id": ..., "dimension": ...}` at construction
  to declare the space as a birth choice (SQLite persists it in a
  `{table}_meta` sidecar in the same file). `store.embedding_pin()` reads it.
- No silent mixing of spaces: a known embedder identity contradicting the
  pin refuses at open; a wrong-dimension write refuses with zero rows; a
  wrong-dimension query vector refuses at read (layer-2 recall converts
  that into the vector channel's labeled `#FALLBACK` and keeps serving
  exact/keyword).
- Pinless legacy stores pin at their first embedded write with a labeled
  `#FALLBACK` warning — creation pinning is the contract for new homes.
  First-write pins carry `claimed_by: "embedder-attribute"`: the model_id
  is a CLAIM read off the bound embedder object, never verified against
  the serving endpoint (the engine cannot know server truth). Creation and
  reembed pins are operator-vouched and carry no claim marker.
- Embed-time failures name the pin: when the configured embedder fails a
  `query_text` embed (e.g. an HTTP 400 naming the requested model), the
  raised error appends `[store pin: <model>@<dim>d (source, claimed by
  ...)]` so claimed-vs-served identity is visible in one line; the vector
  channel's `#FALLBACK` warning carries the same suffix.
- `reembed_store(system, embedder=..., owner_id=...)` is the deliberate,
  operator-gated migration: all-or-nothing re-embed + atomic swap, pin
  updated last, the act journaled as a bookkeeping record. Same memories,
  different neighbors — a substrate effect, on the record, never silent.

Persistence:
- Data (including vectors) is stored in the provided SQLite file path.
- Behavior is covered by [`tests/test_sqlite_triple_store.py`](tests/test_sqlite_triple_store.py)
  and the cross-store parity suite [`tests/test_store_vectors.py`](tests/test_store_vectors.py).

Stored columns:
- `assertion_id` (uuid)
- `subject`, `predicate`, `object`, `scope`, `owner_id`
- `observed_at`, `valid_from`, `valid_until`, `confidence`
- `provenance_json`, `attributes_json` (serialized dicts)
- `text` (canonical text, kept inspectable)
- `embedding` (float32 little-endian BLOB since 0.4.0; JSON text in homes
  not yet upgraded; NULL for vectorless rows)

Reads (backlog 0058):
- Structured reads select the assertion columns only (never `embedding` or
  `text`); `stored_vector(id)` and semantic queries read the vector.
- The owner filter is an index-usable equality; partition reads ("latest N
  in a (scope, owner)", as-of windows, per-pair scans) walk
  `idx_<table>_scope_owner_observed` in query order; reads anchored on ids,
  a subject or an object use their own index. `query_plan(q)` returns
  SQLite's `EXPLAIN QUERY PLAN` lines for a query.
- `assertions_after(cursor, *, scope=None, owner_id=None, limit=1000)` is a
  bounded change feed in rowid order (`(rows, next_cursor)`); the
  maintenance cadence folds it so a check reads only the rows written since
  the previous one. `InMemoryTripleStore` offers the same feed over its
  insertion order.

## LanceDBTripleStore

Source: [`src/abstractmemory/lancedb_store.py`](src/abstractmemory/lancedb_store.py)

Install:
- From source (recommended inside the AbstractFramework monorepo): `python -m pip install -e ".[lancedb]"`
- PyPI (packaged release): `python -m pip install "AbstractMemory[lancedb]"` (release channel may not match this repository checkout exactly; see [`README.md`](README.md))

What it is:
- A persistent, local-path LanceDB table storing append-only assertions.
- A durable vector-capable backend with native vector indexing; choose it
  when you want LanceDB's storage format instead of a single SQLite file.

Dependency note:
- `lancedb` is optional; constructing `LanceDBTripleStore` raises an `ImportError` with an install hint when it is missing.
  - Evidence: [`tests/test_lancedb_triple_store.py`](tests/test_lancedb_triple_store.py)

Persistence:
- Data is stored under the provided `uri` (directory path).
- Behavior is covered by the persistence test: [`tests/test_lancedb_triple_store.py`](tests/test_lancedb_triple_store.py)

As a graph home (`MemoryGraph(LanceDBTripleStore(...), journal)`, the LanceDB connector): the graph creates the triple table with an explicit schema (and adds missing nullable columns to a table `add` created from its first batch), keeps its revision log, history, provenance and derivation tables beside it, and writes each revision log-first so `repair()` can finish a crash-interrupted one. The journal lives elsewhere (a `SQLiteJournal` file). What it cannot promise, unlike SQLite: serialized writers across processes, and invisibility of a half-written revision before `repair()` ([`api.md`](docs/api.md#connectors-abstractmemorygraphbackend), ADR 0003 decision 3).

Stored columns (v0):
- `assertion_id` (uuid)
- `subject`, `predicate`, `object`, `scope`, `owner_id`
- `observed_at`, `valid_from`, `valid_until`, `confidence`
- `provenance_json`, `attributes_json` (serialized dicts)
- `text` (canonical text used for embedding, kept inspectable)
- optional vector column (default: `vector`) when `embedder` is configured

Query mechanics:
- Structured filters compile into a SQL-like `where` clause (see `_build_where_clause(...)`).
- Vector search uses LanceDB search with `metric("cosine")`. Returned rows include `_distance`; AbstractMemory attaches similarity metadata to `TripleAssertion.attributes["_retrieval"]`.

## Shared behavior (important contracts)

Canonicalization:
- `TripleAssertion` canonicalizes `subject`, `predicate`, `object` (trim + lowercase) and normalizes `scope`.
- `TripleQuery` canonicalizes the same fields for exact matching.

Append-only:
- There is no delete API and no general update API. Represent changes by adding a new `TripleAssertion` with updated fields and fresh provenance.
- The one mutable cell is a formed record's digest row: the layer-2 `rewrite_digest` (and `author_world_model`, `narrate_dream`) replaces its `object`, reserved attributes, canonical text, keyword entry and vector in place, in one transaction with the journal's version row (in-memory and SQLite stores; the SQLite journal must share the store's file). `LanceDBTripleStore` refuses with `NotImplementedError` (backlog 0059).
- The in-place window changes: `InMemoryTripleStore` and `SQLiteTripleStore` offer `close_assertion(assertion_id, valid_until)`, which sets `valid_until` on an assertion whose window is still open (`True` when a row changed; `False` for an unknown id or an already closed window), and `narrow_assertion(assertion_id, valid_until)`, which moves an already-closed window earlier and never widens it (`ValueError` for an open window or a value that is not a timestamp; `False` for an unknown id or a date at or after the current end, compared as instants). Every other column, the keyword index and the stored vector stay as written. `LanceDBTripleStore` does not offer `close_assertion`, and its `narrow_assertion` raises `NotImplementedError` (backlog 0057). In a mind written through `abstractmemory.graph`, both happen as a revision's `close` / `narrow` op, which also records the cause (see [`api.md`](docs/api.md#the-memory-graph-revisions-and-provenance)); a `SQLiteTripleStore` used as a graph home also holds the `memg_*` tables listed there.

Timestamps are strings:
- All stores compare `observed_at` / `valid_*` as strings.
- Use ISO-8601/RFC-3339 in UTC (e.g. `2026-01-01T00:00:00+00:00`) to keep ordering/filtering predictable.

Limit semantics:
- `limit <= 0` means “unbounded” (tested in [`tests/test_triple_store_limits.py`](tests/test_triple_store_limits.py)).

Ordering + limit semantics:
- For non-semantic queries, all stores order by `observed_at` and apply `limit` after ordering.
  - Covered in [`tests/test_triple_store_limits.py`](tests/test_triple_store_limits.py) and [`tests/test_sqlite_triple_store.py`](tests/test_sqlite_triple_store.py).
  - Note: `LanceDBTripleStore` enforces this by fetching all matching rows then sorting in Python (no `order_by` on LanceDB query builders as used here). See [`src/abstractmemory/lancedb_store.py`](src/abstractmemory/lancedb_store.py).

Vector consistency (all vector-capable stores):
- To use `query_text` / `query_vector`, assertions must have been written with vectors (store constructed with an `embedder`).
- Keep one embedding model per store: vectors from different models are not comparable, and the store does not enforce this.
- If you override `vector_column` (`InMemoryTripleStore` / `LanceDBTripleStore`), use the same name consistently for writes and queries; `SQLiteTripleStore` stores vectors in its `embedding` column.

## Next

- Query fields and semantics: [`docs/api.md`](docs/api.md)
- System view and boundaries: [`docs/architecture.md`](docs/architecture.md)
- Common questions: [`docs/faq.md`](docs/faq.md)

---

## docs/operator.md

# Operator guide — observing and verifying an entity's memory

This guide shows what you, the human operator, can see and verify inside a summoned entity's memory home, with runnable snippets. It assumes a host (typically AbstractGateway) created the home; every snippet here works directly against the home's files with only `abstractmemory` installed. For the concepts behind these reads, see [`memory-system.md`](docs/memory-system.md).

An entity home contains one store+journal pair:

```
<data_dir>/entities/<slug>/
  spark.yaml        # the attested seed (byte-verbatim)
  memory.sqlite3    # layer 1 (the graph) + the journal (one file)
  home.sqlite3      # the diary book (the host's hash-chained ledger)
  manifest.json     # entity id, spark hash
```

Open it read-only:

```python
from abstractmemory import MemorySystem, SQLiteTripleStore, SQLiteJournal

store = SQLiteTripleStore("memory.sqlite3")
journal = SQLiteJournal("memory.sqlite3")
system = MemorySystem(store=store, journal=journal)

eid = "entity:castor"   # the entity id, from manifest.json (entity:<name>)
```

The home's scope ladder is `("self", eid)` for identity, `("diary", eid)` for diary projections, and `("life", eid)` for lived episodes. Everything below is a **pure read** — nothing you do here strengthens, weakens, or touches the entity's memory (reading is not using; only `commit_selection` deposits).

## 1. Who is this entity right now? (the identity core)

```python
for a in system.self_records(scope="self", owner_id=eid):
    kind = a.attributes.get("record_kind")       # value | purpose | trait
    print(f"[{kind}] {a.attributes.get('title')}: {a.object}")
```

This is the same folded read the summon prelude renders — what the entity is reminded of about itself at every waking. Order is identity order: values first (by precedence), then purposes, then traits.

## 2. The identity card (one composed view)

```python
card = system.entity_card(
    scope_pairs=[("self", eid), ("diary", eid), ("life", eid)],
    owner_id=eid,
)
print(card["identity"]["values"])
print(card["current_state"])         # recent valence over a trailing window
print(card["questions"]["open"])     # what it still wonders
print(card["key_moments"]["moments"])
```

Every section carries a `provenance` string naming its source. The card is *about* the entity, derived from its data; composing it deposits nothing, and `as_of=` renders the card as of any past journal seq.

## 3. What would wake it? (wake reasons)

```python
from abstractmemory import open_questions, open_problems, open_ideas

qs = open_questions(store, scope="diary", owner_id=eid, journal=journal)
ps = open_problems(store, scope="diary", owner_id=eid, journal=journal)
ideas = open_ideas(store, scope="diary", owner_id=eid, journal=journal)
```

Curiosity / wrongness / direction: diary entries the entity wrote that no later entry has resolved.

## 4. How does it feel about things? (gradation)

```python
g = system.gradation(None, scope="self", owner_id=eid)   # every appraised target
print(g.get("person:albou"))   # {net, positive, negative, scarred, bonded, ...}
```

Gradation is the entity's accumulated signed experience per target (people, tools, topics, moments). Scars cap presentation at ≤ 0 until healed; bonds floor it at ≥ 0 unless broken. Both channels stay visible — a hundred +1s then one −10 reads net +90 *with* the −10 visible, preserving ambivalence.

## 5. Watch the graph live (or replay any past stretch)

```python
for item in system.export_replay(since_seq=0):
    print(item["seq"], item["family"], item.get("display"))
```

One stream, six families: `event` (attention: what got selected, what got reinforced), `binding` (visibility transitions), `closure` (belief lifecycle), `trace` (each recall: candidates, scores, what was dropped and why), `snapshot` (what actually entered a context), `valence` (appraisals, scars, bonds). Replay is a bounded read from `since_seq=0`; live is the same read polled with your last seen `seq` as the cursor. AbstractGateway serves this same stream over HTTP (`/api/gateway/entities/<name>/replay` and `/replay/stream` for SSE). Diary display blocks arrive `{"redacted": "diary"}`: topology visible, content sealed (the diary words stay in the book).

## 6. Verify integrity

**Diary chain** (graph plane): owner-direct diary entries carry a content-hash chain; a tampered or truncated chain fails loudly, naming the first broken entry:

```python
from abstractmemory import verify_diary_chain
report = verify_diary_chain(store, scope="diary", owner_id=eid)
# {"intact": bool, "break_at": record_id | None, "entries": n}
```

Diary *projections* attest through the host's book (`home.sqlite3`, a hash-chained ledger) instead — verifying the book is a host surface (for AbstractGateway, the `entity verify` command).

**Journal determinism**: any past reconstruction can be replayed by anchoring `as_of` to its trace's `as_of_seq` — same inputs, same result:

```python
traces = journal.traces(limit=10)          # most recent recalls
snapshots = journal.snapshots(limit=10)    # what entered contexts
```

**Spark attestation** is host-side (`manifest.json` records the spark hash; `spark.yaml` is byte-verbatim).

## 6b. The embedding space (pin, mismatches, repair)

Every home store is ONE embedding space, declared by a pin (`{model_id, dimension, source, pinned_at}`) written at creation — the embedder is a birth choice:

```python
store = SQLiteTripleStore(home / "memory.sqlite3", embedder=embedder,
                          embedding_pin={"model_id": "text-embedding-qwen3-embedding-0.6b",
                                         "dimension": 1024})
store.embedding_pin()   # read it back
```

No silent mixing of spaces: opening with a different known model refuses; a wrong-dimension write refuses with zero rows; a wrong-dimension query refuses at read and recall degrades loudly to exact/keyword (`#FALLBACK` on the vector channel). Homes created before pinning (Castor's) take the FIRST-WRITE path — pinned at their next embedded write from what actually embedded, with a labeled `#FALLBACK` warning naming it (ruled by the door's owner: the door pins only what it knows; a creation-source pin written years after birth would claim a fact nobody attested — the label is the home's history, not a blemish). First-write pins additionally record `claimed_by: "embedder-attribute"` — the model label was read off the embedder object, not asserted by an operator, and the engine cannot verify a label against the serving endpoint (2026-07-11 incident: a first-write pin captured a label the server never recognized). Pinning such a home deliberately remains one operator act: pass `embedding_pin=` on the pinless store and it writes as a creation-source pin from that moment on. When an embed call fails at query time, the error names the pin (`[store pin: <model>@<dim>d (...)]`) so claimed-vs-served is one line.

Changing the embedder is a deliberate, operator-gated repair — never routine:

```python
from abstractmemory import reembed_store
report = reembed_store(system, embedder=new_embedder, owner_id=eid)
# {'rows': n, 'vectored': m, 'old_pin': ..., 'new_pin': ..., 'marker_record_id': ...}
```

Vectors are derived data: the pass re-embeds every stored text, swaps atomically (pin last), backfills vectorless rows, and journals the act as a bookkeeping record — the life stream shows exactly when retrieval geometry changed. Memories, history, counts, and feelings are untouched; retrieval *neighbors* may shift (same memories, different neighbors — a substrate effect, on the record). Run it over a closed home under the maintenance lease. A runnable demonstration of all of this is `examples/entity_home_maintenance_proof.py` (nine numbered proofs; `--live` uses LMStudio).

## 7. What the engine will never do

- **Delete**: there is no delete surface. Forgetting is decay of retrieval strength + closure records + silencing — the substrate is lossless.
- **Compact**: no summarizing-and-replacing. A record's digest may be rewritten in place (`rewrite_digest`), and then every earlier reading stays in the journal as a version you can read (`versions`) and restore (`revert_digest`); the verbatim is never touched.
- **Strengthen on read**: your inspection deposits nothing. Only the entity's own committed use moves its trails.
- **Mix embedding spaces**: the pin refuses wrong-space writes and reads; the only space change is the journaled reembed repair.

## Budget guidance (entity sessions)

Entity sessions RECOMMEND a ~40,000-token context window (`ENTITY_CONTEXT_FLOOR` / `ENTITY_CONTEXT_RECOMMENDED`; operator 2026-08-01: a soft efficiency target, not a wall — smaller windows are accepted with a labeled warning, and if it needs to grow, it needs to grow). The engine ships the budget profile hosts inject at summon:

```python
from abstractmemory import entity_recall_budget, ENTITY_CONTEXT_FLOOR
budget = entity_recall_budget(40_000)                  # token_budget=4800, shelf_size=12
budget = entity_recall_budget(1_000_000)               # token_budget=120_000 — no upper cap
budget = entity_recall_budget(40_000, shelf_size=24)   # widened shelf
```

The token budget is 12% of the context window, uncapped above the 2,400-token starvation floor — wider contexts buy a wider working set at an efficiency tradeoff you can measure. The default shelf of 12 models limited attention (seats, like what a mind holds at once) and is a declared tunable.

## Resident homes and the attention window

The temporal-activation fold reads a bounded window of recent attention events (`AttentionConfig.window_limit`, default 512 — session-scale). A continuously active home emits on the order of 1,000 events per day, so at the default a record heavily used yesterday can fall past the window edge and read temporal-zero today — a cliff rather than the gradual decay the curve provides (at the edge an event still carries ~4% of its weight). For resident homes, size the window to about a week of the home's **measured** cadence:

```python
from abstractmemory import AttentionConfig, MemorySystem
system = MemorySystem(store=store, journal=journal,
                      attention_config=AttentionConfig(window_limit=8192))
```

The GLOBAL access count never windows — the two-count model's global half (`selected_count` / `pair_selected_count`, "what mattered over a lifetime") is untouched by this setting; only the temporal half reads through the window.

---

## docs/faq.md

# FAQ

## Where should I start?

- Installation + first examples: [`getting-started.md`](docs/getting-started.md)
- Concepts and invariants: [`architecture.md`](docs/architecture.md)
- The cognitive model: [`memory-system.md`](docs/memory-system.md)
- Public API contracts: [`api.md`](docs/api.md)
- Store behavior: [`stores.md`](docs/stores.md)

## What is AbstractMemory (and what is it not)?

AbstractMemory is a Python library for durable, append-only agent memory: temporal, provenance-aware triple assertions with deterministic structured queries and optional vector retrieval (layer 1), plus a `MemorySystem` facade that composes them with an append-only journal into a usage-weighted memory graph — typed records, stimulus-driven reconstruction, attention, identity, valence, diary conventions, consolidation, and a replay stream (layer 2).

It is **not**:

- A knowledge-graph reasoner (no inference, joins, or ontologies).
- A text extraction/summarization library (no LLM calls anywhere in the package).
- A runtime or an agent host: it owns memory mechanics; hosts decide when to recall, what enters prompts, and who may write.

## How does AbstractMemory fit into AbstractFramework?

- **AbstractMemory**: the memory substrate (this package) — no dependency on the other packages.
- **AbstractRuntime**: orchestrates when memory is consulted and committed (per turn) and owns host-side surfaces such as the diary book.
- **AbstractGateway**: hosts entity homes, serves the replay stream over HTTP, and can provide embeddings via `AbstractGatewayTextEmbedder`.

Related projects: `https://github.com/lpalbou/abstractframework`, `https://github.com/lpalbou/abstractcore`, `https://github.com/lpalbou/abstractruntime`.

## What is the core data model?

At layer 1, `TripleAssertion` is the single write primitive: `(subject, predicate, object)` plus `scope`, `owner_id`, time fields, and metadata dicts (`provenance`, `attributes`). Stores stamp `assertion_id` on every query result. At layer 2, typed memory records are encoded over the same substrate: one digest assertion per record (the record's graph id is the assertion's subject) plus one assertion per edge.

## Why are `subject` / `predicate` / `object` lowercased?

Canonicalization (trim + lowercase) is part of the matching contract: it prevents missed matches when the same term arrives with different casing or whitespace. To preserve original casing, store it separately (for example `attributes={"raw_subject": "Alice"}`), or set `attributes={"literal": True}` to keep the `object` case-sensitive — typed records use this for their digest text.

## Does AbstractMemory support updates or deletes?

There is no delete API, and one bounded update, by design:

- At layer 1, represent changes by adding a new assertion with fresh provenance. When the new assertion supersedes an earlier one, `close_assertion(assertion_id, valid_until)` (in-memory and SQLite stores) closes the earlier validity window in place, so `TripleQuery(active_at=...)` stops returning it from that moment; nothing else about the row changes. `narrow_assertion(assertion_id, valid_until)` moves an already-closed window earlier and never widens it. A mind written through `abstractmemory.graph` makes both changes as operations of a `Revision`, with a cause and a history.
- At layer 2, belief revision is a closure record (`close_record` / `close_assertions`: retract, supersede with replacements, or reinterpreted when the source is read differently) — the old record leaves ranked retrieval but stays in the store and in history. Visibility can also be withdrawn per scope with a `hidden` binding and restored with `indexed`.
- A record's understanding can be rewritten in place: `rewrite_digest` replaces its digest (and optionally title and keywords), and `retitle` its title alone, while the record keeps its ids, edges, usage and journal; every earlier reading is kept as a version (`versions`, `revert_digest`). Cards are authored the same way through `author_world_model`; dreams gain a narration through `narrate_dream`. The verbatim is never touched.
- Forgetting is decay of retrieval strength plus closures and silencing. The substrate is lossless; there is no compaction.

## What do `scope` and `owner_id` mean?

They partition data. `scope` is a free-form label (lowercased); `owner_id` is an identifier within it. Common conventions: `"session"` + session id, `"run"` + run id, `"global"` for shared memory, and the entity-home ladder `"self"` / `"diary"` / `"life"` + entity id. At layer 2, `"global"` is a broad scope by default: searching it in `reconstruct` requires an explicit `escalation_reason`.

## How are time filters evaluated?

Time fields are stored and compared as **strings**: `since`/`until` compare `observed_at` (`>= since`, `<= until`); `active_at` intersects the `(valid_from, valid_until)` window with an exclusive end. Use RFC-3339/ISO-8601 UTC strings (e.g. `2026-01-01T00:00:00+00:00`) to keep comparisons predictable.

## Which store should I use?

- `InMemoryTripleStore`: dependency-free, volatile — tests, development, ephemeral agents.
- `SQLiteTripleStore`: dependency-free persistent single file with structured queries **and** native vector search (construct with an embedder) — the recommended durable default; a store+journal pair can share one file.
- `LanceDBTripleStore`: persistent vector-capable backend on LanceDB's storage format (optional dependency).

See [`stores.md`](docs/stores.md) for details.

## How do I do semantic search?

Vector search is opt-in and works the same across all three stores:

- `query_text=...` requires a configured embedder (a `ValueError` is raised otherwise; there is no keyword fallback).
- `query_vector=...` bypasses embedding generation.
- Only rows written with vectors participate; `min_score` applies a cosine threshold; results carry `attributes["_retrieval"]`.

## Are queries deterministic?

Structured queries: yes — filters are explicit, and non-semantic results are ordered by `observed_at` then limited. Vector queries rank by similarity; ties are not specified. Layer-2 reconstruction is deterministic given the same journal state: every result carries `as_of_seq`, and anchoring `Stimulus(as_of=...)` reproduces it.

## Do reads strengthen memory?

No. Reconstruction, inspection, replay, the structural report, and the entity card are pure reads. `commit_selection` is the only strengthening path, and it deposits only for records admitted by the stimulus (records rendered from the identity core or from short-term standing are presence, not use). See [`memory-system.md`](docs/memory-system.md).

## What gets embedded for vector search?

On `add(...)`, vector-capable stores embed each assertion's canonical text (`canonical_text(assertion)`): the triple terms plus selected attributes, with digest-bearing records rendered around their digest text. On `query(...)` with `query_text=...`, the query string is embedded and ranked against stored vectors. Edge assertions are never embedded.

## What embedding interface do I need to implement?

The `TextEmbedder` protocol: `embed_texts(texts: Sequence[str]) -> list[list[float]]`. Two implementations ship with the package: `OpenAICompatTextEmbedder` (any OpenAI-compatible `/embeddings` endpoint, e.g. LM Studio or Ollama) and `AbstractGatewayTextEmbedder` (an AbstractGateway deployment; default path `/api/gateway/embeddings`, Bearer auth via `auth_token`).

## Where does vector retrieval metadata appear?

On results, stores attach retrieval metadata to `TripleAssertion.attributes["_retrieval"]`: cosine `score` + `metric` (LanceDB additionally reports `distance`).

## How do I inspect the data on disk?

- SQLite: open the file with any SQLite client. The assertions table includes the canonical triple columns plus `provenance_json`, `attributes_json`, `text`, and `embedding`; the journal's sidecar tables live in the same file when you pass the same path to `SQLiteJournal`.
- LanceDB: open the `uri` path with LanceDB and inspect the table.
- For a whole entity home, prefer the read-only workflow in [`operator.md`](docs/operator.md) — identity core, wake reasons, gradation, replay stream, and the identity card, all pure reads.

## Can I replay the past?

Yes. The journal assigns a monotonic `seq` to every record; `export_replay(since_seq=..., until_seq=...)` streams verbatim history, and `as_of`/`at_seq` parameters on reads (`reconstruct`, `gradation`, `activation`, `entity_card`) fold state to any anchor. One documented limit: triple-store truth is read current (assertions have no seq axis), so store rows added after an anchor still enter candidate gathering; replay is exact while store contents are unchanged.

---

## docs/troubleshooting.md

# Troubleshooting

Symptom-oriented fixes for setup, retrieval, embedding, and maintenance problems.

See also: [`getting-started.md`](docs/getting-started.md) for setup, [`stores.md`](docs/stores.md) for backend behavior, [`api.md`](docs/api.md) for signatures, [`operator.md`](docs/operator.md) for inspecting a live entity home, [`faq.md`](docs/faq.md) for conceptual questions.

## Reading the package's own signals

Two conventions make most problems self-describing before you reach for this page.

- **`#FALLBACK` warnings** mark a degradation the package chose to survive rather than crash on — a vectorless row, a volatile journal, an embedder that failed mid-formation. They are Python `RuntimeWarning`s and appear in result `warnings` lists. They always name what degraded and what to do about it. Treat one as a diagnosis, not noise.
- **Errors are actionable.** Invalid input raises rather than silently doing something plausible: an unknown anchor never quietly means "latest", and an appraisal without a reason is refused because appraisals must be explainable.

## Setup

### `ImportError: cannot import name 'MemorySystem' from 'abstractmemory'`

The installed copy is an older release that predates the `MemorySystem` facade, and it is shadowing your working tree. Confirm which copy is being imported:

```bash
python -c "import abstractmemory; print(abstractmemory.__file__, len(abstractmemory.__all__))"
```

A path under `site-packages` with a small export count means the installed distribution is being used. Reinstall in editable mode from the repository root:

```bash
python -m pip install -e .
```

The import name is `abstractmemory`; the distribution name is `AbstractMemory`.

### `ImportError` mentioning `lancedb`

The LanceDB backend is an optional dependency. Install it, or use `SQLiteTripleStore`:

```bash
python -m pip install -e ".[lancedb]"
```

### Tests pass in the repository but the package fails elsewhere

The test suite bootstraps `src/` onto `sys.path`, so it exercises the working tree whether or not the package is installed. Anything outside the suite uses the installed distribution. Install editable (above) so both agree.

## Retrieval

### `ValueError: query_text requires a configured embedder`

`TripleQuery(query_text=...)` is semantic search, and there is no keyword fallback by design — silently returning keyword results from a semantic query would hide the missing embedder. Either construct the store with an `embedder`, or query with structured filters instead. See [Embedders](docs/api.md#embedders).

### Vector queries return nothing, or miss rows you know exist

Rows written **without** an embedder are stored vectorless, and vector queries skip them. Adding an embedder later does not retroactively vectorize them. Re-derive the index:

```python
from abstractmemory import reembed_store
reembed_store(system, embedder=my_embedder, owner_id="entity:demo")
```

This re-embeds every row and swaps the space atomically, writing the new pin last. See [Maintenance, health, and operator tools](docs/api.md#maintenance-health-and-operator-tools).

### A record exists but never surfaces in recall

Ask the engine rather than guessing — three reads answer this directly:

```python
from abstractmemory import absence_diagnosis, explain_recall, recall_history

absence_diagnosis(store, journal, record_id, scope="life", owner_id="entity:demo")
explain_recall(store, journal, record_id, trace_id=trace_id)
recall_history(journal, record_id)
```

`absence_diagnosis` reports structural reasons in plain sentences (never formed, retracted, hidden, out of scope, vectorless). `explain_recall` answers the same question against one specific recall. Common structural causes: the record is in a scope the ladder does not include; a closure retracted, superseded or reinterpreted it; it is an inactive candidate; or it was formed after the `as_of` seq you anchored to.

### Recall results change between runs with the same input

Reconstruction is a pure function of (store truth ≤ `as_of`, journal ≤ `as_of`, stimulus, params). If results move, one of those inputs moved — usually the journal, because committed use changes retrieval strength. Anchor the read to reproduce it exactly:

```python
result = system.reconstruct(Stimulus(cue_text="…", as_of=result.as_of_seq), scopes=scopes)
```

Note that `reconstruct` is a pure read: rendering a memory does not strengthen it. Only `commit_selection` does.

## Embedding spaces

### `#FALLBACK: embedding pin created at FIRST WRITE`

The home predates creation-time pinning, so the embedding space was inferred from the first embedded write rather than declared up front. Existing homes keep working. New homes should pin the embedder at creation so the space is a stated choice rather than an accident.

### A home refuses your embedder, or reports a dimension mismatch

Each home pins one embedding space. Vectors from different models are not comparable, so the pin is enforced rather than trusted. Inspect a store file without opening it:

```python
from abstractmemory import read_embedding_pin
read_embedding_pin("memory.sqlite3")
```

To move a home to a different space deliberately, use `reembed_store(...)`, which re-derives every vector and swaps the pin last, so a crash never leaves a half-migrated space.

### `#FALLBACK: SQLite vector search uses the Python linear scan`

`SQLiteTripleStore` serves vector queries from an in-process float32 matrix when numpy is importable. This warning (once per store handle) says the Python reference scan answers instead. The results are the same; only the speed differs (seconds instead of milliseconds at tens of thousands of rows). The message names the reason:

- `numpy is not importable`: install the extra, `pip install "abstractmemory[fast]"`.
- `the store holds a vector the float32 matrix cannot score exactly (...)`: a row holds vectors of another dimension, non-finite values, an empty vector or non-numeric JSON. Re-derive the vectors with `reembed_store(...)`; the matrix is rebuilt after the swap.
- a query vector of another dimension than the stored vectors: query with the pinned model.

`store.vector_engine` reports the current path (`"numpy-matrix-cache"` or `"python-linear-scan"`).

### Opening an older home takes a few seconds once

Homes written before 0.4.0 store vectors as JSON text. The first open rewrites them as float32 BLOBs in bounded transactions (about 5 s for 50k rows of 384-d vectors); later opens only look at rows written since. An interrupted upgrade resumes where it stopped. The file shrinks only after a `VACUUM`.

### `ValueError: table_name must match [A-Za-z_][A-Za-z0-9_]*`

Table names are interpolated into SQL identifiers, which cannot be bound as parameters, so they are validated at construction. Use a plain identifier. The same rule applies to `SQLiteJournal`'s `table_prefix`.

## Durability and replay

### `#FALLBACK: InMemoryJournal is volatile`

`InMemoryJournal` loses events, bindings, closures, traces, snapshots, and the seq axis on process exit, so `as_of` replay cannot survive a restart. It is meant for tests and experiments. For durable memory use `SQLiteJournal`, which can share one file with the store:

```python
store = SQLiteTripleStore("memory.sqlite3")
journal = SQLiteJournal("memory.sqlite3")   # sidecar tables in the same file
```

### `ProvenanceRequired` from `MemoryGraph.apply_revision`

The graph refuses a memory that is neither sourced nor derived: name at least one ledger `SourceRef` (a step `(run_id, seq)`, an artifact, a message, an event, a diary entry) or a `derived_from` memory of the SAME owner whose lineage reaches a source. Also refused: a lineage that never reaches a source (a cycle), a `derived_from` across owners (enter the other owner's knowledge as a source), a close or narrow of a store row the graph never adopted (adopt it first with an `import` revision that carries the row as stored), and a close-only revision citing only the memory it closes. The message names the rule. A flat legacy provenance dict (`run_id`, `span_id`, ...) converts with `Provenance.from_legacy`.

### A 0.3.x process finds no vectors in a home

0.4.0 rewrote the home's vectors as float32 BLOBs (a one-way migration; the meta key `vector_encoding = float32-blob` says so). Move every process that shares the home to 0.4.0.

### Replay shows gaps in `seq`

Expected under family filters and under diary redaction. Gaps carry no meaning. The replay stream is an observability surface — not a payload export, not a second source of truth, and not a checkpoint format. See [Replay stream](docs/api.md#replay-stream).

## Maintenance and sleep

### A sleep pass does nothing

Usually correct behavior. `maintenance_due(...)` is a deterministic predicate: a store with zero new formations since the last pass is never due, because the pass would only reproduce its own prior output. Check what it reports:

```python
from abstractmemory import maintenance_due, last_maintenance_seq
maintenance_due(store, journal, scopes=scopes)
last_maintenance_seq(store, journal, scopes=scopes)   # 0 = never slept
```

A quiet night is also a valid outcome of a pass that did run: `dream_pass` forms nothing when nothing crossed its floors, and says why.

### `ValueError: ... open the home with MemorySystem(task_queue=True)`

A title or summary still being written is always stored beside the task that writes it, so the verbs that form pending records refuse a home without a task queue, before writing anything: memory's own passes (`world_model_pass`, `dream_pass`, `mine_candidates_pass`, `consolidation_pass`, `wake_cue_dedup_pass`, `sleep_pass`, `communities_pass`, `embed_missing`), a `remember_many` record with `pending=...`, the ripple verbs, and an `engram` whose unnamed spark items get no authored title. Open the home with `MemorySystem(store=..., journal=..., task_queue=True)`. `report_only` reads still run without it.

### A record's title is `None`

The title is still being written: the record formed pending (its author was missing or failed; `attributes.pending.failure` says why) and its `title.author` task waits in `system.tasks`. Reads serve the missing title as `None` on purpose, never a placeholder. Pass an `author` callable to the passes so records form authored, drain the queue with `retitle` / `rewrite_digest`, and use `pending_records(...)` and `rearm_pending(...)` for a record whose tasks ran out.

### A cancelled night left work unfinished

By design. `sleep_pass(..., should_continue=...)` checks at sub-phase boundaries; the running sub-phase always completes rather than being torn, later ones skip with a stated `skipped_reason`, and the night carries `cancelled_after`. Sub-phases are idempotent, so the next sleep resumes the work.

### Unrelated records are proposed as near-duplicates

Near-duplicate detection fingerprints `title + digest` tokens and flags pairs at or above `SleepTuning.near_dup_jaccard_floor` (0.65). Genuine causes are templated titles or boilerplate shared across records — the tokenizer has a length floor rather than a stopword list, so repeated scaffolding counts as content. Raise the floor through `SleepTuning`, or give the records distinguishing digests. Note that truncation markers deliberately stay **out** of digest text for exactly this reason; the counts ride `attributes._truncation` instead.

## Getting more detail

- `engine_manifest()` returns the machine-readable inventory of what this build contains.
- `mind_mass_report(...)` reports formation cadence, journal mass, duplicate mass, embedding-space integrity, review backlog, and sleep recency in one bounded read.
- `export_replay(...)` streams the journal verbatim in seq order for scrubbing history or tailing live.

---

## skills/abstractmemory/SKILL.md

---
name: abstractmemory
description: Use AbstractMemory's Python APIs for scoped, provenance-aware memory, deterministic replay and read-only inspection.
---

# Use AbstractMemory

Use Python 3.10+ and `python -m pip install AbstractMemory`. Import `abstractmemory`;
`[fast]` adds numpy for SQLite vector scoring and `[lancedb]` adds that optional backend.
AbstractMemory does not run an LLM or supply an embedding model. Configure an embedder
before semantic queries; `query_text` without one raises rather than doing keyword search.

Choose the surface before writing:

- Plain scoped triples: `InMemoryTripleStore`, `SQLiteTripleStore` or `LanceDBTripleStore`,
  with `TripleAssertion` and `TripleQuery`.
- A memory system: `MemorySystem` with a store and journal. Formation is separate from
  selected use; reconstruction is a pure read, `commit_selection` records what was used.
- A mind's graph: `abstractmemory.graph.MemoryGraph`. Apply atomic, idempotent `Revision`s
  with grounded `SourceRef`/`Provenance`; missing provenance raises `ProvenanceRequired`.
  Never edit the backing database directly.

For a new integration, first reproduce this isolated triples example:

```python
from abstractmemory import InMemoryTripleStore, TripleAssertion, TripleQuery
store = InMemoryTripleStore()
store.add([TripleAssertion(subject="Ada", predicate="uses", object="Python",
                          scope="session", owner_id="demo")])
hits = store.query(TripleQuery(subject="ada", scope="session", owner_id="demo"))
assert len(hits) == 1
```

Then choose a persistent store, explicit scope/owner pairs and a real provenance source.
Use stable idempotency keys for retried formations/revisions. Inspect the resulting record
and journal before committing selected use. Anchor historical reads at `as_of`/`at_seq`;
the journal's sequence is the replay axis. Do not invent run or artifact provenance.

Updates append assertions or closures; they do not delete history. Title/summary rewrites
retain earlier versions. A formation retry does not update an existing assertion's attributes.
Read-only inspection, reconstruction, replay and entity cards deposit nothing.

Diary prose belongs in the book. Replay projections describe the act; private projections
must stay content-free. A `resolves` claim alone is not a confirmed resolution: inspect
`resolves_status`; target graph id/title may be absent. Existing projections are not
backfilled by upgrading. Pending authored fields need a host author and a home opened with
`MemorySystem(task_queue=True)`; do not fill them with invented text. Preserve labelled
`#FALLBACK` warnings and handle invalid input errors explicitly.

Full contracts: `docs/api.md`, `docs/memory-system.md`, `docs/stores.md` and
`docs/operator.md`. Use the operator guide for an existing entity home; start with reads.
