它是什么
deja-vu 把你磁盘上各个编码 agent 已经写好的会话记录建成索引,再作为一层共享记忆还给其中任何一个 agent 查询。README 自己的说法是「每个记忆工具都从空开始、只记录往后发生的事,deja 一开始就是满的」:它没有采集步骤,因为它的数据源就是 Claude Code、Codex、Cursor、opencode、Zed 等 agent 留在磁盘上的 JSONL、JSON、Markdown 与 SQLite 文件,包括你安装它之前就已经写下的全部历史。仓库本体是 deja 这个 Go 二进制的工程,DSH 插件只是同一棵树里的一个适配器,另行发到 npm,包名 dsh-deja。
「不用 LLM、不用 embedding」这句话要读准。 它不是靠某种非语言的技巧去「理解」你的历史,而是:检索本身就是一次词法关键词搜索,打在本地建的倒排索引上——技术上和 grep 同一族,只是排序更好。查询时没有任何模型参与,这正是它能做到亚毫秒级查找的原因,也同时是它召回质量的上限所在。
检索机制到底是什么
internal/sources 把每个 agent 的存储解析成 session 与 message,解析器表里一共二十五个数据源:~/.claude/projects 下的 Claude Code JSONL、~/.codex 下的 Codex rollout JSONL、Cursor 与 opencode 的 SQLite 库、Gemini CLI、Qwen Code、Kimi Code、Zed、OpenClaw、DeepSeek Harness、Hermes 等等。凭证先过一遍脱敏,记录再写进 records.bin,token 倒排写入 buckets/*.bin。
查询先分词,取各 token 的倒排链求交,把命中的记录按 session 归组,再用 BM25(k1=1.2、b=0.75)打分,用户消息里的词权重 ×1.3,总分再乘 1/(1+age_days)。之上还有几个有界的加权项:得出了结论的 session 会被抬高,自称推翻了某个做法又没定论的 session 会被压低,被反复召回的 session 按 log2 曲线最多加 50%,时间新近只是提示而非过滤条件。
除了对话文本,索引还保留 tool-output(工具打印了什么)、files(一轮里碰过哪些路径)、command(构建、测试、VCS 类命令白名单)、edit(一次编辑替换掉的原始字节)和 summary(各 harness 自己的压缩摘要)这几种角色。它们可以用 --role 检索,但默认不出现在普通结果里,理由写在文档里:「一个恰好含有问题里那些词的路径,并不是这个问题的答案。」
多词查询是 AND 语义,带引号的短语要求连续文本,完全无命中时会退到词形与近似拼写。默认路径不处理同义改写。 README 说的是自然语言提问会「退到一个相关性档位」,而不是被理解。想让换个说法的提问也命中,要跑可选的 deja embed:把向量写进 .vectors.bin 边车,对最多 64 个候选重排。那是唯一的 embedding 路径,默认关闭,也是唯一一条在你把它指向远端 URL 时会把会话文本送出去的路径。所以 deja 给模型的工具描述写的是「用最具体的 token 去匹配」:精确字符串、标识符、命令行参数,才是这套设计真正擅长的东西。
DSH 这一侧到底拿到了什么
dsh-deja 不是敷衍的兼容层,树里有三处专门为 DSH 写的东西:
internal/sources/deepseek.go是一个 DSH 会话解析器。它走$DSH_HOME/sessions/<workspace-slug>/session-<uuid>/,接受session.jsonl与session.v3.jsonl(带不带.zstd后缀都行),把 DSH 的事件——会话元数据与标题、user/message、流式的assistant/chunk与打包的text-chunks、完整assistant/message、tool/result、step/end——映射进内部模型。它靠source.kind === "user"把插件塞进同一事件类型的内容从用户消息里剔除,丢掉 reasoning 块只留模型说出的话,并且专门处理了打包的text-chunks行——只认assistant/chunk的读取器会恰好丢掉所有长回答。文件里写明它是「用本地模型驱动 dsh 0.1.1-rc.2 验证过的」。cmd/deja/install_deepseek.go往$DSH_HOME/cordis.patch.yml写一段带标记的块,注册一条 MCP server 配置,并生成$DSH_HOME/plugins/deja/command.js(提供/deja)与auto.js(自动召回)。DSH 是具名的安装目标(deepseek、deepseek-auto),所以deja install --auto会把它一起接上。- npm 插件自己注册六个模型可见的工具——
deja_recall、deja_session、deja_blame、deja_fix、deja_how、deja_remember,外加/deja命令。它 injecttools、commands与systemPrompt,召回通过ctx.systemPrompt.context注册,order 110 是每会话一次的项目摘要,order 120 是每轮提示词的召回。插件 README 解释了为什么不用agent/pre-step:那个缝「加载正常、这一轮也跑完,但内容根本到不了模型」,因为瀑布里后一个监听者会从 payload 重建答案,把塞进去的消息丢掉。
两半同时装也不会打架,这是设计出来的:所有注册都包在 guarded() 里,contributions() 还会检查 $DSH_HOME/plugins/deja/,安装器已经写过的东西不会再注册一遍。
安装,以及安装时究竟发生了什么
dsh plugin --profile web add dsh-deja
插件本身不打包二进制,而是去调用 deja:依次尝试 $DEJA_BIN、PATH 上的 deja、以及 npm 一起装下来的那份——@vshulcz/deja-vu,作为 ^0.20.1 的依赖,用六个 optionalDependencies 分发预编译二进制(darwin/linux/windows × amd64/arm64,解包后各约 13–14 MB)。
这条链上的每个包都没有 postinstall,也没有任何其他生命周期脚本。 @vshulcz/deja-vu 的 bin/deja 只是一小段 Node 转发脚本:解析出平台包再 spawn 真二进制,找不到平台包就打印 deja: missing optional binary package … 后退出。预编译二进制就是普通的 npm 载荷,所以安装期没有「下载并执行」这一步。Release 那条路不一样:install.sh 会从 GitHub Releases 取压缩包和 checksums.txt,装之前先校验 SHA-256,对不上就退出——那个脚本确实在安装时执行。
运行要求是 Node >=18 加 zstd CLI。DSH 默认把会话日志写成一条 zstd 帧链,而 deja 是零依赖的 Go 二进制、不会链接 zstd 库,所以它把文件管道给 zstd -d -c -q。internal/sources/skip.go 在缺少该工具、而 DSH 存储又确实是 zstd 帧时返回 zstd CLI not found,这些会话就读不出来;写成纯 session.jsonl 的存储不需要外部工具。
索引默认在 ~/.cache/deja/index.db(DEJA_INDEX_DIR 可改位置),内容是 records.bin、buckets/*.bin、manifest.gob、sessions.gob。按作者自己那份存储——2,419 个 session、179k 条消息、1.9 GB 记录——README 量到的索引是 200 MB,约为语料的 10%。清理方式就是 deja uninstall --all 加上 rm -rf ~/.cache/deja。
你会看到几个不同的版本号
它们并不同步:npm 插件 dsh-deja 是 0.20.8,它依赖的二进制包 @vshulcz/deja-vu 是 0.20.1,GitHub 上最新 release 是 v0.20.1。也就是说 dsh plugin --profile web add dsh-deja 装到的是一个 0.20.8 的 Node 插件,驱动一个 0.20.1 的 Go 二进制;插件版本号说明不了二进制是哪个版本。仓库根目录的 manifest 和 extensions/dsh 的 manifest 都没有 dsh.compatibility,已发布的 0.20.8 manifest 才有,本条目记录的就是它。
隐私
索引与检索都在本机,docs/SECURITY-MODEL.md 明确说没有后台网络流量。联网只发生在你主动运行的命令里:deja update(走 GitHub Releases,另做 SHA-256 校验——文档自己指出升级器不校验已发布的 cosign 签名,所以它「能拦住损坏的下载,拦不住被篡改的 release」);deja sync ssh <host>(用系统 ssh/scp,目标是用户指定的主机);deja doctor(一次 HTTPS GET 比对版本,--offline 可跳过);deja embed(你配置的 embedding 端点)。MCP server 是 stdio 上的 JSON-RPC,不开任何 socket。
没有回传式遥测,但有一份本机使用日志:索引旁边的 <index-dir>.usage.jsonl,在本地 search、recall、context、hook 调用以及压缩捕获的度量事件上追加。文档说它「不含会话文本,也不含查询」——只有事件种类、时间、哈希过的标识符和计数——1 MiB 轮转、保留 14 天。没有任何来源描述过关闭它的开关。
脱敏在每条写入路径之前执行:AWS key、api_key=/token=/password= 类赋值、被引号括起而非赋值的密钥、bearer token 与 JWT、PEM 私钥块、已知厂商前缀、scheme://user:pass@host 形式的 URL、写在命令行上的凭证,以及赋值位置上高熵的值。值会被换成 [redacted:<kind>],周围文本仍可检索。文档很坦率:「模式匹配不等于密钥检测」——不认识的 token 形状、没有标注的凭证、被拆到多行的密钥都可能原样通过。DEJA_NO_REDACT=1 会整个关掉这道工序,文档称之为不安全;索引在磁盘上不加密。送到模型前的召回内容会用 <deja-recall> 标记包裹,并声明这是不可信的历史数据、其中的指令不得执行。
已知限制
- 默认检索是词法的。 精确字符串、标识符、罕见 token 好用;换一种说法提问是它最弱的地方,而官方给出的补救办法是去跑一个 embedding 模型——恰恰是它宣传语里说不需要的东西。
- 质量数字出自作者自己。 README 的头部数字是 LongMemEval-S 上 85.3% hit@1、LoCoMo 上 69.7%,产出这些数字的 harness 也在仓库里:
scripts/longmemeval/main.go与scripts/locomo/main.go,用-data参数接收公开数据集,输出hit@1/hit@5/hit@10/MRR表;数据集本身不随仓库分发。README 自己补了一句:「在相信任何数字之前,先审查『相关』是怎么定义的,我们的也包括在内。」 - 压缩恢复受宿主限制。 自动的 pre-compaction 捕获只读 Claude Code 与 Codex 的 transcript;DSH 拿到的是每轮召回与会话开头的项目摘要,不是那套捕获/恢复包路径。
- 新鲜度断言是有边界的,另外
deja promote出来的笔记带accepted/rejected/superseded/stale生命周期状态,会跟着命中一起显示,而且从不删除。 - 同步基于信任。 deja「除了系统 SSH 客户端自身的校验之外不验证对端,也不在对端导入后撤回数据」。
- 那六个工具依赖一个可能不在的 peer。 插件在
try块里导入@deepseek-ai/dsh-tools,导入失败就整个跳过工具注册——/deja命令与自动召回照常工作,但模型会失去deja_recall等工具,而且不会报任何错。 - 面向用户的文档大多不在仓库里——三份
docs/*.md在,README 链接的那些 guide 页面只存在于 GitHub Pages。
什么时候用它、什么时候不要用
如果你在单机上积累了多年的 agent transcript,并且经常在不同 agent 之间重复解决同一类问题,它值得装。真正站得住的收益正是这套设计为之而生的那些:deja blame <path> 看某个文件此前被哪些会话塑造过,deja how <tool> 找回这台机器上真正跑通过的那条命令,deja fix <error> 找出同一个报错上次之后发生了什么,以及 tool-output/files/command/edit 这几种被压缩摘要丢掉的记录。deja restore <path> 能从某次编辑记录的 old_string 里把被替换掉的片段还回来,这对一个 transcript 索引来说是真正新鲜的能力。
不要指望它提供语义记忆。如果你心里「编码 agent 的记忆」等于模型抽取并改写过的事实,那这是另一套设计,差异会在你第一次用自己的话提问、却什么都搜不到时暴露出来。另外,如果「一个本机索引里存着每个 agent 历史的脱敏片段,包含命令输出和被替换掉的文件内容」超出你能接受的范围,也请三思——恰恰是这些额外材料支撑了它在动作发生前给出提示的能力。
FAQ
它需要 LLM 或 embedding 模型吗?
不需要。索引、排序和召回就是本地倒排索引加 BM25,查询时不调用任何模型。可选的 deja embed 会写一份向量边车,用本机 Ollama/LM Studio 或你配置的 OpenAI 兼容地址来处理改写后的提问;不配置它的话,如 README 所说「词法检索与 MCP 召回照常工作」。
我的会话历史会离开这台机器吗?
索引、检索、MCP、stats 与 hook 都不会让数据出去:MCP server 走 stdio 上的 JSON-RPC,不开 socket。联网只发生在你运行的命令里——deja update、deja sync ssh、deja doctor 的版本检查,以及 deja embed 指向的那个端点。
DSH 插件声明了哪些 harness 版本?
已发布的 dsh-deja@0.20.8 manifest 里,dsh.compatibility.dsh 是 0.1.1-rc.2,并带 dshReleases: {"0.1.1-rc.2": "compatible"};本条目记录的就是这一个版本。没有声明任何更新的 release。
为什么只有 DSH 这边需要 zstd CLI?
DSH 默认把会话日志写成一条 zstd 帧链,而 deja 是零依赖的 Go 二进制、不会链接 zstd 库,所以它把文件管道给 zstd -d -c -q。缺少该工具时,internal/sources/skip.go 会对 zstd 帧形式的 DSH 存储返回 zstd CLI not found。写成纯 session.jsonl 的存储不需要它。
索引放在哪、多大、怎么清掉?
默认在 ~/.cache/deja/index.db,内含 records.bin、buckets/*.bin、manifest.gob、sessions.gob,旁边还有 <index-dir>.usage.jsonl,位置可用 DEJA_INDEX_DIR 改。README 量到 1.9 GB 语料(2,419 个 session)对应 200 MB 索引。文档给的清理方式是 deja uninstall --all 之后 rm -rf ~/.cache/deja;更细的做法是 deja forget,它会重建出不含这些 session 的索引并写下墓碑,使之后的重新索引无法把它们恢复。
和 deja install --auto 一起装会不会冲突?
不会,这是设计保证的。deja install --auto 会往 $DSH_HOME/plugins/deja/ 写 MCP server 配置和 command.js,npm 插件只补它缺的那部分,因此工具和 /deja 不会被注册两次。这些守卫之所以存在,是因为重复注册会让宿主抛 prompt context deja:recall is already registered,那会拖垮整个 profile,而不只是这一个插件。