Agent Memory
Turns remotes from configured Forgejo hosts into gitea// memory namespaces, optionally restricted to a single forge owner.
Turns remotes from configured Gitea hosts into gitea// memory namespaces, optionally restricted to a single forge owner.
Derives memory namespaces from GitHub git remotes as github//, optionally narrowed to a single owner, and applies owner renames to existing namespaces.
Ships as a Home Assistant add-on with an ingress-only Memory panel in the sidebar, showing status tiles, records by type and namespace, audit activity, model server and encoder health, nightly candidate review and anchor sweep, plus a Connect-an-agent card with client commands.
Optional local model server used to enrich session notes and improve ranking; configurable with a primary URL, a fallback URL, direct URLs, a default model and per-kind model chains, with OpenAPI-style servers accepted when the URL ends in /v1.
Supports OpenAI-compatible model servers as the configured enrichment endpoint (URL ending in /v1), used to generate session notes and candidates instead of a paid hosted model.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
ha-agent-memory
Home Assistant add-on: one shared, bounded memory for Claude Code, Codex and other MCP agents, so a session starts from what earlier sessions learned instead of re-reading the repository. Storage and retrieval run in the add-on on SQLite full-text search and never depend on a paid model or a model server; an encoder and a local model server, when configured, improve ranking and enrich session notes.
Screenshots

The Memory panel after a few example sessions: status, the commands that connect Claude Code and Codex, and records by type.

