AstraMemory Local
Provides locally-run LLM and embedding models for memory distillation, extraction, and search, enabling offline operation with zero inference cost.
Provides cloud-based LLM and embedding capabilities via Azure OpenAI for memory distillation, extraction, and search, with pay-per-token usage.
Click on "Install 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., "@AstraMemory Localsearch memories for sqlite-vec configuration"
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.
AstraMemory Local
Local-first memory daemon for AI coding agents — wire-compatible with astramem-plugin (v0.5.2+). See the npm badge above or CHANGELOG.md for the current release — this README doesn't hardcode a version number, it goes stale every release.
Try it in 60 seconds
No install beyond npm, no Ollama, nothing written to disk:
npm install -g astramem-local
astramem-local demoThis boots an in-memory daemon, seeds 5 example memories (decision, fact, lesson, todo,
preference) through a deterministic mock embed/LLM path, runs one sample recall against them, and
keeps serving in the foreground until you hit Ctrl+C. When you're ready for persistent storage,
run astramem-local init to install a real daemon.
Related MCP server: memento
Why it exists
Claude Code sessions compact and terminate, taking context with them. AstraMemory Local captures every session transcript, distills typed memories (decisions, facts, lessons, commands, todos), and serves them back via hybrid search (BM25 + vector + importance + freshness). It runs entirely on your workstation — no cloud account, no data leaves your machine. The plugin's hooks post to the local daemon instead of the SaaS endpoint through a single environment variable swap.
Quick start
npm (recommended):
npm install -g astramem-local
astramem-local init
# follow the wizard — picks embedding (ONNX in-process, default) and LLM (codex-subscription
# default, or Ollama/Azure/Anthropic/OpenAI-compatible) providers, writes config.yaml + secrets.env
astramem-local service install
export MEMORY_API_URL=http://127.0.0.1:7777
export MEMORY_BEARER=$(astramem-local token print)Requires Node.js >= 20. Native deps (better-sqlite3-multiple-ciphers, @napi-rs/keyring) use
prebuilt binaries where available, falling back to a from-source compile otherwise.
One-liner install scripts — no npm needed first, the script does the Node check + npm install for you, then hands off to the wizard. Fastest path if you don't already have a Node toolchain open:
# Linux / macOS
curl -fsSL https://raw.githubusercontent.com/astragenie/astramem-local/main/install.sh | bash# Windows (PowerShell 5.1+ or 7+)
irm https://raw.githubusercontent.com/astragenie/astramem-local/main/install.ps1 | iexIf the script's own output is piped through a non-interactive shell, it stops after installing
and tells you to run astramem-local init yourself — the wizard needs a real terminal to prompt
for provider choice.
Build from a git clone:
git clone https://github.com/astragenie/astramem-local.git
cd astramem-local
bun install
bun run build
node dist/cli/index.js init
# follow the wizard — picks embedding (ONNX in-process, default) and LLM (codex-subscription
# default, or Ollama/Azure/Anthropic/OpenAI-compatible) providers, writes config.yaml + secrets.env
node dist/cli/index.js service install
export MEMORY_API_URL=http://127.0.0.1:7777
export MEMORY_BEARER=$(node dist/cli/index.js token print)If you pick the Ollama LLM tier, first run can take 10-30 minutes if Ollama isn't installed yet. Since v0.18.0 the wizard's actual default LLM provider is
codex-subscription(reuses an existingcodex loginChatGPT session — no local model download at all); Ollama is offered as the free, fully-local alternative and becomes the pre-selected default when no Codex login is detected. The Ollama LLM tier pullsqwen2.5:7b(~4.7 GB — the tested/recommended local extraction model; the previously-triedqwen3:8breturned empty extraction output under thinking mode and was dropped) plusmxbai-embed-large(~670 MB) if you also pick Ollama for embedding — about ~5.4 GB total download, with no separate confirmation prompt. Embedding itself defaults to ONNX (bundled, in-process, no Ollama server required) regardless of which LLM provider you choose. See docs/providers.md for the full provider breakdown and RAM guidance.
Restart Claude Code. All plugin hooks (PreCompact, SessionEnd, SubagentStop) now post to the local daemon. No other plugin changes needed.
Prefer to try before installing anything? See Try it in 60 seconds at the
top of this README — astramem-local demo needs nothing from the rest of this section.
Wire compatibility
The daemon's ingest endpoint now speaks the same wire protocol as the SaaS backend. Both old and new clients work:
Legacy plugin (v0.1.x):
POST /ingest/transcript
Content-Type: application/json
Authorization: Bearer <token>
{
"session_id": "claude-20260630-abc123",
"source": "precompact",
"content": "[Assistant]: Distilled 3 facts..."
}SaaS canonical (v0.2.0+):
POST /ingest/transcript
Content-Type: application/json
Authorization: Bearer <token>
{
"event": "pre_compact",
"session_id": "claude-20260630-abc123",
"turns": [{"role": "user", "content": "..."}, ...],
"wire_version": "v1.0",
"captured_at": "2026-06-30T12:34:56Z",
"client_version": "0.5.0",
"project_id": "my-project",
"cwd": "/home/user/src"
}Both shapes are accepted — no migration needed. See contracts/wire.ts for the complete schema definition.
Two ways to run this: full daemon vs. MCP-only stdio
Everything above (service install, serve) runs astramem-local as a long-lived HTTP
daemon. There is a second, much lighter mode if all you want is the MCP tool surface inside
Claude Code (or another MCP host) and you don't need the LLM-proxy auto-capture path,
background distillation, or sync:
astramem-local mcp-stdioThis spawns one process per MCP client session over the stdio transport
(src/cli/mcp-stdio.ts) — same tool set and same on-disk SQLite store as
the daemon's POST /mcp, but no instance lock, no worker loop, no timers, and no service
lifecycle at all. It exits automatically when the MCP host closes stdin. Point an MCP host at
it directly instead of running service install:
{
"mcpServers": {
"astramem": { "command": "astramem-local", "args": ["mcp-stdio"] }
}
}What you don't get in this mode: remember (direct insert) still works, but transcript
capture from the plugin's hooks (POST /ingest/transcript) and the LLM-proxy auto-capture path
both require an HTTP listener — proxy/server.ts binds one — so both structurally need the
long-lived daemon. So does background distillation of anything that would otherwise sit in the
worker's job queue, and sync. mcp-stdio is a real way to use the MCP tools against your
local store with no daemon install; it is not a replacement for the daemon if you rely on
automatic capture.
One more thing worth knowing: the CLI's own code comment currently labels this "test-enabling
infrastructure" and calls the daemon's POST /mcp "the supported production surface"
(src/cli/index.ts, the mcp-stdio case) — it works today, but the project
doesn't yet treat it as the primary supported path the way this README treats service install.
Architecture
memory-plugin hooks
|
| POST /ingest/transcript (v0.2.0+ — SaaS-canonical envelope)
| Authorization: Bearer <token>
v
+------------------+ SQLite (memory.sqlite)
| HTTP daemon | ---> +-------------------+
| Fastify | | sessions |
| 127.0.0.1:7777 | | messages |
| (v0.2.0+) | | transcripts |
+------------------+ | ingest_idempotency|
| jobs (queue) |
| memories |
| memories_fts (FTS5)|
| memories_vec (vec0)|
| entities |
| memory_entities |
| entity_relations |
| budget_spend |
+-------------------+
|
in-process worker loop
|
8-stage distillation
(cleanup -> normalize ->
chunk -> compact ->
extract -> reduce ->
memory-normalize ->
embed + index)
|
+--------------+--------------+
| | |
memories FTS5 index sqlite-vec
(rows) (BM25 search) (cosine ANN)
| | |
+--------------+--------------+
|
hybrid score fusion
a*BM25 + b*cosine +
c*importance + d*freshness
|
GET /search POST /recall
|
/recall in plugin slash commandsSingle Node process. Workers run in-process on a polling loop. SQLite is the source of truth. Everything derived (vectors, FTS rows, compactions) can be rebuilt by replaying the jobs table.
entities + memory_entities (FEAT-402/FEAT-453) tag named things (people, projects, tools,
decisions, ...) mentioned in a memory, with optional fact-level bitemporal validity hints.
entity_relations (FEAT-481) is one join further: entity-to-entity relation triples
(subject_entity_id, predicate, object_entity_id) extracted alongside the same atom's
entities[] list, retrievable via the get_entity_related / get_entity_timeline MCP tools
(bitemporal as_of aware — a relation sourced from an invalidated/superseded memory stops
reading as valid once as_of passes that memory's own valid_to).
Memory types
Canonical ~10-value type registry (U4 contract-unification, ADR D4),
single-sourced in @astragenie/astramem-contracts
(contracts/schemas/atom.v1.schema.json) and mirrored in
src/contracts/memory.ts MEMORY_TYPES. astramem-local writes the first 7
today; the last 3 are cloud-authored types local can read (e.g. via sync)
but does not yet produce.
Type | Description | Example | Ships from |
| Architectural or design choice made during a session | "Use sqlite-vec for v1 vector storage" | local |
| Objective project fact, configuration detail | "Port 7777 is the default daemon port" | local |
| Something that went wrong and how it was resolved | "sqlite-vec rowid must match memories rowid" | local |
| CLI command or script worth remembering | "bun run build && bun run test -- migrate" | local |
| Outstanding work item surfaced in conversation | "Add reembed job when provider changes" | local |
| Freeform observation not fitting another type | "Team prefers PRs under 400 lines" | local |
| Something that happened at a point in time | "Deployed v0.8.1 to prod" | local |
| User/agent preference atom | "User prefers terse commit messages" | cloud only (today) |
| Outcome of a completed task | "Migration 014 applied cleanly" | cloud only (today) |
| Session/thread summary atom | "Summary of the FEAT-424 filter-unification session" | cloud only (today) |
Provider matrix
Concern | Codex subscription (cloud, shipped default) | Ollama (local, free) | ONNX (local, in-process) | Azure OpenAI (cloud) |
LLM compaction | gpt-5.4-mini | qwen2.5:7b (default tier) | — | gpt-4.1 or any deployment |
LLM extraction | gpt-5.4-mini | qwen2.5:7b (default tier) | — | gpt-4.1 or any deployment |
Embedding | — (no embeddings endpoint) | mxbai-embed-large (1024-dim) | mxbai-embed-large (1024-dim, shipped default) | text-embedding-3-small (1024 via dimensions) |
Cost | ChatGPT subscription quota (not tracked by | $0 (local inference) | $0 (local inference) | ~$0.02/1K tokens + $0.0001/1K embed tokens |
Setup |
|
| none — bundled, no server needed | Azure portal + endpoint + deployment name |
Providers are configurable independently per stage: LLM compaction/extraction is one axis,
embedding is another. The init wizard defaults to codex-subscription for LLM (falling back to
Ollama's qwen2.5:7b tier when no Codex login is detected) and onnx for embedding, but any
combination is valid. Embedding provider is system-wide — switching requires
astramem-local rebuild --reembed to re-index all memories in the new model's vector space.
See docs/providers.md for full setup instructions.
Public endpoints
All endpoints require Bearer token authentication except GET /version and GET /health.
GET /dashboard additionally accepts an HttpOnly session cookie, bootstrapped via a one-time
?token= visit (see Dashboard below).
Endpoint | Auth | Description |
| — | Daemon health probe: |
| — | Version discovery: |
| Bearer | Capture protocol endpoint — accepts |
| Bearer | Hybrid search with type/repo/project/since filters + named |
| Bearer | Top-K semantic recall (alias: |
| Bearer | Token-budgeted memory pack for a repo/project/branch — powers the SessionStart hook |
| Bearer | Direct memory insert, bypasses distillation |
| Bearer | Single memory lookup |
| Bearer | Provenance receipt — extraction evidence + confidence for a memory |
| Bearer | Full |
| Bearer | Soft-delete a memory (lifecycle op) |
| Bearer | Replace a memory with a newer one, linked via |
| Bearer | Promote a memory's scope: personal → team → org |
| Bearer | Record an explicit recall-usefulness signal for a memory |
| Bearer | Session summary digest |
| Bearer, cookie, or one-time | Read-only HTML metrics dashboard, auto-refreshing every 5s |
| Bearer | Model Context Protocol endpoint (auto-discovered tools, see below) |
MCP tools (Claude Code auto-discovery)
The daemon exposes a Model Context Protocol (Streamable HTTP) endpoint at POST /mcp.
Claude Code discovers and calls the tools below automatically when configured in .mcp.json.
Tool | Description | Maps to |
| Hybrid FTS + vector search with optional type/repo/project/since filters + named |
|
| Top-K semantic recall (default k=5) + named |
|
| Direct memory insert, bypasses distillation |
|
| Daemon health probe: |
|
| Provenance receipt — extraction evidence + confidence for a memory |
|
| Session summary digest |
|
| Read-time per-agent "what has this agent learned" profile — top lessons, recent decisions, prior corrections |
|
| Read-time daemon-wide profile (no agent filter) — standing facts, preferences, recurring lessons, recent decisions, known entities. Optional |
|
| Entity-to-entity relation triples ( | (MCP-only, no REST route) |
| Memories mentioning an entity over time, newest-first, optional |
|
| Soft-delete a memory (lifecycle op) |
|
| Replace an old memory with a new one, linked via |
|
| Promote a memory's scope: personal → team → org |
|
| Un-archive a memory previously archived by the W4.4 TTL policy or a manual archive event |
|
| Full |
|
| Explicit recall-usefulness signal — "this memory mattered" (U5 canonical name, shared with cloud) |
|
| DEPRECATED one-release alias for |
|
| Erasure v1 (ADR-006): permanently hard-delete a memory, leaving only a tombstone event |
|
| Pending (or resolved) stage-9 consolidation proposals awaiting a merge/keep decision (ADR-004) |
|
| Accept or reject a pending consolidation proposal |
|
Plugin .mcp.json wiring:
{
"mcpServers": {
"astramem": {
"type": "http",
"url": "${MEMORY_API_URL}/mcp",
"headers": { "Authorization": "Bearer ${MEMORY_BEARER}" }
}
}
}Set MEMORY_API_URL=http://127.0.0.1:7777 and MEMORY_BEARER to your token
(printed by astramem-local token print).
This daemon reads and writes only MEMORY_API_URL (src/cli/init.ts) — /mcp is appended to it
the same way /health or /search would be. If you've seen MEMORY_MCP_URL referenced
elsewhere, that's a separate variable from the companion plugin's docs for an older two-port
SaaS-style deployment (distinct API and MCP servers); it doesn't apply to this single-port daemon.
Automated wiring: astramem-local mcp install --client <name> writes the above for you —
resolves the daemon's URL/port and Bearer token the same way serve does, and updates the
target client's config in place (idempotent, backs up the existing file to .bak first).
| Target file | Written automatically? |
|
| Yes |
|
| Yes |
| Claude Desktop's | Yes |
|
| No — prints a manual snippet |
|
| No — prints a manual snippet |
astramem-local mcp install --client claude-code # writes .mcp.json in the current project
astramem-local mcp install --client cursor --dry-run # preview only, no write
astramem-local mcp install --client claude --url http://127.0.0.1:7777Budget cap
The daily LLM spend cap (default: $10 USD) is enforced before each LLM call.
Ollama always reports
$0cost — the cap only applies to Azure usage.When the cap is reached, pending distillation jobs move to
pausedstate. Ingest continues to accept transcripts (no data loss). Distillation resumes the next UTC day automatically.Override:
astramem-local budget --reset(logged).Check current spend:
astramem-local budget.
Security
Encryption at rest
memory.sqlite is encrypted by default using better-sqlite3-multiple-ciphers (SQLCipher-compatible
cipher driver). The 32-byte key is resolved through a provider chain:
OS credential store — Windows Credential Manager / macOS Keychain / Linux libsecret, via
@napi-rs/keyring.Key-file fallback —
<configDir>/db.key(mode0600) with a WARN log, used only when the credential store throws (e.g. headless Linux with no secret-service session).
A pre-existing plaintext memory.sqlite (from a version predating encryption) is auto-migrated
transparently on daemon startup: the file is checkpointed, re-keyed via PRAGMA rekey, and
verified (row-count match) before the encrypted copy replaces the original. The pre-migration
plaintext file is preserved at memory.sqlite.pre-encryption.bak — nothing is deleted. Migration
is idempotent; an already-encrypted file is a no-op.
Disabling encryption (security.encryption.enabled: false) is a deliberate trust trade-off — the
daemon logs a prominent WARN at startup, and astramem-local doctor reports
encryption: OFF — memory.sqlite is stored in PLAINTEXT.
Stage-0 secret redaction
Every transcript turn and manual /remember write passes through a redaction choke point before
it is persisted — downstream pipeline stages only ever see already-redacted text. Detection runs in
three passes:
PEM private-key blocks (multiline, whole block).
Vendor/pattern detectors — AWS access keys, GitHub tokens, Azure storage keys/SAS tokens, GCP API keys, Slack tokens, JWTs, generic
key=valuecredentials, connection-string userinfo — plus any org-specific regexes fromsecurity.redaction.customPatterns.Shannon-entropy fallback — flags high-entropy strings (default threshold 4.0 bits/char) that pattern detectors missed.
Matches are replaced with a placeholder — [REDACTED:<type>:<hash8>], where hash8 is the first 8
hex chars of SHA-256(secret value) — so the same secret always redacts to the same placeholder
(dedup-safe) while the raw value is never stored or logged. Only counts are persisted, in the
redaction_log table (type, count, session_id, created_at) — astramem-local doctor surfaces
a 7-day breakdown, e.g. redaction: on — 12 secrets redacted (3 aws_access_key, 9 generic_credential) in last 7d.
Toggle with security.redaction.enabled (default true).
Redaction is secrets-only, regardless of extraction profile. distill.extractProfile: conversational (see docs/configuration.md#distillextractprofile) extracts far more non-secret
personal detail (names, relationships, life events, preferences) than the default engineering
profile — that is its entire purpose. This redaction pass is completely unchanged either way: it
never redacts non-secret personal content under any profile, and everything it does not redact is
written to the local SQLite store and (if a cloud LLM provider is configured for extraction) sent
to that provider like any other transcript content. astramem-local doctor reports the active
profile and this same disclosure in its "Extraction profile" row.
Bearer token storage
The daemon's Bearer token is stored the same way as the DB encryption key: OS credential store
first, secrets.env (mode 0600) only as a fallback when the credential store is unavailable. A
token found only in secrets.env is opportunistically promoted into the credential store the next
time the daemon resolves it — secrets.env itself is never rewritten or deleted as part of that
promotion.
Dev no-auth mode
Set auth.devNoAuth: true in config.yaml to hit the API — and /dashboard (see
Dashboard) — from local tools (curl, a browser, a script) without supplying
Authorization: Bearer <token> at all. Dashboard keeps its own independent Bearer/cookie/?token=
auth only while this flag is off; with it on, the dashboard is token-free too, subject to the same
loopback Host-header guard as everything else. Config-file only — there is no env var for this, so
it's always an explicit, visible line in config.yaml, never a stray shell variable someone forgot
was set.
What it protects against: the Bearer token doubles as a DNS-rebinding defense — without it, a
malicious web page the operator's browser visits could fetch('http://127.0.0.1:7777/...') and
read or write the whole memory store. devNoAuth keeps that defense by requiring every request's
Host header to resolve to 127.0.0.1, localhost, or ::1 (with or without a port) — anything
else gets a 403, not a silent pass-through.
What it does not protect against: with devNoAuth on, every other local process — any
script, any browser tab, any other application on the machine — can read and write your memories
with no credential at all. That's the entire trade being made; only enable it on a machine you
trust completely, and never alongside sync pairing to a prod workspace (doctor WARNs on this
every run while it's on).
It only ever activates on a loopback bind. If network.host is set to anything non-loopback (e.g.
0.0.0.0), the daemon refuses to skip auth — it logs a loud warning and keeps Bearer enforcement in
place on every route, rather than either exposing the whole LAN with no credential or refusing to
start at all.
Capture protocol
astramem-local accepts session capture from any tool that can speak one small HTTP contract —
POST /ingest/transcript with an astramem-capture@1 envelope. Two kinds are supported:
transcript(default) — raw turns, run through the full 8-stage distillation pipeline.events— pre-typed atom candidates (decision/fact/lesson/command/todo/note/event) that skip the raw-text/LLM stages and enter directly at the reduce stage. Built for sources that already know their own semantics (e.g.runner-pluginslice grades and lessons).
Both kinds pass through the same stage-0 redaction choke point described above. Writing a new tool integration is a small translator — capture at the tool surface, shape into one envelope per session boundary, POST it. See docs/capture-protocol.md for the full contract, field reference, and a curl example.
Memory lifecycle
Every memory has an append-only history in the memory_events log. Lifecycle operations never
delete a row — they append an event and update derived state:
Operation | Effect |
Invalidate | Soft-deletes a memory (optionally with a reason) — it stops surfacing in search/recall. |
Supersede | Replaces an old memory with a new one; the two are linked via the event log. |
Promote | Widens a memory's scope: |
GET /memory/:id/history (and the memory_history MCP tool) return the full event chain for a
memory — the complete invalidate/supersede/promote provenance trail.
why_memory receipts (GET /memory/:id/why, MCP why_memory) answer "why does the daemon
believe this?" — they return the extraction evidence and confidence that produced the memory, so a
recalled fact or decision can be traced back to its source.
User profile
GET /profile (and the user_profile MCP tool) return a read-time, daemon-wide "what does this
owner keep coming back to" profile, rolled up from the existing atom kinds — zero new schema, zero
persisted document, no rebuild step. Every call is a fresh SQL read against current state, so there
is no staleness window.
Profile section | Rolled up from |
|
|
|
|
|
|
|
|
| linked entities of kind |
standing_facts/preferences/recurring_lessons are ranked by the same ADR-010 usefulness signal
agent_profile's top_lessons uses, then importance, then memory id (a total tie-break, required
for deterministic output). Only currently-active memories count (valid_to IS NULL AND archived = 0) — a superseded or invalidated atom drops out on the next call, no extra event needed.
Defaults to scope=private (ADR-009's default); pass ?scope=team|org|all to widen it. This
daemon has a single global Bearer token and no per-caller scope authorization, so widening scope is
the authenticated caller's own choice, not a privilege escalation — see the FEAT-451 design doc §3
for the full reasoning.
Usefulness metric
The daemon tracks a recall-usefulness rate: of the memories served by a search/recall/pack call,
how many were later marked as actually used (POST /memory/:id/used, MCP submit_feedback — or its
deprecated one-release alias mark_memory_used — or the REST twin). The rate is
distinct atoms used / distinct atoms served in a given time window, computed per memory type and
per surface (mcp / rest / cli).
This is a v1 measure-only signal — it does not yet feed ranking (see ADR-010). Query text is
never stored; only a truncated SHA-256 digest of the query is kept alongside the served/used events,
themselves appended to the same memory_events log lifecycle operations use.
Dashboard
GET /dashboard serves a single-file, auto-refreshing (every 5s) HTML metrics page — no
JavaScript, no CDN, no external assets, dark mode by default. It shows memory counts by type,
recent captures, job-queue state, distill throughput, provider health, today/MTD budget spend vs
cap, and the pending-capture queue depth.
Auth accepts either the usual Authorization: Bearer <token> header, or an HttpOnly session
cookie. To open the dashboard directly in a browser (which can't set an Authorization header),
visit it once with ?token=<bearer> — the daemon exchanges that for the cookie via a 302 redirect
to the clean URL, so the bearer never persists in browser history or gets re-sent by the
<meta refresh> poll. A missing or wrong credential returns a plain-text 401, never HTML, and the
query string is stripped from the log line so a wrong ?token= guess doesn't persist a candidate
secret. With Dev no-auth mode active, none of the above is required at
all — the dashboard opens with no token or cookie, gated only by the same loopback Host-header
check every other route gets under that mode.
Commands reference
Command | What it does |
| Interactive wizard — writes config + secrets, runs migrations, installs service, offers the SessionStart memory-pack hook |
| Start daemon in foreground (dev/debug) |
| Register daemon as a user-scope OS service |
| Show service state |
| Start the service |
| Stop the service |
| Remove the service unit |
| Run all health checks, print table |
| Machine-readable health check output |
| Hybrid search, print results table |
| Filter by memory type |
| Top-5 semantic recall (alias for search k=5) |
| Direct insert, bypasses distillation pipeline |
| Show pending/failed jobs |
| Show only failed jobs |
| Rebuild derived indexes; --reembed re-vectors all |
| List configured providers and their health |
| Ping provider, print latency + dim |
| Show today and month spend vs cap |
| Clear today's spend counter (override, logged) |
| Print the current Bearer token |
| Generate new token, invalidate the old one |
| Write/update an MCP client's config to point at this daemon's |
| Run the MCP tool surface over stdio, one process per client session — no daemon, no service install (see Two ways to run this) |
| 30s zero-dependency trial: in-memory daemon, seeded example memories, one sample recall, no Ollama required |
Further reading
docs/retrieval-presets.md — named retrieval presets (
preset=default|recent|exact|entity|temporal)docs/migration-from-saas.md — switch the plugin from remote SaaS to local daemon
docs/configuration.md — full config.yaml reference
docs/providers.md — Ollama and Azure OpenAI setup
docs/capture-protocol.md —
astramem-capture@1wire contract (transcript+eventskinds)docs/hooks/memory-pack.md — SessionStart memory-pack hook (auto-installed by
init)docs/troubleshooting.md — common issues and fixes
docs/contracts.md — frozen type interfaces (for contributors)
CHANGELOG.md — release history
Retrieval quality gates
Every push and pull request to main runs the ADR-005 retrieval eval harness
(tests/eval/retrieval-eval.test.ts) as part of bun run test — it is a regular test, not an
optional or manually-triggered check. It runs a fixture corpus of graded queries through the
real hybrid search path (BM25 + vector + importance + freshness fusion) with a deterministic
fake embedder, and fails the build if either regression floor is missed: recall@10 ≥ 0.9 or
NDCG@10 ≥ 0.7.
These are enforced floors, not a self-reported benchmark leaderboard: the numbers are computed
by CI from a fixture set checked into the repo (contracts/fixtures/eval/), on every change, by
anyone — not hand-picked and reported after the fact. A PR that regresses ranking behavior fails
the same test suite as any other broken behavior; there is no separate step to remember to run.
No external benchmark figure (e.g. LoCoMo) is published in this README. The only LoCoMo run to date used a 10% sample — one conversation, n=199 — which is not comparable to the full ~1986-question set that published competitor baselines run on, so we're not quoting a number here until a full-sample rerun lands. The CI-gated floor above is what's real and checkable today.
See docs/eval/methodology.md for what each of the project's eval harnesses measures (including the manual extraction-quality benchmark and the LongMemEval adapter's current status) and how to reproduce them.
Known limitations
Documenting these here so they're a five-minute read instead of a surprise:
Temporal queries are weak, and the gap is asymmetric. Bitemporal storage and
as_ofpoint-in-time filtering are correct end-to-end (migrations/006-atom-v3.sql,validityFilterClause()insrc/search/search.ts) — the schema isn't the problem, and it isn't simply unpopulated either.AtomSchema(src/distill/prompts/extract.ts) has bothvalid_atandinvalid_at, but extraction only fills in one of them in practice: a 2499-atom measurement found 423 distinctvalid_fromvalues (thevalid_athalf works) against zero non-nullvalid_to(theinvalid_athalf has never fired). Whether that's a bug or just reflects that conversations rarely state an explicit invalidation is unmeasured. Treat point-in-time recall as schema-ready with one-sided extraction coverage, not fully extraction-populated.Contradicting memories can both be returned by recall. Contradiction detection ships
mode: 'enabled'by default (src/config/config.ts) but only proposes — it doesn't auto-resolve. Until a consolidation proposal is accepted or rejected (list_consolidation_proposals/resolve_consolidation_proposal), the original memory and the one that contradicts it are both live and both eligible to surface in the same search/recall call.Local Ollama models default to non-thinking mode — opt-in, not name-based detection. Thinking-mode local models can silently return an empty
{"atoms":[]}extraction — a well-formed, successful-looking response — if their reasoning trace consumes the whole turn beforeformat:jsonoutput is produced. Earlier versions tried to catch this with a model-name regex, which was incomplete by construction.OllamaLLMConfig.think(src/providers/llm/ollama.ts) now defaults tofalsefor every model regardless of name; reasoning mode requires an explicitthink: trueoverride, so the class of bug is closed for the default path rather than patched per model name.doctor'sllmChatProbe(src/doctor/probes/llm-chat-probe.ts) sends a realformat:jsonrequest and requires a parseable, non-empty response, so a broken provider/model combination shows red instead of a false-green plain-chat probe. Stick to the tested extraction model (qwen2.5:7b— F1 67%, precision 99% in the 2026-07-08 extraction-bench A/B;qwen3:8bwas tried as the default and dropped after it returned empty extraction output); if you switch to another local reasoning model withthink: true, verify with a real extraction run, not justdoctor.
Development
This project uses Bun as the package manager and script runner.
bun install # install dependencies
bun run build # compile TypeScript → dist/
bun run test # run the vitest suiteSee CONTRIBUTING.md for build/test/PR conventions, including the maintainer publishing flow.
Status
Local-first memory daemon with 32 migrations, an MCP surface
whose tool count is CI-enforced against contracts/manifests/mcp-tools.v1.json, hybrid
BM25+vector+importance+freshness search, event-sourced memory lifecycle (invalidate/supersede/
promote) with full provenance (why_memory), consolidation proposals, entity-tagged memory
with point-in-time (as_of) filtering, usefulness-ranked user/agent profiles (live-computed,
zero persisted state), reversible soft-delete for batch purges, and local↔SaaS sync
(shipper/puller/conflict-resolve). CI-gated retrieval eval (ADR-005) runs recall@10 ≥ 0.9 /
NDCG@10 ≥ 0.7 floors on every push.
Companion plugin: github.com/astragenie/astramem-plugin
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Persistent memory for AI agents across Claude, ChatGPT and any MCP client.
Portable memory for AI agents: capture once, recall across Claude, Cursor, and any MCP client.
Private-by-default, local-first memory/context/task orchestrator for MCP apps and agents.
Persistent memory for AI agents — log and recall conversation context over MCP.
Related MCP Servers
- AlicenseAqualityAmaintenanceMCP-native, local-first memory for coding agents that turns real sessions into reusable decisions, gotchas, and domain knowledge.176MIT
- 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
- AlicenseBqualityBmaintenanceLocal-first memory server for AI coding agents that stores work sessions, tasks, and durable memories in Markdown files, exposed through MCP tools for session management and memory retrieval.10111MIT
- AlicenseAqualityBmaintenanceMCP server for persistent, cross-session, local-first memory for AI agents, storing memories as Markdown files with SQLite indexing for hybrid search.24Apache 2.0
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/astragenie/astramem-local'
If you have feedback or need assistance with the MCP directory API, please join our Discord server