Search
how a query finds a session you half-remember
Search is lexical by default — fast, offline, no model required — with graceful fallbacks so natural phrasing still works.
Searching as you type
Run deja with no arguments at a terminal and it opens a full-screen search over every agent's history. Results come in as you type, and the status line says how many sessions were searched and in how many milliseconds. tab moves between this project, all projects and the sessions deja kept after their agent deleted them; a filters by agent; ↑ on an empty box brings back past searches; ^s lists the results newest first under day headings, and again goes back to best match first. The box also takes filters: an agent with a colon (codex:), today, yesterday, week, month and in:<project>. ↵ opens a session at the match. In the reader / finds a word, n/N step through the matches, ]/[ jump between the turns you asked and t unfolds long messages. ? lists every key. When the same question was asked in more than one session, a "deja vu" banner says so and the newest answer is selected. DEJA_TUI=0 keeps the text brief instead.
Query forms
- Tokens —
deja connection poolmatches records containing both words (AND). - Phrases —
deja '"connection pool exhausted"'requires the exact contiguous phrase. - Regex —
deja --re "timeout|deadline exceeded". Case is ignored, and^and$match at each line, as in grep. - Filters —
--harness,--project,--since 30d,--role assistant. Beyond the conversation roles, the index keeps what the agent did:--role filesfor the paths a turn opened,--role commandfor the commands that ran,--role editfor the spans an edit replaced,--role toolfor what a tool printed. These are searchable but stay out of ordinary results, since a path that happens to contain the words of a question is not an answer to it.
What a subagent did
When an agent hands work to a subagent — a Claude Code Task, a Cursor subagent — that run gets its own transcript. The parent session keeps the launch and a summary of what came back; the turns and the tool stream exist only in the child file. Child runs — Claude's, Cursor's, Gemini's, Codex's, Qwen Code's, Kimi Code's, Hermes delegates, OpenClaw spawns, goose's delegate runs, prime-agent's rlm.spawn children, the task runs Copilot CLI keeps inside the parent's log and the other readers' that name their parent — are read as the task they were handed, the answer they came back with and the edits they made, so deja blame can name the subagent that changed a file — measured on a seeded store, that is a 12% larger index against a 345% one for the whole run, and it is where the settled answers sit. Set DEJA_INCLUDE_SUBAGENTS=1 for the whole child transcript, or =0 to leave them out entirely. Each child is a session of its own, naming the session that spawned it, so a hit can say which run it came from. Grok Build records the same edge itself, and deja show names the parent and the children either way.
When nothing matches exactly
Stop words come out of every query first, so natural phrasing like "have we dealt with jwt refresh rotation" is searched on its content words. When no session holds those together, deja widens the net in this order, stops at the first step that finds something, and marks how each hit was found:
- Inside longer words —
codefindsopencode. Tierclose. When a question in plain words lands this way on one incidental session, the relevance ranking below answers instead, with these hits after it. - Word forms —
rotation↔rotatedvia suffix folding; a word no session holds together with the rest is dropped, and deja says which. Tierstemmed. - Typos — close spellings within edit distance. Tier
close. - Words that travel together — for two words no session holds together, a word this history keeps beside one of them, which is often the project's own name for the thing. Tier
stemmed. - Error signature — paste a failing stack trace and deja matches the sessions that hit the same error by its signature, not its words, and can name the command that cleared it last time (
deja fix, MCPfix). Tiererror, and these are matches. - Relevance — sessions ranked by how many of the query's informative words they share, rarer words counting for more. Tier
relevance, which means no session held every word; in--json,strictcounts the ones that did. - Semantic — only when every step above found nothing, and only with a vector sidecar: a rephrased query with no shared words still matches. Tier
semantic. With a sidecar, ordinary results are also reordered by how close their vectors are to the query's.
Each result carries its tier — exact, close, stemmed, error, relevance or semantic — in the CLI, JSON and MCP output, and on close and stemmed the JSON says which spelling matched (tier_detail).
Semantic recall
deja embed # build a vector sidecar from an embedding endpoint deja "ran out of db connections" # matches "connection pool exhausted"
Resolution: DEJA_EMBED_URL, else a probed Ollama or LM Studio. Authenticated endpoints use DEJA_EMBED_KEY; an HTTPS api.openai.com URL also accepts OPENAI_API_KEY. No endpoint, no semantic layer — search is unchanged.
Natural-language queries
Stop words are dropped, word forms bridge morphology in both directions (failing finds fails), and when no session holds every remaining token deja drops up to two — telling you exactly which (ignoring "why" — no session matches it together with the rest). Zero results now mean the content genuinely is not there.
Search by time
Every message also carries its month and year in the postings, and a query with no exact match reads them: deja "what did we do in may" meets May's sessions structurally, and relative phrases resolve against the moment you ask — a week ago, last month, 10 days ago. They rank the fallback, they never filter, and a query whose words already match something is answered on those words alone — so for a hard cutoff, or to narrow a query that does match, use --since. To read the matches by date instead of by relevance, --sort date lists the newest first, in --json too. It reads the newest of the whole match, so on a word thousands of sessions hold the page is still the latest ones; when nothing matches exactly, it orders the ranked fallback, and says so when that was cut.
Earlier attempts
When an old session and a newer one from the same project match the same ground, the older hit is labeled an earlier attempt with the newer session's date — in CLI output, MCP digests, and as an additive superseded field in --json. History stays; the direction the project moved is visible.
What ranks first
Lexical overlap decides who is a candidate; what settled something decides who wins. Above the raw term match, deja weighs a few signals — none of them able to outrank plain relevance, only to break the ties it leaves:
- The session that concluded, not the one that kept asking. A session that reached a decision outranks one that only discussed the topic, and recall surfaces the decision-carrying line, not just where the words appeared — the outcome rather than the first reply, which is usually a plan, and in whichever language the session was held in.
- What held over what was backed out. A session whose own text says it reverted an approach and settled nothing else ranks below one that held — the dead end is still there, marked, but it is not the first thing an agent sees. A session that reverted one thing and settled another keeps its place.
- A recorded judgement over a raw guess. A note you promoted outranks the transcript it came from, and a lifecycle state travels with the session on every hit: tried and rejected, replaced by a later decision, marked stale, with the reason and the date. Nothing is deleted — the agent sees what was decided and that it changed.
- What gets reused. A session agents keep pulling back is lifted a little, on the theory that what the machine keeps needing is worth surfacing — bounded, so popularity never buries a stronger match.
Found this useful? Star deja-vu on GitHub.