fortytwo
Architecture
Packages
Design
forty-two.it
GitHub
Architecture
Packages
Design
forty-two.it
GitHub
  • Packages

    • @justfortytwo/installer
    • @justfortytwo/persona
    • @justfortytwo/memory
    • @justfortytwo/runner
    • @justfortytwo/scheduler
    • @justfortytwo/telegram
    • @justfortytwo/gate
    • @justfortytwo/salience
    • @justfortytwo/marketplace

@justfortytwo/memory

A standalone semantic-memory MCP server. It stores text "memories" with free-form provenance and recalls them by meaning (vector search), by keyword (FTS5), or by structured filter. Backed by SQLite + sqlite-vec, with embeddings from a local Ollama model (or an opt-in remote provider).

It is persona-agnostic: no journal/persona/approval coupling, just a generic memory store and tool surface. It can be used on its own, as a library, or as a Claude Code plugin.

  • Version: 1.0.0-2 · Runtime: MCP server over stdio, bin is fortytwo-memory · Node: >= 20

Why this exists

An assistant without durable memory restarts from zero every session. memory gives the spine a single, local, queryable long-term store: what was learned, where it came from, and when — with corrections that keep history instead of silently overwriting it.

What it stores

A memory is content plus provenance:

fieldmeaning
contentthe text (embedded for recall)
sourcewhere it came from — free-form (owner, web, tool:foo)
observedhow it was observed — free-form (stated, inferred, imported)
dateISO date the memory pertains to (defaults to today, UTC)
tagsfree-form tags for filtering
supersedesid of a prior memory this one replaces (history is kept)

Recall is hybrid: recall (semantic), lexical (FTS5 keyword), and query (structured). reindex + recall_docs index/search a directory of markdown documents separately from the memory store.

MCP tools

The server registers under the id fortytwo-memory, so a consumer calls the tools as mcp__fortytwo-memory__<tool>:

tooldescription
storestore a memory (+ provenance); set supersedes to replace one
querystructured query (source/observed/tag/time; live rows by default)
recallsemantic top-k recall by meaning
recall_docssemantic recall over reindexed markdown
lexicalfull-text keyword search (FTS5)
reindexself-heal the doc index from a markdown directory
export_rangerender a date range of memories to markdown

On connect, the server sends usage instructions in the MCP initialize result (when to search vs. store, which search tool fits, how to correct a memory with supersedes). The text lives in src/server.ts.

Contract version

Consumers depend on the tool surface, not the internals. The contract is versioned:

import {
  MEMORY_TOOL_CONTRACT_VERSION,
  memoryToolContract,
} from '@justfortytwo/memory/contract';
  • A major change to a tool name, its required inputs, or its result shape is a contract break → bump MEMORY_TOOL_CONTRACT_VERSION. Siblings pin a caret range on @justfortytwo/memory, so a major bump forces an explicit opt-in.
  • Additive changes (new optional inputs, new tools) do not bump it.

memoryToolContract is the authoritative human-readable list of tools and their guarantees, kept in sync with the wire schema in src/tools.ts.

Data model

SQLite via better-sqlite3 + knex migrations (src/migrations/, applied by fortytwo-memory-migrate / npm run migrate):

migrationcreates
001_initmemories, index_state
002_ftsmemory_fts (FTS5, content-linked to memories)
003_approvalsapprovals, audit_log (gate seam)
004_jobsjobs (shared with the scheduler, see Scheduler)
005_memory_identitymemory_identity (stable database identity)

Vector columns are fixed at 1024 dims (sqlite-vec); a model with a different dimensionality requires a schema change.

Embedder

Default: OllamaEmbedder against a local Ollama /api/embeddings endpoint.

OLLAMA_BASE_URL=http://localhost:11434   # default
EMBED_MODEL=qwen3-embedding:0.6b         # default model (1024-dim)

If EMBED_MODEL is unset, the server falls back to a deterministic, dependency-free FakeEmbedder — for tests, CI, and first-run smoke checks with zero infra.

