Memstack

io.github.isiomaC由 GitHub 帳號 isiomaC 發布v0.8.5更新於 Oct 24, 2026

MCP server for persistent, searchable memory for AI agents.

概覽

AI 產生的概覽

MemStack 為 AI 代理提供持久、可搜尋的專案記憶:儲存、檢索、摘要與清理代理跨工作階段學到的內容。

可以做什麼
  • 依標籤、重要性與 actor 識別碼儲存記憶,並支援批次寫入
  • 以語意、關鍵字或混合檢索找出相關記憶,並組裝受 token 預算限制的上下文
  • 對舊記憶做摘要、清理過期記憶,以及合併、計數、標記存取與刪除記憶
  • 透過工作階段啟動掛鉤讓 Claude Code 與 Codex 在同一專案共用一份記憶
  • 匯出與匯入完整狀態,用於備份或在不同儲存後端之間遷移
適用情境
當編碼代理或自建代理在工作階段之間總是忘記專案慣例與決策時適用,也適合希望自架記憶管線、而不是把全部歷史塞進上下文視窗的情境。
執行需求
以 npm 套件(@memstack/mcp)透過 stdio 在本機執行,需同時安裝 @memstack/cli 與儲存驅動(例如 better-sqlite3)。透過 memstack init 與 memstack connect 完成設定,會在 ~/.memstack 下寫入設定檔,並把伺服器註冊到 Claude Code 或 Codex。LLM 供應商金鑰為選用;未設定時記憶仍會儲存,只是沒有主題標籤。
安裝前請注意
設定檔會保存 LLM API 金鑰,OPENAI_API_KEY、ANTHROPIC_API_KEY 等環境變數可覆寫它。connect 會修改 ~/.claude.json、~/.claude/settings.json、~/.codex/config.toml 與 AGENTS.md 等代理設定檔,但會先備份,disconnect 可還原。摘要與清理會刪除或壓縮已存記憶,記憶內容會傳送給所設定的 LLM 供應商。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 Memstack,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

MemStack

Persistent memory for AI agents. Claude Code and Codex share one memory per project, and your own agents get the same pipeline: store, retrieve, summarize, and prune.

Website and install guides: memstack.stalewell.com

[npm version] [skills.sh] [MCP Registry] [memstack MCP server – quality and maintenance score on Glama] [CI] [License: MIT] [MCP Reference]

Tell one agent how your project works. The other already knows. Claude Code learns a convention; a new Codex session recalls it and gets the task right:

[Claude Code saves a project convention to MemStack; a new Codex session recalls it and writes pnpm install and fly deploy]

bash
npm install -g @memstack/cli @memstack/mcp better-sqlite3@^11.10.0   # MemStack never installs storage driversmemstack init                  # LLM provider (optional) + storememstack connect claude-codememstack connect codex

Every session then starts with the project's key memories, and both agents save what you ask them to remember. How harness memory works.

Building your own agent?

bash
# Use MemStack in your applicationnpm install @memstack/core
# Give your coding agents the MemStack skill (-g: every project; omit it for this project only)npx skills add isiomaC/memstack -g

@memstack/core is the runtime SDK; the Agent Skill teaches compatible coding agents how to integrate and operate MemStack correctly. Without -g, the skill installs into the current project (.agents/skills/, plus a .claude/skills/ link for Claude Code). Update it later with npx skills update.

The problem: AI agents forget. Every interaction starts from zero. You either stuff everything into the context window (expensive, slow, degrades output quality) or the agent has no memory of past conversations.

What MemStack does: A persistent memory pipeline that lives between your agent and the LLM. It stores every interaction, retrieves only what's relevant, summarizes old memories to save tokens, and prunes stale ones automatically. One method call, no infrastructure required.

Think of it as the open-source alternative to Mem0 — pluggable storage, bring your own LLM, zero vendor lock-in.

[memstack MCP server – quality and maintenance score on Glama]


Table of Contents


Why MemStack

LLMs have context windows, not memory. The difference matters.

ApproachProblem
Stuff everything in contextCost is O(n²). 100 conversations = thousands of tokens = dollars per call. Quality degrades from "lost in the middle" effect.
Use a vector DB directlyYou get similarity search. You don't get summarization, pruning, recency weighting, deduplication, or token budget management. You're building the pipeline yourself.
Use Mem0Proprietary, cloud-only with their hosted API. You don't control where your data lives.
Use MemStackFull pipeline. Pluggable everything. Your data, your infrastructure. Open source.

What MemStack handles that raw vector DBs don't:

  • Summarization — compress 100 old interactions into one paragraph, keep meaning, save tokens
  • Recency weighting — recent memories matter more; MemStack sorts them higher
  • Importance scoring — not all memories are equal; high-importance ones survive pruning
  • Deduplication — identical or near-identical memories are collapsed in context assembly
  • Token budget — compileContext() tells you how many tokens you're spending before the LLM call
  • Memory-type routing — interactions, summaries, observations treated differently at retrieval time
  • Auto-pruning — old, low-importance memories clean themselves up

Quick Start

Claude Code and Codex

Persistent memory across agent harnesses: what you tell Claude Code, Codex recalls in the same project, and the reverse.

bash
npm install -g @memstack/cli @memstack/mcp better-sqlite3@^11.10.0  # MemStack never installs storage drivers for youmemstack init                  # choose an LLM provider and a storememstack connect claude-codememstack connect codex

Then, in Claude Code: "Remember that this project uses Hono." In Codex, in the same repository: "What framework does this project use?" Codex answers Hono. See Harness Memory for how it works.

As a library

bash
npm install @memstack/core

OpenAI

typescript
import { MemStack, OpenAILLMAdapter, OpenAIEmbeddingAdapter, InMemoryStorageAdapter } from "@memstack/core";
const llm = new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! });
const memstack = new MemStack({  llm,  embedding: new OpenAIEmbeddingAdapter({ apiKey: process.env.OPENAI_API_KEY! }),  storage: new InMemoryStorageAdapter(),});

DeepSeek (no embeddings)

