Skip to main content
Glama
astragenie
by astragenie

AstraMemory Local

npm version License: MIT Test

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 demo

This 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 | iex

If 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 existing codex login ChatGPT 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 pulls qwen2.5:7b (~4.7 GB — the tested/recommended local extraction model; the previously-tried qwen3:8b returned empty extraction output under thinking mode and was dropped) plus mxbai-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-stdio

This 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 commands

Single 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

decision

Architectural or design choice made during a session

"Use sqlite-vec for v1 vector storage"

local

fact

Objective project fact, configuration detail

"Port 7777 is the default daemon port"

local

lesson

Something that went wrong and how it was resolved

"sqlite-vec rowid must match memories rowid"

local

command

CLI command or script worth remembering

"bun run build && bun run test -- migrate"

local

todo

Outstanding work item surfaced in conversation

"Add reembed job when provider changes"

local

note

Freeform observation not fitting another type

"Team prefers PRs under 400 lines"

local

event

Something that happened at a point in time

"Deployed v0.8.1 to prod"

local

preference

User/agent preference atom

"User prefers terse commit messages"

cloud only (today)

task_result

Outcome of a completed task

"Migration 014 applied cleanly"

cloud only (today)

summary

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 budget.daily_usd)

$0 (local inference)

$0 (local inference)

~$0.02/1K tokens + $0.0001/1K embed tokens

Setup

codex login

ollama serve + ollama pull <model>

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

GET /health

Daemon health probe: { ok, version, wire_versions_supported, schema_version }

GET /version

Version discovery: { name, version, wire_versions_supported, schema_version, ts }

POST /ingest/transcript

Bearer

Capture protocol endpoint — accepts transcript and events kinds; idempotency via Idempotency-Key header. See docs/capture-protocol.md.

GET /search

Bearer

Hybrid search with type/repo/project/since filters + named preset routing. See docs/retrieval-presets.md.

POST /recall

Bearer

Top-K semantic recall (alias: search with k=5) + named preset routing

POST /recall/pack

Bearer

Token-budgeted memory pack for a repo/project/branch — powers the SessionStart hook

POST /remember

Bearer

Direct memory insert, bypasses distillation

GET /memory/:id

Bearer

Single memory lookup

GET /memory/:id/why

Bearer

Provenance receipt — extraction evidence + confidence for a memory

GET /memory/:id/history

Bearer

Full memory_events log for a memory (invalidate/supersede/promote chain)

POST /memory/:id/invalidate

Bearer

Soft-delete a memory (lifecycle op)

POST /memory/:id/supersede

Bearer

Replace a memory with a newer one, linked via memory_events

POST /memory/:id/promote

Bearer

Promote a memory's scope: personal → team → org

POST /memory/:id/used

Bearer

Record an explicit recall-usefulness signal for a memory

GET /sessions/:id/digest

Bearer

Session summary digest

GET /dashboard

Bearer, cookie, or one-time ?token= bootstrap

Read-only HTML metrics dashboard, auto-refreshing every 5s

POST /mcp

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

search_memory

Hybrid FTS + vector search with optional type/repo/project/since filters + named preset routing

GET /search

recall_memory

Top-K semantic recall (default k=5) + named preset routing

POST /recall

remember

Direct memory insert, bypasses distillation

POST /remember

get_health

Daemon health probe: { ok, version, wire_versions_supported, schema_version }

GET /health

why_memory

Provenance receipt — extraction evidence + confidence for a memory

GET /memory/:id/why

session_digest

Session summary digest

GET /sessions/:id/digest

agent_profile

Read-time per-agent "what has this agent learned" profile — top lessons, recent decisions, prior corrections

GET /agents/:agent/profile

user_profile

Read-time daemon-wide profile (no agent filter) — standing facts, preferences, recurring lessons, recent decisions, known entities. Optional ?scope=team|org|all (defaults to private)

GET /profile

get_entity_related

Entity-to-entity relation triples (entity_relations table, FEAT-481) for one entity — bitemporal as_of aware, optional direction (subject|object|both)

(MCP-only, no REST route)

get_entity_timeline

Memories mentioning an entity over time, newest-first, optional since lower bound

GET /entities/:id/memories

invalidate_memory

Soft-delete a memory (lifecycle op)

POST /memory/:id/invalidate

supersede_memory

Replace an old memory with a new one, linked via memory_events

POST /memory/:id/supersede

promote_memory

Promote a memory's scope: personal → team → org

POST /memory/:id/promote

restore_memory

Un-archive a memory previously archived by the W4.4 TTL policy or a manual archive event

POST /memory/:id/restore

memory_history

Full memory_events log for a memory

GET /memory/:id/history

submit_feedback

Explicit recall-usefulness signal — "this memory mattered" (U5 canonical name, shared with cloud)

POST /memory/:id/used

mark_memory_used

DEPRECATED one-release alias for submit_feedback (same handler) — migrate now

