agent-memory
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@agent-memoryremember that we chose HelixDB for the memory backend"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
agent-memory
Persistent memory for AI coding agents — a v1 replica of rohitg00/agentmemory rebuilt on HelixDB instead of iii-engine + SQLite. One engine provides graph + vector + full-text (BM25) + temporal storage with traversal-scoped prefiltering, so hybrid retrieval (vector, keyword, concept graph) lives in a single query layer — no external search service, no API keys, no model downloads. It ships a dependency-light REST server, a stdio MCP server with 11 tools, and a zero-dependency capture hook, all backed by the same store.
The frozen spec this repo implements is docs/CONTRACT.md.
How it works
Data model
Three node labels, two edge types (CONTRACT §1):
Session sessionId (unique), project, startedAt, updatedAt
Memory memoryId (unique), content, project, sessionId, origin,
importance (0..1), createdAt, embedding (f32[384]),
dedupKey (unique, sha256 of project + normalized content)
Concept name (unique), project
BELONGS_TO Memory ──▶ Session
HAS_CONCEPT Memory ──▶ ConceptMemory.embedding— 384-dim vector, cosine distance, indexed withprojectas the tenant key.Memory.content— BM25 full-text index, also scoped byproject.Memory.dedupKey— write-time dedup key (v1.1): saving the same fact twice returns the existing id (deduped: true) instead of a second row; the hash never leaves the store as raw content. A dedup hit creates noSessionnode — sessions materialize only on novel writes (contract §3).projectis the tenant/scope for every vector and text index; search routes always pass it.
bootstrapIndexes() ensures all 8 indexes: 4 unique (Memory.memoryId,
Session.sessionId, Concept.name, Memory.dedupKey), 2 equality
(Memory.sessionId, Memory.project), 1 vector (Memory.embedding, 384-dim
cosine, tenant project), 1 text (Memory.content, tenant project).
Writes anchor narrow: saveMemory() is a single writeBatch that upserts the
Session (create or bump updatedAt), creates the Memory, links BELONGS_TO,
then upserts each Concept and links HAS_CONCEPT — an empty concepts array is
safe.
Retrieval
POST /memory/smart-search runs up to three independent sources:
vector — query embedded to 384 dims,
vectorSearchWithprefiltered byproject,text — BM25
textSearchWithoncontent, scoped byproject,graph — only when
conceptsare provided:Conceptwherenameinconcepts→.in("HAS_CONCEPT")→Memory, scoped byproject.
They are fused in the app layer (src/search.ts) with Reciprocal Rank
Fusion:
score(doc) = Σ 1 / (60 + rank_i) over every source that returned itRanks are 1-based. Ties break by decayed-then-recall-boosted importance
(desc): first importance · e^(−λ·ageDays) when AGENT_MEMORY_DECAY_LAMBDA
is set (older, never-recalled rows sink; λ unset/invalid/≤0 → plain
importance), then a recall boost + 0.2·n/(n+1) where n is how many times
this process returned the row (in-process recall ledger, cap 10k, resets on
restart) — then createdAt (desc, newest first), then memoryId (asc) so
output is fully deterministic. The returned importance field is always the
stored value; decay and the recall boost are ranking-only.
Both search paths also honor AGENT_MEMORY_TTL_DAYS (v1.1): rows older than the
TTL are hidden and reported as signals: ["ttl: hidden N expired rows"] — explicit
degradation, never silent thinning. Both knobs are off by default.
Failures degrade instead of exploding: each source runs independently, a source
error is caught and recorded in a signals list, and the remaining sources
still contribute rows. Even an all-sources-down search returns 200 with
empty results + signals — never a 500. POST /memory/search is the
BM25-only path with the same degradation rule.
Related MCP server: mcp-memory
Quick start
Requires Node 20+ and a running Helix dev instance (Docker/Podman):
helix start dev --disk --persist # durable default: persists storage mode into helix.toml
npm install
npm run bootstrap # create the 8 indexes, poll until ready
npm run demo # seed 3 sessions, run keyword/semantic/hybrid searches
npm run dev # REST server on http://127.0.0.1:3111
npm run verify # end-to-end verification against the running server
npm run verify-lifecycle # pure dedupKey/decay/TTL/concepts/confidence/merge checks (no server)
npm run verify-capture # 7-event hook E2E vs a local counting server (no Helix)
npm run verify-skills # structural + live round-trip of the 8 skills (running server)
npm run eval # retrieval scorecard -> docs/benchmarks/SCORECARD.md (running server)Run bootstrap before the first write: the dedup lookup, the
consolidation probe, and both searches all depend on the 8 indexes — writes
fail closed (500) while an index is missing.
--disk --persist writes storage = "disk" into helix.toml, and that key —
not the flag — is what decides persistence. This repo's helix.toml already
sets it, so a plain helix start dev keeps data across restarts. A project
whose helix.toml has no storage = "disk" key runs memory storage, and
every restart wipes it.
Scripts (from package.json): bootstrap, dev, demo, verify,
verify-env, verify-lifecycle, verify-capture, verify-skills, eval,
purge, typecheck.
REST API
All routes live under /memory, JSON in/out. If AGENT_MEMORY_SECRET is
set, add -H "Authorization: Bearer $AGENT_MEMORY_SECRET" to every call except
livez (see Authentication).
Method | Route | Body / query | Success |
GET |
| — | 200 |
GET |
|
| 200 |
POST |
|
| 201 |
POST |
|
| 200 |
POST |
|
| 200 |
GET |
|
| 200 |
GET |
|
| 200 |
POST |
|
| 200 |
POST |
|
| 200 |
POST |
|
| 200 |
POST |
|
| 201 |
POST |
|
| 200 |
Defaults: project="default", limit=10, origin="rest", sessionId
auto-generated (crypto.randomUUID()) when absent. importance (v1.2): an
explicit 0..1 value is stored as-is; when omitted the store derives it
from provenance + structure — base lesson 0.75 / hook:* 0.55 / else 0.5,
0.025·min(concepts,8), clamp01 (the old
0.5default is retired; seedocs/CONTRACT.md§3). Every REST body is a strict zod object: unknown keys are rejected with 400 — solessonnever acceptsorigin(sending it → 400; the row is always stored withorigin="lesson") anddeleterequiresreason. MCP input schemas are SDK-mediated instead: unknown keys are stripped, not rejected, andoriginis server-forced either way (memory_lessonstoresorigin="lesson"no matter what the caller sends).
Examples
# liveness
curl -s http://127.0.0.1:3111/memory/livez
# {"status":"ok"}
# health with counts
curl -s 'http://127.0.0.1:3111/memory/health?project=readme'
# {"status":"ok","counts":{"memories":2,"sessions":1}}remember — real captured request/response:
curl -s -X POST http://127.0.0.1:3111/memory/remember \
-H 'content-type: application/json' \
-d '{
"content": "Implemented JWT auth in src/middleware/auth.ts: HS256 signing, 15-minute expiry, httpOnly cookie on login.",
"concepts": ["auth", "jwt"],
"project": "readme",
"sessionId": "readme-example"
}'
# 201
# {"id":"0d850e3b-6ec5-49bb-bd94-42a1973912fa","sessionId":"readme-example","project":"readme","concepts":["auth","jwt"],"deduped":false}Send the same content again (same project, any casing/whitespace) and the
server returns the SAME id with "deduped":true — no second row. Omit
concepts entirely and they're derived for you (top-8 terms of the content),
so plain saves still feed the concept-graph branch of hybrid search.
search (BM25 only):
curl -s -X POST http://127.0.0.1:3111/memory/search \
-H 'content-type: application/json' \
-d '{"query": "jwt token expiry", "project": "readme", "limit": 5}'
# {"mode":"bm25","results":[{"id":"…","memoryId":"…","content":"…","score":2.54,…,"source":"text","signals":[]}],"signals":[]}smart-search (hybrid RRF) — real captured request/response:
curl -s -X POST http://127.0.0.1:3111/memory/smart-search \
-H 'content-type: application/json' \
-d '{"query":"dashboard query latency","concepts":["performance"],"project":"readme","limit":5}'
# 200
# {"mode":"hybrid","results":[
# {"id":"109","memoryId":"0a6b1c4f-bf37-4586-9477-cacb3e7b3ad5",
# "content":"Fixed the dashboard N+1 query by batching user lookups into one IN query; p95 latency dropped from 820ms to 45ms.",
# "sessionId":"readme-example","origin":"rest","importance":0.5,
# "createdAt":"2026-09-22T13:30:39.062Z","score":0.04918032786885246,
# "source":"vector","signals":[]},
# {"id":"108","memoryId":"0d850e3b-6ec5-49bb-bd94-42a1973912fa",
# "content":"Implemented JWT auth in src/middleware/auth.ts: HS256 signing, 15-minute expiry, httpOnly cookie on login.",
# "sessionId":"readme-example","origin":"rest","importance":0.5,
# "createdAt":"2026-09-22T13:30:38.921Z","score":0.016129032258064516,
# "source":"vector","signals":[]}],
# "signals":[]}Each result row carries source ("vector" | "text" | "graph"); fused rows
add a per-row signals array, and the envelope carries top-level signals
(empty when every attempted source succeeded).
sessions / session memories / forget:
# list sessions
curl -s 'http://127.0.0.1:3111/memory/sessions?project=readme&limit=5'
# {"sessions":[{"sessionId":"readme-example","project":"readme","startedAt":"2026-09-22T13:30:38.921Z","updatedAt":"2026-09-22T13:30:39.066Z"}]}
# one session's memories (ordered by node insertion, newest first — see Known limitations)
curl -s 'http://127.0.0.1:3111/memory/sessions/readme-example/memories?project=readme&limit=5'
# {"memories":[{"id":"109","memoryId":"0a6b1c4f-…","content":"Fixed the dashboard N+1 query…","sessionId":"readme-example","origin":"rest","importance":0.5,"createdAt":"2026-09-22T13:30:39.062Z"}, …]}
# hard-delete one memory (incident edges go with it)
curl -s -X POST http://127.0.0.1:3111/memory/forget \
-H 'content-type: application/json' \
-d '{"memoryId":"0d850e3b-6ec5-49bb-bd94-42a1973912fa"}'
# {"forgotten":true} (404 {"error":"not_found"} when the id does not exist)lesson → governed delete (P3.1 — reason is required, receipt is auditable):
# store a lesson (strict body: origin is rejected; row gets origin="lesson")
curl -s -X POST http://127.0.0.1:3111/memory/lesson \
-H 'content-type: application/json' \
-d '{"content":"Always pass an explicit reason on deletes: audit trails depend on it.","concepts":["governance"],"project":"readme","sessionId":"readme-example"}'
# 201
# {"id":"…","sessionId":"readme-example","project":"readme","concepts":["governance"]}
# governed delete with the required reason
curl -s -X POST http://127.0.0.1:3111/memory/delete \
-H 'content-type: application/json' \
-d '{"memoryId":"0d850e3b-6ec5-49bb-bd94-42a1973912fa","reason":"superseded by docs/CONTRACT.md"}'
# {"deleted":true,"receipt":{"memoryId":"0d850e3b-6ec5-49bb-bd94-42a1973912fa","deletedAt":"2026-09-22T13:31:02.114Z"}}
# 400 when reason is missing; 404 {"error":"not_found"} for an unknown idMCP server
src/mcp.ts runs over stdio with the official @modelcontextprotocol/sdk,
backed by the same MemoryStore as the REST server. Handshake exposes exactly
11 tools:
Tool | Purpose |
| Persist one memory (content + optional concepts — derived when omitted; duplicate content returns the existing id with |
| Keyword (BM25) search within a project |
| Hybrid search: vector + BM25 + optional concept graph, RRF-fused |
| List sessions of a project |
| List memories of one |
| Hard-delete one memory by id |
| Liveness + memory/session counts |
| Text recap of one session's (or the project's) recent memories |
| Project handoff digest for the next agent session |
| Persist a lesson (stored with |
| Governed delete: |
OpenCode
Top-level mcp key, command as an array (run from the repo root, or use the
absolute path to src/mcp.ts):
{
"mcp": {
"agent-memory": {
"type": "local",
"command": ["npx", "tsx", "src/mcp.ts"],
"enabled": true
}
}
}Claude Code
mcpServers shape (e.g. in .mcp.json or ~/.claude.json):
{
"mcpServers": {
"agent-memory": {
"command": "npx",
"args": ["tsx", "src/mcp.ts"],
"env": {
"HELIX_URL": "http://localhost:6969"
}
}
}
}When AGENT_MEMORY_SECRET is set, every tool call must carry
_meta.authorization = "Bearer <secret>" (stdio has no HTTP headers, so the
bearer rides in the request's _meta); mismatch is an MCP unauthorized
error. Never commit a real secret — set it in the server's environment.
Hooks
hooks/capture.mjs is plain Node ESM with zero dependencies. It reads the
host's hook JSON on stdin, takes the event name from argv[2] (supported:
SessionStart, PostToolUse, Stop, PostToolUseFailure, PreCompact,
SessionEnd, UserPromptSubmit — 7 events), and POSTs one small observation to
/memory/remember with origin="hook:<event>".
Wiring example (Claude Code settings.json hooks shape):
{
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "node /path/to/agent-memory/hooks/capture.mjs SessionStart" }] }
],
"PostToolUse": [
{ "matcher": "*", "hooks": [{ "type": "command", "command": "node /path/to/agent-memory/hooks/capture.mjs PostToolUse" }] }
],
"PostToolUseFailure": [
{ "matcher": "*", "hooks": [{ "type": "command", "command": "node /path/to/agent-memory/hooks/capture.mjs PostToolUseFailure" }] }
],
"PreCompact": [
{ "hooks": [{ "type": "command", "command": "node /path/to/agent-memory/hooks/capture.mjs PreCompact" }] }
],
"SessionEnd": [
{ "hooks": [{ "type": "command", "command": "node /path/to/agent-memory/hooks/capture.mjs SessionEnd" }] }
],
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "node /path/to/agent-memory/hooks/capture.mjs UserPromptSubmit" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "node /path/to/agent-memory/hooks/capture.mjs Stop" }] }
]
}
}Any host that can run a command with JSON on stdin works the same way:
node hooks/capture.mjs <EventName>.
Guarantees (verified):
Always exits 0 with zero output — even when the memory server is down, with malformed stdin, or with an unsupported event. A dead memory server must never block the coding agent.
Stores a valid observation when the server is up; all 7 events work (
SessionStart/PostToolUse/Stop/PostToolUseFailure/PreCompact/SessionEnd/UserPromptSubmit).Only an allowlisted, host-agnostic summary is stored:
agent session started,tool used: <tool-name>,tool failed: <tool-name>,agent session stopped,context compaction requested,agent session ended, oruser prompt submitted. Hook payloads, file paths, command output, and the user's prompt text are deliberately NOT captured — a planted file path (/tmp/secret-should-not-be-captured.txt) and a prompt canary were both confirmed not stored (privacy, Ley 172-13).Never prints memory content, the hook payload, or the secret.
AGENT_MEMORY_URLdefaults tohttp://127.0.0.1:3111(the REST service, not the raw Helix port);projectderives from the workspace directory name, overridable viaAGENT_MEMORY_PROJECT.A 2s fetch timeout keeps a hung server from hanging the agent.
The OpenCode plugin mirrors this with a 5th hook, tool.execute.before — a
fire-and-forget tool started: <tool-name> observation (own memory* tools
skipped; never awaited, so the tool hot path pays nothing).
Authentication
Set
AGENT_MEMORY_SECRETto a non-empty value to arm the guard: every/memory/*route exceptlivezthen requiresAuthorization: Bearer <secret>; mismatch →401with body{"error":"unauthorized"}.Unset
AGENT_MEMORY_SECRET→ open localhost (matches the upstream default).The secret value is never logged, echoed, or included in error text; the access log records method, path, status, and duration only.
The delete governance line (
memoryId,reason,at) exists for operational audit of destructive deletes only: it goes to process stdout/stderr and nowhere else (this repo keeps no durable store for it), is retained per the host's log retention/rotation, and is deleted by log rotation or process exit — and the delete receipt'sdeletedAtis server time captured immediately after the store confirms the delete, with the receipt omittingreasonby design (reason lives only in that log line). Its fields are a strict allowlist (memoryId, normalizedreason,at— never memory content, embeddings, or headers): don't put PII or secrets inreason(caller responsibility, bearer-auth + length bound), and apply standard log masking/retention on the host that streams it.The MCP server applies the same rule over
_meta.authorization.Never commit or print real secret values.
Verified auth matrix: livez exempt → 200; no header → 401; wrong bearer →
401; correct bearer → 200/201; secret appears 0 times in server logs; auth off
when AGENT_MEMORY_SECRET is unset.
Configuration
Variable | Default | Used by | Purpose |
|
| REST server | Listen port |
|
| hooks, | Base URL of the REST service |
| (unset = open) | REST + MCP + hooks + plugin | Bearer secret; non-empty arms the guard |
|
| store, bootstrap, purge | HelixDB instance endpoint |
| (workspace dir name) | hooks, plugin | Tenant/scope value for captured observations |
|
| REST server | Bind address |
|
| OpenCode plugin | Auto-inject recalled memories into the system prompt; |
|
| OpenCode plugin | Max rows injected per block and per auto-recall query ( |
|
| OpenCode plugin | Auto-recall cache TTL in ms, bounding network cost inside the request hot path ( |
| (unset = off) | REST + MCP searches, store | Hide memories older than N days from search results (reported as a |
| (unset = off) | REST + MCP searches, store | Per-day decay rate λ for the fused-row tie-break: weight = |
|
| REST + MCP | Tier-1 consolidation: near-duplicates with |
|
| eval harness only ( | Adapter selector for the pluggable |
The three plugin rows are read with options > env > default, so a matching
inject / injectLimit / injectTtlMs key on the plugin itself wins over the
environment variable.
Known limitations
Stated plainly — these are real, not hypothetical:
Port conflict with upstream agentmemory. Port
3111is our default, chosen deliberately for drop-in parity with upstream. Ports3111/3112/3113may be held by the real upstreamagentmemory(npx→node …/bin/agentmemory→iii; verified live on this machine). When3111is occupied, start ours on3151and point every HTTP client — hooks, plugin, andverify— at it viaAGENT_MEMORY_URL=http://127.0.0.1:3151(the MCP server needs no reroute: it is stdio and talks to HelixDB directly viaHELIX_URL):AGENT_MEMORY_PORT=3151 npm run dev AGENT_MEMORY_URL=http://127.0.0.1:3151 npm run verifyStarting ours on
3151does not move the clients: hooks, the plugin, andverifystill default tohttp://127.0.0.1:3111— which upstream may hold — so every client process must setAGENT_MEMORY_URL=http://127.0.0.1:3151explicitly. Skip it and captures and recalls are silently aimed at whatever occupies3111.The server prints this exact reroute hint on
EADDRINUSE. Never kill or displace the upstream instance.Persistence is the default; in-memory is opt-in. Persistence is decided by the
storage = "disk"key inhelix.toml, not by any start flag: the Quick start's--disk --persistis what writes that key, and this repo'shelix.tomlalready carries it — so a plainhelix start devkeeps data across restarts. A project whosehelix.tomllacks the key runs memory storage and loses everything on restart.Durability & recovery. Data survives
helix restart devon the Docker volume whilestorage = "disk"is set. After a host reboot the container does not auto-start (no restart policy is configured) — runhelix start devto bring it back. The failure mode is symptom-free: captures keep exiting0silently and auto-recall is simply skipped, so the memory stack goes dark with no error anywhere. If recall suddenly returns nothing, checkhelix statusfirst.Listings are ordered by node
$iddescending (insertion order), not by timestamp. The engine cannot correctly sortdateTimeproperties —orderByon DateTime was verified non-monotone across 8 sessions in the build probe — solistSessionsandsessionMemoriesorder by$iddesc (deterministic newest-created-first) instead. This is an upstream engine limitation, not our choice.Embeddings are a deterministic, hash-based 384-dim embedder (
src/embed.ts: FNV-1a token hashing + TF weighting + L2 normalization — no model download, no API key, no LLM). It is good for structural/rank parity with the upstream, but it does not understand true semantic synonyms. Swapsrc/embed.tsfor a real embedding model when semantics matter.Out of v1 per
docs/CONTRACT.md§4: consolidation tiers 2–4 (upstream's full 4-tier model), LLM auto-compress, viewer UI, session replay, JSONL import, multi-agent adapters (20 upstream), and the full 54-tool MCP surface. (Read-time decay + TTL +purge.tsshipped in v0.4.0 — "corte A"; tier-1 near-duplicate consolidation, derived confidence and the eval harness shipped in v0.5.0 — the remaining tiers stay out of scope.)demoappends on every run — it is not idempotent. Each invocation seeds 3 more sessions into projectdemo, so re-running it produces duplicate rows in later results. Storage is durable by default (storage = "disk"), so a restart no longer clears them — seed into a freshprojectfor a clean demonstration. Data persists now; what is still pending is the P4 hardening story (backup/DR, data-dir control), not an in-memory reset.Vector hits carry
distance, BM25 hits carryscore. Vector rows are projected as$distance(cosine, lower = closer), so a raw vector hit'sscoreis0until RRF fusion assigns one — readdistancewhen ranking or displaying vector results,scorefor BM25/RRF. Both are surfaced on the REST row.Concurrent distinct-variant merges can lose one append — accepted residual (owner: engineering). The consolidation FIFO lock is keyed by content hash, so it serializes identical content only: two concurrent saves of different near-dup variants that pick the same survivor both read the pre-merge content, last writer wins, and both callers still get
consolidated:true. Recoverable — the caller keeps its text and re-saving re-merges. Declared indocs/CONTRACT.md§3 tier-1 (a); tracked inROADMAP.md§1.3 with expiry 2026-12-31 or the start of P4.3 multi-instance work, whichever first — at P4.3 survivor-level serialization becomes mandatory. Gate P1R-P32 / RL-001.With tier-1 ON, an unhealthy text index fails WRITES too. Every novel
rememberruns the near-dupe probe (textSearchWith), soindex_not_foundsurfaces as 500s on writes, not just degraded search —npx tsx scripts/bootstrap.tsrestores writes (idempotent; run it before the first write). The probe sends the full incoming content (≤200 kB) as the BM25 query, bounded by the 15 s per-operation timeout. Fail-closed is the documented posture (gate P1R-P32 / RL-003).
Verification
All run clean:
npm run typecheck(tsc --noEmit) — zero errors; noany, no@ts-ignore, no TODO anywhere in the source.npm run verify(scripts/verify.ts) —214 passed, 0 failed→VERIFY PASS(identity guard → health → remember with concepts → BM25 hits → smart-search hits → sessions list → session memories → forget → gone → counts reflect it, plus embedder determinism, defaults, boundary validation, the P3.1 round-trip: lesson → search hits withorigin:"lesson"→ recap (every bullet session-scoped) → handoff → governed delete with receipt → gone → second delete 404 → counts, the v1.1 P1.3/P1.6 sections: derived default concepts ≤8 → graph-branch proof (fused score == 3/61) → content-hash dedup round-trip: same id +deduped:true+ counts stable, cross-project distinct, concurrent race → same id → dedup × hook first-wins: same fixed hook content in a NEW session → same id, no new row, no Session node for that session, and the v1.2 sections: derived importance (no-caller-value ==deriveWriteImportance(origin, concepts.length), explicit wins, recall-lift ordering) + tier-1 consolidation (3 near-dup variants → 1 row withconsolidated:true, each variant's wording recalls it, healthCount +1, merged-text re-save → exact-dedup loop guard) + the MCP adapter pass-through (InMemoryTransport: save withoutimportance→ the store seesundefined, explicit value wins). Before the first write it probesPOST /memory/recapand aborts (exit 1, no writes) unless the target answers 200 — so when3111is occupied by the upstreamagentmemory, run it against ours:AGENT_MEMORY_PORT=3151 npm run devthenAGENT_MEMORY_URL=http://127.0.0.1:3151 npm run verify(README conflict procedure).npm run verify-lifecycle(scripts/verify-lifecycle.ts) —104 passed→VERIFY PASS: pure dedupKey/hash golden vectors, decay math (λ=0 → 1, half-life exact, monotonic, clamp), TTL filter (OFF/boundary/purity), concept extraction determinism + bounds,oneLineCWE-117 render guard (collapses\n/\r/tabs to single spaces, idempotent, non-corrupting for names/digits/ISO/booleans), plus §F derived confidence (deriveWriteImportance goldens, confidenceBoost monotonic/clamp, recall ledger), §F-bis the decay-THEN-boost order golden (λ on, discriminating), §G consolidation (jaccard, threshold fail-closed OFF, substring guard), §H hand-computed eval-metric goldens (R@5/R@10/MRR/nDCG/aggregate), and §I fail-closed near-dupe probe + TTL×expired-survivor guard + plugin no-default source checks. No Helix, no server — CI-runnable.npm run verify-capture(scripts/verify-capture.ts) —115 checks→ALL PASS: all 7 hook events × exact payload/origin/exit-0/stdout+stderr silence, prompt-text privacy canary, negatives (unsupported event, malformed /empty stdin, dead server), Authorization header, and the plugincaptureToolStarthelper (incl.memory*skip + dead-backend fail-soft). Spawnscapture.mjsagainst a local counting server — no Helix, CI-runnable.npm run bootstrap—bootstrapIndexes: OK (8 indexes ensured)thenREADY — searchByText responding.npm run verify-env(scripts/verify-env.ts) —21 passed: legacyAGENTMEMORY_*migration guards + hook silence.npx tsx scripts/verify-injection.ts—ALL PASS(73): marker idempotency, block size budget, cache TTL/LRU, fail-soft recall.npx tsx scripts/probe3.ts—OVERALL: GREEN: live-instance proof for dedup lookup round-trip, application-side (non-)uniqueness, andltParamstrict older-than ondateTime(feedspurge.ts).npx tsx scripts/probe4.ts—12 passed: live proof thatupdateMemoryContent'ssetPropertyrefreshes BOTH the text and vector indexes (verdict A — the tier-1 merge ships in-place, survivor id stable).npm run verify-skills(scripts/verify-skills.ts) —119 checks→VERIFY SKILLS PASS: 73 structural checks across the 8skills/*/SKILL.md(frontmatter, name == dir, contract route + MCP tool per skill, index links, secret patterns) + 46 live round-trips exercising every skill's frozen route under projectverify-skills, behind the same identity guard asverify.--structuralruns only the 73 checks with no server — that mode is what CI executes.npm run eval(scripts/eval.ts) —EVAL PASS: seeds the in-repo corpus (eval/corpus.ts, 40 docs / 15 queries, projectagent-memory-eval, idempotent via dedup) and writes our own R@5 / R@10 / MRR@10 / nDCG@10 for bm25 + hybrid todocs/benchmarks/SCORECARD.md— upstream's published numbers are never claimed as ours.npx tsx scripts/purge.ts --dry-run— prints would-delete count + ids and deletes nothing; missing--days→ usage + exit 2 (fail closed).npm run demo—demo OK: BM25 hits at scores 2.54 / 1.59 / 0.88, vector hits ranked by cosine distance (e.g.d=0.2972 < 0.3251 < 0.4151— verified discriminating, not tied), and hybrid RRF hits mixingsource: vectorandsource: graph.Auth matrix:
livezexempt 200; no header 401; wrong bearer 401; correct bearer 200/201; 401 body{"error":"unauthorized"}; secret appears 0 times in server logs; auth off whenAGENT_MEMORY_SECRETis unset.MCP stdio handshake → exactly the 11 tools listed above; live
tools/callround-trips confirmed.Hook guarantees: exit code 0 and zero output with the server DOWN, with malformed stdin, and with an unsupported event; valid observation stored when up; all 7 events work; only the allowlisted summary is stored (planted path
/tmp/secret-should-not-be-captured.txtand a prompt canary both confirmed NOT stored).
Skills
Invocable agent skills live under skills/, each mapping one job to
the frozen REST + MCP surface (the frontmatter description is what an agent
matches on — see skills/memory/SKILL.md for the
index): recall (hybrid/BM25 recall), remember (save + dedup/consolidation
semantics), recap, handoff, forget (permanent delete) / governance
delete, lesson (origin-forced), commit-context (capture durable state
before a commit), and session-history (sessions → memories → recap).
npm run verify-skills structurally validates all 8 and live round-trips
every route against a running server.
Contributing & security
CONTRIBUTING.md— prerequisites, exact dev-setup commands, Conventional Commits, the PR/evidence bar, strict-TS rules, and the never-kill-upstream coexistence rule.SECURITY.md— supported versions, private reporting via GitHub Security Advisories (never a public issue), scope, theAGENT_MEMORY_SECRETpolicy, and response expectations.CHANGELOG.md— release history.
Specification
The frozen contract this implementation follows — labels, routes, query
surface, MCP tools, scope, and §4 out-of-scope list — is
docs/CONTRACT.md. Release history lives in
CHANGELOG.md; per-release notes in
docs/specs/30_delivery/RELEASE_NOTES.md.
License: Apache-2.0
This server cannot be deployed
Maintenance
Related MCP Connectors
Persistent memory for AI agents across Claude, ChatGPT and any MCP client.
Hosted MCP memory for coding agents: persistent across sessions, editable markdown, team sharing.
- KogniteOAuthdev.kognite
Hosted agent memory: store, search, and recall facts across sessions from any MCP client.
Persistent AI memory shared across Claude, ChatGPT, coding agents, and compatible MCP clients.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceProvides persistent memory for AI coding agents via MCP, enabling agents to store and semantically recall facts, events, and lessons across sessions, all running locally without cloud dependencies.Apache 2.0
- AlicenseAqualityDmaintenanceProvides persistent memory with semantic search for MCP-based AI agents, enabling them to store and recall information across sessions using vector embeddings.41MIT
- AlicenseNot gradedqualityBmaintenanceProvides a persistent, cross-tool memory layer for AI coding agents via MCP, enabling storage and retrieval of decisions, preferences, and context across different tools and models.2 npm1MIT
- AlicenseNot gradedqualityAmaintenanceProvides persistent memory and knowledge management for coding agents via MCP, including typed decision/snippet/runbook storage, semantic search, explicit session lifecycle, and nightly consolidation.3Apache 2.0