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 { createEmbedder, createStorage, defaultBaseDir } from '@lighthouse-ai/core'
import { BatchedEngine } from '@lighthouse-ai/engine-batched'

// Side-effect imports above self-register 'local-fs' / 'keyword'
// 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 (default) → null (word-match only, zero deps).
// MEMORY_EMBEDDER=local → MiniLM (~25 MB once, then offline). Loaded on demand,
// so the script runs without @lighthouse-ai/embed-local installed.
const embedderName = process.env.MEMORY_EMBEDDER ?? 'keyword'
if (embedderName === 'local') await import('@lighthouse-ai/embed-local')
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 (default) - embedder: null
MEMORY_EMBEDDER=local # semantic MiniLM vectors, 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.

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​