🗄️ Durable Memory on Filecoin
Copy
filecoin.mjsbelow into your own project and run it withnpx tsx filecoin.mjs. It mirrors the runnable demo flow step by step.
filecoin.mjs - the full file
// filecoin.mjs — durable: batched + lh-ipfs-filecoin + local. Needs LIGHTHOUSE_API_KEY.
import 'dotenv/config'
import '@lighthouse-ai/store-lighthouse'
import '@lighthouse-ai/embed-keyword'
import '@lighthouse-ai/embed-local'
import { mkdirSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import { createEmbedder, createStorage } from '@lighthouse-ai/core'
import { BatchedEngine } from '@lighthouse-ai/engine-batched'
import { backupIndex, restoreIndex } from '@lighthouse-ai/cloud-sync'
const apiKey = process.env.LIGHTHOUSE_API_KEY
if (!apiKey) throw new Error('LIGHTHOUSE_API_KEY is not set. Add LIGHTHOUSE_API_KEY=lh_... to project-root .env.')
const namespace = process.env.MEMORY_NAMESPACE ?? 'demo-filecoin'
const dataDir = process.env.MEMORY_DIR ?? join(process.cwd(), '.memory-sdk')
const embedderName = process.env.MEMORY_EMBEDDER ?? 'local'
const embedder = embedderName === 'keyword' ? null : await createEmbedder(embedderName)
console.log('embedder:', embedder?.id ?? 'keyword:off')
const storage = await createStorage('lh-ipfs-filecoin', { apiKey })
const flushEvery = Number(process.env.MEMORY_FLUSH_EVERY) || 5
const memory = new BatchedEngine(storage, { namespace, embedder, dataDir, flushEvery })
// 1. REMEMBER — first four stay pending, 5th trips flushEvery and uploads one blob
const memoriesToStore = [
{ content: 'User prefers dark mode.', tags: ['preference'] },
{ content: 'User works in IST timezone, starts day around 9am.', tags: ['preference'] },
{ content: 'Project deadline is end of September, Filecoin integration first.', tags: ['project'] },
{ content: 'User likes concise code reviews with examples.', tags: ['preference'] },
{ content: 'Staging API runs at https://staging.lighthouse.storage', tags: ['infra'] },
]
const storedIds = []
for (const [i, m] of memoriesToStore.entries()) {
const res = await memory.remember(m.content, { tags: m.tags })
console.log(`remembered ${i + 1}/${memoriesToStore.length}:`, res)
storedIds.push(res.id)
}
// 2. FLUSH — manual only if auto-flush did not fire
const statusBeforeFlush = await memory.status()
if (statusBeforeFlush.pendingMemories > 0) {
console.log('flushed (manual):', await memory.flush())
} else {
console.log('auto-flushed on 5th remember — no manual flush needed.')
}
// 3. RETRIEVE
console.log('recall:', await memory.recall('what theme does the user like?'))
console.log('get:', await memory.get(storedIds[0]))
console.log('list:', await memory.list())
console.log('status:', await memory.status())
// 4. FRESH MACHINE — same namespace, empty dir, rebuild from blobs (no pointer needed)
const freshDir = join(dataDir, 'fresh-filecoin')
rmSync(freshDir, { recursive: true, force: true })
mkdirSync(freshDir, { recursive: true })
const fresh = new BatchedEngine(await createStorage('lh-ipfs-filecoin', { apiKey }), {
namespace, dataDir: freshDir, embedder,
})
console.log('rebuild on fresh dir:', await fresh.rebuild())
console.log('recall on fresh dir:', await fresh.recall('what theme does the user like?'))
// 5-6. CLOUD BACKUP (optional — needs CM_API_URL + `memory-login` once)
if (!process.env.CM_API_URL) {
console.log('SKIP cloud backup: CM_API_URL is not set.')
} else {
const syncOpts = { apiUrl: process.env.CM_API_URL, namespace, network: 'ipfs-filecoin', engine: 'batched' }
try {
console.log('cloud backup:', (await backupIndex(memory, syncOpts)).pointer)
const viaCloudDir = join(dataDir, 'fresh-filecoin-cloud')
rmSync(viaCloudDir, { recursive: true, force: true })
mkdirSync(viaCloudDir, { recursive: true })
const viaCloud = new BatchedEngine(await createStorage('lh-ipfs-filecoin', { apiKey }), {
namespace, dataDir: viaCloudDir, embedder,
})
console.log('restore via pointer:', (await restoreIndex(viaCloud, syncOpts)).result)
} catch (err) {
if (err instanceof Error && err.message.startsWith('Missing Clerk session JWT')) {
console.log('SKIP cloud backup: not logged in. Run cloud-sync login once, then re-run.')
} else throw err
}
}
npx tsx filecoin.mjs
Why Filecoin?
Filecoin (via Lighthouse IPFS) is the default durable storage: quota tracks real bytes, any Lighthouse API key works, and every batch blob is fetchable from gateway.lighthouse.storage. This tutorial is the full batched lifecycle - buffer, flush, retrieve, rebuild on a fresh machine, and optional cloud backup - so treat it as the reference for all durable storages.
Prerequisites
- Offline tutorial completed (you know
remember/recall/status). - The durable packages installed:
npm install @lighthouse-ai/core @lighthouse-ai/engine-batched @lighthouse-ai/store-lighthouse @lighthouse-ai/embed-local dotenv(plus@lighthouse-ai/cloud-syncfor steps 5-6). - A Lighthouse API key from the Files App (API Keys section). Put it in a project-root
.env(git-ignored):
LIGHTHOUSE_API_KEY=lh_...
- Optional for steps 5–6:
CM_API_URL(pointer service) +npx -y @lighthouse-ai/cloud-sync loginonce to save the Clerk JWT (see Cross-Device Backup).
Step 1 - Configure the engine for durability
filecoin.mjs sets namespace = MEMORY_NAMESPACE ?? 'demo-filecoin', dataDir = MEMORY_DIR ?? <project>/.memory-sdk, and embedder = MEMORY_EMBEDDER ?? 'local' (keyword maps to null). Storage is fixed to lh-ipfs-filecoin with flushEvery: 5 (overridable via MEMORY_FLUSH_EVERY).
flushEvery: 5 (overridable via MEMORY_FLUSH_EVERY) means the 5th remember auto-flushes. On Filecoin a lower value is affordable (real-bytes quota); on Walrus you would keep it high (63 MB per blob regardless of size). This contrast is the point of the next tutorial.
Step 2 - Buffer five memories, watch auto-flush
The demo stores five tagged memories (preferences, project deadline, infra URL). Each remember returns { id, flushed, pendingCount, cid?, gatewayUrl? } - the first four are pending, the fifth trips flushEvery and uploads one batch blob.
Why five heterogeneous memories? It gives recall something to discriminate: a theme query should rank the dark-mode memory first, not the deadline or infra URL. Tags (preference, project, infra) let you pre-filter before ranking.
Log line to watch: remembered 5/5: { id, flushed: true, … cid: 'baf…' } - that CID is the batch blob holding all five.
Step 3 - Flush explicitly and read the status
filecoin.mjs checks status().pendingMemories and flushes only when needed:
status() tells you whether anything is still pending. flush() uploads leftovers as one blob and returns { flushedMemories, cid, gatewayUrl }. The demo handles both cases: if auto-flush already fired, it reports auto-flushed on 5th remember; otherwise it flushes manually.
Habit to adopt: flush before ending important sessions, or set MEMORY_FLUSH_EVERY=1 for maximum durability. Unflushed memories die with the local index.
Step 4 - Retrieve three ways
filecoin.mjs then retrieves three ways — semantic recall() (deliberately zero word overlap with User prefers dark mode.), exact get(storedIds[0]), newest-first list(), plus status() for counts + lastSnapshotCid:
The recall query deliberately shares no keywords with User prefers dark mode. - with the local embedder it still ranks first via cosine similarity. Switch to MEMORY_EMBEDDER=keyword and re-run to feel the difference: semantic recall degrades to word-match.
gatewayUrl on any result is directly curlable: curl https://gateway.lighthouse.storage/ipfs/<CID>.
Step 5 - Prove durability on a fresh machine
filecoin.mjs wipes <dataDir>/fresh-filecoin, constructs a new BatchedEngine with the same namespace, and calls rebuild() ({ restored: 5, source: 'blobs' }), then recalls on the fresh engine:
rebuild() lists every mem-batch.<namespace>.* blob and re-reads them - no pointer needed, only the API key. This is the slow path (every blob is fetched), which is why snapshots exist.
Step 6 - Cloud backup via pointer (optional)
With CM_API_URL set and a saved JWT, filecoin.mjs backs up and restores through the pointer service (backupIndex snapshots → one CID and upserts namespace × network → CID; restoreIndex fetches that CID and merges it):
A third engine on yet another fresh dir (fresh-filecoin-cloud) proves the pointer alone is enough — no CID copied by hand. Skipped with SKIP cloud backup: CM_API_URL is not set or the Missing Clerk session JWT path when prerequisites are missing — the durable steps above still pass.
Troubleshooting
| Symptom | Fix |
|---|---|
LIGHTHOUSE_API_KEY is not set | Add it to project-root .env, not the shell profile, so your scripts share it. |
flushedMemories: 0 surprises | Auto-flush already fired - check status().pendingMemories before calling flush(). |
rebuild() returns 0 | Wrong namespace or wrong network (ipfs-filecoin vs lh-ipfs-walrus never mix). |
| Cloud 404 on restore | No backup yet for that namespace × network - run backup first. |
Next steps
- Batched Memory on Walrus - same engine, quota-aware batching + blob IDs.
- Cross-Device Backup & Restore - snapshots + pointer service in depth.
- Rebuild & Recovery - all three recovery paths compared.