POST /memory/:id/used

erase_memory

Erasure v1 (ADR-006): permanently hard-delete a memory, leaving only a tombstone event

DELETE /memory/:id

list_consolidation_proposals

Pending (or resolved) stage-9 consolidation proposals awaiting a merge/keep decision (ADR-004)

GET /consolidation/proposals

resolve_consolidation_proposal

Accept or reject a pending consolidation proposal

POST /consolidation/proposals/:id/accept / .../reject

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).

--client

Target file

Written automatically?

claude-code

.mcp.json (project root)

Yes

cursor

~/.cursor/mcp.json

Yes

claude

Claude Desktop's claude_desktop_config.json (per-OS path)

Yes

windsurf

~/.codeium/windsurf/mcp_config.json

No — prints a manual snippet

vscode

.vscode/mcp.json

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:7777

Budget cap

The daily LLM spend cap (default: $10 USD) is enforced before each LLM call.

  • Ollama always reports $0 cost — the cap only applies to Azure usage.

  • When the cap is reached, pending distillation jobs move to paused state. 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:

  1. OS credential store — Windows Credential Manager / macOS Keychain / Linux libsecret, via @napi-rs/keyring.

  2. Key-file fallback<configDir>/db.key (mode 0600) 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:

  1. PEM private-key blocks (multiline, whole block).

  2. Vendor/pattern detectors — AWS access keys, GitHub tokens, Azure storage keys/SAS tokens, GCP API keys, Slack tokens, JWTs, generic key=value credentials, connection-string userinfo — plus any org-specific regexes from security.redaction.customPatterns.

  3. 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-plugin slice 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: personalteamorg (downward/same-scope transitions are rejected).

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

standing_facts

fact atoms

preferences

note atoms (the closest existing semantic match — see FEAT-451 design doc)

recurring_lessons

lesson atoms

recent_decisions

decision atoms, newest first

known_entities

linked entities of kind actor/tech/product/tool, ranked by mention count

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

astramem-local init [--no-hook]

Interactive wizard — writes config + secrets, runs migrations, installs service, offers the SessionStart memory-pack hook

astramem-local serve [--port N]

Start daemon in foreground (dev/debug)

astramem-local service install

Register daemon as a user-scope OS service

astramem-local service status

Show service state

astramem-local service start

Start the service

astramem-local service stop

Stop the service

astramem-local service uninstall

Remove the service unit

astramem-local doctor

Run all health checks, print table

astramem-local doctor --json

Machine-readable health check output

astramem-local search "<query>"

Hybrid search, print results table

astramem-local search "<query>" --type decision

Filter by memory type

astramem-local recall "<question>"

Top-5 semantic recall (alias for search k=5)

astramem-local remember "<text>" [--type]

Direct insert, bypasses distillation pipeline

astramem-local queue

Show pending/failed jobs

astramem-local queue --state failed

Show only failed jobs

astramem-local rebuild [--reembed]

Rebuild derived indexes; --reembed re-vectors all

astramem-local providers list

List configured providers and their health

astramem-local providers test [name]

Ping provider, print latency + dim

astramem-local budget

Show today and month spend vs cap

astramem-local budget --reset

Clear today's spend counter (override, logged)

astramem-local token print

Print the current Bearer token

astramem-local token rotate

Generate new token, invalidate the old one

astramem-local mcp install --client <name>

Write/update an MCP client's config to point at this daemon's POST /mcp (see MCP tools)

astramem-local mcp-stdio

Run the MCP tool surface over stdio, one process per client session — no daemon, no service install (see Two ways to run this)

astramem-local demo

30s zero-dependency trial: in-memory daemon, seeded example memories, one sample recall, no Ollama required


Further reading


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_of point-in-time filtering are correct end-to-end (migrations/006-atom-v3.sql, validityFilterClause() in src/search/search.ts) — the schema isn't the problem, and it isn't simply unpopulated either. AtomSchema (src/distill/prompts/extract.ts) has both valid_at and invalid_at, but extraction only fills in one of them in practice: a 2499-atom measurement found 423 distinct valid_from values (the valid_at half works) against zero non-null valid_to (the invalid_at half 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 before format:json output 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 to false for every model regardless of name; reasoning mode requires an explicit think: true override, so the class of bug is closed for the default path rather than patched per model name. doctor's llmChatProbe (src/doctor/probes/llm-chat-probe.ts) sends a real format:json request 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:8b was tried as the default and dropped after it returned empty extraction output); if you switch to another local reasoning model with think: true, verify with a real extraction run, not just doctor.


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 suite

See 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

Maintenance

ActivityMaintained
ResponsivenessSyncing

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

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Provides 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
  • A
    license
    A
    quality
    B
    maintenance
    MCP server for persistent, cross-session, local-first memory for AI agents, storing memories as Markdown files with SQLite indexing for hybrid search.
    24
    Apache 2.0

Latest Blog Posts

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