AgentStack
MCP verified MIT Self-run

Mempalace Node

mcp-cn-syndra-mempalace-node · by CN-Syndra

Local AI memory system for Node.js — verbatim storage, semantic search, knowledge graph, MCP server. Port of the Python MemPalace.

No reviews yet
0 installs
14 views
0.0% view→install

Install

$ agentstack add mcp-cn-syndra-mempalace-node

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Mempalace Node? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

mempalace-node

[中文文档](./README.zh.md)

Node.js port of MemPalace. The Python original is the highest-scoring AI memory system on the LongMemEval benchmark (96.6% Recall@5). This port preserves storage and retrieval semantics with full-precision brute-force cosine similarity (no HNSW approximation), so retrieval accuracy should match the Python original. The default SQLite backend handles up to ~100K drawers per palace; once you outgrow that, the pluggable VectorStore interface lets you migrate to LanceDB without touching application code.

Fully local. Fully offline. No API calls. No cloud dependencies.

Features

| Feature | Description | |---|---| | Verbatim storage | No summarization. Original text preserved in full. | | 4-Layer memory stack | L0 identity / L1 essential / L2 on-demand / L3 deep search | | Pluggable vector store | Swap backends via setStoreBackend() (SQLite default, LanceDB ready) | | Palace graph | BFS traversal across rooms via shared wings (tunnels) | | Knowledge graph | Temporal entity-relationship triples with valid-from/valid-to | | AAAK Dialect | Optional lossy compression with entity codes + emotion markers | | Entity registry | Persistent personal name registry with Wikipedia lookup | | Entity detector | Auto-detect people/projects from text via signal scoring | | General extractor | Extract decisions / preferences / milestones / problems / emotional | | Room auto-detection | Infer rooms from folder structure or filename patterns | | Conversation mining | Ingest Claude Code, ChatGPT, Slack, Codex chat exports | | MCP server | 19-tool JSON-RPC server for Claude Desktop / Cursor / Cline | | CLI hooks | session-start, stop, precompact for auto-save workflows | | Spell correction | Optional, preserves technical terms and entity names | | Mega-file splitter | Split concatenated transcripts into per-session files | | Multilingual | Chinese, English, Japanese, Korean, 50+ languages out of the box |

Performance

Real numbers from tests/test-benchmark.ts. Run node dist/tests/test-benchmark.js to reproduce on your hardware.

Hardware: Apple M1 (8 cores), Node 24.7, multilingual model (384-dim)

| Drawers | Cold query | Warm query | Filtered query | Heap | Worker path | |--------:|-----------:|-----------:|---------------:|-------:|-------------| | 100 | 4ms | 5ms | 3ms | 231MB | — | | 1,000 | 6ms | 6ms | 3ms | 247MB | — | | 5,000 | 89ms | 28ms | 7ms | 255MB | engaged | | 10,000 | 151ms | 142ms | 20ms | 239MB | engaged |

What this means in practice:

  • Filtered queries scale almost flat thanks to metadata pre-filtering — a

10K palace filtered down to 1K candidates queries in 20ms, the same shape as a raw 1K palace would. Always pass where: { wing: ... } when you know it.

  • Cold vs warm gap shrinks at scale because the dominant cost shifts from

BLOB decoding to actual cosine math.

  • Worker thread path engages above 5000 candidates, distributing the cosine

work across CPU cores via SharedArrayBuffer (zero-copy).

  • Heap stays bounded around 250MB — the streaming top-K loader never holds

more than 1000 vectors in memory at a time, regardless of palace size.

