Skip to content

dsh-dhe-lore

Verified

dsh-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 删除一条

searchlist 都把项目记忆与本会话的临时便签合成一个视图再排序、再按 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 里的 titleINDEX.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 写明丢弃了多少,不静默截断。自动提取的条目 originauto,可以用 memory { action: "list", origin: "auto" } 单独翻出来审。

只读面板

会话视图里会多出一个 「项目记忆」 标签页。它显示:

  • 项目根与记忆目录的完整路径
  • 已注入条数 / 总条数、固定数、裁剪计数、摘要短哈希
  • 按 固定 / 项目记忆 / 本会话临时记忆 分组的条目列表(标题、标签、更新时间、origin 徽标、摘要); 模型还没看到的条目标为「未注入」。列表最多显示 200 条,超过时顶部会给出封顶提示。

「已注入 N」里的 N 是模型实际收到的条数,不是列表行数——注入会被字节预算裁剪,两者相差很大是 正常现象。

  • 项目根未解析、目录不可读等错误状态——不允许静默空白,而且不会同时说"本项目暂无记忆条目": 读不到库和库是空的不是一回事。
  • 面板是 region,分组标题是 heading,列表是真正的 list/listitem,📌 带可读的 aria-label, 空态与错误态是 status——屏幕阅读器不会把它读成一大段没有结构的文字。

面板没有任何写按钮。要改就改文件,或者用 memory 工具。

数据通道是 session projectionprops.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/schemasteryzod@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 之间那一瞬(不是读-改-写的整段,只是最后一步),仍会丢失。 关掉它需要跨进程锁,而孤儿锁会阻塞之后所有写入,所以不换。
  • forgetupdate 撞上时以删除为准。 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 是否为 truemaxInjectBytes 是否为 00 就是"完全不注入");以及会话 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,或直接删文件。 人工写的条目 origintool,不会被误伤。

两条记忆看起来重复。 去重是归一化的(忽略大小写、空格、标点),但语义去重不存在。用 memory 工具 forget 掉一条, 或直接删文件。没有 embedding 索引是刻意的——在这个规模上,关键词加标签加时间比向量检索更 可预测,而且不需要任何依赖。

许可

MIT