dsh-memory
kagurazaka135/dsh-memory
Layered cross-session memory kept as plain YAML: facts carry a version history instead of being overwritten, a briefing is injected at session start, and a nightly pass consolidates and shards by topic past a threshold. Zero runtime dependencies, so it also runs inside the dsh Android app (verified there).
Installazione
dsh plugin --profile web add github:kagurazaka135/dsh-memoryREADME
dsh-memory
给 DeepSeek Harness(dsh)加一层跨会话的长期记忆。
dsh 的会话是孤立的:新开一场,之前的项目约定、你的偏好、上周定下的方案都不在里面。 这个插件把该长期保留的东西抽出来存成明文文件,下次会话开始时递回给模型。
不依赖数据库、不调外部服务、不需要 API key。记忆就是你磁盘上的几个 Markdown 和 YAML。
状态
| 环境 | 状态 |
|---|---|
| dsh 0.2.x(PC) | 已实测,功能完整 |
| dsh 安卓版(App 内插件) | 已实测,功能完整 |
| dsh 0.1.x | 工具可用;无 schedule 服务时不会自动定时整理 |
验证覆盖:工具注册、写读落盘、版本链、分片、检索、简报注入、跨进程/重启持久、真实环境零污染。
它做什么
存事实,不存聊天记录。
「发布分支叫 stable」「周报周五下午发」这类长期有效的信息写成一条条事实。
值变了就追加新版本,旧版本留在 history 里 —— 所以能回答"这个什么时候改的、改成什么了"。
新会话开始时把简报交给模型。 一段固定的能力说明常驻系统提示(字节稳定,不破坏 prefix 缓存), 当前简报和记忆目录走动态快照,内容变了才更新。
按需整理。
手动调 memory_organize,或在 profile 有 schedule 服务时到点自动跑:
扫最近几场会话,把反复出现、值得长期保留的内容提炼进事实和简报。只增不删。
事实多了按主题拆文件。 默认超过 80 条开始分片,配一份索引;检索先按索引挑文件,判不准就全读 —— 宁可慢,不会漏。 只在首次分片时询问模型分组方案,之后新事实走纯代码增量归属,布局稳定。
记忆会过期
存下来的是写入那一刻的断言,不是现值。文件改了、配置换了,记忆不会自动跟着变。
所以插件在三个地方提醒模型:
- 系统提示里常驻一条规则:涉及文件名、路径、配置值时,行动前先核验当前状态
memory_recall命中内容含具体路径/配置时,结果末尾附提醒- 查不到时不给空列表,而是给出下一步:换更少更独特的词 → 按标签查 → 看简报
0 命中不等于从未记录。 换个措辞就是另一条命中。
数据长什么样
全部在 $DSH_HOME/memory/(默认 ~/.dsh/memory/,可用 memory_dir 改):
memory/
brief.md 当前摘要(Markdown,可直接读改)
facts.yaml 事实(分片之前在这里)
candidates.yaml 待确认的线索
facts/ 分片之后的事实(超过阈值才出现)
_index.yaml 分片索引(缓存,可删,不影响任何事实)
<topic>.yaml 按主题拆出来的分片
unsorted.yaml 归不进去的兜底
.archive/ 旧的 facts.yaml 归档(移动过来,不删)
log/YYYY-MM-DD.json 整理日志(追加式)
.sessions.json 已处理会话台账
一条事实:
- id: "mem-1792927799706-001"
fact: "发布分支"
since: "2026-03-02"
history: # 版本链,最后一条 = 当前值
- date: "2026-03-02"
value: "release"
- date: "2026-04-18"
value: "stable"
note: "用户要求改名"
安装
dsh plugin --profile <你的profile> add <包路径 / 包名>
装上后六个工具立即可用。夜间自动整理需要 profile 里存在 schedule 服务
(如 dsh-experimental-schedule-bundle);没有的话手动整理一样能用。
不需要配 API key 或代理。 整理走宿主自己的模型服务(ctx.llm),
你在 dsh 里配的模型就是它用的模型。默认跟随宿主默认模型,想指定就在配置里写
organizer_provider + organizer_model。
配置
在 profile 的 cordis.patch.yml 里覆盖:
| 配置项 | 默认 | 说明 |
|---|---|---|
memory_dir | $DSH_HOME/memory | 记忆存放目录 |
organizer_provider | 跟随宿主 | 整理用的 provider |
organizer_model | 跟随宿主 | 整理用的模型名 |
daily_time | 04:00:00 | 夜间整理时间(本地时区) |
time_zone | 宿主时区 | IANA 时区名 |
scan_sessions | 12 | 每次整理最多扫多少场会话(上限 40) |
shard_threshold | 80 | 事实超过多少条开始分片;设 0 关闭 |
enable_shard | true | 完全关闭分片 |
enable_schedule | true | 关掉自动注册夜间任务 |
organizer_session | memory-organizer | 整理专用会话 id |
工具
| 工具 | 用途 |
|---|---|
memory_brief | 读当前简报 |
memory_recall | 按关键词或标签检索事实 |
memory_note | 记一条事实(同主题自动出新版本) |
memory_candidate | 记一条待确认的线索 |
memory_promote | 把线索升格成正式事实 |
memory_organize | 整理一次;dry_run 只报告不落盘,reshard 重排已有分片 |
一次整理做什么
顺序固定:
- 归并同义的待确认线索(多余的标
merged保留,不删) - 扫最近几场会话,按 id 剔掉整理会话自身和子 agent 会话
- 把会话摘录 + 现有事实 + 现有候选 + 当前简报交给模型,要一份 JSON
- 落地:事实追加版本、候选去重后新增、简报换新(旧的归档)
- 事实超阈值时分片
它不会做的事(有负面清单约束):
- 不把整理过程本身写进记忆("扫了几场""跑了几次"这类)
- 不记绝对路径、主机名、端口,除非用户明确要求长期记住某个地址
- 不把一次性状态写成事实
- 模型输出解析不了就什么都不写,宁可不记,不编造
所有更新都是只增不删:旧简报进 log/ 归档,旧事实版本留在 history,
被合并的候选标 merged 仍可见。代码里没有删除历史条目的路径。
已知边界
- 检索是子串匹配,不是语义搜索。 换措辞可能查不到,所以 0 命中时会给升级路径。
- 候选去重只处理格式噪音。 会剥掉
(出现 N 次)这类尾巴,真正换说法的仍可能分成两条 —— 这是有意的,误合并比多存一条更糟。 - 整理由模型判断"什么值得记"。 可能漏记也可能多记。文件是明文的,看不过去直接改。
- 分片后新增的事先进
unsorted,下次整理才按标签归位。这是为了布局稳定,不丢数据。 reshard只对已分片的库有效;空库不会凭空造片,首次分片由shard_threshold触发。
已知限制
L4 摘要只存结论,细节回不去
简报是压缩过的——它记得"做了什么",但不记得"为什么那样做"。
干完一段活,那 100 多步的判断过程会被压成几行。下次遇到同类任务,它能想起"上次做过",但想不起"上次那个坑是怎么绕的"。
这个需求是它自己在真机上提的(原文和截图在 docs/field-notes/):
「干完活,这段经历就压缩成几行摘要,细节全丢了。下一代要是能记住'上次那个 docx 为什么那样排''那次点击为什么没反应',会比多两个工具管用得多。」
想好的方向是摘要里留指针,细节沉淀到原文,再给模型一个工具去翻。但还没做——原因和判据记在 TODO.md 第 8 条。
顺手能做的活,欢迎认领。
断档不可感知
服务重启或长时间没跑之后,它拿到简报看不出中间空了一段。它知道现在几点,但不知道自己上次醒着是几点。
对应 TODO 里同一节的后半条。
开发
npm run build # tsc
node e2e/run.mjs # 端到端,默认零网络
e2e 默认用桩数据跑通全链路。要对接真实模型:
E2E_LLM_URL=https://.../v1/chat/completions E2E_LLM_KEY=... node e2e/run.mjs
许可
MIT