Place where AI Agent memories matters
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
A multi-tenant memory palace for AI agents — served as a remote MCP server, backed by Ollama and a swappable vector index (Qdrant for the SaaS, an embedded one for self-hosted).查看完整 README ↗A multi-tenant memory palace for AI agents — served as a remote MCP server, backed by Ollama and a swappable vector index (Qdrant for the SaaS, an embedded one for self-hosted).
agentsmemory is the Go SaaS rewrite of the original Python mempalace:
a semantic, long-term memory store that humans and AI agents read from and write
to. Where the Python tool was a single local user with no auth, this is built for
teams: each agent connects to a network MCP endpoint with a bearer token,
operates inside its team's isolated workspace, and can pull centralised,
versioned skills the team keeps up to date.
Status: early skeleton. The tenancy, auth, skill registry, storage clients and MCP transport are wired and verified end-to-end, and the core memory loop (file a drawer → recall it semantically) now works end-to-end against Ollama + the vector store. Today the server exposes 41 MCP tools, the same 41 whether hosted or self-hosted — the WRITE/FILE + SEARCH/RECALL families, the agent
diary, theam_minepipeline (text → chunked drawers + closet index), hybrid search (vector + BM25 + closet boost), the navigable graph (hallways + tunnels + traverse), the temporal knowledge graph, the skill-registry CRUD, and wing admin. Only the two single-user-local tools (sync,hook_settings) are intentionally not ported. See the Roadmap.
The step-by-step guides live in their own files, so each one can be followed start to finish without reading the rest of this document:
| INSTALL.md | First install — the server, the kit, and what differs on macOS, Windows and Linux |
| UPDATE.md | Upgrading. Three parts update independently, and doing it in the wrong order repoints your hooks silently |
| TROUBLESHOOTING.md | Organised by what you see. Almost every failure here is silent, which is why it needs its own list |
This README is the reference: what the system is, what each tool does, every flag, and why the design is the way it is.
The "memory palace" metaphor is the data model:
| Concept | Meaning |
|---|---|
| Wing | a project / context namespace |
| Room | an aspect within a wing (e.g. backend, decisions) |
| Drawer | one verbatim memory chunk + rich metadata (never summarised) |
| Closet | a topic/quote pointer index used as a search rank-boost (never a gate) |
| Hallway | a within-wing link between entities that co-occur in drawers |
| Tunnel | a cross-wing link (author-made, or auto-generated from a shared topic) |
| Knowledge Graph | a separate temporal store of subject → predicate → object facts with validity windows |
Agents recall context with hybrid search (vector similarity + BM25 + closet
boost, fused), and file new memories that are embedded and indexed. The original
design notes live in the project's memory palace under the agentsmemory wing.
Authorization: Bearer <token>
AI agent ───────────────────────────────────────────► POST /mcp
(Claude, etc.) │
▼
┌────────────────────────┐
│ Streamable HTTP (MCP) │ stateless
│ mark3labs/mcp-go │
└────────────┬────────────┘
HTTPContextFunc: token ──► Tenant on ctx
│ (fail closed if unresolved)
┌───────────────────────────────┼───────────────────────────┐
▼ ▼ ▼
internal/tenant internal/skill internal/palace
teams · users · keys load_skill registry wings · rooms · drawers
· plans (price) (centralised, versioned) hallways · tunnels
│ │ │
▼ ▼ ▼
SQLite (no-cgo) ◄── relational source of truth ──► Qdrant + Ollama
gorm + goose schema collection-per-tenant · bge-m3
Tenant in exactly one
place (tenant.Repo.ResolveToken); every tool reads the tenant off the
context and refuses to run without one.The unit of tenancy and billing is a workspace (the teams table):
personal | enterprise) and a plan (a price
tier from the plans catalog, e.g. Personal $0, Enterprise $50/mo).mempalace_<sha256(teamID)[:16]>_drawers. A missing query filter can
never leak across teams because the data is not even colocated.user ──┬── workspace "personal" (plan: Personal, $0) ── key… ── qdrant collection A
├── workspace "side-project"(plan: Personal, $0) ── key… ── qdrant collection B
└── workspace "acme-corp" (plan: Enterprise, $50) ── key… ── qdrant collection C
Billing today is a
plan_idcolumn on the workspace. A dedicatedsubscriptionstable is the planned evolution when payment lands.
Phase 1 is per-agent bearer tokens; the boundary is designed so OAuth 2.1 can slot in later without touching any tool.
sha256(token) is
stored — the plaintext is shown once.Authorization: Bearer <token> on its MCP connection. The
token's workspace is the tenant scope for that session.member | writer | admin) gate writes to shared artifacts — e.g.
updating a centralised skill requires writer or admin.am_load_skill)Instead of every developer copy-pasting local skill files, a team keeps one shared, versioned source of truth and its agents pull from it:
am_load_skill(name) → returns { id, name, version, description, content, updated_by, updated_at } so the agent can drop the body straight into a skill
slot. Read access for any team member; the lookup is a direct keyed query (no
vector search).am_list_skills (metadata for any member) and am_update_skill (version-bumping,
writer/admin) complete the registry CRUD. The /load-skill <name> Claude
command is the client-side nicety over the tool: it fetches a skill by name and
uses its body directly in the session — no file written, always the live
version (with no name, it lists what's available). Shipped by the
aiagentmemory installer.Every tool is namespaced with the am_ prefix (e.g. am_status, am_search)
so the server can run alongside other memory MCPs — notably mempalace, which
exposes same-named tools — without the client seeing two tools of the same name.
| Tool | Status | Description |
|---|---|---|
am_status | ✅ | Liveness + the team this session is scoped to |
am_load_skill | ✅ | Load a centralised, team-shared skill by name |
am_add_drawer | ✅ | File a verbatim memory (chunked + embedded; idempotent by source) |
am_get_drawer / am_update_drawer / am_invalidate_drawer | ✅ | Read a memory; correct it (sending content supersedes — a new record, the old one ended with your reason, the two linked) or move it (wing/room keeps the id); retract one that nothing replaces. No agent-reachable tool destroys a record — erasure is agentsmemory drawer erase, which needs the database file |
am_list_drawers | ✅ | Paginate drawers, optionally filtered by wing/room |
am_search | ✅ | Hybrid recall — vector candidates re-ranked by vector + BM25 + closet boost, then optionally by a TEI cross-encoder (RERANK_URL). ids_only: true returns a thin page (ids, wing/room, identity, scores, stale marker, content length — no text) at about a tenth of the bytes, to page and then fetch the few you keep with am_get_drawer (ADR-060) |
am_check_duplicate | ✅ | Is content near-identical to an existing drawer? |
am_list_wings / am_list_rooms / am_get_taxonomy | ✅ | Indexed wing/room aggregations of a team's memory |
am_get_aaak_spec | ✅ | The AAAK compressed-memory dialect reference |
am_reconnect | ✅ | Ensure the workspace's vector namespace exists; write-gated because this may create backend state |
am_diary_write / am_diary_read | ✅ | Append to / read an agent's append-only journal (timestamped, newest-first) |
am_mine | ✅ | Mine a text payload into chunked drawers (entities + content date) + the closet index; idempotent by source |
| Concern | Choice |
|---|---|
| Language | Go 1.25+ |
| HTTP router | github.com/go-chi/chi/v5 |
| MCP server | github.com/mark3labs/mcp-go (Streamable HTTP, stateless) |
| Relational store | SQLite no-cgo via gorm.io/gorm + github.com/glebarez/sqlite |
| Migrations | github.com/pressly/goose/v3 (embedded .sql) |
| Vector store | Qdrant (REST, no SDK) — collection per tenant · or embedded chromem-go · or SQLite itself |
| Embeddings | Ollama bge-m3 (1024-dim) via /api/embed |
| CLI / flags | github.com/urfave/cli/v3 |
| Auth (planned humans) | github.com/markbates/goth |
| Web UI (planned) | templ + datastar |
Prerequisites: Go 1.25+ and an Ollama with bge-m3 pulled — every drawer
is embedded on the way in, so the memory loop needs it (see Preparing Ollama
(embeddings) below). A vector service is not a
prerequisite: searches are indexed in-process unless you ask for Qdrant.
# build
go build -o agentsmemory ./cmd/server
# run — migrates an embedded schema, seeds a demo workspace on first boot,
# and prints a one-time bearer token to the log
./agentsmemory --addr :8080 --db agentsmemory.db
On first run you'll see something like:
seeded demo team <team-id>
MCP bearer token (shown once): <64-hex-char token>
agentsmemory listening on :8080 (MCP at /mcp)
Call it like an MCP client would:
TOKEN=<paste the token>
# initialize
curl -s http://localhost:8080/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-03-26","capabilities":{},
"clientInfo":{"name":"demo","version":"0"}}}'
# load the seeded "hello" skill
curl -s http://localhost:8080/mcp \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"am_load_skill","arguments":{"name":"hello"}}}'
A request without a valid token comes back as a fail-closed
unauthenticated tool error.
agentsmemory never embeds text itself — it calls your Ollama, so nothing you remember leaves your machine. The install steps, including what each platform needs and why a containerised Ollama is the wrong choice on macOS, are in INSTALL.md.
Two things about the choice of model, which are reference rather than procedure:
Why bge-m3, and why not to change it casually. It matches the frozen Python
palace (1024-dim), so migrated memories and new ones share one vector space.
Swapping the model changes that space: old and new vectors stop being comparable
and every drawer needs re-embedding. Pick it before you fill the palace, not
after — --ollama-model exists for a fresh one.
A missing model fails loudly and loses nothing. Ollama does not fetch a model
on demand for /api/embed, so an unpulled one comes back as
model "bge-m3" not found. Writes return the embed error, and rows that came
through /import sit in the embed queue and drain by themselves once the model
is there.
--local)Everything above is the multi-tenant SaaS shape: many workspaces, each behind a
token. If you are running this on your own machine for yourself, --local
collapses it to the simplest thing that still runs every tool.
Grab the server binary for your platform — every release publishes
aiagentmemory-server-<os>-<arch> for linux and darwin on amd64 and
arm64, alongside the aiagentmemory CLI:
os=$(uname -s | tr '[:upper:]' '[:lower:]')
arch=$(uname -m); [ "$arch" = x86_64 ] && arch=amd64; [ "$arch" = aarch64 ] && arch=arm64
curl -fsSL -o agentsmemory \
"https://github.com/atvirokodosprendimai/agentsmemory/releases/latest/download/aiagentmemory-server-${os}-${arch}"
chmod +x agentsmemory
Windows, or want checksums? The same releases carry
agentsmemory_<os>_<arch>.tar.gz / .zip archives (Windows included) with a
SHA256SUMS.txt. Prefer to build it yourself? go build -o agentsmemory ./cmd/server. Either way, run it:
./agentsmemory --local --db agentsmemory.db
# agentsmemory listening on 127.0.0.1:8080 (local mode: workspace "local", MCP /mcp, no token required, no dashboard)
Then point your agent at that server. It is the same kit as the hosted
service; --local aims it at http://localhost:8080/mcp and never asks for a
token, because a loopback server has none (pass --token only if you started the
server with one — see serving a home
network):
aiagentmemory install --local # global, Claude (~/.claude) — the default agent
aiagentmemory install --local --agent all # claude | codex | pi | cursor | claude-desktop | both | all
aiagentmemory install --local --sandbox acme # isolated config at ~/.sandboxes/acme
aiagentmemory install --local --sandbox acme --agent codex # …and choose the agent inside it
With --local and no target named, the install goes global and skips the
interactive global-vs-sandbox prompt — a self-hoster setting up their own machine
has an obvious answer to that question. Naming --sandbox <name> still wins (the
name is required), and --mcp-url overrides the endpoint for a server on another
port or host. Registering the MCP is only half the job, though: what makes an
agent actually use the tools is the protocol the kit installs alongside it —
see the server is inert without the
protocol.
⚠ Re-run install the same way you ran it the first time. Without --local
(or --mcp-url) the endpoint falls back to the hosted default, and the default
wins over what is already configured — so a bare aiagentmemory install on a
machine set up with --local repoints every installed hook at the hosted
service. The hooks then talk to a server they hold no credential for, and a hook
that cannot reach its palace goes silent rather than failing loudly, so
nothing looks broken. The installer now says so before it writes:
[!!] this install REPOINTS your hooks: they currently talk to
http://localhost:8080/mcp, and will now talk to https://aiagentmemory.dev/mcp.
To see where your hooks point today, grep the endpoint out of your agent's hooks
file — for Claude, AGENTSMEMORY_MCP_URL is the first thing in each command:
grep -o "AGENTSMEMORY_MCP_URL='[^']*'" ~/.claude/settings.json | sort -u
What changes:
| default | --local | |
|---|---|---|
| Workspaces | many, created from the dashboard | exactly one, slug local, provisioned on first boot |
/mcp auth | Bearer token or OAuth | none — every request is the local workspace (unless --token) |
| API keys | minted per member | none exist; none are stored |
| Quota | per plan | uncapped (plan_unlimited) |
| Dashboard, OAuth, billing webhooks | mounted | not registered (404) |
| Listen address | :8080 (all interfaces) | 127.0.0.1:8080 |
| Search index | sqlite (the source of truth scans itself) | chromem — embedded, no service to run |
All 41 MCP tools behave identically — they only ever see a resolved workspace, and local mode injects one instead of resolving it from a credential.
Point any MCP client straight at it, with no header:
claude mcp add --transport http agentsmemory http://localhost:8080/mcp
Two guardrails worth knowing:
--addr to a
routable interface still works but logs a loud warning — anyone who can reach
the port owns every memory in the file. Two things tighten it:
--token puts a shared secret in front of
the port, and
--socket goes further than
loopback can.Host or Origin names
something other than this machine, answering
403 forbidden: Host <name> does not address this machine. That is not
theoretical tidiness: a page you visit
can point its own domain at 127.0.0.1, at which point your browser treats it
as same-origin, skips the preflight, and CORS never runs — so without this
check any site you open could read and rewrite every memory in the file. The
check is on wherever the boundary is this machine (loopback, --socket, or a
container publishing to the host loopback) and off when you have deliberately
bound a routable address, where --token is the boundary instead.local — including the demo workspace the multi-tenant path seeds on first
boot. Use a fresh --db file, or drop --local.--token)Loopback is the right default for one machine, and the wrong one the moment you
want the laptop in the other room to share the same memory. --token is what
makes that safe: local mode then requires Authorization: Bearer <token> on
/mcp and /import, so the shared secret takes over the job the loopback
boundary was doing.
export AGENTSMEMORY_LOCAL_TOKEN="$(openssl rand -hex 32)"
./agentsmemory --local --token "$AGENTSMEMORY_LOCAL_TOKEN" --addr 0.0.0.0:8080 --db agentsmemory.db
0.0.0.0 binds every interface, which is what makes the server reachable from
another machine. Then install the kit on each of those machines, pointing at the
server's LAN address — one exported variable configures both halves, because the
server and the installer read the same one:
export AGENTSMEMORY_LOCAL_TOKEN="…the same value…"
aiagentmemory install --local --token "$AGENTSMEMORY_LOCAL_TOKEN" \
--mcp-url http://192.168.1.50:8080/mcp
Worth knowing:
/healthz stays open so container health checks keep working. It reveals
nothing but liveness.--token, nothing changes. A loopback or --socket install carries
no credential, and a stray Authorization header left in an agent's config is
still ignored rather than rejected.Local mode also picks its own search index: chromem, a vector database that
runs inside the server process. It keeps the vectors in memory and persists them
next to the database — agentsmemory.db gets agentsmemory.chromem/ beside it,
one directory per workspace inside — so a self-hosted install is one binary, one
file and one folder, with no service to install, start or monitor. sqlite and
qdrant remain one --vector-backend away (choosing the index).
--socket / mcp-stdio)HTTP on a port stays the default and nothing about it changed. But a port is a
weak boundary for an endpoint with no authentication: every user and process on
the machine can open 127.0.0.1:8080. --socket replaces it with a Unix socket
created at mode 0600, so the operating system restricts the server to the
account that started it:
./agentsmemory --local --socket /tmp/agentsmemory.sock --db agentsmemory.db
# agentsmemory listening on unix:/tmp/agentsmemory.sock (local mode: workspace "local", …)
A socket has no URL, so agents reach it through mcp-stdio — a bridge shipped in
the same binary that speaks MCP on stdin/stdout and forwards to the server:
claude mcp add agentsmemory -- /path/to/agentsmemory mcp-stdio --socket /tmp/agentsmemory.sock --wing wing_acme
codex mcp add agentsmemory -- /path/to/agentsmemory mcp-stdio --socket /tmp/agentsmemory.sock --wing wing_acme
The server prints both lines on startup with its own absolute path filled in, so they can be copied straight out of the log. The installer can wire it for you instead — it registers the same bridge and installs the memory protocol alongside it:
aiagentmemory install --local --socket /tmp/agentsmemory.sock
--socket requires --local (the bridge carries no token, so it only reaches a
self-hosted server) and finds the server binary on PATH, or takes
--server-bin /path/to/agentsmemory. It works for Claude and codex; pi has no MCP
client of its own, so install that one with --mcp-url against --addr.
Worth knowing:
mcp-stdio --url http://host:8080/mcp --wing wing_acme (with --token for a multi-tenant server) bridges any endpoint,
which is the escape hatch for a client that only supports stdio transport.--wing becomes
X-Agentsmemory-Wing on every forwarded request, over HTTP or a socket. A tool
call can still pass wing: "*" when it deliberately needs every project.AGENTSMEMORY_SOCKET configures both halves — the server's listen path and
the bridge's dial path — so the pair cannot drift apart.--wing value can come from the environment when a process
manager supplies registration scope that way — see
INSTALL.md.invalid argument.Five files, composed rather than copied: every stack is the base plus zero or more overlays, so there is no variant to keep in sync with another.
| File | What it is | When |
|---|---|---|
docker-compose.yml | the base — one container, embedded chromem index, one volume | one person, one machine. The default, and it stays the default |
docker-compose.full.yml | overlay: Qdrant + a cross-encoder | recall precision matters more than dependency count, or several machines share an index. This is what we run |
docker-compose.ollama.yml | overlay: the embedder in the stack, and points the server at it | you have no Ollama on the host. A profile could start the container but not repoint the server, which is why it is a file |
docker-compose.host.yml | overlay: host networking — Linux only | Ollama on 127.0.0.1:11434 with no OLLAMA_HOST=0.0.0.0 and no host.docker.internal. Does nothing on macOS/Windows, where containers live in a VM |
docker-compose.prod.yml | hosted multi-workspace mode: dashboard, OAuth, billing | you are running the SaaS shape rather than a personal palace |
Overlays stack. -f docker-compose.yml -f docker-compose.full.yml -f docker-compose.ollama.yml is a legitimate combination and means all three.
Every -f flag, every time. An overlay alone is not a complete stack, and
leaving one off does not fail — it starts a valid different stack. Either repeat
the flags, or fix them once per shell with
export COMPOSE_FILE=docker-compose.yml:docker-compose.full.yml, or put that same
line in a .env beside the compose files to make it this directory's default.
The image every one of them builds comes from the Dockerfile at the repo root.
cp .env.docker.example .env.docker # point OLLAMA_URL at your Ollama
docker compose up -d
# then point your agents at it — the kit does the whole wiring, not just the MCP
aiagentmemory install --local --agent all # or: claude | codex | pi | cursor | claude-desktop
--local is what tells the kit the server is yours: it registers
http://localhost:8080/mcp, prompts for no token, and installs globally unless
--sandbox/--config-dir says otherwise. Registering the MCP by hand
(claude mcp add --transport http agentsmemory http://localhost:8080/mcp) still
works and gives you the tools — but not the protocol, the hooks or the subagent
definition, which is the difference between a server and a habit.
Brings up --local with the embedded chromem index, so the whole stack is one
container and one volume — /data/agentsmemory.db (truth) and
/data/agentsmemory.chromem (index) live side by side in it. Ollama is
deliberately not a service: most people already run one, and a second copy
would re-download gigabytes of models — so .env.docker.example covers both
host.docker.internal (Ollama on your machine) and a URL for a remote box.
Both of the things that bite are on the Ollama side, and both are handled in
Preparing Ollama: it binds 127.0.0.1 by
default, which a container cannot reach, and the model must be pulled first.
The port is published as 127.0.0.1:8080:8080, and the loopback prefix is
load-bearing for the same reason as above — plain 8080:8080 would offer an
unauthenticated memory server to your whole network. Inside the container the
process binds :8080 (a published port cannot reach a loopback-bound process),
so it logs the non-loopback warning on boot; there, the published interface is
the boundary, and the warning is expected.
Everything above is a menu. This is the setup the maintainers actually run, and
the one every measurement in docs/adr/ was taken against — the base file plus
the full-quality overlay, with Ollama on the host:
# 0. once: an embedder on the host, and the model pulled (see Preparing Ollama)
ollama pull bge-m3
# 1. the stack
cp .env.docker.example .env.docker # point OLLAMA_URL at your Ollama
docker compose -f docker-compose.yml -f docker-compose.full.yml up -d
# 2. your agents — all four, or name one
aiagentmemory install --local --agent all
Three containers: the server, Qdrant, and the cross-encoder. First boot pulls the
reranker model into a volume, so give it a few minutes and watch
docker compose ... logs -f reranker if you are impatient.
Verify it, rather than assume it. The server prints what it actually resolved, which is the only thing that says the overlay applied:
docker compose -f docker-compose.yml -f docker-compose.full.yml logs agentsmemory | grep 'ranking:'
# ranking: fusion=rrf lex-weight=n/a lex-norm=n/a closet-boost=0.00 rerank=on(pool=10,weight=0.50) unit=memory evidence=lexical
rerank=on(...) is the line to look for. If it says rerank=off, the overlay did
not apply — you almost certainly dropped one of the two -f flags, which starts
a valid base stack rather than failing. unit=memory is the served ranking
unit. evidence=lexical|semantic is the resolved MEMORY_EVIDENCE_SELECTOR.
Redeploying after a change, including proving the running binary carries it:
scripts/redeploy.sh
It refuses to report success on a build it cannot verify: it runs the suite before
building, greps the binary inside the running container for strings the change
introduced, compares its digest against the image just built, runs one real search
through /mcp, and checks the installed client kit against the checkout. A build's
success is a claim about the build; this reads the artifact that is serving.
When recall quality matters more than dependency count, a second overlay swaps the in-process index for Qdrant and adds a cross-encoder in front of the results:
docker compose -f docker-compose.yml -f docker-compose.full.yml up -d
# switching an existing install: replay the stored vectors into Qdrant once.
# Nothing is re-embedded — SQLite is the source of truth.
docker compose -f docker-compose.yml -f docker-compose.full.yml run --rm agentsmemory sync
That is three services: the server, Qdrant as the search index, and a
cross-encoder (bge-reranker-v2-m3, served by llama.cpp) that rescores the
top RERANK_POOL candidates of every search before the page is cut — 10 in
this overlay, not the flag's own default of 50. Cost is linear: the cross-encoder
scores every pair in a full forward pass, measured on this stack at ~435ms per
document, so 50 candidates is ~22s and MCP clients give up long before the
server's budget does. (This sentence said 50 while the overlay shipped 10, which
is the drift TestDocumentedEnvVarsAreRead exists to catch one layer down.) The embedder scores a
drawer against a query it never saw; the cross-encoder reads the pair together,
which is the sharper judgement — and it only runs over what hybrid ranking
already surfaced, so the cost is bounded per search rather than per palace. If it
is down, search falls back to the fused vector+BM25 order instead of failing.
On a Mac, run the cross-encoder on the host, not in the container. Docker on
macOS has no GPU, and on four CPU cores the container needs ~30 s for the pool
the server sends it against a 10 s budget, so every search waits and falls open
— which is the ten-second pause at session start. docs/macos-native-reranker.md
has the measured numbers and the three commands.
docker-compose.ollama.yml)If you have no Ollama at all, a third file adds it — and, unlike a profile, points the server at it:
docker compose -f docker-compose.yml -f docker-compose.full.yml \
-f docker-compose.ollama.yml up -d
It pulls the embedding model on first boot and reports healthy only once that
model exists, so up -d really is the whole setup and nothing starts before it
can embed. On macOS and Windows prefer a host install where you have one: a
containerised Ollama cannot reach Metal and runs on CPU.
Going back is dropping the second -f — the chromem index is still on the
volume, and the server refills it from SQLite if it is not.
On Linux, an override removes the Ollama friction entirely:
# start (both -f flags, every time — the override alone is not a complete stack)
docker compose -f docker-compose.yml -f docker-compose.host.yml up -d
# follow the logs / stop
docker compose -f docker-compose.yml -f docker-compose.host.yml logs -f agentsmemory
docker compose -f docker-compose.yml -f docker-compose.host.yml down
Repeating both files gets tedious, so either export it once per shell —
export COMPOSE_FILE=docker-compose.yml:docker-compose.host.yml, after which
plain docker compose up -d uses both — or put that same line in a .env beside
the compose files to make it the permanent default for this directory.
Without compose at all, the equivalent single container (the embedded index, so nothing else needs to run):
docker build -t agentsmemory:local .
docker run -d --name agentsmemory --network host --restart unless-stopped \
-v agentsmemory-data:/data \
-e VECTOR_BACKEND=chromem -e OLLAMA_URL=http://localhost:11434 \
agentsmemory:local serve --local --addr 127.0.0.1:8080 --db /data/agentsmemory.db
network_mode: host puts the container in the host's network namespace, so
localhost:11434 inside it is your machine's loopback — Ollama works on its
default 127.0.0.1 bind, with no OLLAMA_HOST=0.0.0.0 and no
host.docker.internal. It is Linux-only: Docker Desktop on macOS and Windows
runs containers in a VM, where host networking is an opt-in feature (4.34+)
rather than the default. Note that host networking ignores ports:, so the
loopback publish stops protecting anything and the server's own --addr 127.0.0.1:8080 becomes the boundary — which is exactly what the override pins.
The embedded index needs no network at all, so nothing else changes; a Qdrant
service, if you uncomment one, stays on the bridge network and is reached through
its published loopback port.
docker-compose.prod.yml)The stack above is the self-hosted --local mode. The hosted service — the
multi-tenant shape behind aiagentmemory.dev, with dashboard, auth and billing —
is the same image running its default serve command, one persistent volume,
and the full .env.example configuration behind a TLS reverse proxy:
cp .env.example .env.prod # session key, OAuth, billing — all required
docker compose -f docker-compose.prod.yml up -d
The port is published on the host loopback only; put Caddy/nginx/Traefik in front and forward 443 → 127.0.0.1:8080. Billing is single-provider per deployment and inert until configured. With OpenCollective — a donations platform, so the checkout is a hosted contribution page:
BILLING_PROVIDER=opencollective
OPENCOLLECTIVE_CHECKOUT_PRO_MONTHLY=https://opencollective.com/it-uoga/projects/ai-agents-memory/contribute/pro-monthly-104934/checkout
OPENCOLLECTIVE_CHECKOUT_PRO_ANNUAL=https://opencollective.com/it-uoga/projects/ai-agents-memory/contribute/pro-yearly-104935/checkout
OPENCOLLECTIVE_PROJECT_URL=https://opencollective.com/it-uoga/projects/ai-agents-memory
# Activation: OpenCollective does not SIGN its webhooks, so the server asks
# instead of waiting to be told — it polls the authenticated GraphQL API and
# reconciles contributions into plan changes (ADR-042).
OPENCOLLECTIVE_PERSONAL_TOKEN=oc_pat_... # read-only; this is the switch
OPENCOLLECTIVE_COLLECTIVE_SLUG=ai-agents-memory
OPENCOLLECTIVE_RECONCILE_INTERVAL=15m # optional, this is the default
A paid contribution started from the dashboard's Upgrade button is attributed
back to its workspace and activated within one interval. Two cases still need a
human, by design rather than omission: a contribution made outside that button
carries no attribution, and one whose payer cannot be matched is counted and
logged rather than guessed at. Both — and any deployment that leaves
OPENCOLLECTIVE_PERSONAL_TOKEN unset, which turns polling off entirely — are
settled with:
agentsmemory set-plan --slug <workspace> --plan pro_monthly
CI deploy (opt-in). Tagging vX.Y.Z already builds binaries and the GHCR
image; a third workflow (.github/workflows/deploy.yml) additionally rolls
that exact image to a host over SSH. Enable it by setting the repository
variable DEPLOY_ENABLED=true and the secrets DEPLOY_HOST,
DEPLOY_USER, DEPLOY_SSH_KEY (optional DEPLOY_PORT, default 22;
DEPLOY_DIR, default /opt/agentsmemory). The host needs Docker and a
one-time docker-compose.prod.yml + .env.prod — the workflow fetches the
compose file from this repo if missing, and .env.prod (session key, OAuth,
BILLING_PROVIDER + OPENCOLLECTIVE_*) stays operator-managed. The rollout
waits for the tag's image to finish building (it publishes in the parallel
release workflow), then swaps the container, probes /healthz, and rolls back
to the previously-running tag if the new one never answers. Until enabled, the
workflow is skipped, never failed.
PR test images. .github/workflows/pr-image.yml builds same-repository PRs
automatically without moving a release tag or latest. A maintainer can also
run it manually with a PR number and its reviewed 40-character head SHA,
including for a fork PR; the run fails if that PR head moved. Preview versions
live in the separate agentsmemory-pr GHCR package. The run summary reports
both a readable tag of the form
pr-24-sha256-<64-hex-container-digest> and the canonical
ghcr.io/atvirokodosprendimai/agentsmemory-pr@sha256:<digest> reference.
Deploy the canonical digest so the tested bytes cannot move underneath the host:
AGENTSMEMORY_IMAGE='ghcr.io/atvirokodosprendimai/agentsmemory-pr@sha256:<digest>' \
docker compose -f docker-compose.prod.yml up -d
AGENTSMEMORY_IMAGE is a one-command override; omit it to return to the normal
AGENTSMEMORY_IMAGE_TAG/latest path. A daily cleanup deletes GHCR versions
whose complete tag set consists only of PR digest tags and whose creation time
is more than seven days old. Because cleanup runs daily, practical retention is
about seven to eight days. Release-tagged and latest versions do not match the
cleanup filter.
VECTOR_BACKEND (or --vector-backend) picks what answers searches. SQLite is
written either way, so this is never a decision about your data — switching costs
an index rebuild, never a re-embedding:
| Value | What runs | Choose it when |
|---|---|---|
chromem (default with --local) | nothing extra — an in-process index, held in memory, persisted to <db>.chromem/ | self-hosted on one machine |
sqlite (default otherwise) | nothing at all — the source of truth scans its own vectors per query | you want the smallest possible footprint |
qdrant | a separate Qdrant service | the palace outgrows memory, or several machines share one index |
The chromem index is derived and disposable: delete the directory and the server
refills it from SQLite on the next boot, logging rebuilt namespace … from the SQLite source of truth. Switching to Qdrant is the one case that needs a
command — agentsmemory sync — because refilling a remote index at boot would
mean blocking startup on a service that may be down.
Back up SQLite; ignore the index. SQLite is the source of truth and every
index is rebuildable from it — a chromem directory refills itself on the next
boot, and agentsmemory sync --recreate replays every vector into Qdrant without
re-embedding. Losing an index costs a restart or one command, not your memory.
The volume name is <project>_agentsmemory-data, where the project is name: in
docker-compose.yml — so agentsmemory_agentsmemory-data by default. Confirm
with docker volume ls.
Stop and copy — simplest and unconditionally consistent. A single-user server will not miss two seconds:
docker compose stop agentsmemory
docker run --rm -v agentsmemory_agentsmemory-data:/data -v "$PWD:/backup" \
alpine:3.20 tar czf "/backup/agentsmemory-$(date +%F).tar.gz" -C /data .
docker compose start agentsmemory
Hot backup — no downtime, using SQLite's online backup API. The runtime image
is bare Alpine with no sqlite3, so borrow one:
docker run --rm -v agentsmemory_agentsmemory-data:/data -v "$PWD:/backup" alpine:3.20 \
sh -c 'apk add --no-cache sqlite >/dev/null &&
sqlite3 /data/agentsmemory.db ".backup /backup/agentsmemory-$(date +%F).db"'
Do not just cp the database file while the server is running. .backup
(and VACUUM INTO '/backup/out.db', which additionally compacts) coordinate with
writers; a plain copy can catch a write mid-transaction and hand you a file that
only fails later, when you need it.
Verify the backup before you trust it — an unreadable backup discovered at restore time is not a backup:
sqlite3 agentsmemory-2026-08-16.db "PRAGMA integrity_check; SELECT count(*) FROM drawers;"
Restore into a fresh volume:
docker compose down
docker volume rm agentsmemory_agentsmemory-data
docker volume create agentsmemory_agentsmemory-data
docker run --rm -v agentsmemory_agentsmemory-data:/data -v "$PWD:/backup" alpine:3.20 \
sh -c 'tar xzf /backup/agentsmemory-2026-08-16.tar.gz -C /data && chown -R 10001:10001 /data'
docker compose up -d
docker compose exec agentsmemory agentsmemory sync --recreate # only if using Qdrant
⚠ The chown is not optional. The image runs as uid 10001, while the restore
container writes as root — skip it and the server starts, then fails on the first
write to a database it cannot open. Restoring a single .db file instead of the
tarball needs the same treatment.
Connecting the MCP gives your agent 41 tools and no reason to call any of them. Nothing about a tool catalogue tells an agent to recall before it acts or to write down what it learned; without that instruction the memory simply never gets opened. Delegation comes in three layers, and self-hosting only gets you the first one for free:
| Layer | What it does | How you get it |
|---|---|---|
am_skillset | Server-side wakeup playbook — which tool, in what order — returned over MCP itself | Automatic. Seeded on first boot, including --local |
CLAUDE.md / AGENTS.md | The always-on protocol: recall at session start, persist before stopping | aiagentmemory install writes agentsmemory-bootstrap.md and merges an import into your memory file |
/am, /load-skill + Claude's ten hooks | Task-scoped grounding, the end-of-turn checkpoint that stops memory being lost, a UserPromptSubmit recall that asks the palace about each task in the user's own words, and the two that make a Claude SUBAGENT a session: SubagentStart puts the recall instruction next to its task, SubagentStop asks it for what it found. A UserPromptExpansion recall covers the turns the UserPromptSubmit one refuses by design — a slash command's literal text is a command name, so the recall runs against what it expands into. A PostToolUse recorder lets the end-of-turn nudge name the files the session actually changed. A PreToolUse anchor cue surfaces the memories pinned to a file as it is opened — no query, an exact join on the path, and silent when nothing pins it. A PreCompact note-writer records branch, HEAD, uncommitted count and the touched files in the last moment before a compaction, and the SessionStart recall hands it back on the other side. Codex currently installs only the proven Stop hook. | Same installer |
So after docker compose up, run the kit as well — --local wires it to your
own server:
aiagentmemory install --local
That points the MCP at http://localhost:8080/mcp, never asks for a token (and
drops any AGENTSMEMORY_TOKEN it finds in your environment, rather than writing
a credential into a config where it would imply the server checks one), and
installs globally without the interactive global-vs-sandbox prompt — a
self-hoster is setting up their own machine, so that question has an obvious
answer. An explicit --sandbox <name> still wins if you want a local server in
an isolated config, and --mcp-url overrides the endpoint for a server on
another port or host.
The registration it writes carries no Authorization header at all, on all three
agents: Claude stores a bare {"type":"http","url":...}, codex registers the URL
with no bearer-token variable and no token file, and pi's bridge extension gets
AGENTSMEMORY_LOCAL=1 so it treats the missing token as intentional and connects
anyway instead of reporting "memory tools are off".
aiagentmemory kit)The aiagentmemory binary wires Claude Code,
Codex, Cursor or
pi into your workspace: it registers the agentsmemory MCP,
installs the always-on memory protocol, and adds whatever else that agent can
take — slash commands, lifecycle hooks, a subagent definition whose tool
allowlist names the am_* tools. It replaces the old shell installer; everything
ships in one downloadable binary.
aiagentmemory install # claude, the default
aiagentmemory install --agent claude # the same, said out loud
aiagentmemory install --agent codex
aiagentmemory install --agent cursor
aiagentmemory install --agent claude-desktop
aiagentmemory install --agent pi
aiagentmemory install --agent both # claude + codex
aiagentmemory install --agent all # all five
--agent claude-desktop needs the server binary on this machine — it registers
a mcp-stdio bridge, and a Docker-only install produces no host binary. The
install refuses rather than writing a command that is not there:
go build -o ~/.local/bin/aiagentmemory-server ./cmd/server
The agents do not offer the same surfaces, and the kit installs what each one has rather than pretending. Everything below is measured against a real install, not inferred from documentation.
| claude | codex | cursor | claude-desktop | pi | |
|---|---|---|---|---|---|
| config dir | ~/.claude | ~/.codex | ~/.cursor | ~/Library/Application Support/Claude | ~/.pi/agent |
| MCP registration | claude mcp add | codex mcp add | writes mcp.json — Cursor ships no mcp add | writes claude_desktop_config.json, spawning mcp-stdio | bridge extension |
| memory protocol | CLAUDE.md + @import | inlined in AGENTS.md | rules/agentsmemory.mdc, alwaysApply: true | the MCP handshake — it can hold no file | inlined in AGENTS.md |
| slash commands | /am, /load-skill | /prompts:am, … | none — no commands dir | none | /am, … |
| Stop checkpoint | ✅ | ✅ — native TOML in config.toml | ❌ hook shape not established | ❌ | in the extension |
SessionStart / SessionEnd | ✅ — two hooks share SessionStart: one verifies anchored memories, one performs a recall for the branch and injects it so a fresh context does not start blind (ADR-041). ⚠ SessionEnd is not registered on Windows: process creation costs ~1s there, the hook needs ~3.2s, and it loses the teardown race, so every exit reported Hook cancelled (#150). Ask the server for the same numbers instead — curl -fsS "${AGENTSMEMORY_MCP_URL%/mcp}/stats?hours=2" (the hooks read AGENTSMEMORY_MCP_URL; AGENTSMEMORY_URL is the proxy origin and is unset in an ordinary install). An existing registration from an earlier install is retired on upgrade. On macOS and Linux the hook completes well inside teardown and stays registered; turns the end-of-session report off there without touching the other hooks (the 3,210ms → 634ms figures are from the Windows host that reported #150) |
Two things worth reading twice:
mcp.json, so a registered-but-unapproved server looks identical on disk to a
working one. Run cursor-agent mcp enable agentsmemory once. The install prints
this line every time, because a re-install cannot tell whether you have done it.config.toml hook; Cursor and Claude Desktop get none. Those agents
recall memory and are never prompted to write it — see
ADR-017 for why the advisory half
of a loop does not happen on its own.instructions in the MCP
initialize response — recall before acting, check am_status once, inherit a
named default_wing, and use wing: "*" only for a deliberate cross-project
search. That rule is there because a client without it invented an empty
namespace and proposed searching every project on every recall
(ADR-021).Full reference: clients/claude-code/README.md.
curl -fsSL https://raw.githubusercontent.com/atvirokodosprendimai/agentsmemory/main/clients/claude-code/install.sh | bash
The bootstrap script detects your OS/arch, downloads the latest
aiagentmemory-<os>-<arch> into ~/.local/bin, then runs aiagentmemory install. Anything after -- is forwarded to install.
The full procedure — hosted vs self-hosted, global vs sandboxed, what to pass so recall stays scoped to one project, and the per-platform differences — is INSTALL.md. Upgrading afterwards is UPDATE.md, which is a genuinely separate matter: the binary, the protocol in your config dir, and the server all update independently.
For per-project isolation, see Sandboxed installation below.
Every flag install takes. aiagentmemory install --help is the authority — this
table is checked against it.
| Flag | What it does |
|---|---|
--agent <name> | claude (default) · codex · cursor · claude-desktop · pi · both (claude+codex) · all |
--global | install into the agent's global config dir without the mode prompt |
--sandbox <name> | install into an isolated config at ~/.sandboxes/<name> |
--config-dir <dir> | install into an explicit directory instead |
--local | wire up a self-hosted agentsmemory --local server (http://localhost:8080/mcp); no token is prompted for |
--token <key> | bearer token to present — hosted workspace key, or the one --local was started with. $AGENTSMEMORY_TOKEN, $AGENTSMEMORY_LOCAL_TOKEN |
--mcp-url <url> | the MCP endpoint (default the hosted service) |
--socket <path> | register over stdio against a --local server on a Unix socket; requires --local |
--server-bin <path> | server binary the --socket stdio bridge spawns |
--wing <name> | scope this registration to one project on every MCP call. The installer uses each client's supported channel: HTTP header (Claude/Cursor), URL query (Codex), bridge flag (Desktop/socket), or pi environment. wing: "*" remains an explicit per-call cross-project opt-in |
--scope <scope> | Claude MCP/plugin scope: user (default) · local · project |
--recommended | also install codebase-memory MCP and the codex review plugin |
--copy | seed a sandbox from the agent's global config (logins, MCP servers, plugins, settings). Needs --sandbox/--config-dir |
--shared-auth | link the sandbox's credential files to the global config, so one login serves every sandbox |
Other subcommands: verify (check memories still match the code they describe),
mine-claude, update (the binary), update-skill (the protocol and commands),
init / load (record and launch a project's agent), run / wrap (drive an
agent against a sandbox or the global config), mcp (call a read-only memory tool
from the shell).
The palace has three layers, and they do not fill the same way. Nothing breaks if you do not know that, but a seeded palace will answer recall questions well and graph questions with nothing, which reads as the graph being broken when it is simply empty.
| Layer | Filled by | Automatic? |
|---|---|---|
| Drawers + closets (recall) | every write: am_add_drawer, am_diary_write, mine-claude | ✅ |
| Hallways and tunnels (navigation) | derived on write from rooms and entities | ✅ |
| Knowledge graph (subject→predicate→object) | am_kg_add by an agent, or agentsmemory kg-extract | ❌ never automatic |
Mining does not produce facts. mine-claude files transcripts as drawers; it
writes no triples. An agent following the memory protocol calls am_kg_add as it
works, which is the ordinary way the graph fills. To extract facts from a corpus
you already mined, run agentsmemory kg-extract --wing <wing>.
⚠ kg-extract needs a GENERATIVE model, and the compose overlay does not
provision one. docker-compose.ollama.yml pulls the embedder (bge-m3) and
nothing else — an embedder cannot answer /api/generate, so kg-extract fails
every source with model not found until you supply one:
docker compose exec ollama ollama pull qwen2.5-coder:7b # multi-GB, one time
docker compose exec agentsmemory agentsmemory kg-extract --wing wing_acme
Use --gen-model (or EVAL_GEN_MODEL) to name a model you already have. The
same model serves agentsmemory eval and agentsmemory longmemeval.
agentsmemory longmemeval — does our writing advice actually help?Every other eval here scores ranking against questions generated from our own
drawers. longmemeval scores judged answer accuracy over a (write-policy ×
query-policy) grid on LongMemEval-S,
a corpus written by people who have never seen this codebase — which is the
property a self-derived corpus can never have.
# the dataset is third-party data with its own licence; fetch it yourself.
# upstream ships it as `longmemeval_s`, with no .json extension.
agentsmemory longmemeval --data longmemeval_s \
--write verbatim,question-first \
--query verbatim,named-thing \
--n 3 --out smoke.cells.json
That is a smoke test — four cells at --n 3, about 50 minutes. It answers
"is my stack wired up", not "which policy wins". Size a real run from the table
below before you start one, because the honest ones are measured in hours.
It needs the same generative model as kg-extract and eval, for the reader
and the judge alike, and it writes into throwaway scratch scopes — one per
(cell, question), so no question can retrieve another's history.
Measured over the published file, not quoted from the paper:
| Questions | 500, across 6 question types |
| Largest type / smallest | multi-session and temporal-reasoning (133 each) / single-session-preference (30) |
| Sessions in the haystack | 25,112 total — median 50 per question (39–66) |
| Session length | median 10,012 characters, p90 16,792, max 78,117 |
| Sessions holding the answer | median 2 per question (max 6) |
The session length is why --context-runes defaults to 24,000 and why you should
not lower it casually: a budget under the median cannot fit one whole session, so
the verbatim baseline assembles nothing and scores 0 by construction — every
other policy then looks good against a baseline that could not play.
--nOne (cell × question) takes ≈ 4m15s, measured 2026-09-03 against a remote
Ollama (bge-m3 embedding, gpt-oss:20b as reader and judge) with the
cross-encoder on. About 85% of that is ingest — 48 Add calls totalling 212s
for one question's haystack — so the cost tracks the number of cells, and the
model only sets the remaining ~40s.
Total ≈ write policies × query policies × n × 4 minutes:
| grid | --n 3 | --n 12 | --n 50 |
|---|---|---|---|
| 2 × 2 (4 cells) | 50 min | 3.4 h | 14 h |
| 4 × 2 (8 cells) | 1.7 h | 6.8 h | 28 h |
⚠ There is no fast configuration of this, and that is a property of the benchmark. Each question carries its own ~50-session haystack, which must be embedded before anything can be retrieved from it, and isolation is per (cell, question) so the same haystack is re-embedded for every cell. Concurrency buys roughly 2× on a saturated GPU and nothing on a busy one — measured: 16 embed requests took 23.5s at 16 workers against ~50s sequential.
So pick --n from what you are asking, not from your patience. A run meant
to decide something needs the held-out split ADR-047 pre-registers, which means
n in the low hundreds and an overnight job; anything at --n 20 or below is a
smoke test whatever its table says.
⚠ A pilot run decides nothing. At small --n a paired interval spans zero for
almost any real effect, so a neutral result is what the instrument says at that
size whether the writing rules work or not. ADR-047 records this as a property
rather than a caveat: no rule may be promoted or retired from a pilot.
⚠ The shared context budget is counted in RUNES, not tokens (--context-runes),
because this repository has no tokenizer. Each cell records the endpoint's own
reported prompt-token count beside it where the endpoint supplies one, so the
approximation is measured rather than assumed.
Both exist, they behave oppositely, and each is wrong for the other's job:
| Stop hook (automatic) | mine-claude (manual) | |
|---|---|---|
| Runs | at every session end | when you run it |
| Files | what the agent decided was worth keeping | the raw transcript text |
| Feeds the knowledge graph | ✅ the agent calls am_kg_add | ❌ drawers only |
| Cost | negligible | one embed per changed chunk |
The Stop hook is what keeps a palace current. mine-claude is a backfill tool
for history that predates the install — run it once over your transcripts, not on
a schedule. Re-running it is cheap for unchanged text (only changed chunks are
re-embedded) but it still does not write facts.
A sandbox is just a Claude config directory under ~/.sandboxes/<name>.
Running Claude with CLAUDE_CONFIG_DIR pointed at it isolates that project's
slash commands, settings, MCP servers, and agentsmemory token from everything
else — so a client project and an internal project never share memory, tools, or
credentials. Set one up once, with or without the recommended tools:
aiagentmemory install --sandbox acme # core: commands, hook, our MCP
aiagentmemory install --sandbox acme --recommended # + codebase-memory, codex review
The installer writes into ~/.sandboxes/acme/ and runs every claude
registration with CLAUDE_CONFIG_DIR pinned there, so nothing leaks into your
global config. Sandbox names are plain identifiers (letters, digits, dash,
underscore).
Installing is a one-time setup. To launch Claude against an existing sandbox, just name it — no re-install:
aiagentmemory run acme # open Claude in the acme sandbox
aiagentmemory run acme -p "summarise repo" # args after the name pass straight to claude
run <name> sets CLAUDE_CONFIG_DIR=~/.sandboxes/<name>, then exec-replaces the
process with the Claude CLI — inheriting your terminal and its exit code, so it
behaves exactly like running claude, only against that sandbox. It errors with a
hint if the sandbox hasn't been installed yet. The global counterpart is:
aiagentmemory wrap # run Claude against the global ~/.claude
The Claude CLI it drives is resolved from AIAGENTMEMORY_CLAUDE_BIN, then
claude on your PATH.
aiagentmemory mcp calls the memory tools yourself — same endpoint, same token,
same transport your agents use — so you can see exactly what a tool returns
without asking an agent to relay it:
aiagentmemory mcp # the tools you can call
aiagentmemory mcp status # workspace, wings, quota
aiagentmemory mcp search "auth bug" -a limit=3
aiagentmemory mcp search "auth bug" | jq '.hits[].room'
The bare positional fills the tool's first required argument; everything else is
-a key=value. Output is indented JSON on stdout (notes go to stderr, so it
pipes), and the workspace token is read from an install already on this machine —
--sandbox <name> picks one. It is read-only: the write tools exist on the
endpoint but the CLI refuses them, so a mistyped command can never mutate team
memory. Full flag reference in clients/claude-code/README.md.
--agent codex)Codex is configured the same way Claude is, under different names, so the kit is the same content in different places:
aiagentmemory install --agent codex # into ~/.codex
aiagentmemory install --agent both --sandbox acme # one sandbox, both agents
aiagentmemory run --agent codex acme # launch codex with CODEX_HOME pinned
| Claude Code | Codex | |
|---|---|---|
| Config dir | ~/.claude (CLAUDE_CONFIG_DIR) | ~/.codex (CODEX_HOME) |
| Slash commands | commands/*.md → /am | prompts/*.md → /prompts:am |
| Always-on memory | CLAUDE.md + managed @import | AGENTS.md with the protocol inlined — codex has no @import |
| Stop hook | settings.json | native TOML in config.toml; an install retires its old hooks.json entry |
| MCP auth | Authorization: Bearer <token> header | bearer_token_env_var = "AGENTSMEMORY_TOKEN" |
On upgrade, the installer first lands the native TOML hook, then removes only
agentsmemory's entry from its previous hooks.json representation. Codex
supports both files but merges them and warns when one config layer uses both,
so the installer keeps a single representation for its own hook. It deletes the
JSON file when nothing else remains; foreign hooks are preserved with a warning,
so migration never erases configuration it does not own.
One thing codex needs that Claude does not is the token in the environment — it is written to
<CODEX_HOME>/agentsmemory.env (0600) and exported for you by
aiagentmemory run --agent codex …; for plain codex, source it from your shell
rc. A codex sandbox is a whole CODEX_HOME, so it also needs its own login:
CODEX_HOME=~/.sandboxes/acme codex login.
--copy)A new sandbox starts signed out, with no MCP servers, plugins or skills. --copy
seeds it from that agent's global config dir first:
aiagentmemory install --agent pi --sandbox acme --copy
Credentials, settings, .claude.json (Claude's MCP servers), plugins, skills,
extensions and prompts travel; conversation history, logs, *.sqlite* stores and
caches stay behind. Existing files in the target are never overwritten, modes are
preserved (auth.json stays 0600) — and note the consequence: the sandbox can
act as you until you sign it out.
--shared-auth)--copy snapshots credentials; --shared-auth links them, so a login in any
sandbox is a login everywhere:
aiagentmemory install --agent pi --sandbox acme --shared-auth
Claude on macOS already shares its keychain, so the flag is a no-op there; codex
links auth.json, pi links auth.json and models-store.json. If an agent ever
replaces the link with a real file, aiagentmemory run says so at launch and
prints the command that re-shares it.
--agent pi)pi looks like codex — prompts/ for commands, AGENTS.md for memory — except
that it ships no MCP client and no hooks, both by design. So the installer
writes a bridge extension into <config dir>/extensions/agentsmemory.ts: at
startup it handshakes with your workspace MCP, lists the tools, and re-registers
each one as a native pi tool, so am_* calls work unchanged. The same extension
fires the end-of-turn memory checkpoint that the Stop hook fires elsewhere.
aiagentmemory install --agent pi # into ~/.pi/agent
aiagentmemory install --agent all --sandbox acme # one sandbox, all three agents
aiagentmemory run --agent pi acme # launch pi with PI_CODING_AGENT_DIR pinned
| Codex | pi | |
|---|---|---|
| Config dir | ~/.codex (CODEX_HOME) | ~/.pi/agent (PI_CODING_AGENT_DIR) |
| Slash commands | prompts/*.md → /prompts:am | prompts/*.md → /am |
| Stop hook | config.toml | none — the checkpoint ships in the extension |
| MCP | native, --bearer-token-env-var | bridged by the extension |
The token and endpoint are written to <config dir>/agentsmemory.env (0600)
and exported by aiagentmemory run --agent pi …. A pi sandbox is the whole agent
dir including auth.json, so it starts with no provider credentials.
--recommended adds nothing for pi: codebase-memory is a stdio MCP and the
codex review plugin is a Claude marketplace.
All flags have sensible local defaults:
| Flag | Default | Purpose |
|---|---|---|
--addr | :8080 (127.0.0.1:8080 with --local) | HTTP / MCP listen address |
--local | false | Self-hosted single-workspace mode: one local workspace, unauthenticated /mcp, no dashboard |
--token | (empty) | --local only (AGENTSMEMORY_LOCAL_TOKEN): require this bearer on /mcp and /import, so the server can safely bind a LAN address |
--db | agentsmemory.db | SQLite database path |
--db-reader-pool | 0 | DB_READER_POOL — connections the read-only handle may open at once; 0 derives max(4, NumCPU). The writer is one connection by decision (ADR-052) and has no such knob |
--vector-backend | sqlite (chromem with --local) | Search index: sqlite | chromem | qdrant — SQLite is always the source of truth |
--qdrant-url | http://localhost:6333 | Qdrant base URL |
--qdrant-api-key | (empty) | Qdrant API key (optional) |
--ollama-url | http://localhost:11434 | Ollama base URL |
--ollama-model | bge-m3 | Embedding model (1024-dim) |
--rerank-url | (empty) | RERANK_URL — TEI base URL for cross-encoder re-ranking. Empty disables it |
--rerank-pool | 50 | RERANK_POOL — candidates cross-encoded per search (ignored without --rerank-url) |
--retrieve-k | 0 |
Runtime execution is OpenTelemetry (ADR-025): one parent span per am_search (am.search) with child stages embed, retrieve (hydrate nested), collapse, closet, fusion, recency, rerank (evidence nested when a reranker is configured), record. Each stage reports ran / bypassed / failed_open / failed_closed. MCP tools emit am.tool. Outbound HTTP (embed, rerank, Qdrant) and inbound /mcp are wrapped. Eval wraps each question in am.eval.case and each ranking arm in am.eval.arm so an ablation is a tree, not a forest.
The dump is a debug tool, not a JSON firehose. Every span carries am.code.file / am.code.line / am.code.func (the Start call site). A bypassed or failed stage carries a closed am.reason (scale_zero, weight_zero, band_zero, no_reranker, skip_sqlite, empty, lexical, error, …). Retrieve emits a widen event per doubling round (k, hits, distinct, stop). The am.search parent repeats the resolved knobs (am.fusion, am.closet_scale, am.rerank_configured, …) beside am.profile_id, so you can read a tree against RankingProfile() / am_status and see whether a stage was eligible, whether it ran, and which line of code started it.
--otel-endpoint stdout prints that tree on stderr:
am.search 5040ms ran profile_id=fusion=rrf… closet_scale=0 rerank_configured=true internal/palace/service.go:961
am.search.embed 153ms ran dim=1024 internal/palace/service.go:964
am.search.retrieve 42ms ran reason=exhausted k=50 rounds=1 internal/palace/memory_search.go:69
· widen k=50 hits=3 distinct=1 stop=exhausted
am.search.hydrate 12ms ran count=3 internal/palace/memory_search.go:70
am.search.closet 0ms bypassed reason=scale_zero scale=0 internal/palace/mine.go:320
am.search.record 0ms bypassed reason=skip_sqlite
A closet span that says ran while closet_scale=0 (or bypassed with no am.reason) means the stage is not wired. OTLP to a collector keeps the same attributes for Jaeger; stdout is the local join to source.
search_id is the SQLite search_events.id so a sampled trace can join a durable relevance row. Wing and room appear as booleans (am.has_wing, am.has_room), never names. Raw queries, memory content and tenant ids are not metric labels.
A collector that is down drops observability, not search. SkipTelemetry still skips the SQLite log (eval); OTEL spans are always created and hit the noop provider when --otel-endpoint is empty. Each stage also increments unsampled eligible / selected / effect / fallback counters.
Vector candidate capacity, BM25 and the optional cross-encoder operate once per logical memory. Chunk storage and chunk embeddings remain unchanged: the best passage nominates a memory, the vector prefix widens until it holds the target number of distinct memories, and ranking uses reassembled memory evidence.
MEMORY_EVIDENCE_SELECTOR=lexical is the default control. semantic reuses the
raw query embedding, embeds overlapping windows from the whole reassembled long
memory, and selects up to four distant passages within the same 1600-rune
cross-encoder budget. Short memories pass through unchanged. Any
passage-embedding failure falls back to lexical evidence for the whole
shortlist. Passage requests stay bounded at 128 inputs. With the TEI embedding
backend, the client reads /info.max_client_batch_size (caching only a
successful probe, and retrying after a backoff otherwise) and uses the server's
real limit up to that bound; if /info is unavailable it retains TEI's safe
32-input default. The semantic arm adds embedding latency but no migration;
roll it back by setting the selector to lexical. It is inert unless
RERANK_URL is set. The resolved profile reports unit=memory and
evidence=lexical|semantic. The process environment can override an .env
file, so that profile—not the file—is the authority.
am_search fuses vector similarity, BM25 and the closet boost. All three are
proxies — they score the query and the drawer separately and combine the
numbers. A cross-encoder reads both together, so it judges relevance far better;
it is also far slower, which is why it only ever sees a shortlist.
There is deliberately no RERANK_MODEL service setting. TEI fixes the model
when its own container starts, and its /rerank request carries no model field;
set the model on that container rather than advertising an inert app variable.
Set RERANK_URL and search gains a fourth stage: the top RERANK_POOL fused
candidates are cross-encoded and reordered, and the cross-encoder's score — not
the fused score — decides the page. Both are reported (rerank_score beside
score, bm25_score and closet_boost), so you can see when they disagree.
Widening the pool is most of the win: without it the ranker only ever sees
limit × 3 candidates, so a drawer buried at rank 40 can never surface.
It fails open. A reranker that is down, slow or returning nonsense costs
ordering quality, never recall — search falls back to the hybrid order and logs a
warning. Nothing else changes, so leaving RERANK_URL unset keeps the exact
behaviour every deployment had before.
RERANK_POOL is independent of TEI's own --max-client-batch-size (32 by
default): the pool is split into batches automatically, so you can set it to
whatever search quality needs without reconfiguring the server. Scoring each
pair is independent, so batching cannot change a score.
Run one with TEI:
docker run -d --name reranker -p 12434:80 -v $PWD/tei-data:/data --pull always \
ghcr.io/huggingface/text-embeddings-inference:cpu-1.9 \
--model-id BAAI/bge-reranker-v2-m3
# CUDA host: swap cpu-1.9 -> cuda-1.9 and add --gpus all
# TEI listens on port 80 INSIDE the container — map to 80, not 8080.
export RERANK_URL=http://localhost:12434
Ollama cannot do this job. It exposes only a model's embedding layer, never the cross-encoder classification head, so it has no rerank endpoint (ollama/ollama#10467) — pulling a
bge-rerankertag into Ollama gets you embeddings, not relevance scores. Keep Ollama forbge-m3embeddings and run TEI alongside it.
am_search's optional context argument feeds this stage: it sharpens the
re-ranking without changing which drawers are retrieved.
Bring an existing local Python mempalace into a workspace — every drawer, diary
entry, closet, knowledge-graph fact and explicit tunnel. The vehicle is a small
read-only CLI that reads your palace and streams it to the server's
/import endpoint with your project's API key; the server re-embeds each
memory with its own model (the bundle carries text, not vectors) and rebuilds the
derived graph (hallways/entity-tunnels) afterwards.
# Run where the mempalace package is installed. Inspect first:
python clients/migrate/mempalace_export.py --out palace.ndjson
# Then stream it into your workspace (token = the project's API key):
python clients/migrate/mempalace_export.py --push \
--server https://your-host --token sk_live_xxx
# Or push a bundle exported earlier on another machine:
python clients/migrate/mempalace_export.py --file palace.ndjson --push \
--server https://your-host --token sk_live_xxx
POST /import sits behind the same Bearer gate as /mcp, takes streaming NDJSON
(one record per line, kind-discriminated), and streams progress back. The
import is idempotent — drawer ids are recomputed under the target tenant, so
re-running a partial migration finishes it rather than duplicating. The project
page surfaces the exact command (with your host filled in) under Bring your
mempalace.
Full step-by-step guide, flag reference and troubleshooting:
clients/migrate/README.md.
A workspace member can download everything the workspace holds as a single,
self-contained SQLite file — the right of access and data portability
under BDAR (the Lithuanian implementation of the EU GDPR). The project page
surfaces it under Download your data; it maps to a membership-gated
GET /projects/{teamID}/export. It is the outbound counterpart to /import:
import brings a palace in, export takes your workspace out.
The archive is a standalone, valid SQLite database — open it with any SQLite browser — built from the live source of truth:
sqlite_master, so the
export is byte-faithful to the running schema (no goose re-run, no drift).team_id / namespace, plus the requester's own identity rows (account,
membership, API-key metadata). No other tenant's data can enter the archive.token_hash is replaced and token_enc blanked — the export carries your
data, never usable secrets.# From the browser: project page → "Download your data".
# Or with an authenticated session cookie:
curl -b session.jar https://your-host/projects/<teamID>/export \
-o agentsmemory-<workspace>-<date>.sqlite
Implementation: internal/dataexport
(scoping manifest + redaction) and internal/web/export.go (the download route).
wing export / wing import)The whole-workspace export above takes everything. To move one wing — restore a backup, seed a new workspace, fork a wing under a second name, or lift a project's memory out of a self-hosted install and into the hosted service — use a wing bundle.
The defining property is that a bundle carries no wing name. Not on a record, not in a header, nowhere. Exporting is "take this wing's contents", and the destination is named on the way in:
# Self-hosted: straight against the database, no server and no token needed.
agentsmemory wing export --db ~/.agentsmemory/db.sqlite --wing wing_acme --out acme.ndjson
agentsmemory wing import --db ~/.agentsmemory/db.sqlite --file acme.ndjson --as wing_abc
--as is required. A bundle names no wing, so importing without a
destination would file every memory into an unnamed wing — an import that looks
like it worked and leaves the memories where nobody looks.
On a multi-workspace database add --project <slug> (it defaults to the single
local workspace). The same bundle works over HTTP:
# Agents / scripts: the same endpoint the mempalace migration uses, plus ?as=
curl -X POST -H "Authorization: Bearer $KEY" --data-binary @acme.ndjson \
"https://your-host/import?as=wing_abc&recompute=1"
# Browser: project page → "Move a wing" (download a wing, upload a bundle).
What travels, and what deliberately does not:
| Carried | Left behind |
|---|---|
| Drawers, including diary (agent + topic preserved) | Vectors — the destination re-embeds |
| Closets (the mined pointer index) | Knowledge-graph facts — team-global, not wing-scoped |
| Explicit tunnels with both ends inside the wing | Hallways and derived tunnels — recomputed |
Both omissions are deliberate. Vectors would multiply the file size and silently corrupt search if the destination runs a different embedding model or dimension, so a bundle stays text and the background worker indexes it on arrival. KG facts belong to the whole team rather than to any wing, so shipping them with "one wing" would sweep every other wing's facts along with it.
A tunnel with one endpoint outside the wing is dropped because the importer requires each endpoint room to already hold a drawer — the far end simply isn't in the bundle. Since an explicit tunnel exists to link two different wings, a single-wing bundle usually carries none; the CLI says so rather than leaving you to wonder.
Every record id is deterministic, so importing the same bundle twice upserts rather than duplicates, and importing into an existing wing merges into it. Exporting a wing that does not exist fails and lists the wings that do — an export must never produce a valid, empty file.
Implementation: internal/wingbundle (the
format + exporter), internal/importer (?as=), cmd/server/wing.go (the CLI)
and internal/web/wing.go (the dashboard routes).
import)A project's reference and seed data usually ship as JSONL beside the code that loads them. The rows end up in a database, which is the right home for rows — it answers "which invoices are overdue" better than any vector search will.
What no store answers is what the data means: why every seeded date falls in
one quarter, which of twelve status values the data actually exercises, that
amount is minor units. An agent opening the repository can read the rows and
still not know any of it.
agentsmemory import files one memory per dataset, and the memory is two halves:
agentsmemory import --config agentsmemory-import.toml --out project.ndjson
agentsmemory wing import --db ~/.agentsmemory/db.sqlite --file project.ndjson --as wing_acme
# Or straight into a running server, self-hosted or hosted:
agentsmemory import --config agentsmemory-import.toml \
--push https://your-host/import --as wing_acme --token "$AGENTSMEMORY_TOKEN"
The mapping file is committed in the project's own repository, so a change to a dataset and the description of that dataset are reviewed in the same pull request:
wing = "wing_acme" # a default; --as still names it on the way in
[[dataset]]
file = "data/invoices.jsonl" # relative to THIS file, not to the shell
room = "schema"
title = "Invoice seed data"
why = """
Seeded from one anonymised quarter, which is why every due date lands in Q1.
`amount` is MINOR UNITS — 1200 is twelve euros, not twelve hundred."""
show_values = ["status", "currency"] # the ONLY fields whose values are quoted
why is required. A dataset drawer carrying only a profile records what a reader
could have derived from the file, and filing it would spend recall on nothing.
show_values — the only values that leave the fileEvery field is measured the same way whether or not you name it: type, how many
rows carry it, how many distinct values it took, and its date range. But the
values themselves are quoted only for the fields listed in show_values.
A drawer is recalled by every agent in the wing, and the server embeds and
indexes it on arrival. So status and country and manager_email all look
alike from inside a profiler — twelve distinct strings — and only the person who
wrote the dataset knows which of them may be published. Naming them is that
person saying so.
It is an allowlist, not a list of exclusions, because the two fail in opposite directions and only one failure is recoverable. A column added to the dataset next month is merely absent from the next memory here; an exclusion list written before that column existed publishes it. A re-import replaces the drawer, but nothing un-embeds what the first one already filed.
The cost is that the omission could be silent, so it isn't: an unnamed field still reports its distinct count, and the memory says in as many words that values were withheld.
· status (string), takes 2 value(s) here: open, paid
· email (string), 148 distinct value(s), not listed
⚠2 field(s) above were COUNTED AND NOT QUOTED. …
What lands, and what deliberately does not:
| Carried | Left behind |
|---|---|
The why you wrote, verbatim and first | The rows. They are already in the database this same JSONL builds |
| A profile measured on every run: fields, types, row count, distinct counts, date ranges | The values of any field show_values does not name — counted, never quoted |
The value sets of the fields you named in show_values | Vectors — the bundle is text, the server embeds |
| A count of the fields whose values were withheld, so the gap is visible | Anything below the first level of a nested object |
The measured half cannot drift from the data, because it is re-derived rather than remembered. The written half is the part no tool can infer, which is why it goes first: recall returns a window around a match, and the half that cannot be re-derived has to be the half a snippet finds.
Rows are refused on evidence rather than taste: a larger, more heterogeneous corpus retrieves measurably worse, because unrelated entries do not remove the answer — they add competitors ahead of it.
A drawer's id is a hash of where it goes and what it says, and the memory's text
is a pure function of the dataset — the measurement date rides along as
content_date rather than inside the text. So re-importing an unchanged file
is a no-op, however often it runs: same bytes, same id, one row. That is what
makes a committed mapping file worth committing and a scheduled re-import safe.
A changed dataset is a different matter. The import path absorbs and never
purges by source — it has to, because a batched migration would otherwise delete
the earlier batches of the source it is still uploading — so a new profile is
filed beside the old one and yesterday's numbers stay recallable. Delete the
superseded drawer with am_invalidate_drawer after a real change — its text stays
readable with your reason attached — until the
backlog item that closes this lands.
Pushing straight to a server takes --as, and the CLI refuses the push without
it: the bundle carries no wing, and /import skips a record it cannot
address while still answering 200. The push then reads the endpoint's summary
rather than its status code, for the same reason — a storage failure is reported
inside a 200 — and asks it to rebuild the derived graph on the way out.
Implementation: internal/datasetdoc (the profiler,
the mapping file and the bundle) and cmd/server/importdata.go (the CLI).
cmd/server/ entrypoint: cli flags → migrate → seed → serve
db/ embedded goose migrations (.sql)
internal/
config/ runtime configuration
tenant/ teams (workspaces) · users · memberships · api_keys · plans
skill/ centralised skill registry (load_skill)
store/qdrant/ Qdrant REST client, collection-per-tenant naming
store/chromemvec/ embedded chromem-go index (the --local default)
store/sqlitevec/ SQLite vector source of truth
embed/ollama/ Ollama bge-m3 embedder
auth/ bearer token → tenant context injection
palace/ core memory domain types (wing/room/drawer/hallway/tunnel)
mcpserver/ MCP tool wiring (status, load_skill, …)
dataexport/ per-workspace SQLite data export (BDAR right of access)
wingbundle/ portable single-wing bundle format (carries no wing name)
importer/ POST /import — bundle ingest, ?as= names the target wing
web/ dashboard (templ + datastar): projects, keys, export
Bounded contexts are kept apart (DDD): tenant and skill share only tenancy
and auth, never storage internals; interfaces are declared at the consumer.
go build ./... # compile everything
go vet ./... # static checks
go test ./... # unit tests (skill scoping + role gate, qdrant naming)
goose owns the schema; gorm is the query layer only (AutoMigrate is never
called). Schema changes are additive migrations under db/migrations/.
Migration numbers are allocated at merge, never at authoring, because a
per-branch uniqueness check cannot see another branch. Plain goose.Up refuses a
pending migration sitting below the database's maximum applied version, so when
two branches both add one, whichever merges SECOND renumbers.
That renumber has a cost, and it lands on you rather than on production: if you
ran the branch locally, your database already applied that file under its OLD
number, so goose sees the new number as unapplied and re-runs the same SQL. For
an ADD COLUMN that is duplicate column name, and the server exits on a
migrate error — a crash loop on the next start. A fresh or production database
never sees it, and no test can, because tests migrate from empty.
The repair is to record the new version as applied, after checking your schema already holds what it would create:
sqlite3 agentsmemory.db \
"INSERT INTO goose_db_version (version_id, is_applied, tstamp) VALUES (<new>, 1, datetime('now'));"
Numbering therefore has deliberate gaps — 00027 is one, freed when ADR-034's
migration became 00029 at merge. A gap is a record that a renumber happened,
not an error.
AI Agent Memory is open source and free to self-host. If the hosted service helps you, you can support development on Open Collective — contributions fund the always-on GPU the hosted recall runs on. The Pro plan's checkout is the project's €50/month contribution tier, so paying subscribers and donors land on the same page.
am_load_skill centralised skill registrybge-m3) clientsam_status, am_load_skill)am_diary_write / am_diary_read (timestamped, append-only journal) (16 of 37)am_mine text → chunked drawers (entities + content date) + closet index, idempotent by source (17 of 37)am_list_skills + am_update_skill (role-gated)am_merge_wing + am_memories_filed_away (sync/hook_settings stay single-user-local, not ported)goth) login, project create + one-time API key, monthly usage metering — templ + datastarA faithful Go SaaS rewrite of the original single-user Python
mempalace (frozen) — that repository
is the upstream source this project is derived from. The domain model
(wings/rooms/drawers/closets/hallways/tunnels/KG/AAAK dialect), the 37-tool MCP
contract, the hybrid ranking, and idempotent mining are ported; Chroma, local
ONNX embeddings, and the HNSW repair tooling are dropped in favour of Qdrant +
Ollama from the start. Reference Go stack patterns follow a sibling
project on the same stack (chi · templ · datastar · Ollama · Qdrant · MCP · RRF).
am_list_hallways| ✅ |
Within-wing entity co-occurrence links (derived from mined entities). Rebuilt by am_recompute_graph, which is also how one is removed |
am_create_tunnel / am_list_tunnels / am_find_tunnels / am_follow_tunnels | ✅ | Cross-wing links — explicit (authored, symmetric) + derived (entity) |
am_traverse / am_graph_stats / am_recompute_graph | ✅ | Walk the room↔wing graph, summarise it, rebuild hallways + entity tunnels |
am_kg_add / am_kg_invalidate / am_kg_supersede / am_kg_query / am_kg_stats / am_kg_timeline | ✅ | Temporal knowledge graph — subject→predicate→object facts with validity windows, queryable as-of a point in time. am_kg_invalidate retracts and requires a reason; am_kg_supersede REPLACES a value in one transaction, ending the old window and starting the new one at the same instant — hand-rolling invalidate-then-add leaves both values in effect until the end of the day |
am_list_skills / am_update_skill | ✅ | List the team's centralised skills; create/version-bump a skill body (writer/admin) |
am_bootstrap | ✅ | Start a session in one call: a wing's entry node, its first records inlined, pointers to the rest, and the corrections attached to any of them. Replaces a hand-executed multi-call traversal. Returns resolution: "unknown_term" on a wing whose llm_init drawers were filed before the derived room edges shipped — those edges are written when a drawer is written, and existing corpora are not backfilled |
am_entry_point | ✅ | Where to START in a wing: the entry node and what it points at. Edges naming a record in another wing are dropped and counted in refused, never listed. Same unknown_term condition as am_bootstrap |
am_list_anchors / am_mark_anchors | ✅ | Code anchors pinned to a memory — list them, or re-check them against the tree and mark the drawers whose code has since changed |
am_recall_stats | ✅ | What recall actually did: counts and score distributions over recorded searches, including why a cross-encoder did not order a page |
am_merge_wing / am_memories_filed_away | ✅ | Fold wings together; summarise what the team has filed |
sync, hook_settings | ⛔ | Not ported — single-user-local (on-disk source pruning / local hook config) with no multi-tenant meaning |
AGENTSMEMORY_STATS=off| ❌ not registered; not part of the Codex subagent audit |
| ❌ |
| ❌ |
| ❌ |
PreToolUse / PostToolUse / PreCompact / UserPromptExpansion | ✅ | ❌ | ❌ | ❌ | ❌ |
SubagentStart / SubagentStop | ✅ | ❌ events exist; payload, feedback, and retry contracts remain to measure | ❌ | ❌ | ❌ |
| subagent definition | agents/*.md | agents/*.toml | agents/*.md | ❌ | ❌ no subagent system |
--wing registration scope | ✅ header | ✅ URL query | ✅ header | ✅ mcp-stdio --wing | ✅ bridge env |
--sandbox isolation | ✅ | ✅ | refused — no config-dir variable | refused — same reason | ✅ |
| needs a host server binary | ❌ | ❌ | ❌ | ✅ — the stdio bridge | ❌ |
--claude-bin / --codex-bin / --pi-bin | override the agent CLI to drive |
--dry-run | print every file write and command without touching anything |
--yes, -y | never prompt |
RETRIEVE_K — floor on how many distinct memories Search retrieves before ranking, independent of the page. 0 (default) uses the formula: limit×3, raised to --rerank-pool when a cross-encoder will run. Does not change the page size |
--memory-evidence-selector | lexical | MEMORY_EVIDENCE_SELECTOR — bounded reranker evidence: literal query coverage or query-time semantic passage selection |
--otel-endpoint | (empty) | AGENTSMEMORY_OTEL_ENDPOINT — OpenTelemetry export: empty=off, stdout prints a compact stage tree to stderr, otherwise an OTLP HTTP collector URL (http://localhost:4318). Does not change search results |
mempalacePOST /importGET /projects/{teamID}/export, membership-gated, tenant-scoped, secrets redacted)(team, user), secret shown once, destructive-confirm flow)-1 cap) plan + superadmin set-plan CLI/load-skill Claude command — client-side wrapper over am_load_skill: fetch a team-shared skill by name and install it as a local .claude/skills/<name>/SKILL.md (shipped in the aiagentmemory installer)