Cross-session memory for agents. Four verbs over SQLite + FTS5, exposed via a 4-tool MCP server.
git clone https://github.com/acoyfellow/deja
cd deja && bun install
bun run src/cli.ts init # creates ~/.deja/deja.db, prints MCP wiringLibrary users (Bun-only for now — not yet on npm):
bun add github:acoyfellow/dejaimport { Deja } from "deja";
const d = new Deja();
d.remember("the user prefers vitest over jest");
d.handoff({ summary: "shipped the auth refactor", next: ["wire it into the gateway"] });
// later, in a fresh process:
const r = d.recall("test runner");
// r.hits[0].slip.text === "the user prefers vitest over jest"
// r.activeHandoff.summary === "shipped the auth refactor"Three things to know:
- It's just SQLite. Stored at
~/.deja/deja.dbby default, FTS5-indexed. No network, no auth, no Worker, no daemon. Open the file with any SQLite client to inspect. - It's append-only. Slips don't get edited. Contradictions become new slips that link to the old.
- It's MCP-shaped. Designed to be used by agents through an MCP server. The library is also fine for direct use, in Bun.
d.remember(text, opts?) // jot a draft. drafts auto-expire in 24h
d.keep(ids) // promote drafts to permanent
d.handoff({ summary, next? }) // close the session for whoever comes next. one per session.
d.recall(query) // find slips, plus the most recent handoffMinimum viable dogfood for two local agents. Address messages to the other agent's DEJA_AUTHOR; delivery is async/pull-only. No daemon, no live TUI injection.
d.send({ to: "opencode-reviewer", body: "review the diff" })
d.inbox("opencode-reviewer")
d.reply(messageId, "found one blocker")
d.read(messageId)Plus three signals that don't change the lifecycle:
d.forget(id) // expire a slip (kept or otherwise). no undo
d.used(id) // record that a recalled slip was helpful
d.wrong(id) // record that a recalled slip was misleadingWhen you keep() a slip whose text or tags look "chain-shaped" — a decision, preference, work-in-progress note — and the current session has no handoff yet, deja writes one for you. The rollup makes the slip discoverable on every recall, not just queries that lexically match it.
const slip = d.remember("Decision: use Bun for new TS libs");
d.keep([slip.id]); // also writes a session handoff that mentions the decision
const r = d.recall("anything at all");
// r.activeHandoff.summary contains the decision, even though "anything at all"
// doesn't lexically match the slipDisable per-call (d.keep(ids, { noChainRollup: true })) or globally (new Deja({ noChainRollup: true })).
deja init # create the db, print mcp wiring snippet
deja recall <query> # search slips
deja ls [--session] # list kept slips (or current session's slips)
deja show <id> # show a slip + its links
deja stats # counts and db path
deja handoffs # list recent handoffsThe CLI is for humans poking at the DB. Agents use the library or MCP.
Four tools: recall, remember, handoff, signal. Tool descriptions and responses tell the agent how to use them — no SKILL.md, no AGENTS.md, no system-prompt ceremony.
recall(query)— search. Empty/blank query returns "what's recent" (active handoff + most recent kept slips) instead of running FTS. Use it at session start when you don't have a query yet.remember(text, keep?)— jot. Until you callrecallat least once this session,rememberandhandoffresponses include a one-paragraph "fyi" pointing at the most recent prior handoff. Structural nudge: forgetful agents still see context.handoff({summary, next?})— close the session.signal(id, action)— close the loop on a recalled slip.action:"used"(helpful),"wrong"(misleading),"forget"(expire, no undo).
Run deja init for the wiring snippets for OpenCode and pi.
Three tables: slips, links, handoffs. Plus a virtual FTS5 table over slip text. See src/storage.ts:32 for the schema. ULID primary keys (sortable by creation time). Atomic-immutable: state transitions update the row's state and timestamps, never the text.
DEJA_DB— override DB path (default~/.deja/deja.db).DEJA_AUTHOR— identity recorded with new slips (defaultunknown-agent).DEJA_SESSION— override session id (default: derived per-process).
What deja deliberately doesn't do:
- Not a vector store. Lexical FTS5 only. Bring your own embeddings if you need semantic search.
- Not multi-user. One DB, one user. No accounts, no sharing, no permissions.
- Not synced. Local file. Use Syncthing/rsync if you want it on another machine.
- Not encrypted at rest. Plain SQLite — don't put secrets in it.
- Not a platform. No metrics, no audit log, no rate limits.
- Not magic. Agents reach for memory when the question shape suggests it. Some questions ("world-knowledge" ones) never trigger recall; we measured this in loop 3 s8 and loop 4 c1. It's a model-prior boundary.
Run the retrieval bench locally:
bun run bench/recall.ts
# recall@1: 8/8 (100%) recall@3: 8/8 (100%)Run the behavioral bench locally:
bun run bench:behavior
# writes docs/bench/behavior-latest.mdRead:
docs/bench/latest.txt— retrieval bench, regenerated by CI on every push tomain.docs/bench/behavior-latest.md— behavioral hypotheses, metrics, evidence, and recommendations.docs/bench/claims.md— claim → evidence map.docs/agents/parallel-dogfood.md— how to dogfood deja with parallel headless agents.
The full evidence base lives in docs/loops/ — four research loops, each with hypothesis, battery, results, and what we changed because of them.