💻 Offline Memory - No Keys, No Network
Copy
memory.mjsbelow into your own project and run it withnpx 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 tsxavailable:
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
localembedder —npm install @lighthouse-ai/embed-localfor 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 prefers ≠ preferences); 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
| Symptom | Fix |
|---|---|
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 local | First run needs network for ~25 MB. Use MEMORY_EMBEDDER=keyword to stay offline, then retry local when online. |
| Empty recall | Tags filter too strict, or query tokenizes to nothing (1-char tokens are dropped). Call list() to confirm the memory exists. |
Next steps
- Durable Memory on Filecoin - same calls, blobs on IPFS/Filecoin with flush + rebuild.
- Batched Engine reference -
flushEvery,flush(), scoring formula. - Choose a Network - when to leave
local-fs.