
This implements episode storage with compression and citation-based validation to help agents build continuity without accumulating noise. You get four tools: record observations and decisions as typed episodes, recall by keyword or type, prepare_wrap to fetch uncompressed episodes for LLM summarization, and save_continuity to validate and store the compressed output with hash-chained audit trails. The validation layer checks for cited evidence and flags patterns without episode support. Reach for this when you need persistent agent memory that degrades gracefully instead of drifting into hallucinated patterns. Ships as a Python library first with CLI and MCP transports over the same core. Works with LangGraph, CrewAI, OpenAI Agents, or any framework that can call record and recall hooks around agent loops.
Living memory for AI agents. Episodes compress into identity.
Memory without grounding is amplification infrastructure.
Persistent user memory profiles increase agent sycophancy 16–45% across models (Gemini 2.5 Pro at 45%, others lower). Production deployments accumulate 97.8% junk entries within weeks. Clinical research documents memory scaffolding delusions across sessions. The failure mode here isn't memory. It's memory with nothing checking what gets kept.
anneal-memory puts a check between what an agent writes down and what it comes to believe. That idea isn't rare any more. MemTxn refuses a fact update whose values don't appear in the source it cites, and Agent Zero Memory answers only from evidence its reader actually opened.
Here's what I think is actually distinct, going by the 2026 systems I've read. A pattern in anneal is an abstraction, not a copied fact, and it has to earn its rank: it moves up a level (1x → 2x → 3x, up to a 9-digit level) only when it cites real episodes its explanation is grounded in, and a failed citation drops it a level (or holds it there for a while, marked carried-forward, if it was grounded recently). The consolidator can't quietly gut the memory it rewrites, either. On a store that declares an identity layer, a save that collapses those sections is refused, and consolidators wearing down their own memory is a documented failure. Then there's the assembly. An episodic evidence store, a bounded always-loaded continuity file, a long-term pattern tier recalled on cue, prospective tasks, a single-writer consolidation gate and a hash-chained audit log all ship in one package, with zero dependencies and no embeddings. None of those pieces is unique on its own. The combination is, as far as I've found.
The citation checks are narrow and structural: cited IDs must exist, the explanation must share words with the episode it cites, fabricated citations demote (unless carryforward holds a recently grounded pattern), one episode cited too often raises a flag, and re-citing old episodes doesn't count. They are not a complete defense against memory drift. Honest scope below says what they catch and what they don't.
And it's memory you own and govern. A local store, zero dependencies, no vendor in the loop — you decide what graduates into long-term memory, every change is recorded in a tamper-evident chain (best-effort on the write side — see Hash-chained audit trail for what verify can and cannot see), and the consolidation step that rewrites an agent's identity is gated to a human, not run unbidden (the Single-consolidator gate below). Own the substrate; govern what enters it.
Four cognitive layers — episodic store, compressed continuity, Hebbian associations, affective state tracking — plus two sibling stores: prospective spores (what the agent intends to do next) and a crystallized pattern store (graduated wisdom held out of always-loaded context and recalled on cue, so a large body of proven knowledge stays effective without clogging attention). Together they implement Complementary Learning Systems — see The Memory Architecture below. The Hebbian layer records links between episodes, but in three months of measurement on my own long-running store it never changed what recall returned. The part of recall that works is the citation edge (see Associations through consolidation). Zero dependencies (Python stdlib only). Works with any agent framework.
pip install anneal-memory
anneal-memory runs on Windows. What to know:
fcntl and degrades to no lock at all on Windows. The sites (grep -n "fcntl.flock" anneal_memory/*.py) include store.py's continuity lock, crystal.py's CrystalStore._transaction, spores.py's SporeStore._transaction, and audit.py's audit manifest and audit append locks. On Windows, concurrent writers to one store — whether separate processes or threads within one process — can race or lose updates, since there is no lock of any kind to serialize them there. A single writer (one process, one thread) is unaffected: the unique-tmp + atomic-replace write still prevents a torn file.flock locks follow the open file description, which a forked child inherits. Do not fork a process while an AuditTrail in it is in use; a child must open its own AuditTrail.ANNEAL_MEMORY_DB is unset and no home directory can be found (a launcher that drops USERPROFILE/HOMEPATH, or on POSIX a container uid with no passwd entry and no HOME), only a command that opens the default path needs a home, and it refuses with exit 1, naming --db and ANNEAL_MEMORY_DB.chmod are skipped there, since Windows/NTFS doesn't restrict access the same way, as are the cross-process lock test (see the write-locking bullet above) and the missing-home-directory simulation that clears the whole environment (the no-home tests that patch Path.expanduser run everywhere).The library is the core product. Import it, use it in any framework or script.
from anneal_memory import Store, EpisodeType, prepare_wrap, validated_save_continuity
# Initialize (creates DB + continuity file automatically)
store = Store("./memory.db", project_name="MyAgent")
# Record episodes during work
store.record("Connection pool is the real bottleneck", EpisodeType.OBSERVATION)
store.record("Chose PostgreSQL as the database because ACID outweighs speed", EpisodeType.DECISION)
# Recall before decisions
result = store.recall(episode_type=EpisodeType.DECISION, keyword="database")
for ep in result.episodes:
print(f"[{ep.type}] {ep.content}")
# Compress at session end — this is where the cognition happens
wrap = prepare_wrap(store) # fetches episodes, marks wrap in progress
if wrap["status"] == "ready":
# Feed wrap["package"] to your LLM. Compression IS the cognition —
# patterns emerge from the act of compressing, not from storage.
compressed = your_llm.compress(wrap["package"])
validated_save_continuity(store, compressed) # full immune system pipeline
# "empty" status means no new episodes to wrap — skip
store.close()
See Library Quickstart for the full guide.
Inspect, debug, and manage agent memory from the command line — a full operator CLI with machine-readable --json output. Agents with shell access (Claude Code, Aider, etc.) can use the CLI directly for the full memory workflow.
# Point the demo at its own store. Without --db or ANNEAL_MEMORY_DB every
# command uses ~/.anneal-memory/memory.db, your default store.
export ANNEAL_MEMORY_DB=./demo.db
# Initialize. Global flags such as --db and --project-name go before the
# subcommand. --project-name names the memory in the continuity header and is
# not stored: pass it on each command (the default is "Agent").
anneal-memory --project-name MyAgent init
# Record and recall
anneal-memory record "Chose PostgreSQL as the database for ACID" --type decision
anneal-memory search "database"
# Consolidation is the operator's act: a new store wraps only under a session
# you have claimed the baton for. Claim once (init --no-baton opts out).
anneal-memory baton claim me
# Agent-driven compression (same workflow as library and MCP)
anneal-memory --project-name MyAgent prepare-wrap --session-id me # Get compression package
# Agent compresses...
anneal-memory --project-name MyAgent save-continuity out.md \
--session-id me --wrap-token "$WRAP_TOKEN" # the 'Wrap token:' line
# Operator commands (things MCP can't do)
anneal-memory stats # Detailed analytics
anneal-memory graph --format dot # Association graph (Graphviz)
anneal-memory diff --wraps 5 # Wrap metric progression
anneal-memory audit --since 7d # Read audit trail
anneal-memory audit-repair # Rebuild a quarantined audit manifest, or set aside a corrupt sealed week (--set-aside-unreadable: one that only fails to read)
anneal-memory export --format json # Full store export
# Done with the demo: return the CLI to your default store
unset ANNEAL_MEMORY_DB
See examples/agent-instructions.lean.cli.example for the agent workflow snippet.
For MCP-capable agent harnesses — Claude Code, Codex and Gemini CLI.
The server is one command — anneal-memory --project-name MyProject serve — wired into the harness's MCP config. The serve subcommand starts the MCP server; --project-name (and optional --db) are global flags and come before serve. Each harness has its own config file and format:
Claude Code — mcpServers JSON (a project .mcp.json):
{
"mcpServers": {
"anneal_memory": {
"command": "uvx",
"args": ["anneal-memory", "--project-name", "MyProject", "serve", "--session-id", "claude-code"]
}
}
}
Codex — ~/.codex/config.toml (or a project .codex/config.toml):
[mcp_servers.anneal_memory]
command = "uvx"
args = ["anneal-memory", "--project-name", "MyProject", "serve", "--session-id", "codex"]
Gemini CLI — ~/.gemini/settings.json (or a project .gemini/settings.json):
{
"mcpServers": {
"anneal_memory": {
"command": "uvx",
"args": ["anneal-memory", "--project-name", "MyProject", "serve", "--session-id", "gemini"]
}
}
}
Gemini CLI starts MCP servers only from a folder it trusts: in an untrusted folder gemini mcp list shows a server from the user-level settings as Disconnected and does not list one from a project .gemini/settings.json at all, even though the config is right (trust the folder, or set GEMINI_CLI_TRUST_WORKSPACE=true; measured with Gemini CLI 0.42.0, where the variable made the server show Connected).
Then add the agent-instructions snippet — agent-instructions.lean.example (the always-loaded baseline) or the .full.example reference — to the harness's instructions file (CLAUDE.md for Claude Code, AGENTS.md for Codex, GEMINI.md for Gemini CLI). It teaches the agent when and how to use the memory tools; without it the tools are available but the agent won't know the cognitive workflow. (See Claude Code / agent-harness adopters below for the lean/Skill/full layering.)
Consolidation is yours. Agents record episodes freely. Rewriting the agent's identity layer (the wrap) is the operator's act, so a new store consolidates only through a server whose --session-id you have claimed the baton for, once, from your own shell: uvx anneal-memory baton claim claude-code (or anneal-memory baton claim claude-code with a pip install). Until then the agent's wrap downgrades to capture-only, and both the downgrade and the status tool say how many episodes are waiting and the command for you to run. Set the id in the client's config as above, not in a shell profile (ANNEAL_MEMORY_SESSION_ID also works, but an exported one reaches every client launched from that shell). Every conversation started from one config shares its id, so after the claim each of that client's conversations may consolidate: the claim designates a client, not a single conversation. Move the baton between clients with baton claim NAME --take; see who holds it with baton status. A store created before this policy, or with init --no-baton, ignores --session-id and wraps as before.
What the gate does and does not hold: no MCP tool lets an agent choose its id or claim the baton, so an agent following the protocol cannot consolidate on its own initiative, and parallel sessions cannot each recompose your agent's identity. It is not a lock against an agent with a shell that ignores its instructions: such an agent can run baton claim or edit its own config. Keeping the baton with you is part of the operator's job.
Pinned install:
uvxfetches the latest published version on each run. For a pinned install,pip install anneal-memory, then set"command": "anneal-memory"with"args": ["--project-name", "MyProject", "serve", "--session-id", "claude-code"]— or pointcommandat an absolute path to the installed binary.
CLI and MCP are thin transport adapters over the same library — not separate implementations. Every access pattern calls the same prepare_wrap(store) and validated_save_continuity(store, text) pipeline under the hood, preserving the same workflow: record episodes during work → compress at session boundaries → load continuity at session start. The agent that records is the agent that compresses; that compression can't be delegated.
| Library | CLI | MCP | |
|---|---|---|---|
| Install | pip install anneal-memory | Same | uvx anneal-memory or same |
| Record | store.record(content, type) | anneal-memory record "..." --type T | record tool |
| Recall | store.recall(keyword=...) | anneal-memory search "..." | recall tool |
| Compress | prepare_wrap(store) → agent → validated_save_continuity(store, text) | prepare-wrap → agent → save-continuity | prepare_wrap → agent → save_continuity |
| Best for | Framework integration, custom agents | Agents with shell access, operators | MCP-enabled editors |
Recall reads one committed state. A recall reads one committed state, as of its start: an episode deleted or erased before recall begins is never returned; a delete that commits while a recall runs may or may not be reflected, as with any database read.
anneal-memory works with any agent framework through the Python library. Each guide below shows where to call the four core functions — record(), recall(), prepare_wrap(), validated_save_continuity() — within the framework's lifecycle.
| Framework | Integration Point | Guide |
|---|---|---|
| LangGraph / LangChain | AgentMiddleware (before/after agent + model) | docs/integrations/langgraph.md |
| CrewAI | BaseEventListener (event bus) | docs/integrations/crewai.md |
| OpenAI Agents SDK | RunHooks (agent lifecycle) | docs/integrations/openai-agents.md |
| Anthropic Agents SDK | agent-instructions snippet + Stop hook | docs/integrations/anthropic-agents.md |
| Google ADK | Callbacks + custom MemoryService | docs/integrations/google-adk.md |
| Pydantic AI | AbstractCapability with Hooks | docs/integrations/pydantic-ai.md |
| smolagents | step_callbacks dict | docs/integrations/smolagents.md |
| LlamaIndex | Instrumentation BaseEventHandler | docs/integrations/llamaindex.md |
| Haystack | Custom Tracer | docs/integrations/haystack.md |
| CAMEL-AI | WorkforceCallback | docs/integrations/camel-ai.md |
| AutoGen / AG2 | register_hook() | docs/integrations/autogen.md |
| DSPy | BaseCallback | docs/integrations/dspy.md |
These guides show integration patterns based on each framework's current API. The library works with any Python framework — the pattern is always the same: initialize a Store, call record() at meaningful moments, recall() before decisions, and run the wrap sequence at session end. Don't see your framework? The library quickstart shows the 4-function pattern that works everywhere.
Using anneal-memory inside an agent harness — Claude Code, Codex, Gemini CLI — rather than a Python framework? The Skill and snippets are repository artifacts: they live in this GitHub repo (and the source distribution), not in the pip/uvx wheel — clone the repo or download the files directly from GitHub to use them. They layer, they aren't a menu: the lean snippet is the always-loaded baseline; the Skill adds depth on demand.
examples/agent-instructions.lean.example (MCP) or examples/agent-instructions.lean.cli.example (CLI) into your CLAUDE.md / AGENTS.md / GEMINI.md. ~45 lines, always in context — the start-of-session continuity load, recording, recall-before-decisions, and the wrap sequence, with depth delegated to the Skill. (Copy-paste text; nothing to install.)skill/anneal-memory/ directory into .claude/skills/ (per-project) or ~/.claude/skills/ (global). SKILL.md carries the comprehensive workflow and is model-invoked when you're doing memory work — primarily before decisions and on wrap. It is depth-on-demand, not a replacement for the lean snippet: a model-invoked skill won't reliably fire at a bare session start, so keep the lean snippet in place for the start-of-session step.examples/agent-instructions.full.example / .full.cli.example are the complete snippets (immune-system internals, the affective-state shape, the operator-command catalog) for adopters who'd rather keep it all in their instructions file than install a Skill.After upgrading the package, run anneal-memory migrate check — it proposes edits to your instructions so they don't silently drift from the substrate (it never edits your files), then anneal-memory migrate ack.
Three independent production failures share one root cause: no quality mechanism between memory write and memory read.
Sycophancy amplification. Agents with persistent user memory profiles become 16–45% more sycophantic than memoryless baselines, depending on model (Gemini 2.5 Pro at 45%, others lower). Memory recalls what the user liked hearing, the agent learns to repeat it, and stored approval patterns compound across sessions (Jain et al., CHI 2026; measured with user memory profiles across Gemini and Llama variants).
Junk accumulation. A detailed production audit on Mem0's tracker documents a deployment that generated 10,134 memory entries over 32 days — 224 were usable. The rest were duplicates, self-referential loops, and hallucinated entries: recalled memories re-extracted as new memories in a feedback loop that no one designed but nothing prevented.
Harmful reinforcement. Clinical research documents AI systems with persistent memory scaffolding delusional content across sessions — stored context creates feedback loops between recalled memories and generated responses, with cases of documented real-world harm (Morrin et al., Lancet Psychiatry 2026).
Most memory servers store memories and retrieve them. Few ask: is this memory still true? Was it ever true? Is it making the agent worse?
anneal-memory asks the first two at the pattern level, and has started measuring the third (see Does a recalled memory help? below):
[evidence:] tag at all. The explanation-grounding check (≥2-word lexical overlap with episode content) raises the cost of fabricated evidence chains but is not a semantic-coherence check — see Honest scope below for what this catches and what it doesn't.The result: memory as a living system, not a filing cabinet. Episodes accumulate fast, get compressed at session boundaries — and that compression is where patterns emerge and get validated. Co-cited episodes form lateral Hebbian associations (recorded and decayed at every wrap; recall does not read them, see What the links do, measured). The continuity file stays bounded and always-loaded, getting denser rather than longer.
The agent memory ecosystem is converging on consolidation as the right approach — even Anthropic's Claude Code now runs a periodic consolidation pass over accumulated session data. This validates the direction: raw accumulation doesn't scale, and compression at session boundaries is where intelligence emerges.
But consolidation alone doesn't solve the problem. A system that consolidates faithfully and a system that consolidates sycophantically produce the same kind of output — compressed, structured, always-loaded. The difference is whether anything checks the quality of what got consolidated. That's the immune system.
The library implements a set of structural defenses around how patterns earn graduation. They are narrow and specific — naming them honestly is part of the architecture.
Citation-validated graduation. Patterns start at 1x. To graduate to Proven tier — 2x and every level above it, up to the 9-digit bound (a longer digit run is a malformed marker and is never matched) — they must cite specific episode IDs as evidence. The server verifies those IDs exist in the current wrap's frozen episode snapshot — the cross-session episode set is intentionally out of scope. No matching episode IDs, no promotion.
Explanation-grounding check. For each cited episode, the explanation in [evidence: ID "explanation"] must share ≥2 meaningful words (>2 chars, non-stopword) with the cited episode's content. This catches citations whose explanations are fabricated wholesale (no overlap → demote). It is lexical, not semantic — see Honest scope below.
Active demotion of ungrounded citations. A graduation that fails either check above demotes by one level — from any level, not just from 3x; there is no ceiling short of the 9-digit bound on a level (4x → 3x, 12x → 11x) — and gets marked (ungrounded) — unless it is carried forward (see below). Bare graduations (no [evidence:] tag) demote after the first wrap with citations_seen=True (first-wrap exemption protects onboarding), marked (needs-evidence) — also unless carried forward (see below).
Activation-aware carryforward. A failed citation means "this session's domain didn't cleanly re-ground the pattern," which is not the same as "the pattern is fading." So a pattern that is at or below its earned high-water mark (pattern_history.max_level_reached) and was grounded recently (last_seen_at within carryforward_cold_days, default 7) is held at its level and marked (carried-forward) instead of demoted — demotion no longer decays a Proven by session-domain rather than importance. This applies on both demotion paths: the ungrounded-citation path and the bare-graduation sunset path (a Proven carried forward and re-stamped to today without a fresh citation, the common "didn't re-exercise it this wrap" case). Forgetting stays ruthless where it should: the held line loses/omits its evidence tag (so it doesn't refresh recency and ages out on its own if it keeps failing to ground), a brand-new bald name | Nx with no earned high-water still sunsets (the level guard blocks inflation), the cross-session sycophancy-overlap path is never carried forward, and a carry at 3x or higher surfaces an assisted "graduate out to a stable home or retire" warning. A bare line at or below its mark that has gone cold is not eroded either: it is held, dated back to its last grounding, and surfaced so the operator decides to re-exercise it, graduate it out or retire it (a cited line whose citation fails while cold still demotes, because a failed citation is a real event). The dead-Hebbian-graph warning (AM-WARN) counts only cited carries, so a bare carry — which has no citation — cannot fabricate a wrong-namespace alarm. Carryforward is inert for stores with no pattern_history, and opt-out via carryforward_cold_days=None.
One rung per wrap, measured against what the store last saved. Every check above reads the line the agent wrote, so on its own it would trust the line's date, level and tag shape. The last check reads the store's own record of the level each pattern held when it was last saved: each line is cut to that level, or one rung above it if the line validated this wrap. A pattern new to the store enters at 1x. A pattern dropped from the file and added back returns to the level it was saved at, never to a high-water mark it was later demoted from. The continuity file can lower a level (an operator's hand demotion stands) but never raise one, and a line added to the file outside a save counts as new. So a back-dated line, a bare line, a line whose [evidence:] tag isn't adjacent to its marker, and a line that jumps several rungs on one citation all land where the record entitles them, and a carried-forward hold never lifts a line above it either. A cut line is marked (level-capped) (the mark clears once the line stands at a level it is entitled to), and the cut is reported: level_capped on the save result, the MCP reply and the CLI output, a UserWarning, and the audit chain. Before this check, a brand-new claim | 9x (yesterday) was saved at 9x. Lines are keyed by operator name, and a name's level is its highest line across the graduating sections, as every reader takes it; a renamed pattern is a new one, and a freeform line is keyed by its text, so rewording it makes it new. Only a line's own marker earns its rung: evidence on a second marker later in the line does not. A store's first save under this check takes the file as its prior. A crystal's level is never a prior: a pattern crystallized out of the file before then re-enters as new and re-earns its rungs (its crystal is untouched). Every | Nx marker on a line in a graduating section is governed, dated or not; a line's identity is the text before its first marker, and a line with none (- | 9x) is always new, every marker on it at 1x. The save dates the wrap by the day prepare_wrap gave the composer, so a wrap saved after midnight keeps the graduations it stamped. The check governs what a canonical save writes: a continuity written by the raw Store.save_continuity() or by an older anneal is outside it until the next canonical save, which bounds it against the record again. Schema generation 2, which 1.0 introduces, arms the anneal_writer_schema() write triggers, so an older anneal refuses a store that 1.x has migrated, and anything pinned to one refuses it until upgraded (docs/stability.md, On-disk format).
Citation-gaming flag. When any single episode ID is cited ≥3 times in one wrap, the wrap result surfaces the gaming-suspect list. The flag is informational — it does not auto-demote. Useful operator signal; not a hard gate.
Staleness flagging. Patterns whose dates fall behind the staleness threshold (default 7 days, configurable) surface in the next wrap's compression package. The agent decides removal vs refresh-evidence vs carry-forward; the library does not auto-demote on staleness alone.
Replay-attack block (structural). Each wrap's valid_ids set is scoped to the frozen episode snapshot from prepare_wrap. Patterns citing prior-session episode IDs that are not in the current snapshot demote — re-graduating a stale pattern requires fresh evidence from the current session.
Hash-chained audit trail. Every memory mutation appends to a SHA-256 hash-chained JSONL audit log. AuditTrail.verify walks the chain and detects post-hoc tampering at the exact entry where the chain breaks. What it does not detect, stated plainly: the append is best-effort after the mutation is durable — a sink failure (disk full, permissions, a failed rotation) is recorded and warned, never raised, because failing the caller would report a completed operation as failed. verify therefore proves the integrity of the entries that were written; it cannot see one that was never written, and will return valid=True over such a gap. The loss is not silent: the count of dropped writes rides into the next entry that lands as dropped_before (so it is itself hash-chained), is surfaced by Store.status() as audit_write_failures / audit_last_failure, and is logged on the anneal-memory logger. audit_write_failures is durable and lifetime-scoped — persisted in the store's SQLite metadata (a different I/O path from the failing sink) and seeded at open, so anneal-memory status --json from a cron job reports losses caused by a process that has already exited, and the number never resets: a trail that lost an entry is permanently incomplete. Poll it rather than relying on having caught the warning. Bounded, not absolute — the guarantee is that a loss recorded here survives the process, not that every loss is recorded: the metadata write is itself best-effort and issued after the mutation committed, so a full volume can fail it too; and a failure raised inside a caller's open transaction cannot commit there, so its delta is written at the end of that batch or, failing that, at close() — a process killed before either loses that last delta. (The dropped_before marker is the narrower of the two: it pins where the gap is in the chain, and it reaches the chain only if a later write lands in the same process. And it can over-report, in one measured case: if you register an on_event callback and that callback raises a terminal exception — KeyboardInterrupt, SystemExit — it escapes the append, which by then has already succeeded and fsynced. The caller cannot distinguish that from a failed write, records a drop, and the next entry carries dropped_before naming an entry that is sitting on disk. Ordinary exceptions from the callback are caught and logged and do not do this. So the marker's honest reading is "a write was reported lost here", not "a write is missing here".) One Store per thread — Store is documented as not thread-safe, not task-safe, not reentrant. Separate processes, each with its own Store, may record into one database: audit appends are serialized by a cross-process lock on POSIX (see "One database is one entity's memory" below), and on Windows the trail needs one writer at a time.
A crash during a week's first audit append: a declared limit that a person decides. After a crash during a week's first audit append, anneal cannot always tell from disk whether that entry committed. It refuses further appends until you run audit-repair, which records a POSSIBLE gap and names the preserved attempt files (.first.discarded-*). Inspect them to decide. anneal keeps them and never deletes them. On a readable manifest the gap is POSSIBLE only when, at repair time, the begun record was still set and at least one attempt was preserved (repair then withdraws the begun record); otherwise it is recorded as a definite gap. A manifest rebuilt from quarantine records POSSIBLE whenever the active file holds no valid entry, with or without preserved attempts.
Catastrophic-shrink gate (structural; partnership entities). A wrap can pass structure validation — every required section present — while gutting the content beneath it, so the latest session quietly compresses over the accumulated identity. For entities whose continuity declares a felt/identity layer (the Configurable continuity structure below), save_continuity refuses a wrap that collapses a protected section below its retain floor — the felt (timeless relationship) and graduated-identity sections must each keep ≥50% of prior mass, the whole file ≥25% — and leaves the wrap recoverable so the agent re-wraps preserving the full arc, or passes allow_shrink=True for a deliberate diet (fail-closed: only a literal True bypasses). A corrupt section schema fails the wrap closed rather than silently degrading an entity to ungated behavior. Ops entities, which legitimately consolidate many graduated patterns into a few dense ones, are not gated. A save-boundary structural invariant, not a proportion-check the agent is trusted to hold.
No save refusal on unlinked wraps (AM-LINKGATE). A wrap whose offered co-citation pairs recorded no Hebbian link saves, and AM-WARN Signal B (below) warns. allow_unlinked is still accepted and does nothing, and linkgate_overridden is always false. Every save reports citation_spread, the number of distinct episodes from this wrap that its graduation lines cited (demoted lines included), so a citation habit stays visible as a number.
Single-consolidator gate (structural; on by default for new stores). Consolidation is the operator's act. A store created by init or by the MCP server requires the consolidate baton; init --no-baton creates an open one, and an existing store keeps whatever policy it has. The operator claims the baton for a session id once (anneal-memory baton claim NAME); a CLI wrap names it with --session-id, and the MCP server takes it from its launch config (serve --session-id NAME or ANNEAL_MEMORY_SESSION_ID), never from a tool argument. This holds against an agent consolidating through the protocol on its own initiative; it is not a lock against an agent with a shell that runs baton claim itself, which is the operator's to manage. Recording episodes is afferent — append-only, parallel-safe, every session does it. Consolidating — recomposing the compressed identity layer — is efferent: it rewrites shared state, so it's gated to human authority. When several sessions run in parallel over one store (the real operating mode of a multi-conversation operator), each could independently recompose the felt/identity layer from its own narrow slice of context, and the identity memory thrashes. The gate makes the discipline structural: a consolidation proceeds only if this session holds the consolidate baton — otherwise it auto-downgrades to capture-only and surfaces a flag. The operator designates one session as the consolidate seat by claiming the baton, and taking it from another session requires an explicit take=True, so automation cannot take it by accident. (Before 0.9.13 a session that was the only live one was authorized without the baton; that rule is now an explicit allow_sole_live=True opt-in, because "only live session" is judged from a registry snapshot a resumed session can race.) The policy, Store.set_consolidate_requires_baton(True) (on by default for a store init or the MCP server creates, written in the transaction that creates the schema; a Store() opened from Python keeps it off unless set, or unless it creates the store with consolidate_requires_baton_on_create=True), extends the gate to callers that pass no session_id: they downgrade, and a save must name the baton holder (session_id) as well as carry the prepare wrap_token. A wrap prepared with a session_id on any store is committed only by that same session_id (a save that omits it, or names another session even if it now holds the baton, is refused, since the token identifies the wrap, not who may commit it); a gated wrap that cannot be finished is abandoned with wrap-cancel, which for a gated wrap needs its --wrap-token, the preparing --session-id, or an explicit --force (MCP: wrap_token, session_id, force). Because the baton is a file outside the store's transaction, a take landing in the milliseconds between a save's final check and its commit is not seen; the take then applies to the next wrap. The layer engages when a caller passes a session_id or the store carries that policy, and lives in coordination sidecars next to the store, never in the store's single-writer DB. Drift becomes a safe downgrade, not a silent identity-thrash.
Dead-Hebbian-graph warning (AM-WARN), two signals and a quiet stat. A citation-gated association layer can silently stop wiring — the failure runs invisibly for months while every wrap still reports success. AM-WARN warns at the end of a wrap in two cases. (A) graduated patterns carried evidence citations but none resolved to an episode in this store — typically ids minted in another namespace — so the graph cannot form at all. (B) co-citation pairs were available but nothing formed or strengthened, meaning the association write path itself is mis-wired. Both are structural and false-positive-free: they fire only on a genuine mis-wire. (C) (AM-LINKGATE) — graduations validated and their citations resolved, but no graduation offered a co-citation pair, so zero links formed — is a quiet stat, not a warning: it shows as associations_formed == 0 and associations_strengthened == 0 in the save result. It is quiet because recall does not read links and one genuine citation per pattern is often the honest one. Nothing in AM-WARN refuses a save. A wrap with no graduations at all stays silent. Only cited carryforwards are counted, so a bare carry cannot fabricate a wrong-namespace alarm. AM-WARN checks that links get written. Recall does not read them (see What the links do, measured).
The primitives above defend the citation layer. Two more defend the compression package itself — the instructions prepare_wrap hands the agent. Both follow the same rule as the contradiction scan: the library is zero-dependency with no LLM and no embeddings, so it surfaces the corpus and the agent judges.
Pattern dedup scan (AM-SEMDUP). The immune system catches a pattern re-cited with overlapping vocabulary, and a pattern that contradicts an existing one. It does not catch the same principle re-graduated under fresh words and a new name — a silent duplicate that forks the pattern graph, so two names for one principle each accrue half the evidence. Fresh vocabulary is by definition low lexical overlap, so a lexical or embedding detector is structurally blind to exactly this case. Instead, prepare_wrap renders a Pattern Dedup Scan block listing existing graduated patterns across all named levels as name (Nx): one-line meaning, with a merge-don't-fork instruction — the meaning line is what lets the agent judge semantic rather than merely nominal overlap. Capped at 50 with an announced overflow; never silently truncated. Public helpers: extract_pattern_summaries(text, ...) -> list[PatternSummary] and the PatternSummary NamedTuple (name, level, summary).
Schema role check (AM-ROLECHECK). The wrap package is assembled conditionally on section roles: the pattern-line format and contradiction scan emit only for a graduating role, and the felt proportion-check only for narrative-timeless. validate_schema already refuses a schema with no graduating section or duplicate headings, and the shrink gate refuses catastrophic felt collapse at save — but a schema that validates, keeps a graduating section, and is mis-roled elsewhere slips between them. The load-bearing case is narrative-timeless → narrative, which silently drops both the felt proportion-check and the shrink gate's felt protection, so the package quietly comes back thinner and nothing says so. schema_role_warning(schema) -> str | None warns on role drift off a named schema (naming the section, expected vs actual role, and the fix) and enforces a no-graduating floor for a raw schema. prepare_wrap surfaces it both as a UserWarning and as the schema_warning key on PrepareWrapResult. Known limitation: a fully custom novel-heading schema with a felt mis-role is not caught, because there is no named reference to drift from.
anneal proves grounding; the human holds truth. The library checks, structurally, that what graduates is lexically grounded in real episodes it can cite. Whether a grounded claim is true, and whether it contradicts something already Proven, is the operator's to judge by design: an AI grading its own memory would be the substitution this library exists to avoid. The operator gets instruments for it, not a verdict: drift probes (below), the contradiction-scan record in every wrap, and the hash-chained audit.
The same line holds for updates. Whether one fact replaces another is the writer's claim,
made with an explicit link or a shared state key; anneal checks that the link is well formed
(and, for an explicit link, lexically grounded) and never decides it semantically. The
operator's instruments are anneal-memory state (each key's current holder and what it
replaced), search --include-superseded, state --unset to take a wrongly keyed episode out of
its slot, and unsupersede to undo a wrong explicit link.
Origin keys and section edits are contracts with anneal's own code. Every anneal version
that writes a store keeps its episode keys right: triggers key an older version's inserts
and retire its deletes. A raw SQLite writer (a shell, a script, a dropped trigger,
PRAGMA ignore_check_constraints) is outside that contract, as any hand edit of the
database is. replace_section is safe against wraps run by this version or later; a wrap
prepared by an older anneal can overwrite an edit that lands in the few milliseconds before
it starts, so upgrade every process that wraps a store before editing sections in it.
Drift probes. Declare what must survive consolidation, a Proven pattern at a level (its
current level by default) or a fact whose words must stay in one sentence or bullet:
anneal-memory probe add --pattern NAME / --fact TEXT [--section HEADING]. Every save
checks each probe against the saved text and records held / changed (every word kept but the
negation flipped) / weakened / crystallized / lost with that wrap. probe status shows each
verdict with its detail; the save result's drift and the continuity_saved audit event
carry only the counts and each probe's id and status, so neither hands the probe text back
to the composer being measured. Probes are not part of the wrap package,
so they measure drift rather than obedience, and a probe never blocks a save (one that cannot
be checked is reported unchecked). probe status also lists what that wrap graduated with a
validated citation: the worklist for judging truth and contradiction with your Proven
patterns. The check is lexical: numbers and negation count, but a distortion that keeps every
word (two roles swapped, say) reads held, and that residue is the operator's.
The defenses above are structural at the citation layer. They catch fabricated citation evidence (fake IDs, missing IDs, wholesale-invented explanations), naive replay (re-citing prior-session episodes), per-ID citation gaming (single episode pumped across patterns), and post-hoc audit tampering. They do not catch:
citation_reuse_max=1, the per-ID gaming detector cannot trip. The shape bypasses gaming detection cleanly. What it cannot bypass is the operator's worklist: probe status lists every pattern a save graduated at 2x and up with a validated citation, whatever the detector said. Run on 2026-10-07, five absurd claims built this way all graduated and all five were on the worklist. Whether a grounded claim is true is the operator's call, as above; the per-ID detector is a hint, not the check.prepare_wrap lists the existing Proven and renders a contradiction-scan step directly into the compression package, recognizes per-pattern [contradicts: name] / [no-contradicts] declarations, and records any new Proven that lands without a stance into the hash-chained audit log — so the discipline travels with the wrap instead of depending on an external doc an adopter has to keep in sync. Turning that substrate into semantic closure is the operator's review, on their own cadence and outside the zero-dep library; drift probes and the audit are its instruments. The library records whether the discipline was followed; it doesn't catch the contradictions itself.verify invalid while status counted no audit failure); with it, six processes writing 250 each left one valid chain of 1,500 entries, and four processes appending across a week rotation left one valid chain of 800. Every writer must run a version that takes the lock: an older anneal-memory writing alongside breaks the chain as before. On Windows, or a filesystem without flock, the lock is not held and the audit trail needs one writer at a time.The library does now catch some attacks earlier versions didn't: complete dropout of a Proven-tier pattern surfaces as an omitted_patterns audit signal on the save result and in the hash-chained log (a signal, not a save-time gate — intentional retirement is fine, and a rename currently reads as a false-positive omission), and the cross-session check demotes lexical-rephrasing sycophancy on both the graduation path and the association path. These gaps are documented because they're reachable under adversarial-agent or drift-leaking conditions, not because they're hypothetical. "Immune system" names the citation-layer structural primitives above plus those audit and cross-session checks — not a complete defense against every form of memory drift. The honest line: the structural layer raises the cost of fabrication and catches the mechanical attacks; the semantic gaps it can't reach by construction are what the operator-review pass is for. Own the substrate, govern what graduates, run the review for what the structure can't see.
During compression, when an agent cites multiple episodes on a pattern line that claims 2x or higher (a 1x first sighting forms no link), those episodes form lateral associations — Hebbian-style links. A link gains strength only when the same pair is co-cited again in a later wrap, and it decays at every wrap it is not. In practice that repetition is rare: on my own long-running store (2026-10-04), 594 links, the strongest at 1.325 and none above 2.0, where a direct co-citation adds 1.0. So the links mostly record single co-citations rather than build up, and recall does not read them (measurement below).
This differs from how other systems form associations:
| Approach | When links form | What the link reflects |
|---|---|---|
| Co-access (BrainBox) | Episodes retrieved in the same query | Shallow — reflects search patterns, not understanding |
| Co-retrieval (Ori-Mnemos) | Episodes returned together at runtime | Better — but still driven by the retrieval system, not the agent |
| Co-citation during consolidation (anneal-memory) | Agent explicitly connects episodes while compressing | Formed by the agent's own judgment while compressing, not by search patterns (what recall gets from them so far: see below) |
The association network is gated by the immune system where gaming is actively detected: citations to non-existent episodes form no links, and citations the cross-session anti-sycophancy check flags as suspected re-graduation are refused. Grounding quality — whether a pattern's prose explanation lexically matches its cited episodes — governs whether the pattern graduates, not whether the co-cited episodes associate: a real but paraphrased co-citation still records that those episodes fired together (it just doesn't level the pattern up). The topology is built on real co-occurrence of real episodes under active gaming defense, not on retrieval frequency.
Strength model: Direct co-citation adds 1.0, session co-citation adds 0.3. Links decay 0.9x per wrap (unused connections fade). Strength caps at 10.0 to prevent calcification. Cleanup at 0.1 threshold.
What the links do, measured. Pattern recall (retrieve_relevant, crystal recall) does not follow Hebbian links, and this is why. I measured recall that also followed one Hebbian hop from the episodes a query matched. On my own long-running store (15,774 recall events, June 21 to September 30, 2026; the store is private and the replay script isn't published), recall exposed a crystallized pattern 788 times: 744 through the citation edge (query → matched episode → the patterns that cite it), 44 by keyword, and 0 through the Hebbian hop, because the episodes the links connect and the episodes the patterns cite turned out to be disjoint sets. I then gave a copy of the store the cheapest fix (link each pattern's own evidence episodes to each other) and replayed real prompts against it: the hop still surfaced nothing that recall without it had not. So the hop is not in recall. The links still form and decay at every wrap and feed the association statistics and the graph export; recall no longer reads them.
During compression, the agent can self-report its functional state — what it found engaging, uncertain, or surprising about the material it just processed. This gets recorded on the associations formed during that wrap.
Transformers don't natively maintain persistent state between sessions. This layer provides infrastructure for it: a record of what the agent's processing was like, not just what it processed. Over time, the affective topology may diverge from the semantic topology — an agent might know two things equally well but care about them differently.
Pass affective state during a wrap:
# Via library — pass AffectiveState to validated_save_continuity
from anneal_memory import prepare_wrap, validated_save_continuity, AffectiveState
wrap = prepare_wrap(store)
if wrap["status"] == "ready":
compressed = your_llm.compress(wrap["package"])
validated_save_continuity(
store,
compressed,
affective_state=AffectiveState(tag="curious", intensity=0.8),
)
# Via MCP tool
save_continuity(text="...", affective_state={"tag": "curious", "intensity": 0.8})
# Via CLI
anneal-memory save-continuity continuity.md --affect-tag curious --affect-intensity 0.8
This is experimental infrastructure. The associations and strength model work without it. Affective tagging adds a layer of signal for agents and researchers exploring persistent state.
Episodes (fast) Continuity (compressed)
┌─────────────┐ ┌──────────────────────┐
│ observation │ │ ## State │
│ decision │── wrap ───→│ ## Patterns (1x→Nx) │
│ tension │ compress │ ## Decisions │
│ question │ │ ## Context │
│ outcome │ └──────────────────────┘
│ context │ always loaded, bounded
└─────────────┘ human-readable markdown
SQLite, indexed
│ Associations (lateral)
│ ┌──────────────────────┐
└── co-citation ───→│ episode ↔ episode │
during wrap │ strength + decay │
│ affective state │
└──────────────────────┘
Hebbian, evidence-based
Four cognitive layers, modeled on how memory actually works:
These four describe how the store works. Two sibling stores sit alongside them (separate files, same atomic-write durability discipline) and address a different axis — when a thing is loaded, and whether it's retrospective or prospective:
<stem>.crystal.json) — proven, stable patterns held out of the always-loaded continuity and recalled on cue. The long-term semantic store that splits working memory from long-term memory; see The Memory Architecture (Complementary Learning Systems) below for why this completes the design.<stem>.spores.json) — prospective tasks that open and self-clean. Memory's forward-looking sibling; see Prospective memory — spores below.Six episode types give the immune system richer signal:
| Type | Purpose | Example |
|---|---|---|
observation | Pattern or insight | "Connection pool is the real bottleneck" |
decision | Committed choice | "Chose Postgres because ACID > raw speed" |
tension | Tradeoff identified | "Latency vs consistency — can't optimize both" |
question | Needs resolution | "Should we shard or add read replicas?" |
outcome | Result of action | "Migration done, 3x improvement on hot path" |
context | Environmental state | "Production DB at 80% capacity, growing 5%/week" |
The continuity file's structure is a per-store section schema: an ordered list of {heading, role} specs, where the role tells the system how each section behaves. It defaults to the classic four sections — State / Patterns / Decisions / Context — and a partnership entity adds more. graduating is where the immune system's citation/graduation scan runs (more than one allowed); narrative is compressed work narrative (temporal, rewritten each wrap); narrative-timeless is the felt relationship layer — dateless, carried forward and evolved rather than rewritten; live-state is volatile current focus that never graduates; frozen is preserved verbatim; durable holds facts that survive by mechanism (below). A section may be marked "optional": True (only a durable one may): it is not required at save and is ignored when a schema is matched to its name.
DEFAULT_SCHEMA is the historical four sections plus an optional Durable Facts section after State, so a text without it validates exactly as before. FLOW_SCHEMA is the reference partnership schema — it adds an Active Threads live-state section, the optional Durable Facts, and an Understanding narrative-timeless section. The persisted schema is the authority: a store that persisted the default or partnership schema before Durable Facts existed keeps that schema, reads back under the same name, and behaves exactly as before; a new store (init, or init --schema partnership) and a store with no persisted schema get the section. An existing store opts in by re-setting its schema (anneal-memory set-schema partnership, or store.set_section_schema(FLOW_SCHEMA)); flow's own store is opted in by its operator. The distinction is load-bearing: only entities that declare a narrative-timeless section carry the felt layer, the richer compression instructions that protect it, and the catastrophic-shrink gate above. Ops agents stay lean — zero extra weight, zero behavior change. Zero new dependencies; stdlib only.
from anneal_memory import Store, FLOW_SCHEMA
# A partnership entity: gains the felt layer + the shrink gate.
store = Store("./memory.db", project_name="flow", section_schema=FLOW_SCHEMA)
# An existing store stays on DEFAULT_SCHEMA untouched — or migrate in place:
store.set_section_schema(FLOW_SCHEMA) # validated; frozen during an active wrap