github vshulcz/deja-vu v0.23.0

latest release: nightly
6 hours ago

Big stores are cheap to keep up to date. A pass that replaces a few sessions
appends their records instead of writing the whole log again and reads only
what changed: on a 40,000-session store 20 rewritten transcripts take 1.7 s
instead of 24 s, and peak memory drops from 7 GB to under 1 GB. A search for a
word most sessions hold answers in about a second instead of 24. Text taken
out of a transcript leaves the index on the next pass. For busy machines there
are DEJA_NO_AUTO_INDEX and DEJA_SEARCH_ONLY, and deja index exits 3 on a
partial run. Sub-agents are sessions of their own in every reader that marks
one, forks no longer take credit for what they copied, and recalled history
can no longer run a command in opencode or end a dsh turn. The index is rebuilt
once on upgrade.

A search for a word most sessions hold no longer reads all of them to show a page. Past 1,000 matches it ranks them first from the postings and the manifest, then reads the 1,000 that can reach the page and 200 more that check its length estimate; the total still counts every match, and --all scores every one as before. On a 40,000-session store a word in every session takes 1.1 s and 370 MB instead of 24 s and 2.6 GB, and MCP recall 2.5 s instead of 30 s (#5467).

The first index pass after a full build that rewrites, deletes or renames transcripts no longer reads the whole record log for the command-failure table: on a 40,000-session store, 20 rewritten transcripts take 4.0 s instead of 21 s, and the build leaves the table's state for that pass to start from (#5465). Renaming a transcript that shares its session with another file reads the session again only when the rename moves it past another of its files; 20 renames on the same store take 3.2 s instead of 20 s (#5466).
deja --re anchors ^ and $ at each line, as grep does. They anchored the whole message, so ^panic: missed a panic printed after any other tool output (#5448).
deja embed no longer sends a whole Cursor, Continue or Cline session again each time it grows. Those harnesses date unstamped turns from the file's modification time, and the vector identity held the time; it now holds only the session, role and text, so a session grown by one exchange embeds 2 records instead of every one. A version 2 sidecar is rekeyed on the next run without embedding again (#4976).

A search excerpt keeps the line that matched when it looks like tool noise. Searching assignment to entry in nil map showed the stack frames of a Go test panic and dropped the panic: line itself; panic:, goroutine N, npm ERR! and numbered lines are still left out when they do not hold the match (#5456). A --re search that finds nothing suggests a looser pattern, dropping ^/$, or plain words, instead of "try fewer words or --re" (#5457).

deja sync import also leaves out a peer's records for a store DEJA_EXCLUDE_HARNESSES (or a harness: line) excludes and for a project the policy ignore rule covers, and says how many; a relay no longer sends an excluded store's sessions on (#5435). The sync-timer unit carries the DEJA_* and XDG_* settings of the shell it was installed from, so a timer run syncs into the same index under the same switches (#5436).

A dsh turn no longer fails when recalled history contains {{...}}. dsh read deja's project digest and recall as prompt templates, so a past session quoting {{cwd}} ended every new turn with unknown prompt variable before the model was called; both contexts now pass interpolate: false and split the braces ({ {cwd} }), which dsh before 0.2.1-alpha.2 needs, in the npm plugin and in deja install dsh-auto (#5421).
deja sync holds a peer's records to this machine's switches: DEJA_INDEX_TOOL_OUTPUT=0, DEJA_INDEX_COMMANDS=0, DEJA_INDEX_PATHS=0, DEJA_INDEX_EDITS=0, DEJA_INDEX_WRITES=0 and DEJA_INCLUDE_SUBAGENTS=0 leave the same records out of an import as out of a local read, and the import says how many (#5425).
Recalled history can no longer make opencode run a command or attach a file. opencode expands !`cmd` and @path in the /deja prompt text and @path in a sub-agent's task prompt, so a past session quoting either ran it in the shell or shipped the file to the model; deja now breaks up those tokens in the recall it hands opencode and Kilo, and the model still reads every word (#5433, #5439).
A deleted transcript that shared its session with another file keeps every turn it held. Deleting a copy that did not own the row dropped the turns only it had, a rebuild filed shared turns under the copy still on disk, and a later rewrite of that copy took them out of the index. A renamed copy and a file that moved to another session id now give the row to the file a rebuild picks, and a new file holding only a deleted transcript's last turn is no longer taken for the transcript moved (#5441, #5442, #5443, #5444, #5445).
A session that says the search in its own words in one message is no longer buried by sessions that hold other forms of the same common words: on a 3,000-session Russian test store where every word is common, the 12 queries worded as the target said them find it in the top five 12 times, against 9 (first 10 against 8) (#5369).

Text pasted into a Claude Code prompt is indexed as the pasted text, without the <pasted_content id="…"> wrapper Claude Code puts around it. The tags titled sessions (<pasted_content id="dd73"> Set up and test…), gave the install's demo question the topic "pasted_content set" and were indexed and recalled as terms; the prompt hook no longer searches on them either. The index rebuilds once to take them out of sessions already stored, including those whose transcript Claude Code has since deleted, and a peer on an older build syncs its prompts in unwrapped.

A sub-agent run is a session of its own under the session that spawned it.
Sub-agents are now read by default as their task, answer and edits, the way
Claude Code's are, so deja blame names the sub-agent that wrote a line;
DEJA_INCLUDE_SUBAGENTS=0 leaves them out and =1 takes the whole run in
every reader that marks one, Gemini, Hermes, OpenClaw, Codex, opencode, Grok,
pi, Crush, Kiro, Devin, dsh, Muse and CodeBuddy included (#5301). A CodeWhale
fork is a fork, not a sub-agent, and is read whole. deja resume on a Cursor CLI
subagent names the parent to resume instead of saying the child left
cursor-agent's store; Hermes and OpenClaw children reopen as they are (#5303).
Copilot CLI task runs are split out of the parent's log, OpenClaw children and
Hermes delegates hang under their parent whatever source they ran under, and
Gemini, Kimi and OpenClaw sub-agents are titled by their task rather than by
injected context or their answer. Sub-agent runs stay out of the start digest.

A fork no longer takes credit for what it copied. A Codex fork_context
sub-agent, a Grok --fork-session copy, a Kimi /fork, a goose fork and an
OpenClaw fork child keep only their own turns, and Claude Code /branch,
Copilot /fork, Grok, Kimi, goose and OpenClaw forks name the session they came
from. Where a copy still holds its source's records at their original times
(Claude /branch, Qwen /branch, opencode --fork), blame credits the source.
Relative paths in opencode apply_patch, Hermes write_file and patch, Kimi
Write/Edit, and a Codex apply_patch run through the shell are filed under
the session's directory, so blame finds their writer.

A session is no longer recalled to itself or to its own lineage. A fork,
/branch, resume or sub-agent in Claude Code, opencode, Copilot, Gemini,
Hermes, Qwen, Kimi, Cline, Kiro, aider, Grok, pi, goose and dsh leaves out the
session it came from, and a dsh session's sub-agents stay out of its recall at
any depth. A resume gets one start digest, not two, Grok and pi included. The
session that just ended shows up in the next one's digest, including the first
session in a new project, sessions Hermes still holds in state.db-wal and
Kimi Code 2.x sessions, which now get their project.

deja install and uninstall leave the client's own config alone. A
"disabled": true that the client ignores is no longer read as off, Claude
Code and Kimi Code included, and the client's own switch, extra keys, comments
and empty blocks survive install and uninstall for Codex, Cursor, Gemini,
Hermes, Kiro, goose, dsh, pi, Qwen, Grok, aider, Kimi, Copilot, OpenClaw and
opencode. A read-only file is refused before anything is written, Cline and
Kimi included. pi 0.99 and later need no MCP adapter, and
PI_CODING_AGENT_DIR is followed. OpenClaw install and doctor say when
plugins.allow or plugins.enabled keeps deja's plugin off.

OpenClaw, Codex, Hermes and Grok start MCP servers with only a few
variables, so deja mcp there lost a moved index (DEJA_INDEX_DIR,
XDG_CACHE_HOME), built a second one and answered recall with the asking
session. Their MCP entries now name the launcher the hooks run through; a
one-line Hermes entry is written out as a block with its keys and comment kept.

An OpenClaw heartbeat poll no longer stamps the main DM as in use, which hid
it from recall in the user's other sessions for twenty minutes after each poll,
and a poll and its NO_REPLY are no longer indexed as a session.

openclaw chat and openclaw tui --local run no plugin or hook, so deja
cannot give them a digest or per-prompt recall; install and doctor now say
so. MCP recall there leaves out the session OpenClaw wrote to last when no
hook stamped one, instead of answering with the conversation asking.

deja resume and deja handoff --to openclaw open OpenClaw's gateway TUI
(openclaw tui) when a gateway answers, where deja's plugin and hooks run,
and say so when only the hookless local mode (openclaw chat) is there.

deja install openclaw and openclaw-auto refuse a section openclaw.json
keeps in its own file ($include) and say what to add there by hand. Writing
beside the $include made OpenClaw's own edits of that section land in
openclaw.json instead of the included file. The launcher, plugin and hook pack
those entries name are written before the refusal, doctor reads the included
files, switches and the conversation-access grant there included, and names
the file a switch is in, and uninstall names the entries it leaves there. An upgrade that moves
an entry to the launcher no longer says it replaced the same deja.

An OpenClaw sub-agent on a JSONL store stays under the window that spawned
it after a /new; it was filed under the key's newer session.

A fork's source stays on the fork's page when the source went on after the
fork, and recall, the per-prompt hook and the start digest give the fork only
what the source did from the fork point on. Search drops a fork's copied hits
before the cap, so they no longer take a slot or count in the total, and deja last marks a fork beside its source. Blame no longer credits a Claude
--fork-session copy with a line its source wrote, and file blame lists the
session that wrote the file above one that only named it.

The pre-edit note counts a fork's source only for edits after the fork, and a
Claude Code --resume <id> --fork-session fork leaves its source out of the
first prompt's recall and the first tool's note too; deja reads the source id
from claude's command line (not on Windows).

A transcript with a new mtime and the same size is no longer re-read, and one
file that can't be appended no longer sends the rest of the pass through a full
re-parse: a 400 MB transcript that grew 281 KB next to a touched 100 MB one
took 25 s to index, now about 2 s.

A pass that rewrites the index no longer re-encodes and re-tokenizes every
session it keeps: they are copied as stored and keep their postings. On a
12,000-session store a pass over 9 changed transcripts took 45 s, now 7 s
(#5383).

A pass that replaces a few sessions no longer writes the whole store again: it
appends their records, marks the old ones dead and puts the new postings in
small overlay files, and the log is written out again only once dead records
are a quarter of it. On a 25,000-session store a pass over 9 rewritten
transcripts wrote 798 MB and peaked at 2.5 GB of memory; now it writes 21 MB,
most of it the manifest, and peaks at 310 MB. A new transcript no longer
rewrites every postings file either: 225 MB and 2.4 GB there, now 18 MB and
150 MB. The index is rebuilt once for the new layout (#5387). The replaced
records are zeroed where they stand on the same pass, and so is a word only they
held in the postings files, so text taken out of a transcript leaves the index
then rather than at the next compaction (#5429), and an older deja
meeting the index can no longer bring them back as live (#5428). CJK words
are overwritten too (#5452), a show running across the pass no longer gets the
session back empty (#5449), and deja index on an index that looks up to date
finishes the zeroing a killed pass left (#5450). A lost
overlay file reads as damage and rebuilds (#5426).

A pass that reads Codex's, TRAE's or Claude Code's history.jsonl beside other
changes replaces only the sessions with a new prompt in it, not every session
known from the log alone (#5403).
A search no longer pays for the size of the store before it has found
anything. On a 12,000-session store a Russian recall that looks for a word
glued to another took 1.8-3.3 s, now 0.15-0.3 s, and checking what changed
since the last pass takes 30 ms instead of 190 ms (#5386).
A Codex line of a hundred megabytes, a command's whole output, no longer
holds several copies of itself in memory: a JSONL line past 1 MB is decoded as
it streams, each string kept to the 1 MB a message is cut to anyway. An
incremental pass over one went from 628 MB to 42 MB peak. Every line that is
not one JSON object now counts as skipped; a null line or one with more after
the object went uncounted (#5391).
A Claude Code or Cherry Studio line of a hundred megabytes is streamed the
same way, and so are the checks the incremental pass makes before it reads a
Claude, Copilot, TRAE or Codex tail, and deja changes: indexing a Claude
transcript with a 100 MB tool output went from 925 MB to 36 MB peak, 4.3 s to
0.9 s (#5414).

After a pass rewrites the index, the friction texts, the command-failure table
and the record offset table are brought up to date from the sessions the pass
changed instead of each reading the whole record log again: 2.0 s to 1.75 s
per pass on a 12,000-session store (#5402).

When two transcripts hold the same session id, a pass that rewrites one reads
the other again too, so a turn both hold is no longer indexed twice and a copy's
turns are no longer dropped until deja index --rebuild.

OpenClaw: a heartbeat answered with HEARTBEAT_OK is not a session either; a
sub-agent whose spawning window was archived as .zst keeps its parent, and
finding the spawning window reads each transcript once instead of once per
child.

Codex no longer warns on every start that deja's SessionEnd timeout was
clamped to 3 s. After a kimi -p run the session is no longer hidden from
the next session's recall for 20 minutes. In the light theme on a 256-colour
terminal, key chips no longer melt into their panel.
ё and е are one letter in the index and in queries, so «ещё» finds a session
that says «еще» and the other way round.

Semantic search works with models trained with instruction prefixes, such as
multilingual E5: DEJA_EMBED_QUERY_PREFIX and DEJA_EMBED_PASSAGE_PREFIX are
put in front of queries and indexed texts. Vectors made with another model or
passage prefix are no longer compared with the query: search says so once and
goes on lexically. A long-running deja mcp keeps the parsed vector sidecar
between semantic searches and lets it go two minutes after the last one.

DEJA_NO_AUTO_INDEX=1, or "auto_index": false in policy.json, stops every
automatic index update, for a busy machine that runs deja index from cron.
Search, show, MCP tools and hooks answer from the last finished index and say
when it was updated; with no index they print deja index instead of building
one, and neither the agent nor a hook can start a refresh (#5385).
deja index without --rebuild now notices a transcript it may not read and
a store directory it may not list, and says so on every run while they stay
locked instead of "index is up to date". Sessions already indexed from them stay
searchable, a locked directory no longer reads as deleted, and deja doctor
names a locked transcript even when it is not the newest one (#5393).

deja index exits 3 when it wrote the index but left part of the input
unread (a path it could not read, a line it skipped), so a cron job or a timer
can tell that from a clean run; 1 still means the index was not written. Piped
or under cron it prints one line per phase. A transcript read that moves no
bytes for DEJA_STORE_TIMEOUT (ten minutes by default) is skipped and named,
and the rest of the run goes on; a slow read that keeps moving is not cut.
--rebuild reads every line again, so a store with an old line deja cannot
use exits 3 on every rebuild (#5396).
While a refresh runs, search and recall say which index they answer from and
how much of the history it holds: "answering from the index of 2026-10-19
14:02 (3 minutes ago), 7000 sessions — refreshing in the background, 3000 of
10000 transcripts are new or changed since". After a first build cut off past its early slice they say it
covers only the 200 most recent of the sessions found and that the older ones
are missing, where recall used to say the newest might be (#5390).
DEJA_SEARCH_ONLY=1, or "search_only": true in policy.json, builds the index
for search alone and skips mining fixes, co-occurrence, commands, command
failures and session facts: a rebuild of a 3,000-session tool-heavy store
takes 2.7 s of CPU instead of 3.3 s. It is applied by the first build and by
deja index, so a hook running under another environment does not rebuild
the store back.
deja fix then says the index is search-only instead of that nothing ever
followed the error, and deja how and deja restore name DEJA_INDEX_COMMANDS=0
or DEJA_INDEX_EDITS=0 when those kept their data out. The commands guide lists
what each DEJA_INDEX_* switch leaves out and what stops working (#5395).
DEJA_INDEX_TOOL_OUTPUT=0 now leaves tool output out of Claude Code, Cherry
Studio, Crush, goose, Grok, pi and Command Code sessions too, and
DEJA_INDEX_COMMANDS=0 leaves commands out of goose and Copilot agent
transcripts. Every DEJA_INDEX_* switch is applied to what each reader
returns and to the context a compaction capture keeps, so a new reader gets
them without testing them itself; opencode sessions re-read for their diffs
get them and DEJA_INCLUDE_SUBAGENTS too. If you run with either switch, run
deja index --rebuild once to drop what was indexed before (#5413).

Changelog

Features

  • feat(embed): instruction prefixes for e5-style models (#5364) (@danzerzine)
  • feat(index): file ё as е so «ещё» and «еще» are one word (#5361) (@danzerzine)
  • feat(search): a session saying the whole query in one message reaches the page on a common-word store (#5430)
  • feat: DEJA_NO_AUTO_INDEX stops every automatic index update; answers say how old the index is (#5398)
  • feat: DEJA_SEARCH_ONLY skips fix and command mining, and fix, how and restore say when the data was kept out (#5409)

Fixes

  • fix(atomicfile): a publish retries only the rename windows refuses while a reader holds the file (#5418)
  • fix(ci): test / gates pass on green shards instead of reading them as queued (#5417)
  • fix(completion): offer --quiet to index, not to search (#5239) (@notsubash)
  • fix(completion): offer only --limit and --json for fix, friction, tests (#5176) (@beiryu)
  • fix(completion): promote, files, how, recap, log, view, wip, brief and update offer only the flags they take (#5240) (@notsubash)
  • fix(ctx): a cut context digest says what it left out and points at deja show (#5399)
  • fix(dsh): recalled {{...}} no longer fails every dsh turn with unknown prompt variable (#5427)
  • fix(embed): a session dated from its file's mtime keeps its vectors when it grows (#5454)
  • fix(help): the --role flag row names summary (#5078) (#5116) (@cestercian)
  • fix(hook): a recalled @path in an opencode subagent's prompt no longer attaches that file (#5440)
  • fix(index): DEJA_INDEX_TOOL_OUTPUT=0 and DEJA_INDEX_COMMANDS=0 hold in every reader (#5419)
  • fix(index): a Claude transcript whose sessionId changes partway is filed under the last id on an incremental pass, as a rebuild files it (#5382)
  • fix(index): a deleted transcript keeps the turns it shared with a copy, and a renamed copy gives its row to the smallest path (#5447)
  • fix(index): a pass that rewrites one copy of a session reads its other copies too, so shared turns are neither doubled nor dropped (#5375)
  • fix(index): a replacing pass zeroes the records it replaced and the words only they held, so removed text leaves the index on that pass and an older deja can't revive them (#5438)
  • fix(index): a session several transcripts hold keeps the row a rebuild gives it, deleted copies included (#5431)
  • fix(index): an excluded harness is not walked or indexed on any pass, not only a rebuild (#5400)
  • fix(index): an incremental pass names a transcript or store directory it cannot read instead of calling the index up to date (#5404)
  • fix(index): deja index exits 3 when it left input unread, prints its phases off a terminal, and skips a read that stops moving (#5405)
  • fix(index): removed CJK text leaves the postings, a reader across a replacing pass keeps the session, and deja index finishes a scrub a killed pass left (#5455)
  • fix(log): refuse a second count instead of keeping the last one (#5074) (#5118) (@cestercian)
  • fix(mcp): a recalled !cmd or @path in the slash command no longer runs or attaches a file on the host (#5434)
  • fix(recall): a recall whose filters leave no substring hit stops reading the whole log, 13.4 s to 1.2 s (#5377)
  • fix(recall): the per-prompt recall ranks from one index generation, and a session read survives a second pass (#5463)
  • fix(search): --re anchors ^ and $ at each line, as grep does (#5448) (#5453)
  • fix(search): an excerpt keeps the panic: or npm ERR! line that matched, and a --re miss suggests a looser pattern (#5458)
  • fix(sources): a Claude Code paste is indexed as its text, not inside its <pasted_content> wrapper (#5372) (@maxi-moss)
  • fix(sync): an import leaves out excluded harnesses and ignored projects, and the sync timer runs with the shell's deja settings (#5437)
  • fix(sync): an import leaves out the tool output, commands and sub-agent runs this machine's switches turn off (#5432)
  • fix(tui): the light theme's header bar and code blocks stay apart from the page on 256-colour terminals (#5423)
  • fix(uninstall): taking a deja entry out of a commented JSONC file leaves no dangling comma (#5376) (@littfed)
  • fix: OpenClaw MCP reaches a moved index, sub-agents follow DEJA_INCLUDE_SUBAGENTS everywhere, a fork gets its source only from the fork point, and a touched transcript is no longer re-read (#5342)
  • fix: a session no longer recalls its own lineage, and install leaves each client's config alone (#5201)
  • fix: answers during a refresh say how much history they cover, and a cut-off first build says the oldest are missing (#5408)
  • fix: check and completion name an extra argument instead of calling the first one missing (#5077) (#5115) (@cestercian)
  • fix: install keeps its wiring record when stdout closes early (#5322) (#5384) (#5384) (@BraveTonni)
  • fix: share and restore refuse a second argument instead of dropping it (#5076) (#5117) (@cestercian)
  • fix: sub-agents and forks are their own sessions under their source, and blame names the session that wrote the line (#5289)

Performance

  • perf(index): a 100 MB Claude line is streamed, not held, 925 MB to 36 MB peak (#5420)
  • perf(index): a 100 MB Codex line is streamed, not held, 628 MB to 42 MB peak (#5410)
  • perf(index): a pass that reads history.jsonl replaces only the sessions with a new prompt, 258 to 2 (#5406)
  • perf(index): a pass that replaces 20 of 40k sessions takes 1.7 s instead of 3.9 s, and 20 renames 1.4 s instead of 3.1 s (#5472)
  • perf(index): a pass that replaces 9 sessions of a 25k store writes 21 MB instead of 798 MB and peaks at 310 MB instead of 2.5 GB (#5422)
  • perf(index): a replacement pass copies kept records and moves their postings, 45 s to 7 s on 12k sessions (#5389)
  • perf(index): a rewriting pass updates friction texts, command failures and the offset table from the sessions it changed (#5411)
  • perf(index): the first pass after a full build that rewrites 20 of 40k sessions takes 4 s instead of 21 s, and 20 renames 3 s instead of 20 s (#5468)
  • perf(recall): a Cyrillic recall takes 0.39 s instead of 0.77 s on a 4050-session store (#5388)
  • perf(search): a Cyrillic recall on a 12k-session store takes 0.15-0.3 s instead of 1.8-3.3 s (#5407)
  • perf(search): a common word on a 40k-session store answers in 1.1 s instead of 24 s (#5469)

Documentation

  • docs(changelog): note recalled text no longer runs commands or attaches files in opencode (#5446)
  • docs(guide): add CodeBuddy and WorkBuddy session history deletion page (#5104) (@masyarakatai)
  • docs: changelog for 0.23.0 (#5478)
  • docs: tests, recap and secrets help say the --limit default and what 0 does (#5079) (#5290) (@WynandVStaden)

Other

  • chore: pin the manifests to 0.22.2 (#5114)
  • test(cmd/deja): resolve symlinks on cwd in TestDoctorReadsTheClientsOwnOffSwitches (#5365) (@webdevsamran)
  • test(sources): measure a long line's cost with a 16 KB cut so pool misses can't fail it (#5474)
  • test(sources): the long-line allocation test no longer counts pooled buffers lost by chance (#5464)

Install

curl -fsSL https://raw.githubusercontent.com/vshulcz/deja-vu/main/install.sh | sh
go install github.com/vshulcz/deja-vu/cmd/deja@v0.23.0
brew install deja-vu

Don't miss a new deja-vu release

NewReleases is sending notifications on new releases.