# AbstractMemory (llms-full)

> Durable, append-only agent memory for Python: temporal, provenance-aware triple assertions with deterministic 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 — reconstruction, attention, identity, valence, diary, consolidation, and a replay stream (layer 2). Part of the AbstractFramework ecosystem.

This file aggregates the core documentation corpus in one place for LLMs and tools.
For a concise index, see [llms.txt](llms.txt). The canonical documentation lives in the
linked files; regenerate this aggregate when they change. Relative links below are
normalized to resolve from the repository root.

## Document Index

- [README.md](README.md): project overview, install, quick examples for both layers
- [docs/getting-started.md](docs/getting-started.md): setup, first triples, first MemorySystem session
- [docs/architecture.md](docs/architecture.md): the two-layer model, journal-as-time-axis, the reconstruction union, invariants
- [docs/memory-system.md](docs/memory-system.md): the cognitive model — emergent working memory, valence, identity, dreams
- [docs/api.md](docs/api.md): full API reference, grouped by surface
- [docs/stores.md](docs/stores.md): backend behavior and persistence details
- [docs/operator.md](docs/operator.md): read-only inspection of an entity's memory home
- [docs/faq.md](docs/faq.md): common questions and current limits
- [docs/troubleshooting.md](docs/troubleshooting.md): symptom-oriented fixes for setup, retrieval, embedding spaces, durability, maintenance
- [docs/development.md](docs/development.md): local setup and test workflow

---

## 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+.

## 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.
- **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.

---

## 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.

There is no compaction and no rewriting-in-place. Degradation through summarize-and-replace cannot originate in this package.

## 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` |
| 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=...)` 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.
- 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:

- `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 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.

## 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. 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)
- [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 change the stores allow. 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 2). 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(), clock=None, broad_scopes=None, ablation=None)`.

- `embedder` enables the vector retrieval channel.
- `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()`.

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`, `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`.
- `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).

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>"`.
- `MemoryRecordInput(kind, title, digest, intents=(), outcomes=(), keywords=(), participants=(), edges=(), payload_ref=None, topic=None, confidence=None, attributes={}, provenance={})` — one record to remember. 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).

### 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.

### 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) -> 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`.
- `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) -> 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. Unresolved dreams chain via `parent_dream_ids`.
- `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) -> 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) and carries each sub-phase's self-describing dict. **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.

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

### 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`. `family="host"` is reserved for host-authored markers (accepted by the filter, never emitted by this package).
- 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.
- 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.

- `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") -> dict` — replace a card's words with AUTHORED prose. The engine never invents text: this applies words authored elsewhere through the same append-only revision chain the sleep pass uses.
- `world_model_update(system, *, scopes, owner_id, targets, scan_limit=400, tuning=SleepTuning()) -> 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.

## 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()) -> 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.
- `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) -> dict` — the deterministic cadence predicate. 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` — apply authored digests to labeled-mechanical records.
- `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) -> 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.
- `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 |
| `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 update/delete API. Represent changes by adding a new `TripleAssertion` with updated fields and fresh provenance.
- 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 rewriting, no summarizing-in-place. Degradation through compaction cannot originate here.
- **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 update or delete API, 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`.
- 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.

### 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.

---

## docs/development.md

# Development

## Local setup

Editable install:

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

Dev extras (tests):

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

Optional LanceDB tests/backends:

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

## Run tests

```bash
python -m pytest -q
```

Notes:

- Most layer-2 suites run against both substrate stacks (in-memory and SQLite) via a parametrized fixture in [`tests/conftest.py`](tests/conftest.py), which also bootstraps `sys.path` for monorepo layouts.
- LanceDB-dependent tests are skipped when `lancedb` is not installed (see [`tests/test_lancedb_triple_store.py`](tests/test_lancedb_triple_store.py)).
- Integration tests marked `lmstudio` need a local OpenAI-compatible server with an embedding model and are skipped automatically when unreachable (see markers in [`pyproject.toml`](pyproject.toml)).

## Design backlog

Design notes and planned work live under [`backlog/`](docs/backlog/overview.md) (maintainer-facing). The user-facing documentation set is indexed in [`README.md`](docs/README.md).