Records per namespace, and the last 24 hours of audited writes by action and client.
Related MCP server: pathmark
Install
Select the first button to add this repository, then the second to open Agent Memory and install it. The add-on runs on amd64 and aarch64.
Setup
Start it. On first start it generates a token for each of the two default clients,
claude-codeandcodex, and shows each token once in the Log tab with the command that uses it.Claude Code: run the logged command,
claude mcp add --scope user --transport http agent-memory http://<home-assistant-host>:8765/mcp --header 'Authorization: Bearer <token>', with the address you reach Home Assistant at.Codex: set
AGENT_MEMORY_TOKENto thecodextoken, and in~/.codex/config.tomladd[mcp_servers.agent_memory]withurl = "http://<home-assistant-host>:8765/mcp"andbearer_token_env_var = "AGENT_MEMORY_TOKEN".Open the Memory panel in the sidebar: the Connect an agent card lists the clients and repeats the commands without the token.
Another agent: add a client with its name and scopes on the Configuration tab and restart; its token appears in the log. A new token for an existing client: remove it, restart, add it back and restart. Nothing else needs to be typed: enrichment and embedding retrieval stay off until a model server URL is set.
Options
All optional. The numeric and model options have built-in defaults.
Option | Default | Effect |
|
| Who may call: |
| unset: every owner | Narrows |
| none, unset | Gitea or Forgejo hosts whose remotes become |
| none | Repository namespaces that share a |
| none | Namespaces whose search and task match reach every other repository namespace. |
| none |
|
|
| Local time of the nightly candidate review; |
| 14 | Days an extracted candidate waits for review before the nightly review forgets it; |
|
| Keep each session's closing summary as a candidate. |
| 6 | Interval of the entity anchor check, 1 to 168. |
| 4000, 8000 | Default and maximum |
| 30 | Forgotten records and recorded queries are purged after it. |
| unset: no enrichment | Ollama, or an OpenAI-style server when the URL ends in |
|
| Default model, per-kind chains, call limit (Model routing). |
| unset: full-text only, | Encoder for embedding retrieval (Embedding retrieval). |
Design
Piece | Choice |
Storage | SQLite in WAL mode under |
Agent interface | MCP over streamable HTTP with JSON responses: |
Namespaces |
|
Retrieval order | repository, its linked projects, given topics, global; a hub's search and task match then reach every other repository namespace |
Ranking | reciprocal rank fusion of FTS5 and cosine over record embeddings when the encoder answers, FTS5 otherwise (Embedding retrieval) |
Budgets | bootstrap 500 tokens over the whole response (2,000 bytes), search 750, records 2000, packet 4000 (hard limit 8000); every response reports its estimate and truncation |
Auth | per-client bearer token (generated by the add-on, or a sha256 in options) or source address; scopes read, write, delete, export, admin, checked by one rule for MCP tools and REST routes; an address client gets |
Requests | a request with an |
Filtering | every persisted field and queued payload: credentials, key material, JWTs, URL passwords and the |
Health |
|
Page | ingress only, dark, drawn as graphics: status tiles, records by type and status, namespaces, audit rows per hour, model servers and encoder, nightly review and anchor sweep, unused records, latest audit rows |
Tools
memory_namespace, memory_bootstrap, memory_search, memory_get, memory_timeline, memory_project_context, memory_answer, memory_question_add, memory_questions, memory_store, memory_update, memory_corroborate, memory_forget, memory_session_close, memory_export, memory_anchors, memory_anchor_report, memory_anchor_sweep. Flow: bootstrap → search → get selected ids, or project_context(namespace, task) for one bounded packet with a reason per record: task matches first, then the repository's constraints and decisions, open work, known failures and procedures. bootstrap lists the titles of active records, each under the shortest id prefix of 8 or more characters no other live record shares, which memory_get takes in place of the full id, and counts candidates in candidates_pending; memory_timeline(namespace, status="candidate") lists candidates for review with their revisions and counts those pending (the nightly candidate review forgets duplicates and session state first), memory_update(id, revision, {status: active}) promotes one, memory_forget drops it.
Write protocol
Status | set by the server: |
Writer | a token client's name replaces |
Revision | an integer on every record, advanced by every mutation (content, anchors, anchor state, evidence, status, supersession, forget, rename). |
Transactions | each mutation and the reads it depends on commit together: an update, a store with its supersession, an anchor report with its evidence, an extraction's candidates with the job's completion, an answer with its job's. Every statement runs under the store lock, so no read sees another thread's open transaction |
Supersede |
|
Screen | every persisted string, list and job payload; a URL anchor with userinfo or a query or fragment parameter named as a credential (judged by the name's words: |
Session close |
|
Bootstrap | the 500-token budget is 2,000 bytes on the wire: the REST body, or the whole JSON-RPC reply over MCP; an item that does not fit is left out, the first included, and |
Anchors
Anchor | Checked by | Where |
| the add-on's sweep, every | the add-on, against Home Assistant's states through the Supervisor |
| a client that can see the path, through | the host holding the path |
| a client, through | any host on the LAN |
| a client, through | any host that can reach the URL |
Each anchor keeps its last definitive state, unchecked, ok or missing, and unavailable_since while its checker could not look. A report of unavailable keeps the state and files nothing. A change between ok and missing files one observation in the record's evidence trail. Confidence counts an anchor once, by its state now: one contradiction while it is missing, nothing once it resolves; witnesses (memory_corroborate) count as in Evidence. A change of state advances the record's revision; memory_anchors lists each anchor with its record's namespace. A record with a missing anchor carries missing_anchors in search, get, timeline, bootstrap and project context. /health anchor_sweep carries the last sweep, read back from its audit row after a restart.
Usage and recall
Search arguments |
|
Counted |
|
Storage | table |
Unused | the page lists active records older than 30 days that nothing returned in 30 days, oldest first, once counting has run 30 days (audit row |
Embedding retrieval
embed_url names an OpenAI-style /v1 server with an embedding model. Frozen eval (tools/retrieval-eval/), MRR, with the paired 95% interval against FTS5:
Store | Queries | FTS5 | Cosine alone | Fusion |
24 records | 72 | 0.83 | 0.94 | |
150 records | 42 (targets still live) | 0.74 | 0.82 (-0.023 to +0.178) | 0.81 (+0.014 to +0.122; better on 11 targets, worse on none) |
Used when | the encoder answered its last probe, the query's vector arrives within |
Otherwise | FTS5; |
Records ranked | every live record in the scopes and hub reach, the same set full-text search reads |
Fusion | the FTS5 list and the cosine list, each cut to |
Query text |
|
Record text |
|
Storage | table |
Stale | an edit, another model or another snapshot changes the digest; search uses FTS until the worker re-embeds |
Worker | embeds missing and stale records at start and after each store or edit; probes the encoder every 30 s |
Snapshot | the last path segment of |
Removed |
|
Not sent | a query that screens as carrying a secret |
Cosine | each hit carries |
Not affected | the demand signal counts FTS hits; standing answers take FTS sources |
A miss on a query's vector marks the encoder down until the next probe, so a dead encoder costs one search embed_timeout_s and later searches nothing.
embed_url: http://<encoder-host>:8080/v1
embed_model: qwen3-emb-0.6b
embed_timeout_s: 2Measure a retrieval change offline before shipping it: python tools/retrieval-eval/eval.py . --embeddings tools/retrieval-eval/embeddings.json ranks the frozen queries through agent_memory/embed.py with the vectors embed_eval.py fetched.
Candidate review
Each night at review_at (local HH:MM; absent means 03:00, off turns it off) the enrichment model reads every pending candidate beside the five records nearest it that its namespace's search reaches (active records, and candidates written earlier) and returns one verdict. It forgets; it never promotes. Before the verdicts, an extracted candidate still pending candidate_expiry_days (default 14, 0 off) after it was written is forgotten, whether or not a model is live; candidates an agent stored never expire. POST /api/review (the delete scope) runs it at once; /health candidate_review carries the last run.
Verdict | Applied when | Effect |
| the named record was shown, is not a session summary standing in for another type, and holds every number, version, path, identifier and quoted phrase in the candidate | forgotten |
| the candidate was extracted from session notes, or an agent stored it as a type other than decision, constraint, requirement, procedure or preference | forgotten |
| always | stays for the agent review at session start |
A forget applies only while the record is a candidate at the revision the model read, and its audit row names the verdict, the record it duplicates, the model and the reason. Eval (tools/review-eval/, 397 records agents had reviewed): a forget verdict on 188 of 373 they forgot and on 2 of 24 they promoted, each of those 2 a duplicate of an older copy that the eval restores as a candidate.
Standing questions
bootstrap returns an index and project_context retrieves afresh every call. Neither is a maintained answer, so a session needing settled knowledge rediscovers it. A standing question is answered once in the background and read for nothing afterwards.
Where questions come from |
|
Demand signal | asked often AND answered badly; a well-answered question scores near zero |
Who answers | the existing job queue and model worker, one refresh per idle poll, no scheduler |
Reading |
|
Freshness |
|
Grounding | the answer cites |
Brevity | at most four sentences, and cut to |
A stale answer stays readable and is labelled stale, never silently served as current. Model output passes the same secret screen as a record.
Model server
| Client | Availability | Chat | Thinking off | Extraction JSON |
ends in | OpenAI-style (llama-server, llama-swap) |
|
|
|
|
anything else | Ollama |
|
|
|
|
ollama_fallback_url names a second server with the same models. The two are routed like a load balancer:
Live | a server that answered its last availability probe; a probe runs at least every 15 s from the worker's poll, never inside a call; a server not yet probed counts as live |
Chosen | the live server with the fewest calls in flight, |
Tiers |
|
A server that does not answer a call | marked down until its next probe; the call moves to the next live server |
An HTTP error from a server that answered | that model's failure: the next model in the chain on the same server, then the next server |
A model that is not running | the call goes to the server anyway; llama-swap loads a configured model on its first request |
/health enrichment.servers lists each server with live and in_flight; each extract and answer log line ends with the server that served it, and each review forget's audit row names it.
Both use temperature 0.1. The OpenAI-style client caps output at 2048 tokens for extraction and 512 for an answer. Model names are what the server serves: for llama-swap, its model IDs and aliases. Extraction asks for a JSON schema, and a reply inside one Markdown code fence is read inside it.
Model routing
ollama_models names a chain per job kind, most preferred first. Extraction pulls typed fields out of messy notes; answering writes four sentences of prose. Without the option the worker uses ollama_model for both.
ollama_models:
- "extract:qwen3.5:9b"
- "extract:ornith:9b"
- "answer:ornith:9b"
- "answer:granite4.1:8b"The chain above is the measured one (7 real notes, 18 frozen questions, 3 runs). Both requests set think: false: a thinking model fills Ollama's 4096-token default context before it writes the reply.
The chain is also the failure path: a model missing from the host, or one whose reply will not parse, costs the next model rather than the job. The configured default always ends the chain, and a job every model failed still fails. Records and audit name the model that actually served.
The option is a flat list of kind:model entries and is optional, so an add-on update never needs it set.
Evidence
confidence is what the writer asserted. memory_corroborate(id, quote, stance) attaches one dated, attributed, quoted observation, so a second session can agree or disagree without superseding the record, which would erase its history and the disagreement.
Stances |
|
Derived |
|
Unwitnessed record | keeps the confidence it was given |
Each corroboration | closes a third of the remaining distance to 1.0 |
Each contradiction | halves what is left, so one refusal outweighs two agreements |
Ranking | search uses |
A contradicted record stays active and readable: disagreement is recorded, not resolved by deletion. Quotes pass the same secret screen as a store.
Clients
A client listed by name alone gets a token the add-on generates; it is logged once and only its sha256 is kept, in /data/client_tokens.json. A client whose token is made elsewhere lists its sha256 as token_sha256. An agent sends Authorization: Bearer <token>: Claude Code through --header on claude mcp add or a headersHelper, Codex through bearer_token_env_var. tools/retrieval-eval/fetch.py reads AGENT_MEMORY_URL and sends AGENT_MEMORY_TOKEN_CLAUDE (--codex: AGENT_MEMORY_TOKEN_CODEX), from the process environment, then the Windows user environment.
Develop
python -m pytest -q tests
python -m ruff check .
python -m ruff format --check ..github/workflows/ci.yml runs these with yamllint, shellcheck, hadolint and the add-on linter, then builds the image for amd64 and aarch64 and runs tests/run_image.sh against each. .gitea/workflows/validate.yml runs the lint and tests on a Gitea or Forgejo runner, and .gitea/workflows/image.yml builds the image there for amd64 and aarch64 and starts each.
This server cannot be deployed
Maintenance
Related MCP Connectors
Shared memory for coding agents. Stop re-explaining your codebase every session.
Persistent memory for AI agents across Claude, ChatGPT and any MCP client.
Persistent AI memory shared across Claude, ChatGPT, coding agents, and compatible MCP clients.
Private persistent memory for Claude, ChatGPT & Gemini via MCP - semantic search, zero-code setup.
Related MCP Servers
- AlicenseBqualityAmaintenancePersistent, local memory for AI coding agents that learns how you work, not just what you said. Supports Claude Code, Codex CLI, Cursor, and any MCP client.79249 PyPI73MIT
- AlicenseBqualityAmaintenanceLocal-first memory for MCP clients. It provides shared durable memory without requiring hosted accounts, vector databases, or API keys, and works with Codex, Claude Code, Cursor, and other MCP clients.2582 npm4MIT
- AlicenseBqualityAmaintenanceLocal-first, auditable memory for Codex, Claude Code, and MCP clients. It stores scoped user/project memory in SQLite or Postgres, serves read-only recall and inspection tools by default, and supports opt-in governed writeback with review and forget controls.8183 npm20MIT
- AlicenseAqualityAmaintenancePersistent memory for AI coding agents, storing decisions, bug fixes, conventions, and discoveries in a local SQLite database and automatically recalling them when relevant. Works with Claude Code, Codex, Cursor, Gemini CLI, and other MCP-compatible agents.224MIT