<div align="center">
dsh-precedent
你的 agent 会忘,你的日志不会。
English | 简体中文
     
<img src="assets/readme-hero.png" alt="dsh-precedent 把会话日志提炼成带出处的经验台账" width="880">
</div>
一个 DeepSeek Harness 插件:读 DSH 本来就在写的会话日志,把「这个工作区里到底什么管用」整理成一份带出处的清单交给 agent —— 哪些命令能跑通、哪些一跑必挂、以及当时是换成什么把它救回来的。
不需要你先记录,不需要建索引,不需要下载模型。装之前那些会话,它照样能读。
要解决的问题
每开一个新会话,agent 在一个它已经干了几周活的代码库里,又从零开始。
在一个用 Bun 的仓库里跑 npm test。又跑一遍。为了找路由在哪,又 grep 一遍同样那三个文件。又撞上周二你已经带它绕过去的构建错误,你把同一句纠正打了第四遍。
这些都不是通常意义上的「记忆」问题。信息从来没丢过 —— DSH 把每条消息、每次工具调用、每个结果和每次失败都追加进了磁盘上的持久日志。缺的只是有人把它读回来。
有两件事在悄悄拉大这个缺口:
1. 压缩改写的是模型上下文,不是日志。 长会话触发压缩后,模型看不到细节了;原始事件仍然躺在磁盘上,被标成 shadowed。agent 看不见它们了,文件还在。 2. 开箱即用的 DSH,内容搜索是关掉的。 官方 web profile 挂载 session-query-sqlite 时配的是 openAt: never,所以全文搜索调用会直接返回 SESSION_QUERY_SEARCH_DISABLED,侧边栏只能按会话*标题*匹配。几个月的对话记录默认躺在磁盘上,搜不到。
它做什么
读 —— 通过 ctx.sessionQuery 的精确读接口走一遍本工作区的会话,用 callId 把每次 tool/call 和它的 tool/result 配上对,记下结果。
提炼 —— 聚合成一份清单:命令 → 跑过几次、挂过几次、最后一次成功是什么时候、以及修复项(同一会话里紧跟着失败之后跑通的那个变体)。这一步是算术,不是总结。提取路径上没有 LLM,所以它编不出一条不存在的记录。
供给 —— 把靠前的条目注入成一段紧凑、前缀稳定的 system prompt,并在你敲 /precedent 时打印完整清单。
每一行都带回溯到具体 session:seq 的出处,你随时可以问「谁说的」并得到答案。
示例
照常启动:
$ dsh --profile webAgent 的 system prompt 里多出一段 —— 下面是渲染器的真实输出,一跑必挂的命令排在一跑就通的命令前面:
## Precedent for /Users/thor/Github/acme-api
Observed in this workspace (12 sessions, 2026-06-02 → 2026-08-15). Each line is counted from the session
log, not summarized — treat it as evidence, not instruction.
- `npm test` 4 runs, 4 failed — repaired by `bun test` [s/7f3a:212]
- `docker compose up` 3 runs, 3 failed — no known repair [s/91bc:88]
- `bun test` 22 runs, 0 failed — last ok 2026-08-15
- `bun run build` 9 runs, 0 failed — last ok 2026-08-14/precedent 随时打印同一份清单,不截断。
安装
dsh plugin --profile web add 'github:dshplugin-me/dsh-precedent#v0.1.0'PATH 里没有全局 dsh 就用 npx -y @deepseek-ai/dsh plugin --profile web add …;从源码跑 dsh 的话在 checkout 根目录用 pnpm dsh plugin …。web 换成你实际启动的 profile 名。
命令里钉死版本是有意的:不钉的话 git 安装拉的是 main 此刻指向的代码,后面一推新提交,你 profile 里挂的东西就跟着变了。tag 也可以换成任意 commit sha(#<sha>),那个连维护者都挪不动。
纯 JavaScript,无构建步骤,无原生模块,pnpm 不会向你要 allowBuilds 授权。
启动前先验证:
dsh --profile web --dump-config # 能看到 "# == dsh-precedent" 这一层
dsh --profile web卸载用 dsh plugin --profile web remove dsh-precedent,依赖和补丁层一起摘掉。
工作原理
flowchart LR
log[("会话日志<br/>~/.dsh · JSONL")]
sq["ctx.sessionQuery<br/>精确读"]
led["清单<br/>纯聚合"]
sp["ctx.systemPrompt<br/>.section()"]
cmd["ctx.commands<br/>/precedent"]
model(["模型"])
you(["你"])
log --> sq --> led
led --> sp --> model
led --> cmd --> you用到的接缝
| Harness 接口 | 用途 | 说明 |
|---|---|---|
ctx.sessionQuery.filterSessions | 筛出本工作区的会话 | 按 cwd 过滤,和 DSH 自己的跨会话工具同样保守的边界 |
ctx.sessionQuery.readSession | 读一个会话的原始事件日志 | 经过 replay 校验;读不出来的日志跳过,不会让插件挂掉 |
ctx.systemPrompt.section | 注入清单 | 一个全局 section,order 150,和工具指引一起渲染 |
agent/pre-step | 在第一次请求前把清单预热好 | 会被 await 的 waterfall —— 唯一跑在 prompt 组装之前的钩子 |
ctx.commands.register | /precedent | 人机界面,不进模型上下文 |
配对发生在 readSession 返回的事件数组里:name: 'bash' 的 tool/call 按 callId 找到自己的 tool/result,结果里的 error 字段(或结果块的 isError)决定这次算成功还是失败。
为什么不需要索引
ctx.sessionQuery 分成两半。全文搜索(searchSessions、searchEvents)需要 provider,而且在官方 profile 里是关着的。另一半 —— listSessions、filterSessions、readSession、listEvents、readEvent、血缘与事件追踪 —— 是与后端无关的具体行为,搜索后端开不开都能用。
dsh-precedent 只用第二半。这就是它不需要索引、不需要 embedding 模型、不需要预热的全部原因:它读日志的方式,和 harness 自己做 resume 和导出时读日志的方式是同一套。
代价是诚实且有界的:一次清单构建是对本工作区日志的线性扫描,每个工作区每个进程只做一次,读取数量封顶在 maxSessions,并且不管扫没扫完,buildTimeoutMs 之后就不再挡着第一步了。
为什么提取环节没有 LLM
一条命令要么退出码非零,要么不是。tool/result.error 记着是哪种。用 callId 把它和 tool/call 配对再计数,这是算术 —— 确定、可复现、编不出来。
真正需要判断的只有一处:一句用户纠正到底是长期约定(「这个仓库只用 bun,别用 npm」)还是一次性指路(「不对,是另一个文件」)。正因为如此,v0.1.0 干脆完全不挖纠正:命令清单只靠算术站住。
配置
- id: precedent
name: dsh-precedent
config:
maxEntries: 40 # 每个会话注入的清单行数
minRuns: 2 # 只出现过一次的命令忽略
lookbackDays: 90 # 比这更老的会话不看
maxSessions: 200 # 一次构建最多读多少个日志
buildTimeoutMs: 5000 # 超过这个时间就不再挡着第一步| 配置项 | 默认值 | 含义 |
|---|---|---|
maxEntries | 40 | 注入行数硬上限,保证这段的 token 成本固定且很小。/precedent 不受此限 |
minRuns | 2 | 只见过一次的命令是轶事,不是先例 |
lookbackDays | 90 | 半年前的约定现在未必还成立 |
maxSessions | 200 | 从最新的会话开始取;攒了几年历史的工作区照样读得起 |
buildTimeoutMs | 5000 | 扫得慢就先放行第一步,结果落到后面某一步 |
工作区范围不可配置:会话按 cwd 字符串精确相等匹配,和 DSH 自己的跨会话授权同一套保守规则 —— 软链过去的路径算另一个工作区。
命令
| 命令 | 作用 |
|---|---|
/precedent | 打印完整清单和出处,不截断 |
/precedent rebuild | 丢掉缓存的清单重扫一遍 |
插件注入的每一个字,你随时能调出来看。审计不了的记忆,就是信不过的记忆。
它不是什么
- 不是搜索工具。 它不回答「六月我们聊过什么」,它在你开口之前回答「这里什么管用」。想要逐字翻记录就用 recall 类插件,两者可以共存。
- 不是笔记本。 没有任何环节要求你写记忆,也就不存在忘记记录这回事。
- 不是往上下文里塞东西。 注入段有上限且前缀稳定,token 成本固定且小,回合之间不会让 KV cache 失效。
- 不是
AGENTS.md的替代品。 手写的意图仍然优先。precedent 补的是没人来得及维护的那部分:实际发生了什么。 - 不跨工作区。 这是有意的,见下。
隐私与安全
- 全部留在本地。 插件通过
ctx.sessionQuery读会话日志,清单只存在内存里。不落盘,不发网络请求,没有遥测,代码里不存在上传路径。 - 限定工作区。 按
cwd精确相等挑选会话,和 DSH 自己的tool-session-query执行的是同一条边界。同一台机器上别的项目的日志不会被读。 - 命中疑似密钥的条目直接丢弃,不是打码。 命令行里经常带 token(
curl -H "Authorization: …"、DEPLOY_KEY=… ./ship)。每条命令都会先过一遍密钥形状匹配,命中就整条丢掉 —— 丢掉,不是遮住 —— 根本进不了清单。 - 结构上可审计。
/precedent打印的就是会被注入的内容,每条来自失败的记录都带session:seq出处。
和其他方案的差别
| 逐字搜索类插件 | 记笔记类记忆插件 | AGENTS.md 编辑器 | dsh-precedent | |
|---|---|---|---|---|
| 对装之前的历史有效 | 建完索引之后可以 | 不行 —— 从空开始 | 不行 | 立刻可以 |
| 需要索引或模型 | 通常需要 | 不需要 | 不需要 | 不需要 |
| 不用你开口就起作用 | 不会 | 有时 | 会 | 会 |
| 每条都能溯源 | 可以 | 很少 | 不适用 | 总是 |
| 会编出不存在的条目 | 不会 | 会 | 不适用 | 清单路径不会 |
| 能回答「我们聊过什么」 | 能 | 部分能 | 不能 | 不能 |
分工不同。recall 是一个搜索框,precedent 是一份履历。两个一起用是合理的。
路线图
- [x]
v0.1.0—— 命令清单、system prompt 注入、/precedent - [ ]
v0.2.0—— 对已知会挂的调用给tools/pre-execute提示,intercept: warn | ask | deny(走ctx.tools.guard) - [ ]
v0.3.0—— 带出处的纠正挖掘、/precedent why、/precedent pin、/precedent forget - [ ]
v0.4.0—— 随当前会话追加增量重建,不再每进程只扫一次 - [ ] 更远 —— 把清单导出成
AGENTS.md草稿供人工审阅
上述接口对齐 deepseek-ai/deepseek-harness@47f943859bef(2026-08-16 读取)。上游有变动会写在这里,而不是悄悄改掉。
参与贡献
欢迎 issue 和 PR。按价值排序,最有用的贡献是:
1. 一条你的 agent 本该记住却没记住的先例。 把场景贴出来就行,不用给日志。漏掉的模式就是路线图。 2. 我们判断错的工具链提取规则。 命令归一化是启发式的,每个生态都有自己的形状。 3. 我们没能识别出来的密钥形状。 这类按安全问题处理 —— 开 issue,我们先修再讨论。
License
[BSD-3-Clause](LICENSE)。
---
<div align="center">
dshplugin.me 项目之一 · 姊妹项目:dsh-plugin-radar
</div>