# 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]"
```

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; `[all]`, `[all-apple]`, and `[all-gpu]` install the LanceDB backend.

## 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]])
```

## 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); 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]"
```

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.

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` or `supersede` with replacements) — 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` |
| 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` |

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

## 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)
- [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(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; files created before the vector column existed upgrade in place on open. Rows added without an embedder stay vectorless (vector queries skip them; layer-2 recall labels the degradation `#FALLBACK`).
- `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.

---

## 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`), mandatory `reason`, `replacement_ids` (required for supersede).
- `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`).
- `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.
- `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.
- Cosine ranking runs in Python over the SQL-filtered candidates (shared
  `vector_scoring.py` — both stores rank identically). The scan is linear,
  comfortable at single-home scale; ANN indexing is on the design backlog.
- 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` (JSON-encoded float list; NULL for vectorless rows)

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

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.

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.
- At layer 2, belief revision is a closure record (`close_record` / `close_assertions`: retract or supersede with replacements) — 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 or superseded 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.

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

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