Skip to main content

💻 Offline Memory - No Keys, No Network

Copy memory.mjs below into your own project and run it with npx tsx memory.mjs.

Why start here?

This is the smallest possible memory: batched engine, local-fs storage, keyword or local embedder. Nothing leaves your machine, there are no keys to create and no quota to burn. It proves your install works and teaches the three calls every other tutorial reuses - remember, recall, status.

Use it whenever you want a scratchpad for an agent, a CI-safe test, or a baseline before going durable.

Prerequisites

  • Node 18+ and a fresh project with the offline packages installed plus npx tsx available:
npm install @lighthouse-ai/core @lighthouse-ai/engine-batched @lighthouse-ai/store-local-fs @lighthouse-ai/embed-keyword
  • No API keys. No network (except the one-time ~25 MB MiniLM download if you use the local embedder — npm install @lighthouse-ai/embed-local for that path).

memory.mjs - the full file

// memory.mjs — offline: batched + local-fs + keyword/local. No keys, no network.
import { join } from 'node:path'
import '@lighthouse-ai/store-local-fs'
import '@lighthouse-ai/embed-keyword'
import '@lighthouse-ai/embed-local'
import { createEmbedder, createStorage, defaultBaseDir } from '@lighthouse-ai/core'
import { BatchedEngine } from '@lighthouse-ai/engine-batched'

// Side-effect imports above self-register 'local-fs' / 'keyword' / 'local'
// so createStorage / createEmbedder can find them (skip one and you get
// `Unknown storage "…". Registered: [...]`).

const storage = await createStorage(process.env.MEMORY_STORAGE ?? 'local-fs', {
dir: join(defaultBaseDir(), 'blobs'), // MEMORY_DIR ?? ./.memory-sdk, blobs underneath
})

// MEMORY_EMBEDDER=keyword → null (word-match only, zero deps).
// Anything else → createEmbedder(name); default 'local' (MiniLM, ~25 MB once, then offline).
const embedderName = process.env.MEMORY_EMBEDDER ?? 'keyword'
const embedder = embedderName === 'keyword' ? null : await createEmbedder(embedderName)

const memory = new BatchedEngine(storage, {
namespace: process.env.MEMORY_NAMESPACE ?? 'demo',
embedder,
})

const stored = await memory.remember('User prefers dark mode.', { tags: ['preference'] })
console.log('remembered:', stored)
// keyword: { id, flushed: false, pendingCount: 1 } — single write stays pending
// (flushEvery defaults to 10).

console.log('recall:', await memory.recall('what are user preferences?'))
console.log('status:', await memory.status())
MEMORY_STORAGE=local-fs MEMORY_EMBEDDER=keyword npx tsx memory.mjs

Step 1 - Choose where bytes live

createStorage('local-fs', { dir }) points blobs at <baseDir>/blobs under .memory-sdk/ (dir comes from defaultBaseDir()MEMORY_DIR or ./.memory-sdk). No apiKey is needed — the factory only requires one for lh-* kinds.

Why local-fs first? Every other storage speaks the same StorageAdapter interface, so code you write here runs unchanged on S3, Walrus, or Filecoin later.

Step 2 - Choose how recall ranks

MEMORY_EMBEDDER=keyword   # word-match only, zero deps - embedder: null
MEMORY_EMBEDDER=local # semantic MiniLM vectors (default), hybrid 0.7/0.3

Try both: run once with keyword, once with local (needs npm install @lighthouse-ai/embed-local first), and compare recall('what are user preferences?') against the stored User prefers dark mode. With keyword, recall() scores purely on token + tag overlap (tokenize() lowercases and splits — no stemming, so preferspreferences); with local, it embeds the query and each memory in-process and blends cosine similarity with the keyword score.

Try both: run once with keyword, once with local, and compare recall('what are user preferences?') against the stored User prefers dark mode. The keyword run matches on the word preferences/prefers stem overlap plus tags; the local run matches on meaning even if you rephrase the query.

Step 3 - Store your first memory

const stored = await memory.remember('User prefers dark mode.', { tags: ['preference'] })

tags are not decoration - they are a second retrieval axis. recall(query, { tags: ['preference'] }) restricts to memories carrying at least one of those tags, and each matching tag adds +0.5 to the keyword score. Adopt a small lowercase vocabulary (preference, project, infra) and keep it consistent across agents.

The return is { id, flushed, pendingCount } - with the default flushEvery: 10, a single memory stays pending. That is fine here: pending memories are searchable immediately; they just live only in the local index until flushed.

Step 4 - Recall and inspect

console.log('recall:', await memory.recall('what are user preferences?'))
console.log('status:', await memory.status())

recall returns ranked RecallMatch[] — with keyword you get score + keywordScore (no semanticScore; that only appears with the local embedder). status() reports engine, storage, namespace, memories, pendingMemories, embeddings (here keyword:off — which tells you keyword-fallback mode is active), and the index path.

Expected output: one match with the dark-mode content, pending: true (the single write stays pending until flushEvery: 10 trips), score: 0.5 (from the preference tag — note prefers vs preferences share no exact token, there is no stemming), and status.memories: 1.

Troubleshooting

SymptomFix
Unknown storage "…"You forgot the side-effect import (import '@lighthouse-ai/store-local-fs'). The registry only knows what was imported.
Model download stalls on localFirst run needs network for ~25 MB. Use MEMORY_EMBEDDER=keyword to stay offline, then retry local when online.
Empty recallTags filter too strict, or query tokenizes to nothing (1-char tokens are dropped). Call list() to confirm the memory exists.

Next steps