DeepSeek provides chat completions but has no embedding API. Use the OpenAI-compatible LLM adapter with baseURL and omit the embedding adapter — retrieval falls back to keyword + recency + importance ranking. You still get the full pipeline: store, summarize, prune, and compileContext.

typescript
import { MemStack, OpenAILLMAdapter, InMemoryStorageAdapter } from "@memstack/core";
const llm = new OpenAILLMAdapter({  apiKey: process.env.DEEPSEEK_API_KEY!,  baseURL: "https://api.deepseek.com/v1",  defaultModel: "deepseek-flash",});
const memstack = new MemStack({  llm,  storage: new InMemoryStorageAdapter(),  // No embedding adapter — retrieval uses keyword matching});

OpenRouter / Together AI / any OpenAI-compatible API

Same pattern — change baseURL and defaultModel:

typescript
// OpenRouterconst llm = new OpenAILLMAdapter({  apiKey: process.env.OPENROUTER_API_KEY!,  baseURL: "https://openrouter.ai/api/v1",  defaultModel: "openai/gpt-4o-mini",});
// Together AIconst llm = new OpenAILLMAdapter({  apiKey: process.env.TOGETHER_API_KEY!,  baseURL: "https://api.together.xyz/v1",  defaultModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo",});
// Gemini (OpenAI-compatible endpoint)const llm = new OpenAILLMAdapter({  apiKey: process.env.GEMINI_API_KEY!,  baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",  defaultModel: "gemini-2.0-flash",});

Store and retrieve

typescript
// 1. Store what happenedawait memstack.memory.store({  actorId: "support-bot-42",  content: "User reports login failing with error 503 on Chrome 125.",  tags: ["login", "bug", "chrome"],  importance: 0.8,});
// 2. Later, retrieve relevant contextconst memories = await memstack.memory.retrieve({  actorId: "support-bot-42",  query: "login error",  strategy: "hybrid",});
// 3. Assemble an LLM-ready contextconst ctx = await memstack.memory.compileContext({  actorId: "support-bot-42",  maxTokens: 2000,});
const response = await llm.complete({  system: `You are a support bot. Here is what you remember:\n${ctx.systemPrompt}`,  user: "The user is back and still can't log in. What do you do?",});
console.log(response.text);// "Based on our history, the user has been experiencing 503 errors on Chrome 125..."
// 4. Every 100 interactions, summarization triggers automatically.// Old interactions are compressed into a paragraph. Token costs stay flat.

Harness Memory (Claude Code & Codex)

Coding agents forget everything between sessions, and they don't share what they learn with each other. MemStack gives Claude Code and Codex one memory per project: a decision made with one agent is known to the other, and to every later session.

How it works

  • Saving. Each agent gets five MemStack tools (memory_store, memory_retrieve, memory_get, memory_delete, memory_stats) through the MCP harness profile. When you say "remember that…", or the agent learns a durable fact, decision, preference, or rule, it calls memory_store. MemStack asks your LLM for a few topic tags, so "uses Hono" can later be found by "which framework?". If tagging fails, the memory is still saved.
  • Recalling. A session-start hook loads the project's most important memories into every new session, so the agent starts out knowing them. During a session the agent calls memory_retrieve with plain questions. Recall runs locally with keyword ranking (BM25 with stemming) and never calls the LLM, so it is fast and works offline.
  • Scope. Memories belong to the current project. Preferences that apply everywhere can be saved as global (scope: "global") and are recalled in every project. One project can never read or delete another's memories.
  • Safety. Agents don't get bulk or destructive tools, and the LLM key stays in ~/.memstack/config.json (readable only by you), never in agent configs. Agents are told never to store secrets, and MemStack checks too: a memory that contains a credential (a private key, a cloud or source-host token, a JWT, a password in a connection string, a password = "..." assignment) is refused before the LLM or the store sees it, and the error names the kind of secret without repeating it. The check is pattern-based, so a secret in a format it doesn't know will pass; MEMSTACK_SECRET_POLICY=redact stores the memory with [REDACTED:<kind>] in place of the secret, and off disables the check.

Commands

CommandWhat it does
memstack initChoose an LLM provider (optional) and a store. Verifies the key with a real request and writes ~/.memstack/config.json. Without a key, memories are saved without topic tags and recall works the same. Non-interactive: --provider, --base-url, --model, --api-key-env <VAR>, --no-llm, --store, --path, --url, --yes.
memstack connect <claude-code|codex>Registers MemStack with the agent. Checks the server works first, and undoes everything if a step fails. --dry-run shows the changes; --no-hooks and --no-agents-md skip those parts.
memstack disconnect <claude-code|codex>Removes everything connect added. Your memories are kept.
memstack statusConfig, storage, the current project, and what each agent has connected.
memstack doctorDiagnoses setup problems and prints the fix for each. --live also tests the LLM key.
memstack memories [query]Lists or searches this project's memories. --global for global ones, --delete <id> to remove one.
memstack projectShows this repository's project ID. project pin <id> fixes it in .memstack.json; project merge <old-id> moves memories from an old ID.

What connect changes

connect changes only these entries, backs up each file first, and disconnect restores each file exactly:

AgentFileChange
Claude Code~/.claude.jsonA user-scope memstack MCP server, added with claude mcp add-json.
Claude Code~/.claude/settings.jsonA SessionStart hook running memstack-mcp hook session-start.
Codex~/.codex/config.tomlA memstack MCP server, added with codex mcp add.
Codex~/.codex/hooks.jsonA SessionStart hook. Codex runs it after you approve it once with /hooks.
Codex~/.codex/AGENTS.mdA short block between memstack:begin/memstack:end markers telling Codex to save memories with memory_store.

The registered command is the memstack-mcp you installed, run by absolute path, so it works even when the agent starts without your shell's PATH. Restart Claude Code, or start a new Codex session, to load it.

Projects

The project ID comes from the repository itself, with nothing stored, so it stays the same across clones, worktrees, renamed remotes, moved folders, new machines, and storage switches:

  1. a pinned ID in .memstack.json at the repository root (memstack project pin <id>; commit it to share it with your team);
  2. otherwise, the repository's first commit;
  3. for shallow clones, the origin remote;
  4. for repositories without commits, the git directory, and outside git, the folder. Memories saved before a repository's first commit move to the new ID automatically.

A fork shares its first commit with the original repository, so on one store the two share memories until one of them runs memstack project pin.

Storage

Any supported store works; pick it in memstack init. MemStack never installs storage drivers: install the one you need alongside @memstack/mcp.

StoreDriver to installNotes
sqlitebetter-sqlite3@^11.10.0Default; one local file, safe for both agents at once.
postgrespostgres@^3.4.9Shared across machines.
redisioredis@^5.11.1
disk, markdownnoneSingle process only; doctor warns when both agents use them.

Switching stores keeps project IDs; move memories with memstack export and memstack import.

Troubleshooting

Run memstack doctor first; it checks the config file and its permissions, the LLM key, the storage driver, each agent's registration, hook, and guidance, and starts the server to confirm it answers.

  • "memstack-mcp is not installed" or a missing driver: run the npm install -g … command it prints.
  • Codex doesn't load memories at session start: approve the MemStack hook in Codex with /hooks.
  • Codex doesn't save when asked: check memstack status shows the AGENTS.md guidance as present; reconnect if not.
  • Memories seem missing: memstack project shows which project you are in. Use memstack project merge <old-id> to bring memories from an old ID.

Details on the MCP server itself: docs/MCP_SETUP.md.


The Memory Pipeline

MemStack's core is a five-stage pipeline. Each stage can be used independently.

1. Store

Every agent interaction becomes a Memory with metadata that controls how it's retrieved, summarized, and pruned later.

typescript
interface Memory {  id: string;  actorId: string;               // Who this memory belongs to (user ID, agent ID, session ID)  memoryType: MemoryType;        // "interaction" | "summary" | "observation" | "fact" | "reflection" | "preference" | "decision" | "instruction"  content: string;               // The actual text  importance: number;            // 0-1 — higher = survives pruning, ranks higher in retrieval  emotionalValence: number;      // -1 to 1 — for tone-aware retrieval  tags: string[];                // Filter by tag: "bug", "billing", "urgent", etc.  embedding?: number[];          // Computed automatically if embedding adapter is configured  metadata?: Record<string, unknown>;  // Your custom fields  expiresAt?: Date;              // Auto-pruned after this date  sourceId?: string;             // Link back to the originating event  createdAt: Date;}
typescript
// Simple storeawait ms.memory.store({  actorId: "agent-7",  content: "Customer asked about refund policy for Q2 purchases.",  tags: ["billing", "refund"],});
// Batch store — embeddings are batched into one API call for efficiencyawait ms.memory.storeBatch([  { actorId: "agent-7", content: "First interaction" },  { actorId: "agent-7", content: "Second interaction" },  { actorId: "agent-7", content: "Third interaction" },]);

2. Retrieve

Pull back what's relevant — by keyword, by meaning (semantic), by recency, or by importance.

typescript
const memories = await ms.memory.retrieve({  actorId: "agent-7",              // Scope to one actor  query: "refund policy",          // What to search for  strategy: "hybrid",              // How to rank: "recent" | "important" | "semantic" | "hybrid"  limit: 10,                       // Max results  memoryTypes: ["interaction"],    // Only certain types  tags: ["billing"],               // Only certain tags});

Strategy behavior:

StrategySorts byRequires embeddingsBest for
recentNewest firstNoKnowing what just happened
importantHighest importance firstNoFiltering noise, keeping signal
semanticCosine similarity to queryYes"Find memories about X"
hybridSemantic + importance blendYesBest of both worlds

No embedding adapter? semantic and hybrid fall back to keyword matching + importance sort. No API costs, just less precise.

3. Compile Context

compileContext() takes retrieval results and assembles an LLM-ready system prompt — deduplicated, sorted by recency and importance, with a token estimate so you know the cost before calling the LLM.

typescript
const ctx = await ms.memory.compileContext({  actorId: "agent-7",  maxTokens: 2000,               // Budget — assembler stops when it hits this  memoryTypes: ["interaction", "summary"],});
// ctx.systemPrompt:// ## Important Memories// - The customer has been attempting login for 3 days. (importance: 0.85)// - Refund was processed for order #4521 on Jan 12. (importance: 0.72)// // ## Recent Interactions// - Customer asked about refund policy for Q2 purchases.// - Customer reported login error 503 on Chrome 125.
console.log(ctx.tokenEstimate);  // ~280
// Inject into your LLM callconst currentMessage = "The user is asking about their refund status.";const response = await llm.complete({  system: ctx.systemPrompt,  user: currentMessage,});
console.log(response.text);

compileContext() handles deduplication, token budgeting, and splits context into important-vs-recent sections. Without it, you'd be concatenating raw retrieval results and risking context-window overflow.

4. Summarize

When an actor has hundreds of interactions, retrieval gets expensive and context gets bloated. Summarization compresses old interactions into a single paragraph using the configured LLM.

typescript
const { summary, deletedCount } = await ms.memory.summarize({  actorId: "agent-7",  olderThan: new Date(Date.now() - 7 * 86400000),  // Older than 7 days  skipMostRecent: 10,        // Never touch the 10 most recent  targetCount: 50,           // Summarize at most 50 memories  memoryTypes: ["interaction"],  keepOriginals: false,      // Delete originals after summary});
// summary.content:// "Over the past week, the customer reported recurring login failures (error 503)//  on Chrome 125. Multiple troubleshooting attempts including cache clearing and //  password reset were unsuccessful. A refund was processed for order #4521."
console.log(deletedCount);   // 47 — 47 interactions compressed into 1 summary memory

Auto-summarization: Set summarizationThreshold in config (default: 100). Every 100th interaction for an actor triggers summarization automatically.

Warning: keepOriginals: false deletes the summarized memories. Set keepOriginals: true to preserve them alongside the summary.

Custom summarization prompt:

typescript
const ms = new MemStack({  llm,  defaults: {    summarizationPrompt:      "You are an enterprise support memory compressor. Highlight: customer name,       product, severity, resolution status, and any open issues.",  },});

5. Prune

Not all memories deserve to live forever. Pruning removes low-value memories to keep storage and retrieval fast.

typescript
// Remove memories older than 30 daysawait ms.memory.prune({ type: "byAge", maxAge: 30 * 86400000 });
// Keep only memories above importance 0.3await ms.memory.prune({ type: "byImportance", minImportance: 0.3 });
// Keep at most 500 memories per actorawait ms.memory.prune({ type: "byCount", maxPerActor: 500 });
// Remove specific typesawait ms.memory.prune({ type: "byType", memoryTypes: ["observation"] });
// Custom logicawait ms.memory.prune({  type: "custom",  shouldRemove: (memory) => memory.content.includes("[RESOLVED]"),});
// Dry run first — see what would be removedconst { wouldPrune, count } = await ms.memory.dryRunPrune({  type: "byAge",  maxAge: 86400000,});console.log(`Would remove ${count} memories:`, wouldPrune);

Auto-prune on every process() call by setting pruneStrategy in config:

typescript
const ms = new MemStack({  llm,  defaults: {    pruneStrategy: { type: "byImportance", minImportance: 0.05 },  },});

Real-World Use Cases

Support Agent

typescript
// detectUrgency and classifyIntent are your own business logic.// They could be simple keyword matchers, regex, or an LLM call.function detectUrgency(msg: string): number {  if (msg.match(/urgent|asap|immediately/i)) return 0.9;  if (msg.match(/error|fail|broken/i)) return 0.7;  return 0.5;}
function classifyIntent(msg: string): string[] {  const tags: string[] = [];  if (msg.match(/bill|refund|charge|payment/i)) tags.push("billing");  if (msg.match(/error|bug|fail|crash/i)) tags.push("bug");  if (msg.match(/login|password|account/i)) tags.push("account");  return tags;}
// Every customer message becomes a memoryasync function handleMessage(customerId: string, message: string) {  await ms.memory.store({    actorId: `customer:${customerId}`,    content: message,    importance: detectUrgency(message),    tags: classifyIntent(message),  });
  // Retrieve everything relevant to this customer's history  const ctx = await ms.memory.compileContext({    actorId: `customer:${customerId}`,    maxTokens: 1500,  });
  const response = await llm.complete({    system: `You are a support agent. Customer history:\n${ctx.systemPrompt}`,    user: message,  });
  return response.text;}
// Every 100th interaction, old history auto-compresses.// A customer with 10,000 messages still fits in a $0.02 LLM call.

RAG Pipeline

typescript
// Suppose you have documents from your knowledge baseconst documents = [  { text: "Authentication uses JWT tokens with 15-minute expiry.", url: "/docs/auth", section: "security" },  { text: "Refunds are processed within 5-10 business days.", url: "/docs/billing", section: "billing" },];
// Index documents as observation memoriesfor (const doc of documents) {  await ms.memory.store({    actorId: "knowledge-base",    content: doc.text,    memoryType: "observation",    metadata: { source: doc.url, section: doc.section },  });}
// Query with semantic searchconst relevantDocs = await ms.memory.retrieve({  actorId: "knowledge-base",  query: "How does authentication work?",  strategy: "semantic",  limit: 5,});
const ctx = await ms.memory.compileContext({  actorId: "knowledge-base",  memoryTypes: ["observation"],});
// Prompt the LLM with retrieved contextconst answer = await llm.complete({  system: `Answer using only these documents:\n${ctx.systemPrompt}`,  user: "How does authentication work?",});

Multi-User Chatbot

typescript
// Each user gets their own memory spaceasync function chat(userId: string, message: string) {  await ms.memory.store({    actorId: userId,    content: message,  });
  const ctx = await ms.memory.compileContext({    actorId: userId,    maxTokens: 1000,  });
  return llm.complete({    system: `You are a friendly assistant. Conversation history with this user:\n${ctx.systemPrompt}`,    user: message,  });}
// Get statsconst total = await ms.memory.count();const userCount = await ms.memory.count({ actorId: "user-42" });

Memory Type Reference

TypePurposeExample
interactionDefault. Direct exchanges between agent and user/other agent."User asked about billing."
summaryCompressed collection of old interactions. Created by summarize()."Over 3 weeks, user reported 5 login failures..."
observationPassive knowledge — facts, documents, things the agent knows but didn't interact with."Company refund policy is 30 days from purchase."
factVerified knowledge — discrete truths the agent has confirmed."The user's subscription tier is Enterprise."
reflectionSelf-generated insight — the agent thinking about its own experiences."I tend to over-explain billing policies — should be more concise."
preferenceHow the user likes things done."Prefer small pull requests with one concern each."
decisionA choice made, ideally with its reason."Chose Hono over Express for edge runtime support."
instructionA standing rule to follow."Never commit directly to main."

Types control retrieval behavior — compileContext() treats interaction and summary differently from observation. Use types to separate "what happened" from "what I know."


Retrieval Strategies

Four strategies, each with a purpose:

typescript
// "What just happened?" — most recent firstawait ms.memory.retrieve({ actorId: "x", strategy: "recent", limit: 3 });
// "What matters most?" — highest importance, ignoring ageawait ms.memory.retrieve({ actorId: "x", strategy: "important" });
// "What relates to this query?" — cosine similarity search (needs embeddings)await ms.memory.retrieve({ actorId: "x", query: "login bug", strategy: "semantic" });
// "Balance relevance and importance" — semantic + importance blendawait ms.memory.retrieve({ actorId: "x", query: "login bug", strategy: "hybrid" });

Choosing a strategy:

  • Use recent for chatbots, ongoing conversations, anything time-sensitive
  • Use important for long-running agents where signal-to-noise matters
  • Use semantic for RAG, document search, knowledge base queries
  • Use hybrid for most agent memory — it balances meaning with significance

Keyword recall on any storage adapter

LexicalRetriever answers natural questions without embeddings or an LLM call, and behaves the same on every storage adapter. It loads the memories in the given actors through retrieve(), then ranks them in MemStack with BM25 over content and tags, with stemming and prefix matching. When nothing matches, it returns the most important memories instead.

typescript
import { LexicalRetriever } from "@memstack/core";
const retriever = new LexicalRetriever(storage);const { hits, fallback } = await retriever.recall({  actorIds: ["project:abc", "global"], // Searched together  query: "What framework does this project use?",  limit: 10,                           // Max results  maxChars: 8000,                      // Max total content; the top hit is always returned});

Up to 2,000 memories per actor are ranked (candidateLimit). Only returned memories are marked as accessed.


Embeddings

Embeddings power semantic search. They're optional — without them, retrieval uses keyword matching.

With embeddings vs Without embeddings

With embeddings (embedding adapter configured):

typescript
import { MemStack, OpenAILLMAdapter, OpenAIEmbeddingAdapter, InMemoryStorageAdapter } from "@memstack/core";
const ms = new MemStack({  llm: new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! }),  embedding: new OpenAIEmbeddingAdapter({ apiKey: process.env.OPENAI_API_KEY! }),  storage: new InMemoryStorageAdapter(),});
// store() computes a 1536-dim vector automaticallyawait ms.memory.store({  actorId: "agent-7",  content: "Customer asked about refund policy for Q2 purchases.",});
// retrieve() with "semantic" or "hybrid" uses cosine similarity// Query: "refund" finds the refund policy memory even though the word "refund"// appears differently across stored memories.const results = await ms.memory.retrieve({  actorId: "agent-7",  query: "how do I get my money back",  strategy: "semantic",});// Matches "Customer asked about refund policy" — semantic match, not keyword match.

Without embeddings (no embedding adapter):

typescript
const ms = new MemStack({  llm: new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! }),  storage: new InMemoryStorageAdapter(),  // no embedding adapter});
// store() works identically, just no vector computedawait ms.memory.store({  actorId: "agent-7",  content: "Customer asked about refund policy for Q2 purchases.",});
// retrieve() with "semantic" or "hybrid" falls back to keyword matching// plus importance/recency sorting. No API costs, no setup required.const results = await ms.memory.retrieve({  actorId: "agent-7",  query: "refund",  strategy: "hybrid", // falls back to keyword + importance});// Still works — finds "refund" via substring match. Less precise for// paraphrased queries ("money back" won't match "refund").

Batch embedding: storeBatch() sends all texts in one embedding API call, reducing cost and latency.

typescript
// Disable auto-embedding if you only need keyword searchconst ms = new MemStack({  llm,  embedding: new OpenAIEmbeddingAdapter({ apiKey }),  defaults: { embedOnStore: false },});

Vector dimensions and model compatibility

Different embedding models produce vectors of different lengths. Cosine similarity only works between vectors of the same dimension. If you change embedding models, existing vectors become incompatible — they can't be compared to new ones.

AdapterDefault modelDimensions
OpenAIEmbeddingAdaptertext-embedding-3-small1536
OpenAIEmbeddingAdaptertext-embedding-3-large3072
CohereEmbeddingAdapterembed-english-v3.01024
CohereEmbeddingAdapterembed-english-light-v3.0384
CohereEmbeddingAdapterembed-english-v2.04096
CohereEmbeddingAdapterembed-multilingual-v3.01024

What happens if dimensions don't match: If you store memories with one model (e.g., 1536 dims) then switch to another model (e.g., 1024 dims), the storage adapter receives query vectors and stored vectors of different lengths. Cosine similarity between vectors of different dimensions is undefined — results depend on the storage backend's behavior. Most will either error, return empty results, or produce meaningless scores.

Recommendation: Pick one embedding model per storage instance and stick with it. If you need to switch models, create a new storage instance and re-embed from scratch.

DeepSeek users: DeepSeek has no embeddings API. If you use DeepSeek as your LLM, you must either:

  1. Omit the embedding adapter and use "recent" or "important" retrieval strategies (no API costs, less precise)
  2. Pair DeepSeek with a separate embedding provider (e.g., OpenAI for embeddings, DeepSeek for chat)

Adapters

MemStack is provider-agnostic. Every boundary is an interface — bring your own LLM, embedding model, and storage backend.

LLM Adapters

Used by summarize() and compileContext(). Ships with OpenAI, Anthropic, Ollama, and Groq built-in — and via baseURL, the OpenAI adapter works with any OpenAI-compatible API (DeepSeek, Mistral, Gemini, Together AI, Perplexity, Fireworks, xAI, and dozens more).

typescript
// OpenAIimport { OpenAILLMAdapter } from "@memstack/core";const llm = new OpenAILLMAdapter({ apiKey: "..." });
// Any OpenAI-compatible API — just change baseURLconst deepseek = new OpenAILLMAdapter({ apiKey: "...", baseURL: "https://api.deepseek.com/v1" });const mistral = new OpenAILLMAdapter({ apiKey: "...", baseURL: "https://api.mistral.ai/v1" });const together = new OpenAILLMAdapter({ apiKey: "...", baseURL: "https://api.together.xyz/v1" });
// Anthropicimport { AnthropicLLMAdapter } from "@memstack/core";const llm = new AnthropicLLMAdapter({  apiKey: process.env.ANTHROPIC_API_KEY!,  defaultModel: "claude-sonnet-4-5-20250929",});
// Ollama (built-in)import { OllamaLLMAdapter } from "@memstack/core";const llm = new OllamaLLMAdapter({  baseURL: "http://localhost:11434",  defaultModel: "llama3.2",});

Embedding Adapters

Used by semantic retrieval. Ships with OpenAI and Cohere built-in — and via baseURL, the OpenAI adapter works with any OpenAI-compatible embedding API (Together AI, Voyage AI, Jina, Nomic, and more).

typescript
import { OpenAIEmbeddingAdapter, CohereEmbeddingAdapter } from "@memstack/core";
// OpenAInew OpenAIEmbeddingAdapter({ apiKey: "...", model: "text-embedding-3-small" }); // 1536 dims
// Coherenew CohereEmbeddingAdapter({ apiKey: "..." }); // embed-english-v3.0, 1024 dims
// Any OpenAI-compatible embedding APInew OpenAIEmbeddingAdapter({ apiKey: "...", baseURL: "https://api.voyageai.com/v1", model: "voyage-3" });

Storage Adapters

MemStack contains 18 storage-adapter implementations. Twelve are exported from @memstack/core; six remain experimental source implementations. Core has no runtime dependencies, and database clients are injected by callers.

Support levels:

  • Production-ready means exported from the public package, covered by unit tests, and supported as part of the public API.
  • Real-service E2E verified means the adapter also passes against its actual database implementation in pnpm test:e2e.
  • Mock-tested means unit coverage uses an injected fake client rather than a live cloud service.
  • Experimental means implemented in source but not exported from the published package.

Public package exports

Built-in (zero external deps):

AdapterBackendUse case
InMemoryStorageAdapterIn-memory MapTesting, prototyping
DiskStorageAdapterLocal JSON filesSimple local persistence
MarkdownStorageAdapterAppend-only .md filesHuman-readable, git-diffable, debug-friendly
HybridStorageAdapterCompose any two StorageProvidersCache + durable, edge + durable

Relational / SQL:

AdapterBackendVector search
PostgresStorageAdapterPostgreSQL + pgvectorHNSW native
SQLiteStorageAdapterSQLite (better-sqlite3)Cosine in-memory

Vector databases:

AdapterBackend
QdrantStorageAdapterQdrant
WeaviateStorageAdapterWeaviate
LanceDBStorageAdapterLanceDB
MongoDBStorageAdapterMongoDB Atlas Vector Search

Cache / KV:

AdapterBackend
RedisStorageAdapterRedis (ioredis)

Graph:

AdapterBackend
Neo4jStorageAdapterNeo4j

Experimental (mock-tested or missing an optional E2E capability)

These implementations are available to source contributors but are not part of the published package API.

AdapterBackendBlocker
TursoStorageAdapterTurso (libsql)Cloud-only (needs Turso account)
ChromaStorageAdapterChromaDBEmbedding function dependency
PineconeStorageAdapterPineconeCloud-only (needs API key)
UpstashStorageAdapterUpstash Redis + VectorCloud-only (needs API key)
Mem0StorageAdapterMem0 OSS or CloudCloud-only (needs API key)
ZepStorageAdapterZep Cloud or CECloud-only (needs API key)

Live cloud compatibility remains unverified for Pinecone, Upstash, Mem0, Zep, and Turso. Chroma's real-client E2E suite is skipped when its optional default embedding function is unavailable. LLM and embedding-provider tests use mocks; live-provider testing is opt-in and is not part of CI.

Quick-start per backend:

ts
// Postgresimport { PostgresStorageAdapter } from "@memstack/core";const storage = new PostgresStorageAdapter({ connectionString: "postgres://..." });
// Redisimport Redis from "ioredis";import { RedisStorageAdapter } from "@memstack/core";const storage = new RedisStorageAdapter({ redis: new Redis() });
// Markdown (append-only, human-readable)import { MarkdownStorageAdapter } from "@memstack/core";const storage = new MarkdownStorageAdapter({ dir: "./memories" });
// Hybrid (Redis cache + Postgres durable)import { HybridStorageAdapter } from "@memstack/core";const storage = new HybridStorageAdapter({  cache: new RedisStorageAdapter({ redis: new Redis() }),  durable: new PostgresStorageAdapter({ connectionString: "postgres://..." }),});

Custom storage:

ts
import type { StorageProvider, MemoryStoreInput } from "@memstack/core";
class MyStorage implements StorageProvider {  async store(input: MemoryStoreInput): Promise<Memory> { /* ... */ }  async get(id: string): Promise<Memory | null> { /* ... */ }  async retrieve(query: MemoryRetrieveQuery, embedding?: number[]): Promise<Memory[]> { /* ... */ }  async count(filter?: MemoryCountFilter): Promise<number> { /* ... */ }  async delete(id: string): Promise<void> { /* ... */ }  async deleteMany(ids: string[]): Promise<number> { /* ... */ }  async storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]> { /* ... */ }  async initialize(): Promise<void> { /* ... */ }  async close(): Promise<void> { /* ... */ }}

Backend Comparison

BackendVector searchTouchStatus
InMemoryCosine in-memoryYes✅ Production
Disk (JSON)Keyword + importanceYes✅ Production
MarkdownKeyword + importanceNo✅ Production
Postgrespgvector HNSWYes✅ Production
RedisRediSearch KNN (auto-detect)Yes✅ Production
QdrantANN nativeNo✅ Production
WeaviateBM25 + vector hybridNo✅ Production
LanceDBDiskANN nativeNo✅ Production
MongoDBAtlas Vector SearchNo✅ Production
Neo4jNeo4j vector indexNo✅ Production
HybridDelegates to cache/durableIf durable supports✅ Production
SQLiteCosine in-memoryYes✅ Production

Full API Reference

MemStack Client

typescript
import { MemStack } from "@memstack/core";
const ms = new MemStack({  llm: LLMProvider,                    // Required — for summarization  embedding?: EmbeddingProvider,       // Optional — for semantic search  storage?: StorageProvider,           // Optional — defaults to InMemoryStorageAdapter  defaults?: {    summarizationThreshold?: number,   // Auto-summarize every N process() calls. Default: 100    embedOnStore?: boolean,            // Auto-embed on store(). Default: true    pruneStrategy?: PruneStrategy,     // Auto-prune during process() (throttled). Default: disabled    pruneInterval?: number,            // Run auto-prune every N process() calls. Default: 100    autoImportance?: boolean,          // LLM-score importance in process() when not provided. Default: false    autoTags?: boolean,                // LLM-extract tags in process() when not provided. Default: false    summarizationPrompt?: string,      // Custom prompt for the summarizer  },  hooks?: {    onMemoryStored?: (memory: Memory) => void;    onMemoryPruned?: (ids: string[]) => void;    onSummaryCreated?: (summary: Memory, deletedCount: number) => void;    onError?: (error: Error, context: string) => void;  },});

Auto-behaviors run inside process(), not store(). process() tracks a per-actor call count: summarization fires every summarizationThreshold calls, and pruning fires every pruneInterval calls (when pruneStrategy is set). store() is the low-level write and never triggers these.

Memory Subsystem

All methods accessible via ms.memory.*:

typescript
// Storems.memory.store(input: MemoryStoreInput): Promise<Memory>ms.memory.storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]>
// Retrievems.memory.retrieve(query: MemoryRetrieveQuery): Promise<Memory[]>ms.memory.get(id: string): Promise<Memory | null>
// Context assemblyms.memory.compileContext(options: ContextOptions): Promise<CompiledContext>
// Lifecyclems.memory.summarize(options: SummarizeOptions): Promise<{ summary: Memory; deletedCount: number }>ms.memory.prune(strategy: PruneStrategy): Promise<{ pruned: string[]; count: number }>ms.memory.dryRunPrune(strategy: PruneStrategy): Promise<{ wouldPrune: string[]; count: number }>
// Managementms.memory.count(filter?: MemoryCountFilter): Promise<number>ms.memory.delete(id: string): Promise<void>ms.memory.deleteMany(ids: string[]): Promise<number>ms.memory.touch(id: string): Promise<void>ms.memory.purgeActor(actorId: string): Promise<number>ms.memory.merge(ids: string[]): Promise<Memory>ms.memory.stats(actorId?: string): Promise<MemoryStats>ms.memory.summarizeStream(options: SummarizeOptions): AsyncIterable<{ chunk: string; text: string }>

Export / Import

Snapshot and restore full state for persistence, backups, or migration:

typescript
import * as fs from "node:fs";
// Saveconst snapshot = await ms.export();fs.writeFileSync("state.json", JSON.stringify(snapshot, null, 2));
// Restoreconst data = JSON.parse(fs.readFileSync("state.json", "utf-8"));await ms2.import(data);

Each memory's original createdAt is preserved on import, so export → import is a lossless round-trip — safe for backups and cross-backend migration (e.g. disk → Postgres). All storage adapters honor a createdAt supplied on store()/storeBatch(); when omitted, they default to the current time.

Health & Close

typescript
const status = await ms.health();// { storage: true, llm: true, embedding: true }
await ms.close(); // graceful shutdown

Harness Memory API

The harness features are built on HarnessMemory, which works with any storage adapter. Use it to build your own agent integration:

typescript
import { HarnessMemory, defaultRecallNamespaces, projectNamespace } from "@memstack/core";
const memory = new HarnessMemory({ storage, llm });
await memory.remember({  namespace: projectNamespace("acme-api"),   // or "global"  content: "This project uses Hono",  kind: "decision",                           // fact | preference | decision | instruction | observation | ...  source: { harness: "my-agent", project: "acme-api" },});
const { hits, fallback } = await memory.recall({  namespaces: defaultRecallNamespaces("acme-api"), // project, then global  query: "Which framework do we use?",  limit: 10,  maxChars: 8000,});
await memory.get(id, namespaces);       // null outside namespacesawait memory.forget(id, namespaces);    // refuses ids outside namespacesawait memory.stats(namespaces);         // { "project:acme-api": 3, global: 1 }await memory.moveNamespace(from, to);   // e.g. when a project's ID changes

remember asks the LLM for topic tags (disable with autoTags: false); every other method makes no LLM call. recall uses LexicalRetriever.


Configuration

typescript
const ms = new MemStack({  llm: new OpenAILLMAdapter({ apiKey: "..." }),
  // Defaults control auto-behavior (all applied during process())  defaults: {    summarizationThreshold: 50,      // Summarize every 50 process() calls (default: 100)    embedOnStore: false,             // Don't auto-embed — saves API costs    pruneStrategy: {                 // Auto-clean during process(), throttled by pruneInterval      type: "byAge",      maxAge: 90 * 86400000,         // 90 days    },    pruneInterval: 100,              // Run the prune check every 100 process() calls (default: 100)    autoImportance: true,            // Let the LLM score importance when you don't pass one    autoTags: true,                  // Let the LLM extract tags when you don't pass any  },
  // Hooks for observability  hooks: {    onMemoryStored: (m) => logger.debug("memory:stored", { id: m.id, actor: m.actorId }),    onMemoryPruned: (ids) => logger.info("memory:pruned", { count: ids.length }),    onSummaryCreated: (summary, n) => logger.info("memory:summarized", { count: n }),    onError: (err, context) => logger.error("memory:error", { context, message: err.message }),  },});

Harness configuration file

The CLI and the MCP harness profile read ~/.memstack/config.json (or $MEMSTACK_HOME/config.json), which memstack init writes with permissions readable only by you:

json
{  "version": 1,  "llm": { "provider": "openai-compatible", "apiKey": "…", "baseURL": "https://api.deepseek.com", "model": "deepseek-flash" },  "storage": { "type": "sqlite", "path": "/Users/you/.memstack/memstack.db" }}

Environment variables override the file one section at a time: any LLM variable (OPENAI_API_KEY, ANTHROPIC_API_KEY, MEMSTACK_OPENAI_BASE_URL, MEMSTACK_LLM_MODEL) replaces the whole llm section, and MEMSTACK_STORAGE replaces the whole storage section, so a key is never sent to another provider's URL.


Advanced Usage

Custom Storage

Implement StorageProvider for any database. The interface is 9 methods. See the reference section above for the full contract.

Optional members, none of them required:

  • capabilities: { multiProcess?, textSearch? } declares whether several processes can share the store safely and whether search() is native.
  • search(query) provides native full-text search. LexicalRetriever uses it when textSearch is declared and ranks memories itself otherwise.
  • retrieve() should honor touch: false by returning memories without marking them as accessed.

SQLiteStorageAdapter enables WAL and a 5-second busy timeout so several processes can share one database file. Set walMode: false or busyTimeoutMs to change this.

Custom LLM / Embedding

Implement LLMProvider or EmbeddingProvider for any service:

typescript
import type { LLMProvider } from "@memstack/core";
class TogetherAIAdapter implements LLMProvider {  async complete(req: { system: string; user: string; model?: string }) {    const res = await fetch("https://api.together.xyz/v1/chat/completions", {      headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" },      body: JSON.stringify({ model: req.model, messages: [{ role: "system", content: req.system }, { role: "user", content: req.user }] }),    });    const data = await res.json() as any;    return { text: data.choices[0].message.content, tokens: { prompt: data.usage.prompt_tokens, completion: data.usage.completion_tokens, total: data.usage.total_tokens } };  }}

Event Hooks

Monitor memory operations without modifying code:

typescript
const ms = new MemStack({  llm,  hooks: {    onMemoryStored: (m) => metrics.increment("memory.stored"),    onSummaryCreated: (_, n) => metrics.gauge("memory.summarized_count", n),    onMemoryPruned: (ids) => metrics.increment("memory.pruned", ids.length),  },});


LongMemEval-S Retrieval Benchmark

MemStack includes a Val benchmark pack for retrieval-only evidence-session recall on the public, cleaned LongMemEval-S split. The runner stores each question's sessions through MemStack's harness memory API, uses the local lexical retriever and in-memory storage, and does not call an answer model or judge. Secret scanning is disabled only for this benchmark's public corpus in the temporary in-memory store; do not use the runner with private data. Its recall metrics are not end-to-end QA accuracy and are not directly comparable to the official LongMemEval QA leaderboard. The published dataset is public, not a hidden evaluation set.

The dataset is not included in this repository. Validate the pack, then fetch only its explicitly named split with Val:

bash
val benchmark validate benchmarks/longmemeval/manifest.jsonval benchmark fetch benchmarks/longmemeval/manifest.json --split public-evaluation

Val prints the SHA-256-addressed cache path. Pass it to the runner; --limit is a partial diagnostic run and must not be reported as the full benchmark:

bash
pnpm benchmark:longmemeval -- --dataset "$HOME/.cache/val/benchmark-packs/objects/d6f21ea9d60a0d56f34a05b609c79c88a451d2ae03597821ea3d5a9678c3a442" --limit 5pnpm benchmark:longmemeval -- --dataset "$HOME/.cache/val/benchmark-packs/objects/d6f21ea9d60a0d56f34a05b609c79c88a451d2ae03597821ea3d5a9678c3a442"

The runner rechecks the exact declared size and SHA-256 before reading the file. It streams the JSON dataset, evaluates the default K values 1, 3, 5, and 10, excludes abstention questions marked with the upstream _abs question-ID suffix, and prints macro/micro evidence-session recall, question hit rate, and median/p95 retrieval latency. The upstream LongMemEval attribution and license are recorded in the manifest; this pack is not an official LongMemEval result.


Development

Setup & Tests

bash
git clone https://github.com/isiomaC/memstack.gitcd memstackpnpm install
pnpm test             # 407 core tests, no external services neededpnpm test:packages    # 78 package tests after dependency-ordered buildspnpm test:e2e         # 80 pass, 1 optional Chroma skip (requires Docker services)pnpm test:e2e:run     # Start services, run E2E once, preserve failure logs, clean uppnpm smoke:artifacts  # Built core, CLI, MCP, and server black-box checkspnpm smoke:packages   # Pack and install publishable tarballs in a clean projectpnpm smoke:docker     # Build and exercise the server imagepnpm verify           # Complete local verification pipelinepnpm test:watch       # Watch core testspnpm build:all        # Build core and all workspace packagespnpm check:all        # Type-check core and all workspace packagespnpm check:benchmark  # Type-check the LongMemEval benchmark runner

CI exposes a stable verification job. Configure that job as a required status check in GitHub branch protection for main.

Debugging

Use hooks for observability — MemStack has no built-in logging:

typescript
const ms = new MemStack({  llm,  hooks: {    onMemoryStored: (m) => console.debug("[memstack] stored:", m.id, m.content.slice(0, 80)),    onMemoryPruned: (ids) => console.debug("[memstack] pruned:", ids.length),  },});

Common issues:

SymptomCauseFix
CONFIG_ERROR: LLM provider is requiredNo LLM adapterPass any LLMProvider to config
Empty retrieval resultsWrong actorId or no memories storedCheck await ms.memory.count({ actorId })
Semantic search not workingNo embedding adapter or embedOnStore: falseAdd embedding adapter or use strategy: "recent"
High memory usage in productionUsing InMemoryStorageAdapterImplement StorageProvider for Postgres/Redis/etc
Poor summarization qualityDefault prompt doesn't match your domainUse summarizationPrompt in defaults config

Inspecting state at runtime:

typescript
// How much data do we have?const total = await ms.memory.count();const perActor = await ms.memory.count({ actorId: "user-42" });
// What does one actor's memory look like?const snapshot = await ms.export();const actorMemories = snapshot.memories.filter(m => m.actorId === "user-42");console.log(`User-42: ${actorMemories.length} memories`);actorMemories.forEach(m => console.log(`  [${m.memoryType}] ${m.content.slice(0, 60)} (imp: ${m.importance})`));

Publishing to npm

bash
# Bump version, then:pnpm build && pnpm check && pnpm testnpm loginnpm publish --access public

The @memstack scope requires --access public.


Contributing

Most needed contributions:

  • LLM adapters: Google Gemini (native), Amazon Bedrock, Vertex AI
  • Embedding adapters: local inference (transformers.js, ONNX)
  • Benchmarks: retrieval quality, latency, cost comparisons
  • Python port: pip install memstack

Open an issue or PR at github.com/isiomaC/memstack.


License

MIT © MemStack

來源:README.md,提交 8518a3f

工具

0
SourceWeft 尚未在沙箱中啟動過該軟體包,因此其工具未知。

版本歷史

4
  1. v0.8.5最新Oct 8, 2026
  2. v0.8.1Oct 4, 2026
  3. v0.8.0Oct 3, 2026
  4. v0.7.2Sep 16, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分