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 pool matches 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 files for the paths a turn opened, --role command for the commands that ran, --role edit for the spans an edit replaced, --role tool for 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:

  1. Inside longer words — code finds opencode. Tier close. When a question in plain words lands this way on one incidental session, the relevance ranking below answers instead, with these hits after it.
  2. Word forms — rotation ↔ rotated via suffix folding; a word no session holds together with the rest is dropped, and deja says which. Tier stemmed.
  3. Typos — close spellings within edit distance. Tier close.
  4. 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.
  5. 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, MCP fix). Tier error, and these are matches.
  6. 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, strict counts the ones that did.
  7. 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.