dsh-dhe-lore
Verifieddsh-dhe-lore · v0.3.0 · MIT · Web UI
DSH plugin: project-level shared memory for DeepSeek Harness — every session in a project shares one human-readable, git-committable memory store. · DSH 项目级共享记忆插件。
Install
dsh plugin add dsh-dhe-lore Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Readme
dsh-dhe-lore
给 DeepSeek Harness 用的项目级共享记忆插件。
同一个项目里的所有会话共享同一份记忆。记忆是纯 Markdown,放在项目内的 .dsh/lore/,
可以读、可以改、可以提交进 Git、可以 code review。
会话 A:记住这个仓库用 pnpm,npm 会写坏 lockfile
└─ 写入 <项目>/.dsh/lore/entries/use-pnpm--k3f9x2.md
会话 B(第二天、另一台机器、换个人):打开项目
└─ 上下文里已经带着这条记忆了
目录
它解决什么问题
会话是短命的,项目是长命的。每个新会话都要重新发现一遍:构建命令是什么、为什么当初选了 这个方案、那个看起来多余的分支是在绕哪个坑、测试为什么要连数据库。
这些知识现在活在已经关掉的会话记录里。dsh-dhe-lore 把它挪到一个项目级的、人和模型都能读
写的地方——就是项目自己的目录树。
三条设计取舍,都是有意的:
共享,不是个人。 没有全局作用域。记忆属于项目,跟着仓库走,跟着 Git 走。队友拉下代码 就拿到了同样的记忆。
可读,不是黑盒。 一条记忆一个 Markdown 文件,带一小段 front matter。你可以用编辑器打开、
改、删、git diff、在 PR 里讨论。没有数据库,没有二进制 blob,没有 embedding 索引。
人写和模型写是同一种东西。 origin 字段只区分来源(tool / auto),不区分待遇。你手写的
条目和模型提取的条目在同一张列表里,用同一套规则参与注入。
和 AGENTS.md 的分工
两者都会被注入上下文,但回答的问题完全不同,不要互相替代:
AGENTS.md |
dsh-dhe-lore |
|
|---|---|---|
| 是什么 | 项目章程,人写给模型的 | 会话积累的事实 |
| 谁写 | 人(通常只在建项目时写一次) | 人和模型都写 |
| 怎么改 | 改文件、提 PR | memory 工具,或直接改文件 |
| 增长方式 | 稳定、少量 | 持续增长、会说错话 |
| 权威性 | 高——是规则,必须遵守 | 低——是线索,可以过时,用前先验证 |
| 典型内容 | "不许用 any"、"测试必须连真实数据库" | "pnpm build 要加 --filter,否则 OOM" |
注入块里明确写了记忆是低权威的、可能过时的。这是刻意的:一条被盲目相信的过时记忆, 比没有记忆更糟。
安装
# 从 registry
dsh plugin --profile web add dsh-dhe-lore
# 或从本地目录
dsh plugin --profile web add E:\path\to\dsh-dhe-lore
dsh plugin 是一个薄薄的 pnpm 转发器。退出码为 0 时,它会把声明了 dsh.bundle 的依赖自动追加
进该 profile 的 dsh.profile.bundles,插件随之下次启动时挂载。
然后重启 dsh web(宿主半边的改动需要重启才生效),并刷新浏览器(客户端 bundle 是内容
寻址的,重启后 URL 会变)。
依赖关系
宿主半边从 profile 的模块树解析 @deepseek-ai/* 与 zod——它们都是 peerDependencies(全部标了
optional),由运行时提供,不会被装成第二份。用 link: 装本地目录时 Node 会 realpath 到你的源码
目录,从那里向上找不到 profile 的 node_modules;此时可参考 开发 一节的做法。
react 是例外:它在宿主 Node 进程里根本解析不了(profile 里那条是指向不存在的
…/@deepseek-ai/dsh/node_modules/react 的坏 junction)。客户端 bundle 的 React 来自 web shell 的
静态模块表——所以绝不能把 React 打包进去,那会让 React 身份分裂、hooks 在插槽出口里失效。
用法
工具
只有一个工具 memory,用 action 区分操作。五个操作共用一套参数词汇,合成一个工具是为了省
上下文——每多一个工具,每个请求的前缀里就多一整份 schema。
| action | 作用 |
|---|---|
add |
记录一条。重复会被拒绝,并返回已存在条目的 id |
search |
按关键词和/或标签查。返回匹配条目的 id、标题、标签和正文(正文截断)。先搜再写 |
list |
列出条目(id、标题、摘要)。项目记忆与本会话便签合成一个列表统一排序:固定的在前、最近的在前 |
update |
按 id 改写一条。别人同时改了同一字段时拒绝写入,并把当前版本交回来(见「存储布局」) |
forget |
按 id 删除一条 |
search 和 list 都把项目记忆与本会话的临时便签合成一个视图再排序、再按 limit 裁剪:limit
是合并后的总数上限,不是"每个来源各 limit 条"。同一个 query 在两边含义也相同——任一个词命中即计分,
标题 3 分 > 标签 2 分 > 正文 1 分。
两者都可以加 origin 只看某一来源:origin: "auto" 是自动提取写的,origin: "tool" 是人或会话写的。
这是审计和清理提取噪声的入口。不传就是全部。
memory { action: "add", content: "这个仓库用 pnpm;npm 会写坏 lockfile。",
title: "包管理器是 pnpm", tags: ["build", "tooling"], pinned: true }
memory { action: "search", query: "构建" }
memory { action: "list", origin: "auto" } # 只看自动提取的
memory { action: "update", id: "k3f9x2", content: "……", pinned: false }
memory { action: "forget", id: "k3f9x2" }
加 scope: "session" 写本会话临时记忆:只在这个会话里可见、不落盘、不共享。适合临时草稿。
会话结束时(agent/disposed)这些条目会被释放,所以长跑的 dsh web 不会把它们一直攒在内存里。
memory { action: "add", scope: "session", content: "正在排查 #4821,怀疑是缓存未失效" }
直接改文件
memory 工具只是便利入口。文件就是事实来源:
cd <项目>/.dsh/lore/entries
code .
手写的条目和工具写的完全等价。INDEX.md 会在下一次写入时重建。
在提交里带上记忆
把 .dsh/lore/ 提交进 Git 即可(默认就该提交)。没有"个人草稿"需要忽略:scope: 'session' 的条目只存在于进程内存里,从不落盘;.dsh/lore/ 下每个 .md 都是共享记忆。
唯一的坑是 .gitignore。 不少项目(包括本插件的仓库)一开始就写了 .dsh,因为 .dsh/ 下还有 profile、缓存这类纯本机状态。那一行会让记忆永远进不了版本库,而且完全无声:git status 干净,记忆却只在你这台机器上。目录一旦被排除,就无法再排除其中某个文件,所以必须显式用 ! 把 lore/ 捞回来;顺序也有意义——*.tmp 那行必须在两个 ! 之后,否则它又会被重新包含:
.dsh/*
!.dsh/lore/
!.dsh/lore/**
# 原子写会短暂产生 entries/<名字>.md.<pid36><8位hex>.tmp,正常情况下写完立即
# `rename` 掉。残留(进程被强杀)时忽略的是暂存文件本身,而不是 tmp-*(它匹配
# 不到任何东西)。
.dsh/lore/entries/*.tmp
改完确认一下真的生效(check-ignore 会打印命中的那行,!.dsh/lore/** 说明是反排除):
git check-ignore -v .dsh/lore/INDEX.md
git status --short # 应能看到 .dsh/lore/ 是未跟踪(??)
存储布局
<项目根>/.dsh/lore/
├── INDEX.md # 自动生成的索引,请勿手改
└── entries/
├── use-pnpm--k3f9x2.md
└── note--a7b1c9.md # 纯中文标题的 slug 会退化为 note
一条记忆一个文件,这不是为了整齐,是为了并发安全:两个会话同时写不同条目会碰不同的文件,
不存在"读—改—写"的丢失窗口。全项目唯一的整文件重写是 INDEX.md,它是派生产物,允许竞态。
两个会话在同一瞬间记录同一条事实,仍可能各写一份——查重与写入之间没有互斥。所以写入之后
会再读一次对账,判负的一方删掉自己刚写的那份(幂等,对手可能已经删过)。判胜用的是条目的 created,
同毫秒再比 id,所以胜者未必是先写的那个;当判定要求删掉的是对手那份文件时,代码不碰它——
那份 id 已经交给了另一个会话,撤销它比多留下一条重复更糟。
于是这一格会留下两条重复,但不再静默:add 的结果里会带 nearDuplicate,日志写一条点名两个 id
的 warn,工具还会直接告诉模型该 forget 哪一条、或 update 幸存者做合并。重复是可见、可读、一条
命令就能清掉的噪声——不是数据损坏。(为什么闭不上、以及为什么"写入顺序"当不了判据,见 DESIGN §5.5.1。)
并发改同一条则不再是谁后写谁赢。update 在覆盖之前会重读一次文件并逐字节比对,如果这一版
不是你读到的那一版:
- 别人改的字段和你改的不重叠 → 你的改动被施加到他那版之上,两边的工作都留下;
- 重叠(都改了正文/标签/固定状态)→ 一个字节都不写,返回
ok: false与当前版本(含正文), 并点名冲突字段,让你重新施加; - 他删了这条 → 不复活;文件被改到读不出 front matter → 不覆盖,让你去看那个文件。
这不是"永不冲突"的魔法:两个写入者恰好卡在"校验完成"与 rename 之间那一瞬仍会丢失。
要彻底关掉需要跨进程锁,而锁的孤儿状态会阻塞之后所有写入——不值得。
一个条目的样子:
---
id: k3f9x2
title: "包管理器是 pnpm"
tags: ["build", "tooling"]
scope: project
origin: tool
created: 2026-02-14T10:22:33.123Z
updated: 2026-02-14T10:22:33.123Z
sourceSession: 9f2c1a
sourceCwd: "E:\\work\\my-project"
pinned: true
---
这个仓库用 pnpm。用 npm 会写坏 `pnpm-lock.yaml`,并且 CI 会因为
`--frozen-lockfile` 失败。要重装依赖时用 `pnpm install --force`。
| 字段 | 含义 |
|---|---|
id |
6 位 base36 随机 id,update / forget 用它 |
tags |
小写主题标签,用于检索 |
origin |
tool(人写的)或 auto(模型提取的) |
pinned |
固定:裁剪时最后才丢,排序最前 |
sourceSession / sourceCwd |
溯源:哪次会话、哪个子目录写的 |
文件名里的 slug 只是给人看的;唯一性由 --<id> 保证。所以纯中文标题退化成 note--<id>.md
不是缺陷,只是不好看。
文件名在条目创建时定死,之后改 title 不会改文件名。这是有意的:名字跟着标题走的话,每次改名都得
"先删旧文件、再写新文件",写入一旦失败就整条丢失,两个会话同时改同一条还会留下两个同 id 的文件。
名字固定之后,每次更新都是一次原子替换。要看最新标题,读 front matter 里的 title 或 INDEX.md。
id 会被拼进文件名,所以头部里的 id 必须是 6 位 base36 形状;写了 id: ../../x 这类值,该文件会被
当作畸形条目跳过(有 warn),而不是载入。
配置
写在 profile 的 cordis.patch.yml 里:
- id: dsh-dhe-lore
name: dsh-dhe-lore
config:
maxInjectBytes: 16384
injectSummaryOnly: false
injectEnabled: true
toolEnabled: true
toolName: memory
autoExtract:
enabled: true
everyNTurns: 2
provider: deepseek
model: deepseek-chat
| 键 | 默认 | 说明 |
|---|---|---|
projectRootMarkers |
['.git'] |
向上找项目根时的标记目录/文件 |
memoryDir |
.dsh/lore |
相对项目根的存储目录 |
maxInjectBytes |
16384 |
单次注入的硬上限(UTF-8 字节)。设为 0 表示完全不注入——不发任何消息,而不是发一个最小空框 |
maxEntryBytes |
2048 |
单条正文的 UTF-8 字节上限,写路径与注入路径共用。超长写入被拒绝(报错,不截断、不静默丢弃);手写超长的文件不被改动,只在注入时截断 |
maxSnapshotBytes |
32768 |
落进会话日志的那份快照的 UTF-8 字节上限,覆盖整个包(身份字段在内)。超了丢行(按 pinned → 最近更新的顺序从尾部丢,先项目后会话),面板据此显示"N / 共 M 条"。连身份字段都放不下时整份不发,所以 0 = 一个字节都不写(注入本身不受影响,只少了面板那份数据)。写入拒绝、视图裁剪——两者语义刻意不同 |
injectSummaryOnly |
false |
只注入标题,不带正文 |
injectEnabled |
true |
关掉后完全不注册注入监听器,本插件不再触碰任何一步 |
toolEnabled |
true |
是否注册工具 |
toolName |
memory |
工具名。注册表对重名会抛错,这是改名逃生口;注入文本与面板都引用解析后的名字 |
autoExtract.enabled |
true |
自动提取总开关 |
autoExtract.everyNTurns |
1 |
每 N 轮提取一次 |
autoExtract.minTurnChars |
200 |
本轮内容短于此值就跳过 |
autoExtract.maxInputBytes |
32768 |
送给提取模型的轮次文本上限 |
autoExtract.maxEntriesPerTurn |
3 |
单轮最多接受几条(0 关闭) |
autoExtract.timeoutMs |
20000 |
提取调用超时 |
autoExtract.provider / .model |
未设 | 显式指定路由;必须同时给,否则回退到会话自身的路由 |
上下文注入的行为
唯一的注入点是 agent/pre-step。 一个注入点就同时覆盖新会话、恢复的会话、以及中途记忆发生
变化的会话。
具体来说,插件不会另外挂 agent/session-start + agent.inject():那两个在
source === 'resume' 时会和 pre-step 一起触发,导致每次恢复都注入两遍。
按内容摘要去重,而不是"注入过没有"。 摘要覆盖模型能看到的每一个字段:每条目的
(id, title, tags, pinned, updated, 正文) 排序后求 SHA-1,注入预算、工具名与错误提示也一并计入
(它们决定这段文本长什么样)。摘要没变就不注入,所以:
- 记忆没变 → 一个字节都不花(但仍会读一次 store 做比对)。
- 手工编辑条目文件也算"变了",包括只改正文或标签、
updated原封不动。这正是 0.2.1 修掉的静默 故障:早先摘要只看(id, updated, pinned, title),手改正文永远进不了模型上下文。 - 记忆变了 → 下一步重新注入完整记忆块(不是增量 diff),无论会话是新的、恢复的,还是已经跑了几十轮。
- 进程重启后 → 摘要从会话可见面里恢复,所以恢复不会重复注入。
压缩会被感知。 压缩可能把注入块从模型上下文里挤掉。插件同时看
session.surface.replaceGeneration(位置替换的单调计数)和节点数,一旦发现可见面被替换或收缩,
就重新扫描并重新注入——不会出现"记忆悄悄消失、直到内容变化才回来"的静默故障。
裁剪顺序:固定条目 → 最近的 → 只剩标题。最具体的内容最后才丢,丢了多少都会在注入块和面板 里明确报出来,绝不静默丢弃。
防注入。 记忆正文是模型或仓库文件可以书写的内容,所以它被包在
<system-reminder> 框架里,且正文中任何字面量 </system-reminder> 都会被转义成
<\/system-reminder>,无法提前闭合框架、逃逸到指令流里。
自动提取
一轮结束时(agent/turn-stopping)后台跑一次提取,把这一轮里的耐久事实挑出来存档。
- 不阻塞。 监听器同步返回
undefined,提取是 detach 出去的。为了维护记忆而拖慢模型的回答 是不划算的。 - 失败只记日志。 提取坏掉绝不能让会话坏掉。
- 路由跟随会话。 默认从最新的
request/context读会话实际在用的 provider/model,而不是写死 一个默认值。 - 真正的降噪闸门是去重,不是提示词。 提示词里反复强调"宁可返回
[]",但最终拦住重复的是 store 里的三层去重(标题 / 正文 / 近似包含)。重复的记忆是会复利式变坏的那种故障。
提取的输入排除插件自己注入的上下文——把记忆再喂回提取器,等于让它把自己知道的东西重新 推导一遍,然后通过复述慢慢漂移。
送进模型的转写是一个 JSON 数组({ role, text },前面一句固定导语),不是把 [user] / [assistant]
标签拼成一段文字。区别是安全性的:拼字符串时,用户消息或仓库文件里写一句 [assistant] 就能伪造一个
角色边界、在提示词里替模型说话;包成 JSON 之后,它只能落在某个字符串值里面。
每次提取都会留一行 debug 日志,无论成功、跳过还是超时:
dsh-dhe-lore: extraction turn=3 stage=ok 812ms tokens=in:1200,out:88,total:1288 candidates=2 accepted=1 dropped=1
stage 说明它走到了哪一步(ok / skipped:short-turn / skipped:no-route / timeout / …),
candidates 是模型提议的条数,accepted 是真正落盘的条数。没有这行日志就没法调提示词:只看
"落盘了几条"分不清"模型什么都没提"和"全被去重挡下了"。想看就把日志级别开到 debug。
这条日志在每一条出口上都会出现,包括会话结束:aborted:disposed 表示提取还在飞的时候会话就没了
(此时流会被主动取消,结果丢弃,不打任何 warn),aborted:turn 表示这个回合被中止。另外
timeoutMs 是硬的:即使 provider 完全无视取消信号、流永远不结束,我们也会按 timeoutMs 收手,
记 stage=timeout 并写一条 warn,而不是永远挂着。
候选数超过 maxEntriesPerTurn 时会记一条 warn 写明丢弃了多少,不静默截断。自动提取的条目
origin 是 auto,可以用 memory { action: "list", origin: "auto" } 单独翻出来审。
只读面板
会话视图里会多出一个 「项目记忆」 标签页。它显示:
- 项目根与记忆目录的完整路径
- 已注入条数 / 总条数、固定数、裁剪计数、摘要短哈希
- 按 固定 / 项目记忆 / 本会话临时记忆 分组的条目列表(标题、标签、更新时间、
origin徽标、摘要); 模型还没看到的条目标为「未注入」。列表最多显示 200 条,超过时顶部会给出封顶提示。
「已注入 N」里的 N 是模型实际收到的条数,不是列表行数——注入会被字节预算裁剪,两者相差很大是 正常现象。
- 项目根未解析、目录不可读等错误状态——不允许静默空白,而且不会同时说"本项目暂无记忆条目": 读不到库和库是空的不是一回事。
- 面板是
region,分组标题是heading,列表是真正的list/listitem,📌 带可读的aria-label, 空态与错误态是status——屏幕阅读器不会把它读成一大段没有结构的文字。
面板没有任何写按钮。要改就改文件,或者用 memory 工具。
数据通道是 session projection(props.useProjection('dshLore')),不是自定义 RPC。
官方插件 dsh-context 在 v0.9 明确把自定义 RPC 通道删掉换成了同一条路径。宿主端注册一个纯折叠
单元,客户端只是读——浏览器侧零计算。
开发
cd dsh-dhe-lore
npm test # 两个套件,纯 node,无测试框架、无打包器
npm run test:behavior # 130 项行为测试
npm run test:packaging # 45 项打包契约检查(对着 profile 解析)
这两个套件只能在 DSH 的 profile / checkout 内跑:宿主半边 import 的 @deepseek-ai/* 由运行时
提供(都是 optional peer),所以从 npm 装出来的包直接 npm test 会在解析 peer 时失败。test/ 仍然
打进发布包里(dsh-session-tabbar 也这么做),是为了让这些断言可被核验,而不是承诺它能在裸环境
跑通。
行为测试覆盖真实文件系统(临时目录里的真实读写、24 路并发写入)、注入去重循环(含恢复与压缩两
条失效路径)、并发 update 的合并/报冲突每一格(用"第 n 次读取后再插对手写入"的 io 包装器把
交错钉死,不靠时序运气)、以及渲染→投影的 schema 契约。
打包检查断言 DSH 加载器真正会强制的那几条规则——都是"违了才知道贵"的类型:dsh.client 存在
就必须有 exports['./client'](否则加载器抛错);bundle patch 的行 id、客户端注册 id、npm 包名
三者必须一致;客户端 require() 只能用浏览器种子表里的 id;宿主半边每个裸 import 都必须声明为
optional peer。
模块解析
宿主半边 import 了 @deepseek-ai/dsh-tools、@deepseek-ai/dsh-llm、@deepseek-ai/schemastery
和 zod(@deepseek-ai/dsh-atomic-write 的声明已在 0.2.0 删除——那个 peer 从未被 import)。它们由
运行时提供,所以要在本仓库里跑测试,需要让它们从本包解析到。开发期用一个 junction 指过去即可:
$pkg = "E:\path\to\dsh-dhe-lore"
$src = "$env:USERPROFILE\.dsh\profiles\node_modules"
New-Item -ItemType Directory -Force "$pkg\node_modules" | Out-Null
foreach ($n in @("@deepseek-ai", "zod")) {
New-Item -ItemType Junction -Path "$pkg\node_modules\$n" -Target "$src\$n"
}
node_modules/ 已在 .gitignore 里。
注意:判断"某个 peer 能不能解析"必须从 profile 的模块树发起,而不是从本包目录——
npm run test:packaging 里的 createRequire(profiles/package.json) 就是干这个的。开发期的
junction 只是最小集合,拿它判定会得出相反结论(react 就这么被误判过一次)。
纯逻辑是零依赖的。 entry.js / project-root.js / store.js / render.js / session-scope.js
不引任何第三方包,可以用裸 node 直接测;只有集成层才碰 @deepseek-ai/*。这是刻意的分层,
不是巧合。
没有构建步骤
lib/client.js 是手写的部署产物:一个 window.__ModuleLoader__.load({...}) 调用,经典脚本,
顶层只有这一次调用。改它直接生效,刷新浏览器即可(客户端 bundle 内容寻址,重启后 URL 变化;
再不行就硬刷新)。
这也意味着:不能用 import/export,不能用 JSX,require('react') 必须从加载器取——
打包进第二份 React 会让 hooks 在插槽里失效。所有副作用(包括 CSS 注入)都在 factory 闭包内,
因为 factory 会被 HMR 重新物化。
已知边界
诚实清单,不粉饰:
- 面板列出的是"上次注入时的磁盘快照",并逐行标记哪些条目真的进了本会话的上下文。 投影是每会话的;
"已注入 N" 的 N 是模型实际收到的条数,不是列表行数——注入被预算裁剪时两者相差很大。列表本身
最多 200 条,超过时顶部给出封顶提示,
omitted/bodiesOmitted也照常显示。完整条目请看目录。 - 会话级记忆不持久化。 它在进程内存里,进程退出即丢失。这是"草稿纸"该有的语义。
它刻意不写进会话日志:往日志里追加自定义事件类型会让整个会话在下次恢复时不可读
(事件词汇表是构建期生成的、仅限仓库内部的闭集,而
Session.append()没有路径去标记ignorable)。写会成功,故障会推迟到下一次恢复才爆发——这个坑不值得踩。 - 项目根依赖
.git等标记。 不在版本库里的目录会退化到"会话自己的 cwd",此时插件会注入 一条明确的提示,而不是静默地什么也不做。 - 并发安全依赖"一条目一文件"。
INDEX.md是尽力而为的派生产物,极端并发下可能短暂滞后; 它是给人看的,不参与任何逻辑。 update的并发保护有一个微秒级残余。 字段重叠时一定会报冲突、绝不静默覆盖;但两个写入者 若恰好卡在"校验完成"与rename之间那一瞬(不是读-改-写的整段,只是最后一步),仍会丢失。 关掉它需要跨进程锁,而孤儿锁会阻塞之后所有写入,所以不换。forget与update撞上时以删除为准。update发现文件已经被别人删掉会返回missing, 不会把条目重建出来。- 自动提取是尽力而为的。 失败只 warning。质量主要由去重门控保证,提示词是第二道。
- 记忆是低权威输入。 注入块里写明了它可以过时、用前先验证。别把它当规则用;规则写进
AGENTS.md。 - 第三方客户端插槽不是冻结契约。 DSH 0.1.5-rc 阶段没有发布面向第三方的版本化插件 API 文档, 插槽目录是「生成 + 由模型查阅」的形态。插件为此做了降级:插槽不存在时面板静默不出现, 而不是报错。
- 只支持宿主看得见的工作目录。 读写两条路径都走
node:fs(写路径不能用ctx.fs:它没有 delete,forget就无从实现),所以真正跑在远端/sandbox fs provider 上的工作区不被支持—— 那种环境下本插件读写的会是宿主本地路径,而不是远端工作区。这是刻意的取舍:只把读搬到 provider 会让读写分属两个文件系统,比不做更糟。设计文档 §3.5 与 §14.1 第 4 条记了这笔账。 - 在
read-only沙箱下本插件仍会写盘。 它用node:fs写项目内固定推导出来的路径 (<projectRoot>/<memoryDir>/),不接受模型传入的路径,绕过了ctx.fs的 fence——这正是它能 直接写项目目录的前提。respectSandbox这个开关在 0.2.0 被删除,因为它从来没有读者。 - Windows 上的原子替换重试窗口是 1100 ms(20→200 ms 封顶、8 次,与
@deepseek-ai/dsh-atomic-write一致,win32-only)。更长的争用仍会让写入失败。 - 面板的数据来自投影的
viewSchema.parse()。 该 schema 由框架在每次快照读取时校验,所以render.js产出的快照形状和projection.js的 schema 必须严格一致——测试里有专门一条守住 这个契约。
故障排查
记忆没被注入。
按顺序查三件事:injectEnabled 是否为 true;maxInjectBytes 是否为 0(0 就是"完全不注入");以及会话 cwd 之上是否存在 .git。第 3 项不满足、或会话根本没有 header.cwd 时,注入块里会有一条明确的提示,而不是静默生效于别的项目。
面板没出现。
先确认宿主半边已挂载(profile 的 dsh.profile.bundles 里有 dsh-dhe-lore),再刷新浏览器。
conversation.view 插槽未声明时面板静默不出现,这是设计好的降级。
改了 lib/client.js 但浏览器没变。
客户端 bundle 是内容寻址的,重启 dsh web 后 URL 会变;或者硬刷新。若
@deepseek-ai/dsh-client-hmr 已挂载,/plugins/events 会自动触发重载。
自动提取一条都没写。
先看宿主日志里那一行 dsh-dhe-lore: extraction turn=… stage=…(把日志级别开到 debug)——它直接写着
为什么没写:skipped:short-turn 是这一轮太短,skipped:no-route 是拿不到 provider/model,
ok:no-candidates 是合法且常见的「这轮没有值得记的」。要更勤快就把 autoExtract.minTurnChars 调低、
everyNTurns 调成 1,并确认会话有过一次 request/context(也就是真的发过一次模型请求)。
dropped 大于 0 说明模型提了但被去重或超限挡下——超限会另有一条 warn。
想清理自动提取的噪声。
memory { action: "list", origin: "auto" } 只列提取写的条目,逐条 forget,或直接删文件。
人工写的条目 origin 是 tool,不会被误伤。
两条记忆看起来重复。
去重是归一化的(忽略大小写、空格、标点),但语义去重不存在。用 memory 工具 forget 掉一条,
或直接删文件。没有 embedding 索引是刻意的——在这个规模上,关键词加标签加时间比向量检索更
可预测,而且不需要任何依赖。
许可
MIT