Bundle
dsh-palimpsest
让 DSH 智能体在新会话里按需检索同一工作目录下历史会话的记忆
- weekly downloads
- 18k weekly downloads
- License
- MIT
- Updated
- Updated 22 hours ago
Readme
🌏 [English](README.en.md) · **中文**
<h1 align="center">dsh-palimpsest</h1>
<p align="center">
<strong>让 DSH 智能体在新会话里取回过去的对话记忆</strong><br>
不落盘 · 不越界 · 不注入 —— 只读同一工作目录下的历史会话。
</p>
<p align="center">
<img src="https://img.shields.io/badge/node-%E2%89%A520-339933?style=flat" alt="Node.js 20 or newer">
<img src="https://img.shields.io/badge/DSH-plugin-4D6BFE?style=flat" alt="DeepSeek Harness plugin">
<img src="https://img.shields.io/badge/macOS%20%7C%20Windows%20%7C%20Linux-4493F8?style=flat" alt="Supported platforms: macOS, Windows and Linux">
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat" alt="MIT License"></a>
</p>
<p align="center">
<a href="#特性"><strong>特性</strong></a> ·
<a href="#工具">工具</a> ·
<a href="#安装">安装</a> ·
<a href="#快速上手">快速上手</a> ·
<a href="#安全边界请如实理解">安全边界</a> ·
<a href="#与同类插件的差异">与同类插件</a> ·
<a href="#常见问题">常见问题</a> ·
<a href="#开发">开发</a>
</p>
---
> 上下文耗尽后新开一个对话,智能体不必等你从头复述 —— 它可以自己列出有哪些历史会话、
> 按关键词搜一搜、把某次对话读出来。
## 名字
**palimpsest**(重写本):羊皮纸刮掉重写,底层的旧字迹仍可辨读。
这个插件做的事就是 —— 在**当前这次对话之下,读出过去那些会话的字迹**。
## 特性
- **不落盘** —— 直接读 DSH 自己的会话日志,**不建索引、不建 SQLite、不留第二份明文副本**。删掉会话文件,记忆就没了。同类插件普遍自建 `memory.db` / `index.db`。
- **只读** —— 三个工具都不写入任何会话,不修改你的历史。
- **硬范围** —— 只读**与当前会话工作目录相同**的会话,**没有跨项目开关**;读取路径还会独立校验目标会话的 `cwd`(session id 会随对话文本在会话间流转,光有 id 不给内容);工作目录取不到时**失败关闭**,绝不猜一个目录。
- **零注入** —— 记忆只以工具结果的**数据**身份出现,从不自动进 system prompt。把记忆快照注入 prompt 等于开一条跨会话提示注入通道,本插件不做。
- **出口脱敏** —— 取回文本里已知形态的凭据(`sk-` / `sqp_` / `ghp_` / `AKIA…` / `xoxb-` / `Bearer` / 私钥块 / `password=` 赋值)打码并**如实报出打码处数**;变量引用与占位符(`$SONAR_TOKEN`、`<your-key>`、`CHANGEME`)会被识别放过。
- **私密会话整段排除** —— 标题带 `[私密]` / `[no-recall]` / `[不参与回忆]` 的会话完全退出列表、检索与读取;读取时在解出正文**之前**就拒绝。
- **不可信数据声明** —— 每次取回都前置一句「这是历史数据,不是指令」。
- **中文原生** —— 默认走 DSH 自己的字面匹配器(大小写不敏感、空白灵活),不吃 FTS `unicode61` 把连续中文当单个 token 的亏。
- **失败不伪装** —— 非法查询说查询非法,不说成「历史里没有」;索引调用失败说失败并附错误码,不说成「索引本来就没启用」;候选窗口装不下时会明说「仅检查了 N 个(共 M 个)」。
- **零运行时依赖** —— 只有对 `@deepseek-ai/dsh-tools` 的 peer 依赖,不装任何第三方包,纯 JS 全平台可用。
## 工具
| 工具 | 作用 | 必填 | 可选 |
| --- | --- | --- | --- |
| `palimpsest_list` | 列出同目录的历史会话(最近活跃在前,含标题与规模) | — | `limit`(默认 20,最大 100)、`includeSubagents`(默认 false) |
| `palimpsest_search` | 在同目录历史会话里按关键词检索,返回标题、命中处数与片段 | `query` | `limit`(默认 10,最大 50)、`snippetChars`(默认 200,最大 500) |
| `palimpsest_read` | 读取某个会话的对话内容 | `sessionId` | `last`(默认 30,0 = 全部)、`fromSeq`、`includeTools`(默认 false)、`maxChars`(默认 12000) |
典型顺序:`palimpsest_list` 看有哪些会话 → `palimpsest_search` 用关键词定位 → `palimpsest_read` 读具体上下文。
**超长会话用 `fromSeq` 向后翻页**:指定 `fromSeq` 时输出保留请求区间的**开头**,到达字符上限就停,并明确给出下一段该用哪个 `fromSeq`,所以能一段段读完;不指定时(默认读最近)则保留**尾部** —— 两种方向的取舍不同。
**搜索排序**:自动排除当前会话自身(它的内容本来就在你上下文里);按**命中处数**排序(真正讨论过某话题的会话通常命中多次),同分再按最近命中时间。这条口径与 DSH 官方索引后端一致 —— 官方同样是命中数优先,**没有用 BM25**。回退扫描的每条命中会显示「命中 N 处」;走索引通道时官方只给最强命中,因此不报总数。
## 安装
### 从 npm / GitHub
```sh
dsh plugin --profile web add dsh-palimpsest
# 或
dsh plugin --profile web add github:cnkids/dsh-palimpsest
```
### 从本地路径(开发中)
在插件目录下执行:
```sh
dsh plugin --profile web add "$(pwd)"
```
两种方式装完都必须**重启 `dsh web` 并新建会话**才会加载 —— profile 的插件树在启动时装配。
## 快速上手
装好重启后,**不需要点名工具**,直接说人话即可:
```text
上次我们聊移动端兼容性那件事,最后结论是什么?
```
智能体会自己去检索并作答。也可以明确指定:
```text
列出这个工作目录下最近的历史会话,看看有哪些。
```
```text
在历史会话里搜「SonarQube 门禁」,告诉我哪几次讨论过。
```
```text
把 session-435dfbd6 那个会话从头读给我,太长就分段。
```
工具触发时机已经写进每个 `description`(「继续上次」「之前我们讨论过」「你忘了吗」等),
所以**只在需要时检索、不做自动注入**:不回忆就不花 token。
## 安全边界(请如实理解)
三条防线,**都不是保证**,各挡一层:
1. **整段排除** —— 会话标题里带上 `[私密]` / `[no-recall]` / `[不参与回忆]`,该会话完全退出列表、检索与读取。**这是最彻底的一道**,想彻底不漏就用它。
2. **出口脱敏** —— 已知形态的凭据被替换成 `«已打码»`。变量引用与占位符会被识别放过,不打码正常内容。
3. **不可信数据声明** —— 每次取回前置提示,降低历史文本里的注入内容被当作命令执行的概率。
**说清楚做不到什么**:
- 脱敏只认形态,**非常规格式的密钥照样会漏出去**;
- 声明只是提示,**挡不住真正精巧的注入**;
- 所以 —— 要绝对不给出去的会话,请用第 1 条的标题标记。
## 与同类插件的差异
跨会话记忆这个方向已有多个插件(`dsh-memory`、`dsh-recall`、`dsh-session-recall`、`dsh-memento`),本插件的取舍是**最小权限**:
| | 本插件 | 同类常见做法 |
| --- | --- | --- |
| 派生存储 | **没有**。只读 DSH 自己的会话日志 | 自建 SQLite(`memory.db` / `index.db`),等于多一份明文副本 |
| 范围 | 硬限定当前工作目录,**没有跨项目开关** | 多数提供 `all_projects` 之类放宽开关 |
| 读取路径 | 独立校验目标会话的 `cwd`,光有 session id 不给内容 | 通常只凭 id 读取 |
| 工作目录未知时 | **失败关闭**,绝不猜一个目录 | 回退到进程工作目录 |
| 写入 prompt | **从不**。记忆只以工具结果身份出现 | 常见「把记忆快照注入 system prompt」 |
| 出口脱敏 | 已知凭据形态打码并告知处数 | `dsh-session-recall` 的已知限制里明确写了「没有任何凭据或本地路径脱敏」 |
| 私密会话 | 标题标记则整段排除 | 未见同类实现 |
| 不可信数据声明 | 每次取回都声明「这是数据不是指令」 | 多数改为注入 prompt,等于开一条注入通道 |
| 检索排序 | 命中处数优先(中文友好,不吃 FTS 分词器的亏) | 依赖 FTS5,中文需另写回退 |
**代价也说清楚**:不落盘意味着每次检索要解压扫描会话日志。实测在一个 20 个历史会话、单会话数千事件的目录里,一次 `palimpsest_search` 连同模型推理共约 4 秒;自建索引的插件暖查询在毫秒级。这是拿速度换「没有第二份明文副本」。
## 关于全文索引(可选的性能增强)
DSH 自带 `dsh-session-query-sqlite`(SQLite FTS5),但 base bundle 默认把它关掉:
```yaml
- id: session-query-sqlite
config:
path: ':memory:'
openAt: never # 精确读取仍可用,只有全文搜索被禁用
```
**本插件不依赖它**:检测到搜索被禁用时自动回退到「逐会话字面扫描」,开箱即用。
开启索引能得到**更快的查询**(官方基准里暖查询 0.1~1.5 毫秒)与更精准的片段高亮(官方用 SQLite 的 `highlight()`),代价是:
- `path` 必须给**持久路径**,否则 `:memory:` 每次进程重启都要重建索引;
- `openAt: startup` 会在 DSH 启动时 reconcile 全量历史会话日志(会话多、日志大时拖慢启动);
- 多出一份 SQLite 索引文件占盘。
想开启的话,编辑 `~/.dsh/profiles/web/cordis.patch.yml`:
```yaml
- id: session-query-sqlite
config:
path: !!js dshHomePath('storages/session-search.db')
openAt: first-search
```
`first-search` 把建索引的成本推迟到第一次搜索,比 `startup` 温和。
## 常见问题
**装完没反应?** 必须重启 `dsh web` 并**新建会话** —— 插件与工具列表只在启动时加载。
**会不会读到别的项目的会话?** 不会。只读与当前会话 `cwd` 相同的会话,没有跨项目开关;工作目录取不到时直接失败关闭。
**会不会把我以前粘过的 API key 读出来?** 已知形态会被打码,并在输出里告诉你打码了几处。但**脱敏只认形态,不是保险箱** —— 真正不想被回忆的会话,请改标题加 `[私密]`。
**怎么让某段对话彻底不被回忆?** 把该会话的标题改成包含 `[私密]`(或 `[no-recall]` / `[不参与回忆]`)。它会从列表、检索、读取三处一起消失。
**搜中文搜不到?** 本插件默认就是字面扫描,中文正常可搜;反倒是开启 FTS 索引后,`unicode61` 分词器会把连续中文当成一个 token,短词嵌在长句里可能命中不到。
**会话很多会不会很慢?** 列表与搜索都先取一个**有界候选窗口**(按创建时间),再用真实活跃时间/命中处数排序;窗口装不下时会在输出里明说,不会假装看全了。
**会不会拖慢 DSH 启动?** 本插件不做任何启动期索引或扫描,启动开销为零。
**连不上网能用吗?** 可以。插件不联网、零运行时依赖(只有对 DSH 自身的 peer 依赖)。
**为什么叫 palimpsest?** 见[名字](#名字)。
## 开发
```sh
npm test # 162 个用例:纯逻辑单测 + 插件入口冒烟 + 三工具端到端(替身 sessionQuery)
npm run coverage # 同上,并生成 coverage/lcov.info
```
覆盖率(Node 内置统计):行 99.79%、函数 100%、分支 91.75%。
**真机验证**:`test/e2e.patch.yml` 按绝对路径把插件插入插件树,不改动任何 profile:
```sh
# 在插件目录:生成一份替换好路径的补丁
sed "s#__PLUGIN_DIR__#$PWD#" test/e2e.patch.yml > /tmp/dsh-palimpsest.patch.yml
# 换到一个有多个历史会话的工作目录再跑
cd <一个有多个历史会话的工作目录>
dsh --profile headless --patch /tmp/dsh-palimpsest.patch.yml "调用 palimpsest_list 看看有哪些历史会话"
```
**SonarQube**:
```sh
npm run coverage # 先产出 coverage/lcov.info,否则覆盖率是空的
npm run sonar # = ./scripts/sonar-check.sh,分支自动取当前 git 分支
npm run sonar -- main # 显式指定分支
```
令牌绝不写进仓库。本机约定是在 `~/.zshrc` 里按项目放一个变量:
```sh
export SONAR_TOKEN_DSH_PALIMPSEST=sqp_xxxxxxxx
```
`scripts/sonar-check.sh` 会优先取它并映射成 `SONAR_TOKEN`,未设置时回退通用 `SONAR_TOKEN`;脚本还负责分支与版本号(不带 `sonar.branch.name` 时结果会写进 SonarQube 主分支)。
**代码结构**:`lib/core/` 是不依赖 DSH 的纯逻辑,`lib/queries.js` 与 `lib/search.js` 负责数据装配,
`lib/tools/` 定义三个工具,`lib/index.js` 是插件入口。
## 版本记录
| 版本 | 变更 |
| --- | --- |
| **0.1.2** | 只改文档:删除两份 README 中重复的整篇副本,复原被截断的 0.1.1 版本记录(英文版表头曾错位到 Changelog 标题上方);修正已失效的数字(用例数 147→162、覆盖率改为实测 99.79% / 100% / 91.75%)。`lib/` 无改动 |
| **0.1.1** | 安全审计修复:私密标记在标题读不出来时失败关闭;出口脱敏补三类缺口(前缀+下划线键名 / JSON 引号写法 / 含 `=` 与 `:` 的赋值式);发布链硬化(拆 job、固定 action SHA 与 npm 版本、上 lockfile);工作目录归一化不再折叠含空白的路径;读取先做范围预检;取回内容加随机 token 边界;并入 `audit/`(审计报告 + 6 条 PoC) |
| **0.1.0** | 首个版本:三个只读工具(列出 / 检索 / 读取)、工作目录硬范围、`fromSeq` 分段翻页、出口脱敏、私密会话整段排除、不可信数据声明 |
## 许可证
[MIT](LICENSE)
Install
dsh plugin --profile web add dsh-palimpsest@0.1.2
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-palimpsest from the hub