For larger palaces (100K+) or sub-50ms latency at scale, swap to the LanceDB backend — see [Vector Store Architecture](#vector-store-architecture-pluggable-backends) below.

Comparison with Python original (ChromaDB)

To validate the port, both engines were benchmarked on the same dataset with the same embedding model (all-MiniLM-L6-v2, 384-dim) on Apple M1.

Reproduce yourself:

  • Node: MODEL=english node dist/tests/test-benchmark.js
  • Python: pip install chromadb && python3 -u tests/benchmark-python.py

| Metric | Drawers | Python (ChromaDB) | Node (this) | |---------------------|--------:|------------------:|------------:| | Cold query latency | 100 | 87ms | 7ms | | Cold query latency | 1K | 86ms | 10ms | | Cold query latency | 10K | 78ms | 305ms | | Filtered query | 1K | 77ms | 6ms | | Filtered query | 10K | 81ms | 29ms | | Heap (10K palace) | 10K | 936MB | 37MB |

Why Node is faster on small + filtered cases:

  • JSON1 metadata pre-filter — where: { wing: ... } cuts the candidate pool to

10% of the palace before any vector math runs

  • Brute-force is exact — every result is the true top-K, no recall loss vs

HNSW's ~95% approximation

Why Python wins on the 10K unfiltered query case:

  • ChromaDB uses HNSW (hierarchical navigable small world graph), an O(log N)

approximate nearest neighbor index. At 10K+ candidates with no metadata filter, the index amortization pays off.

  • For workloads that hit this regime, plug in the LanceDB backend (also HNSW)

via the pluggable interface — see below.

The takeaway: for the typical agent-memory workload (5–50K drawers per wing, always wing-filtered), this port delivers single-digit-millisecond queries with a ~25x smaller memory footprint than the ChromaDB backend.

Install

npm install mempalace-node
# or
pnpm add mempalace-node

Optional peer dependencies:

  • nspell + dictionary-en — English spell correction

Quick Start

import {
  MemoryStack, searchMemories, createStore,
  KnowledgeGraph, setModel,
} from 'mempalace-node';

// 1. Choose embedding model (optional, default: multilingual)
setModel('multilingual');  // 50+ languages

// 2. Store memories (uses default SQLite backend)
const store = createStore('~/.mempalace/palace');
await store.upsert('memory-1', 'Alice loves hiking and dislikes meetings', {
  wing: 'alice',
  room: 'preferences',
});

// 3. Search semantically
const results = await searchMemories('what does Alice like?');

// 4. 4-Layer Memory Stack — wake-up context for AI
const stack = new MemoryStack();
console.log(stack.wakeUp());                   // L0 identity + L1 essential (~600-900 tokens)
console.log(stack.recall('alice'));            // L2 on-demand by wing
console.log(await stack.search('hiking'));     // L3 deep search

// 5. Knowledge graph with time travel
const kg = new KnowledgeGraph();
kg.addTriple('Alice', 'works_on', 'ProjectX', { validFrom: '2026-01-01' });
kg.addTriple('Alice', 'likes', 'TypeScript');
console.log(kg.queryEntity('Alice', '2026-03-15'));  // facts valid on that date
kg.invalidate('Alice', 'works_on', 'ProjectX', '2026-06-01');  // mark as ended

Vector Store Architecture (Pluggable Backends)

The library uses an abstract VectorStore interface so you can swap storage implementations without changing any other code.

Default: SQLite (brute-force cosine similarity)

  • Best for ≤100K drawers (typical agent memory use case)
  • Accuracy 100% (exact, not approximate)
  • Speed ~5-10ms for 1K drawers, ~50ms for 100K drawers
  • Dependencies better-sqlite3 only — no native vector index
  • Cross-platform Works everywhere better-sqlite3 works
import { createStore, setStoreBackend } from 'mempalace-node';

setStoreBackend('sqlite');  // explicit (this is the default)
const store = createStore('~/.mempalace/palace');
SQLite optimizations (all enabled by default)

The SQLite backend includes four optimizations that push the brute-force ceiling from ~10K to ~100K drawers without any vector index:

  1. Float32Array direct compute — cosine similarity runs on Float32Array

in contiguous memory instead of number[], 3-5× faster in V8.

  1. LRU vector cache — decoded Float32Array views are kept in memory

keyed by drawer ID. Repeat queries against the same wing/room re-use already-decoded vectors. Default capacity: 5000 vectors (~7.5MB at 384-dim). Tune via the VECTOR_CACHE_SIZE constant in store.ts.

  1. Streaming top-K with min-heap — instead of loading all candidate

vectors into memory and sorting, the store fetches rows in batches of 1000 and maintains a bounded min-heap. Memory peak is constant regardless of palace size — 100K drawers won't OOM the process.

  1. Worker thread parallelism — when the candidate set exceeds 5000,

the store automatically distributes the cosine work across os.cpus().length - 1 worker threads via SharedArrayBuffer (zero-copy). On a 4-core machine this gives ~3× speedup.

The first three optimizations always run. The worker threads only spin up above the 5000-candidate threshold to avoid overhead on small queries.

To free memory or release threads:

import { shutdownWorkerPool } from 'mempalace-node';

store.clearCache();        // free LRU cache without closing the DB
store.close();             // close DB + clear cache
shutdownWorkerPool();      // terminate all worker threads (call on app exit)

Migrating to LanceDB at scale

LanceDB uses an HNSW-style approximate nearest neighbor index, dropping query complexity from O(N) to O(log N). The trade-off: returned results aren't guaranteed to be the true top-K (typical recall is ~95%); in exchange, query latency stays roughly flat regardless of palace size — whether you have a thousand drawers or a million, you're looking at about 15ms.

It ships an official Node.js client (@lancedb/lancedb) with prebuilt binaries for every platform. Because the project routes everything through the pluggable VectorStore interface, swapping backends doesn't touch any of your application code:

import { createStore, setStoreBackend, registerStoreFactory } from 'mempalace-node';
import { LanceVectorStore } from './my-lance-store';  // implement VectorStore interface

registerStoreFactory('lance', (path) => new LanceVectorStore(path));
setStoreBackend('lance');

const store = createStore('~/.mempalace/palace');  // now uses LanceDB

Implementing your own backend

Any class that implements the VectorStore interface can be plugged in:

import {
  VectorStore, DrawerMetadata, GetOptions, GetResult, QueryOptions, QueryResult,
  registerStoreFactory,
} from 'mempalace-node';

class MyCustomStore implements VectorStore {
  async upsert(id: string, document: string, metadata: DrawerMetadata): Promise { /* ... */ }
  delete(id: string): void { /* ... */ }
  get(options?: GetOptions): GetResult { /* ... */ }
  async query(options: QueryOptions): Promise { /* ... */ }
  count(): number { /* ... */ }
  close(): void { /* ... */ }
}

registerStoreFactory('my-custom', (path) => new MyCustomStore(path));
setStoreBackend('my-custom');

The rest of the library (miner, layers, searcher, graph, mcp-server) never imports a concrete store class — only the VectorStore interface and createStore() function — so swapping is a zero-touch change at the call site.

Memory Palace Architecture

Concepts

| Concept | Meaning | Example | |---|---|---| | Wing | A person or project | alice, my-app | | Room | A topic category | technical, decisions, interests | | Drawer | A verbatim text chunk (~800 chars) | The actual stored content | | Hall | Connection within a wing | Links between rooms | | Tunnel | Cross-wing connection | Same room appearing in multiple wings |

4-Layer Memory Stack

| Layer | Tokens | When Loaded | Purpose | |---|---|---|---| | L0 Identity | ~100 | Always | "Who am I?" from ~/.mempalace/identity.txt | | L1 Essential | ~500-800 | Always | Top 15 highest-weight drawers, grouped by room | | L2 On-Demand | ~200-500 | Topic triggered | Wing/room filtered retrieval | | L3 Deep Search | Unlimited | Explicit search | Full semantic vector search |

Wake-up cost: ~600-900 tokens. Leaves 95%+ of context free.

Complete Feature Guide

1. Embedding Models

import { setModel } from 'mempalace-node';

setModel('multilingual');  // paraphrase-multilingual-MiniLM-L12-v2 (384-dim, ~120MB)
setModel('english');       // all-MiniLM-L6-v2 (384-dim, ~23MB) — matches original
setModel('bge-m3');        // bge-m3 (1024-dim, ~560MB) — best multilingual

> Warning: Switching models invalidates existing embeddings. The palace database records which model created it and warns on mismatch.

2. File Mining

import { mine } from 'mempalace-node';

const result = await mine({
  projectDir: './my-project',
  palacePath: '~/.mempalace/palace',
  wingOverride: 'my-app',
  respectGitignore: true,
});

Requires a mempalace.yaml (auto-generated by mempalace init):

wing: my-project
rooms:
  - name: frontend
    description: UI components
    keywords: [react, component, css]

3. Conversation Mining

import { mineConvos } from 'mempalace-node';

await mineConvos({
  convoDir: '~/chat-exports',
  palacePath: '~/.mempalace/palace',
  wing: 'conversations',
  extractMode: 'exchange',  // or 'general' for memory-type extraction
});

Supported formats (auto-detected):

  • OpenClaw JSONL sessions (~/.openclaw/agents//sessions/*.jsonl) — native support, parses message blocks including tool calls
  • Claude Code JSONL sessions
  • OpenAI Codex CLI JSONL
  • Claude.ai JSON export
  • ChatGPT conversations.json
  • Slack channel JSON
  • Plain text with > markers

4. Search

import { searchMemories, checkDuplicate } from 'mempalace-node';

const results = await searchMemories('database migration', undefined, 'my-app', 'backend');
// { query, filters, results: [{ text, wing, room, sourceFile, similarity }] }

const dup = await checkDuplicate('some text');
// { isDuplicate, similarity, existingText? }

5. Palace Graph

import { traverse, findTunnels, graphStats } from 'mempalace-node';

const connected = traverse('frontend', undefined, 2);   // BFS, max 2 hops
const tunnels = findTunnels('my-app', 'docs');          // bridging rooms
const stats = graphStats();

6. Knowledge Graph

import { KnowledgeGraph } from 'mempalace-node';

const kg = new KnowledgeGraph();

kg.addEntity('Alice', 'person', { role: 'engineer' });
kg.addTriple('Alice', 'works_on', 'ProjectX', {
  validFrom: '2026-01-01',
  confidence: 0.9,
});

kg.queryEntity('Alice');                       // current facts
kg.queryEntity('Alice', '2026-03-15');         // time-filtered
kg.timeline('Alice');                          // chronological history
kg.invalidate('Alice', 'works_on', 'ProjectX', '2026-06-01');

kg.stats();
kg.close();

7. AAAK Dialect

import { Dialect } from 'mempalace-node';

const dialect = new Dialect({ entities: { Alice: 'ALC', Bob: 'BOB' } });

const compressed = dialect.compress(
  'Alice decided to use GraphQL instead of REST because of better performance',
  { wing: 'tech', room: 'decisions' },
);
const stats = dialect.compressionStats(originalText, compressed);

> Note: AAAK is lossy summarization, not compression. The 96.6% benchmark uses verbatim storage. AAAK scores 84% but saves tokens.

8. Entity Registry

import { EntityRegistry } from 'mempalace-node';

const registry = EntityRegistry.load();

registry.seed('personal',
  [
    { name: 'Alice', relationship: 'partner', context: 'personal' },
    { name: 'Riley', relationship: 'daughter', context: 'personal' },
  ],
  ['MyApp'],
  { Riley: 'Rileigh' },
);

registry.lookup('Riley', 'I went hiking with Riley today');
// → { type: 'person', confidence: 1.0, ... }

const wiki = await registry.research('Sam');
const learned = await registry.learnFromText(longSessionText);
const names = registry.extractPeopleFromQuery('Did Alice meet with Riley?');

9. Entity Detector

import { detectEntities, scanForDetection } from 'mempala

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [CN-Syndra](https://github.com/CN-Syndra)
- **Source:** [CN-Syndra/mempalace-node](https://github.com/CN-Syndra/mempalace-node)
- **License:** MIT
- **Homepage:** https://www.npmjs.com/package/mempalace-node

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.