Remote providers are opt-in via EMBED_PROVIDER=openai; only models that emit (or truncate to) 1024 dims are accepted (text-embedding-3-small/large, voyage-3-large, BAAI/bge-m3, Qwen/Qwen3-Embedding-0.6B). Any other EMBED_MODEL fails at startup with the accepted list.

Switching models on an existing database: two models with the same dimension do not share a vector space. Stored vectors are not comparable with new query vectors, and recall will not error — it will return plausible but wrong neighbours. Re-embed with the library's reembed(h, embedder, id) (one id at a time; no bulk command or MCP tool) and re-run reindex for docs after switching.

Library API

import {
  openDb, runMigrations, OllamaEmbedder, embedderFromEnv,
  store, recall, deleteByIds, enrich, enrichFromTurn,
  memoryHead, memoryDelta,
} from '@justfortytwo/memory';
  • openDb(path) / runMigrations(knex) — open and migrate the database.
  • store / recall / query / lexical — same operations the MCP tools expose.
  • deleteByIds(h, ids) — hard-delete (row + vector + FTS). Intentionally a library API and NOT an MCP tool: deletion is owner-privileged, and exposing it to the assistant's turn loop would let prompt-injected content trick it into erasing memories. Selecting which ids to remove is the caller's job — see the installer's forget.
  • Incremental reads: memoryHead(h) returns { database, position } using a persistent database identity and SQLite's AUTOINCREMENT high watermark (not timestamps or MAX(id)). memoryDelta(h, cursor, limit) returns ascending insertion pages, includes superseded rows as evidence, never skips unreturned rows; deleted-id gaps remain safe; invalid or backwards cursors fail closed. Library APIs only — the MCP tool contract is unchanged.

Continuous enrichment

enrich(h, embedder, candidates) folds a batch of candidate memories into the store: drops low-salience candidates, dedupes near-duplicates by meaning, and writes the survivors with provenance — honoring an explicit supersedes to replace a stale belief. enrichFromTurn(h, embedder, turn, extractor) runs an injected SalienceExtractor and feeds its candidates to enrich.

The salience extractor itself is model-driven and lives in the sibling @justfortytwo/salience — memory owns only the dedupe + write, never the model call.

Peer seams

memory depends on two sibling packages one-directionally (optional peers, no cycle):

  • @justfortytwo/gate — memory ships GateApprovalStore (src/gate-approval-store.ts), a durable SQLite-backed implementation of gate's ApprovalStore + AuditLogger interfaces. Pass it to gate's decide(..., { store, audit }) to back the safety gate's one-shot approvals with memory's db instead of gate's standalone JSONL store.
  • @justfortytwo/salience — the model-driven salience extractor injected into enrichFromTurn.

Running it

npm install @justfortytwo/memory
npm run build                      # tsc; the server runs built JS
DB_PATH=./memory.db npm run migrate
DB_PATH=./memory.db EMBED_MODEL=qwen3-embedding:0.6b fortytwo-memory

Register in any MCP host via .mcp.json (command: "npx", args: ["-y", "@justfortytwo/memory"]). As a Claude Code plugin, .claude-plugin/plugin.json + .mcp.json launch it with DB_PATH=${CLAUDE_PLUGIN_DATA}/memory.db, which survives plugin updates.

Testing

npm test                # vitest run
RUN_OLLAMA_TESTS=1 ...  # opt-in live-Ollama embedder test

Scope

In scope: the memory store, hybrid recall, contract-versioned MCP tool surface, doc indexing, enrichment intake, gate approval store, incremental delta reads.

Out of scope: the salience model call (owned by @justfortytwo/salience), deletion over MCP (owner-privileged, via the installer's forget), persona or journal semantics.

Edit this page
Last Updated: 10/20/26, 9:42 PM
Prev
@justfortytwo/persona
Next
@justfortytwo/runner