让 DSH 的长期记忆跨 IDE 共享 —— Trae / WorkBuddy / ZCode / 任何支持 MCP 的 agent 都能读能写同一份记忆。
🤖 给 AI agent 的入口:如果你是 agent 而不是人,先读
AGENTS.md(动手前必须知道的事实与不变量); 需要让别的 agent/工具发现本仓库,用llms.txt。 本文是写给人的完整说明,可以按需跳读。
@restregular/dsh-memory 是一个只服务于 DeepSeek Harness 的插件:
它的记忆库、召回注入、管理面板都长在宿主里,离开 DSH 就完全用不上。
本项目把同一套记忆能力抽出来做成一个独立的 MCP (Model Context Protocol) 服务:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ DSH 插件 │ │ Trae │ │ WorkBuddy │ │ ZCode │
│ (原生面板) │ │ (MCP 客户端) │ │ (MCP 客户端) │ │ (MCP 客户端) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │ │
└──────────────────┴──────────────────┴──────────────────┘
▼
<store-dir>(两边必须配成同一个)
global.jsonl + projects/<sha1>.jsonl
+ .vec 向量边车
一处写入,多处可见。 在 Trae 里记下的偏好,回到 DSH 继续对话时 agent 也能检索到。
@restregular/dsh-memory |
本项目 | |
|---|---|---|
| 形态 | DSH 插件(cordis loader entry) | 独立 MCP 服务(stdio) |
| 可用宿主 | 仅 DSH | Trae / WorkBuddy / ZCode / 任何 MCP 客户端,DSH 不需要开 |
| 工具数 | 4 个(memory_get 定义了但漏注册,见 上游 #2) |
5 个(补上 memory_get) |
| 管理面板 | ✅ 设置页「记忆」 | ❌ 面板仍建议在 DSH 里用 |
| 自动召回注入 | ✅ 每轮自动注入 | ❌ 由调用方 agent 主动调 memory_search |
| 存储 | JSONL + .vec 边车 |
完全相同的格式,共用同一个 storeDir |
| 检索算法 | 词法 IDF + CJK bigram + 本地向量 | 逐条对齐(复用上游 scoring.js / scope.js) |
| 多进程安全 | ❌ 见 上游 #3 | ✅ 独占锁 + 每次事务重新读盘 |
- 五个模型工具:
memory_save/memory_search/memory_list/memory_get/memory_forget - 与 DSH 插件共用同一份数据(
storeDir),无需导入导出,两个方向都实时互通 - 不需要开 DSH —— IDE 自己拉起进程、自己拉起向量后端
- 检索算法与上游逐条对齐,不是"近似实现"
- 跨进程安全:独占锁文件 + 读-改-写在锁内重新读盘,不会覆盖别的进程刚写的记录
本 MCP 服务完全独立,不依赖 DeepSeek Harness 是否运行。
由 IDE 的 MCP 配置启动 —— 不是某个脚本,也不是随 DSH 插件启动。IDE 读自己的
mcp.json,看到 dsh-memory 条目后 spawn 出这个进程:
{
"mcpServers": {
"dsh-memory": {
"command": "C:\\Program Files\\nodejs\\node.exe",
"args": ["C:\\path\\to\\dsh-memory-mcp\\src\\index.js", "--store-dir", "C:\\Users\\me\\.dsh-memory-store"]
}
}
}启动后的链路:
你打开 Trae / WorkBuddy / ZCode
└─ IDE 读 mcp.json(或 ZCode 的 ~/.zcode/cli/config.json)
→ spawn: node src/index.js --store-dir ...
├─ 读磁盘上的 JSONL 记忆(不等 DSH)
└─ 探活 8199 → 没活 → 自己 spawn llama-server(向量后端)
想在任务管理器里确认它:Trae 开着时,能看到一个 node.exe 跑着
D:\dev\dsh-memory-mcp\src\index.js,命令行参数与上面 mcp.json 里写的完全一致。
| 组件 | 谁启动它 | 依赖谁 |
|---|---|---|
| DSH 记忆插件 | DSH 自身的 cordis 插件系统 | — |
| 本 MCP 服务 | IDE 的 MCP 配置 | 不依赖 DSH |
llama-server 向量服务 |
各自 spawn(插件用 8078,MCP 用 8199) | 互不干扰 |
三者只共用同一个数据目录(<store-dir>,DSH 插件与 MCP 必须配成同一个),记忆因此互通 —— 仅此而已,
没有父子关系、没有共享进程。
stdio 型 MCP 不是 Windows 服务,而是 IDE 按需拉起的子进程:
| 场景 | MCP 服务 |
|---|---|
| Trae / WorkBuddy / ZCode 需要调记忆工具时 | ✅ 由 IDE 拉起 |
| 只有 DSH 开着 | ❌ 不存在(DSH 走自己的插件,不经过 MCP) |
| IDE 关闭 | ❌ 随之退出,不留孤儿进程 |
记忆不会因为进程重启而丢失 —— 下次 IDE 拉起时读的是磁盘上同一份 JSONL。
想开机就常驻可以自己注册计划任务,但通常没必要:IDE 关掉自动回收更干净。
scripts/acceptance-standalone.mjs(在 8078/8199 全空闲、无任何 llama-server 的状态下,
DSH_MEMORY_EMBED_EXE / DSH_MEMORY_EMBED_MODEL 已配好):
✓ MCP 服务在无 DSH 情况下冷启动成功
✓ MCP 自己拉起了 llama-server(8199)
✓ DSH 的 8078 始终未被占用(两者互不干扰)
✓ 读到了 DSH 之前写入的记忆(命中 5 条)
✓ memory_save 在无 DSH 时成功
✓ 语义相近但措辞不同也能召回
✓ MCP 退出后向量后端随之关闭(无孤儿进程)
服务端日志:vector: 已启动 llama-server pid=35584 http://127.0.0.1:8199
⚠️ 别用端口状态反推宿主状态。「8199 没监听」只说明向量服务此刻不在跑, 不代表 DSH 没运行 —— 这个推理错误本项目犯过一次,特此记下。
- 安装
- 启动
- 命令行与退出码
- 验证你的部署
- 环境变量
- 五个工具
- Trae / WorkBuddy / ZCode 配置
- 与 DSH 共存的设计
- scope 语义(与 DSH 的差异)
- 向量开关
- 已知限制
- 自测
- 面向 AI agent
要求 Node.js >= 20.3.0(见 package.json 的 engines)。
这个下限不是随手写的,也不是"Node 越新越好"的意思 —— 它由两个硬约束取较严者决定:
| 约束 | 要求 | 出处 |
|---|---|---|
源码里用到的 AbortSignal.any() |
>= 20.3.0 | src/vector.js(AbortSignal.any 在 Node 20.3.0 才出现;20.2 及以前是 undefined) |
依赖树里的 @hono/node-server |
>= 20 | MCP SDK 的传递依赖 |
所以 20.3.0 是真实下限。低于它会在运行到向量检索时抛 AbortSignal.any is not a function,
而且那个报错不会告诉你"是 Node 太旧"—— 这是部署时第一道容易撞上的门槛。
Node 18.20 上实测整套自测也能过,但那是走运:
AbortSignal.any只在「带 signal 调embed()」这条 运行期路径上才会执行到。靠"恰好没踩到"的兼容不算兼容,所以下限没有定在 18。
两种装法,任选一种:
# ① 从源码(推荐:能跑完整自测)
git clone https://github.com/tearslee/dsh-memory-mcp.git
cd dsh-memory-mcp
npm install
# ② 作为依赖装进别的项目 / 全局(装完就能自证)
npm install dsh-memory-mcp
npx dsh-memory-mcp --version依赖清单:
| 包 | 版本 | 用途 |
|---|---|---|
@modelcontextprotocol/sdk |
1.32.2(精确版本) |
MCP 协议实现(Server / StdioServerTransport / Client) |
存储层(src/store.js / src/scoring.js / src/scope.js / src/scope-names.js / src/lock.js)零外部依赖,只用 Node.js 内置模块。
SDK 只在 src/server.js 里用到三行 import;其余 8 个文件对 SDK 一无所知。
npm install后依赖树会变大(96 个包)是正常的 —— SDK 自带express/ajv/zod等传递依赖。
# 最简:store 用默认目录(~/.dsh-memory-store),工作区 = 当前目录
node src/index.js
# 指定工作区(决定 project 作用域)
node src/index.js --project D:\dev\my-app
# 指定存储目录(**与 DSH 插件共用时必须显式给**,见下方警告)
node src/index.js --store-dir D:\dev\.dsh-memory-store
# 两者一起
node src/index.js --project D:\dev\my-app --store-dir D:\dev\.dsh-memory-store
⚠️ 默认 storeDir 是~/.dsh-memory-store,它是平台中立的,但不一定是你 DSH 在用的那个。DEFAULT_STORE_DIR只保证"任何机器上都能跑起来",不保证"跑对了库"。真正要共用的是 DSH 记忆插件的storeDir。两者不一致时服务一切正常,只是你看到的是另一份(空的)记忆库 —— 这是本仓库最容易让人误判「没生效」的地方。用默认值启动时,stderr 会打一条显式提示:
[dsh-memory-mcp] ℹ️ 未指定 --store-dir / DSH_MEMORY_STORE_DIR,使用默认目录:C:\Users\me\.dsh-memory-store [dsh-memory-mcp] ⇒ 请确认它与 DSH 记忆插件的 storeDir **是同一个目录**,否则两边看到的是两份互不相通的记忆。看到这条就说明你在用默认库。想确认对不对,最直接的办法是看那个目录里有没有
global.jsonl。
启动参数(都支持 --key value 与 --key=value 两种写法):
| 参数 | 说明 | 缺省 |
|---|---|---|
--project <dir> / --project-dir <dir> |
当前工作区目录(绝对或相对)。决定 project / project:<路径> 作用域。 |
DSH_MEMORY_PROJECT → process.cwd() |
--store-dir <dir> / --store <dir> |
记忆存储目录。 | DSH_MEMORY_STORE_DIR → ~/.dsh-memory-store |
-h / --help |
打印用法后退出(0)。 | — |
-v / --version |
打印版本后退出(0)。 | — |
进程是 stdio 传输:stdout 只输出 MCP 协议消息,所有日志走 stderr。
SIGINT / SIGTERM 会优雅退出,并且只终止自己拉起的 llama-server(复用的外部服务原样保留)。
CLI 是自描述的,不需要读源码就能查用法:
$ node src/index.js --help # 打到 stdout,exit 0
$ node src/index.js --version
dsh-memory-mcp 0.1.0--help / --version 有两个刻意的性质:
- 不加载 MCP SDK —— 所以
node_modules没装、依赖缺失时,帮助照样能打出来。 这是部署排障的第一步("先确认我参数写对没")。 - 不碰磁盘 —— 不建 store 目录、不探活向量后端。用一个不存在的
--store-dir跑--help, 跑完那个路径仍然不存在。
| 退出码 | 含义 |
|---|---|
0 |
正常退出(--help / --version;服务在 stdin 关闭后也以 0 退出) |
1 |
启动失败(store 目录不可用、依赖缺失等),会打印可操作的中文提示 |
2 |
参数错误(未知参数、参数缺值),不会启动服务、不碰 store |
两条容易踩的规则:
- 未知参数不再被静默忽略。
--store_dir、--storedir、--storeD这类笔误以前会让服务 悄悄回退到默认 store(在别人机器上等于写到一个莫名其妙的目录),现在一律 exit 2 并指出是哪个参数写错了。 - flag 缺值同样报错。
--store-dir后面什么都没有、或紧跟另一个--xxx,都算缺值 —— 不会把--help当成目录名吃掉。
启动失败时给的是可操作的中文诊断,不是 Node 裸栈:
[dsh-memory-mcp] ❌ 启动失败:记忆存储目录不可用:Q:\nope\store
[dsh-memory-mcp] ⇒ 原因:盘符 Q: 不存在(这台机器上没有这个盘)
[dsh-memory-mcp] ⇒ 改 --store-dir(或环境变量 DSH_MEMORY_STORE_DIR)指向一个已存在且可写的目录;默认是 C:\Users\me\.dsh-memory-store
[dsh-memory-mcp] 原始错误:ENOENT ENOENT: no such file or directory, mkdir 'Q:\nope\store\projects'
需要完整调用栈时设 DSH_MEMORY_DEBUG=1。已覆盖的诊断:盘符不存在、父目录不存在、路径已存在且是个文件、
无写入权限、缺依赖(npm install 提示)。
刚 npm install 完,别急着配 IDE —— 先用下面 5 步确认你自己这台机器是好的。
全部用临时目录,不会碰你真实的记忆库。
下面命令块是 bash / zsh 写法;PowerShell 的差异标在每行后面的注释里。 ④⑤ 里的
--store-dir指向一个临时目录,跑完可以直接删。
# ① Node 版本够不够(要 >= 20.3.0;低于它会在向量检索时才炸,很难查)
node -v
node -p "require('./package.json').engines.node"
# ② 依赖 + 版本 + storeDir 一次查完(只读,不建目录、不碰记忆库)
node scripts/check-install.mjs
# 想连"我会落到哪个 storeDir"一起确认,就把要用的参数带上:
# node scripts/check-install.mjs --store-dir <你的 DSH storeDir>
# ③ CLI 能自描述(--version / --help 都不加载 SDK、不建目录,依赖没装也能用)
node src/index.js --version
node src/index.js --help | head -3
# PowerShell: node src/index.js --help | Select-Object -First 3
# ④ 实际生效的 storeDir 对不对 —— 看 stderr 首三行
# 注意 stdin 必须关掉/喂空,否则 stdio 服务会一直等输入不退出
echo '' | node src/index.js --store-dir /tmp/dsh-probe
# PowerShell: echo '' | node src/index.js --store-dir "$env:TEMP\dsh-probe"
# ⑤ MCP 握手通不通(喂一个 initialize 给 stdin,stdout 应回一行 JSON-RPC)
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"selfcheck","version":"1.0.0"}}}' \
| node src/index.js --store-dir /tmp/dsh-probe
# PowerShell 用上面这条原样即可(echo 是 Write-Output 的别名,单行 JSON 没问题)每一步该看到什么(以下输出在 Windows(Node 26)与 Linux(Node 20/22/24)上逐条实际跑过; 除路径写法以外一致)。
① 版本检查 —— engines 会原样打出来,自己比一下:
v26.3.0
>=20.3.0
② 部署自检 —— 三行绿色 ✅ 就是好的:
✅ Node 版本 26.3.0(要求 >= 20.3.0)
✅ 依赖 @modelcontextprotocol/sdk 1.32.2
file:///C:/path/to/dsh-memory-mcp/node_modules/@modelcontextprotocol/sdk/dist/esm/index.js
✅ 入口 src/index.js 存在
然后它会告诉你会落到哪个 storeDir。如果用的是默认值,你会看到一条 ⚠️:
⚠️ storeDir = C:\Users\me\.dsh-memory-store
⇒ 这是**默认值**。它只保证能跑,不保证就是你 DSH 在用的那个库。
看到这条 ⚠️ 就要警惕了 —— 接着带上真实参数再跑一次,确认它认得出你的库:
node scripts/check-install.mjs --store-dir <你的 DSH storeDir>
# 期望多出一行:✓ 该目录里有 global.jsonl,看起来是一份已有的记忆库为什么不用
npm ls @modelcontextprotocol/sdk --depth=0:从 npm 装进别的项目时node_modules是**平铺(hoisted)**的,本包目录下没有自己的node_modules,npm ls会打出-- (empty)—— 那表示"依赖被提升到上一层",不是"没装", 很容易让人误判成安装失败。check-install.mjs用 Node 自己的解析算法,两种布局下都准。
③ 自描述 —— 退出码 0,且 --help 第一行带版本号:
dsh-memory-mcp 0.1.0
dsh-memory-mcp 0.1.0 —— 把 DSH 的长期记忆库暴露成独立 MCP 服务(stdio)
④ 这一步最容易查出问题。前几行 stderr 就是实际生效的配置(不是你以为的那个)。
第 4 行起是向量后端的日志,因机器而异(关闭 / 复用已有服务 / 自行拉起 / 启动失败退化),
不影响判断 —— 只看前 3 行。看到 已启动 dsh-memory-mcp v0.1.0(stdio) 即启动成功:
[dsh-memory-mcp] store-dir = C:\Users\me\AppData\Local\Temp\dsh-probe
[dsh-memory-mcp] project = D:\dev\my-app(project 作用域 = project:D:\dev\my-app)
[dsh-memory-mcp] 默认保存作用域 = global
[dsh-memory-mcp] 已启动 dsh-memory-mcp v0.1.0(stdio) ← 这行 = 启动成功
store-dir必须等于你 DSH 插件正在用的那个目录。不相等 = 两边看到两份互不相通的库。- 没传
--store-dir时会额外多打一行ℹ️ 未指定 --store-dir … 使用默认目录,说明你正在用默认库 (~/.dsh-memory-store),多半不是你 DSH 那个。 - 对照办法:去那个目录
ls,有没有global.jsonl。 - 若看到
vector: llama-server 启动失败(…),向量后端不可用,退化为纯词法检索:不是错误, 服务照常工作,只是没有语义召回。换机器后想恢复向量,按 环境变量 那张表配DSH_MEMORY_EMBED_EXE+DSH_MEMORY_EMBED_MODEL,或直接设DSH_MEMORY_VECTOR=off关掉它。
⑤ 握手 —— stdout 只有一行 JSON-RPC(日志全在 stderr,这是 stdio 传输的硬要求):
{"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"dsh-memory-mcp","version":"0.1.0"}},"jsonrpc":"2.0","id":1}看到这行 = 进程能拉起来、协议层是通的。想让 IDE 之外更完整地验一遍工具清单,直接跑
npm test(见 自测)。
④⑤ 里
--store-dir指向的临时目录跑完可以删;它只证明"能启动",不代表配好了。 真正要共用记忆时,必须显式--store-dir指向 DSH 插件的storeDir。
| 变量 | 默认 | 说明 |
|---|---|---|
DSH_MEMORY_PROJECT |
process.cwd() |
当前工作区目录,等价于 --project。 |
DSH_MEMORY_STORE_DIR |
~/.dsh-memory-store |
存储目录,等价于 --store-dir。 |
DSH_MEMORY_SCOPE_ALL |
false |
1/true/yes/on ⇒ memory_save 省略 scope 时默认写 project 而不是 global。 |
DSH_MEMORY_VECTOR |
开启 | off(或 0/false/no/disabled)⇒ 完全关闭向量,退化为纯词法检索。 |
DSH_MEMORY_EMBED_ENDPOINT |
— | 直接指定已有的 embedding 服务(如 http://127.0.0.1:8078)。设了它就永不 spawn 进程,也永不杀进程。 |
DSH_MEMORY_EMBED_EXE |
D:\dev\llamacpp\llama-server.exe |
llama-server 可执行文件。默认值是作者本机路径,换机器必须设。 |
DSH_MEMORY_EMBED_MODEL |
D:\dev\models\Qwen3-Embedding-0.6B-Q8_0.gguf |
gguf 模型文件。同上。 |
DSH_MEMORY_EMBED_PORT |
8199 |
端口(避开 DSH 的 8078,两者可共存)。 |
DSH_MEMORY_EMBED_THREADS |
6 |
llama-server 线程数。 |
DSH_MEMORY_EMBED_POOLING |
按模型推断 | 池化方式。换模型时务必确认这一项(见下)。 |
DSH_MEMORY_DEBUG |
关闭 | 1 ⇒ 启动失败时额外打印完整调用栈(默认只打一行原始错误)。 |
⚠️ 向量相关的三个默认值(EXE/MODEL)是作者本机的绝对 Windows 路径。 别人 clone 后直接跑,向量后端会 spawn 失败 —— 但服务不会崩,只是静默退化为纯词法检索。 换机器时三选一:设DSH_MEMORY_EMBED_EXE+DSH_MEMORY_EMBED_MODEL;或用DSH_MEMORY_EMBED_ENDPOINT指向已有的 embedding 服务;或直接DSH_MEMORY_VECTOR=off关掉向量。
所有工具都返回 { content: [{ type: 'text', text }], structuredContent: {...} },
文本是给人(和模型)看的友好渲染,structuredContent 是给程序用的结构化数据。
保存一条长期记忆。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
content |
string | ✅ | — | 一句自包含的陈述句,不依赖上下文也能读懂。 |
scope |
string | global | 作用域。省略时写入 global(见下方说明)。 |
|
path |
string | — | 工作区目录。给出后写入 project:<解析后的绝对路径>。 |
|
kind |
enum | note |
fact / decision / preference / procedure / note / rule。 |
|
tags |
string[] | [] |
自由标签。 | |
importance |
number | — | 0~1,越界报错。 |
|
supersedes |
string | — | 被本条取代的记忆 id。 |
默认 scope =
global,这是与 DSH 最重要的差异。 DSH 里tools.defaultScope是session,但 Trae / WorkBuddy / DSH 各自的 session id 完全不同, session 作用域无法跨 IDE 共享。真正能共享的只有global与project:<绝对路径>, 所以 MCP 默认写global,让三个 IDE 看到同一份记忆。想要「只对当前工作区生效」,显式传
scope: "project"或path: "<工作区目录>"。也可以用
DSH_MEMORY_SCOPE_ALL=true把默认改成project。默认不开启 —— 默认写global的收益是「Trae 里学到的偏好,WorkBuddy 里立刻能用」, 而代价是 global 作用域会混入各个项目的记录。对多数个人使用者来说, 跨 IDE 共享的价值大于作用域混在一起的代价,所以默认选global。
输出示例:
已保存记忆 ae2061aa @ global
按关键词或语义检索。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
query |
string | ✅ | — | 关键词或一句话描述。 |
scope |
string | 当前项目 + global | all / * = 所有已发现作用域(只读)。 |
|
topK |
number | 5 |
1~20。 |
输出示例:
找到 2 条记忆(检索范围:D:/dev/my-app, global)
[0.4213] (global/procedure) 本机 DSH 的 desktop profile 由 Electron 应用独占管理
id=ae2061aa tags=DSH,desktop profile importance=0.90 updated=2026-10-19 12:03:11
[0.1875] (project:D:/dev/my-app/decision) 前端统一用 TypeScript 严格模式
id=be1c02d7 importance=0.70 updated=2026-10-20 09:15:22
提示:需要完整内容时用 memory_get 传入上面的 8 位 id 前缀。
列出记忆,按最近更新优先。
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
scope |
string | 当前项目 + global | all = 所有作用域。 |
|
kind |
string | — | 按种类过滤。 | |
limit |
number | 50 |
1~200。 |
按 id 取回具体内容(可写 8 位前缀)。已知 id 时用它,不要为此再跑一次 search。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
ids |
string[] | ✅ | id 或 8 位前缀。自动去重,最多取前 50 个。 |
删除记忆。id 与 query 恰好二选一。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string | 二选一 | 精确 id(或 8 位前缀)。跨作用域唯一,scope 参数不适用。 |
query |
string | 二选一 | 按内容子串批量删。 |
scope |
string | query 路径省略时只作用于当前工作区(不会误删 global)。 |
- 都不给 →
TARGET_REQUIRED - 都给 →
TARGET_AMBIGUOUS scope: "all"与query同用 →SCOPE_NOT_WRITABLE(防批量误删)
输出示例:
已删除 1 条记忆 ae2061aa @ global
每个工具除了给模型的文本,还返回 structuredContent,字段名与上游 DSH 插件保持一致
(同一个 agent 在两边可以用同一套读取习惯):
| 工具 | structuredContent 关键字段 |
|---|---|
memory_save |
{ ok, id, deduped, scope, kind } |
memory_search |
{ ok, count, scopes, records: [{ id, shortId, score, scope, kind, content, tags, importance, updatedAt }] } |
memory_list |
{ ok, count, scopes, records: [...] } |
memory_get |
{ ok, count, records: [...], missing: [...] } |
memory_forget |
{ ok, deleted, ids: [...] } |
出错时返回 isError: true + { ok: false, code, message }。不抛栈,文本里写明怎么改。
参数校验类错误码:
| 错误码 | 触发点 |
|---|---|
CONTENT_EMPTY |
memory_save 的 content 为空或非字符串 |
QUERY_EMPTY |
memory_search 的 query 为空或非字符串 |
KIND_INVALID |
kind 不在六种之内(memory_save 与 memory_list 共用) |
IMPORTANCE_RANGE / TOPK_RANGE / LIMIT_RANGE / IDS_EMPTY |
对应数值越界,或 ids 是空数组 |
TARGET_REQUIRED / TARGET_AMBIGUOUS |
memory_forget 的 id 与 query 都不给 / 都给 |
PATH_INVALID |
path 是空串 |
PATH_SCOPE_CONFLICT |
同时给 path 和 scope: global/session/all |
作用域类错误码:
| 错误码 | 触发点 |
|---|---|
SCOPE_INVALID |
作用域名不合法(含 : / \ ..、控制字符、超长、保留名) |
SCOPE_NOT_WRITABLE |
用 all 写;或 memory_forget 把 all 与 query 同用 |
SCOPE_NAME_INVALID / SCOPE_NAME_TAKEN |
别名本身非法 / 该别名已被另一个路径占用(来自 scope-names.js) |
存储与兜底类错误码:
| 错误码 | 触发点 |
|---|---|
LOCK_TIMEOUT / LOCK_RECURSION |
取锁 10 秒超时 / 同进程对同一把锁递归加锁 |
STORE_ERROR |
存储层返回了无法识别的结果(适配层兜底) |
UNKNOWN_TOOL / TOOL_ERROR |
工具名不存在 / 任何未归类的异常 |
注意
memory_search的记录数组字段是records(不是results),与上游lib/tools/index.js的{ records: [...] }对齐。
<store-dir>/
├── global.jsonl ← scope 键 "global"
├── global.vec ← 向量边车,首行 #qwen3-embedding-0.6b\t1024
├── projects/<sha1(scope)>.jsonl ← 其他任何 scope 键
├── projects/<sha1(scope)>.vec
├── scopes.json ← 作用域别名(只有登记过别名才存在)
├── .locks/ ← 每 scope 一个锁文件(sha1(scope).lock)
├── llama-server-mcp.log ← 自己 spawn 的向量后端日志(仅 spawn 过才存在)
└── settings.json ← DSH 的注入设置(本 MCP **不读不写**)
排查"记忆去哪了"时,就看这个目录里有没有 global.jsonl、以及 mtime 是不是刚刚。
llama-server-mcp.log 是排查"语义检索为什么不生效"的第一现场。
Trae 与 WorkBuddy 的 MCP 配置格式一样(mcpServers 对象)。ZCode 格式不同,单独一节。
下面几段直接贴进对应配置文件即可。
配置文件:%APPDATA%\Trae CN\User\mcp.json(本机实测路径)。
也可以在 Trae 里 设置 → MCP 服务器 → 添加 手动填。
配置文件:%USERPROFILE%\.workbuddy\mcp.json。
配置文件:%USERPROFILE%\.zcode\cli\config.json。
ZCode 的键名与形状和上面两个不一样:不是顶层 mcpServers,而是嵌套的 mcp.servers。
少一层 mcp 会被 ZCode 当成「没配 MCP」而静默忽略。
{
"mcp": {
"servers": {
"dsh-memory": {
"type": "stdio",
"command": "C:\\Program Files\\nodejs\\node.exe",
"args": [
"C:\\path\\to\\dsh-memory-mcp\\src\\index.js",
"--store-dir",
"C:\\Users\\me\\.dsh-memory-store"
],
"env": {
"DSH_MEMORY_EMBED_EXE": "C:\\path\\to\\llama-server.exe",
"DSH_MEMORY_EMBED_MODEL": "C:\\path\\to\\Qwen3-Embedding-0.6B-Q8_0.gguf",
"DSH_MEMORY_EMBED_POOLING": "last"
},
"timeoutMs": 60000
}
}
}
}ZCode 专属注意事项(均为本机对 ZCode.exe / resources/glm/zcode.cjs 实测求证):
- schema 是严格的:多写一个不认识的键,整个 server 会被丢掉而不是被忽略。
可用键只有
type/command/args/cwd/env/enabled/timeoutMs/alwaysLoad/protocolVersion。command必须是字符串(写成数组["npx","-y",...]会让 ZCode 的设置页崩在command.trim is not a function)。 cwd不用写:不写时 ZCode 直接拿当前工作区当 MCP 进程的 cwd, 所以project作用域天然跟着工作区走 —— 这正好替代了下面「关于--project」那节 在 Trae/WorkBuddy 里要靠项目级配置解决的问题。- 模板变量不展开:
${...}只对插件自带的 MCP 生效,配置文件里要用绝对路径。 - 改完要重启会话(ZCode 每个工作区起一个
app-server子进程,配置在它启动时读一次); 状态看 设置 → MCP,会显示 connected / failed 与工具数量。 - 没有
zcode mcp add命令:ZCode 的 MCP 只能靠改这个 JSON 文件,或走设置页图形界面。 - 兜底位置是
%USERPROFILE%\.agents\mcp.json(顶层mcpServers), 但仅当同一个作用域的.zcode里一个 MCP server 都没有时才被读; 既然上面已经往.zcode写了,那份就不会生效。
下面把本机的绝对路径换成了占位符。三个
{
"mcpServers": {
"dsh-memory": {
"command": "C:\\Program Files\\nodejs\\node.exe",
"args": [
"C:\\path\\to\\dsh-memory-mcp\\src\\index.js",
"--store-dir",
"C:\\Users\\me\\.dsh-memory-store"
],
"env": {
"DSH_MEMORY_EMBED_EXE": "C:\\path\\to\\llama-server.exe",
"DSH_MEMORY_EMBED_MODEL": "C:\\path\\to\\Qwen3-Embedding-0.6B-Q8_0.gguf",
"DSH_MEMORY_EMBED_POOLING": "last"
}
}
}
}
command写node 的绝对路径而不是node—— IDE 拉起 MCP 时继承的 PATH 常常不含 node, 写node会报「不是内部或外部命令」。
--store-dir必须指向 DSH 插件在用的那个目录,否则两边看不到同一份记忆。不想配向量后端的话,把
env整段删掉、改成"DSH_MEMORY_VECTOR": "off",只保留词法检索 —— 记忆照样能读写,只是没有语义召回。
⚠️ 上面这份配置里没有--project,这是刻意的。MCP 配置是全局的(一个配置文件服务所有项目),而
--project是单一值。 一旦写死成某个具体项目,那么你在 Trae 里打开任何其它项目时, 它的project作用域都会错误地落到那个目录 —— 所有项目的记忆混进同一个桶。不写
--project时,服务退回process.cwd()。正确用法是按项目单独配一份:
- 项目级配置(推荐):在项目根放
.trae/mcp.json(Trae 需在设置里开启「项目级 MCP」), 或.workbuddy/mcp.json(WorkBuddy),此时加--project <项目绝对路径>。- 或者干脆不用 project 作用域:
memory_save默认写global,memory_search默认检索「当前项目 + global」。全局配置下项目识别可能不准, 但global那部分始终正确 —— 而跨 IDE 共享的本来就是global。- 想让保存默认落到项目而不是 global,加
"DSH_MEMORY_SCOPE_ALL": "true"。
如果 IDE 不支持 env 字段,把环境变量写进 command 的调用脚本里。
别指望"省略 --store-dir 就行" —— 默认目录是 ~/.dsh-memory-store,与你的 DSH storeDir 多半不是同一个。
不用真的让 agent 去调工具,先确认进程能被拉起来:
# CLI 层(不建连接):确认参数/版本/路径都对
node <repo>/src/index.js --version
node <repo>/src/index.js --help
# 真正起一次服务,看 stderr 首几行(stdio 型服务在 IDE 外跑需要喂一个 initialize)
echo {} | node <repo>/src/index.js --store-dir <你的目录>首三行 stderr 会打印实际生效的 store-dir / project / 默认保存作用域 ——
「记忆写错地方」90% 在这一屏就能看出来。
ZCode 用户可以一条命令验完:scripts/probe-zcode-handshake.mjs 会读你自己的
~/.zcode/cli/config.json,完全照 ZCode 的 spawn 方式起进程(工作区当 cwd、
{...进程环境, ...配置里的 env} 合并、stderr 走 pipe),然后 initialize →
tools/list → 真调一次 memory_search:
node <repo>/scripts/probe-zcode-handshake.mjs [工作区绝对路径]它打印握手耗时、工具清单和一次真实检索结果。它只读不写记忆库,
所以对真实 store 跑是安全的(不像 acceptance-*.mjs 那样必须先指到临时目录)。
存储目录相同,进程独立,互不干扰。
<store-dir>/
├── global.jsonl ← scope 键 "global"
├── global.vec ← 向量边车,首行 #qwen3-embedding-0.6b\t1024
├── projects/<sha1(scope)>.jsonl ← 其他任何 scope 键
├── projects/<sha1(scope)>.vec
├── .locks/<sha1(scope)>.lock ← 每个 scope 一把独占锁
├── scopes.json ← 作用域别名(登记过别名才存在)
├── llama-server-mcp.log ← 自己 spawn 的向量后端日志(仅 spawn 过才存在)
└── settings.json ← DSH 的注入设置(MCP 不读不写)
- 作用域键 → 文件名:
global→global.jsonl;其余 →projects/sha1(scope).jsonl(哈希文件名结构上不可能路径穿越,也没有 Windows 保留名问题)。 - 记录格式与上游逐字节兼容:
{"id":"<uuid>","scope":"global","kind":"procedure","content":"...","tags":[], "source":{"origin":"explicit"},"createdAt":1792403053326,"updatedAt":1792403053326, "lastAccessedAt":1792403053326,"accessCount":0,"importance":0.9} - 向量边车:首行
#<model>\t<dims>,之后每行<id>\t<base64(Float32Array 原始字节)>。 头不匹配(换过模型)时整个边车作废、重新嵌入。
上游 DSH 插件的 MemoryStore 是进程内快照 + 全量重写:
loadScope() 每个 scope 只读盘一次,flush() 拿内存快照重写整个文件。
两个进程共用时,后者的快照里没有前者刚写的记录,一次 flush 就确定性抹掉。
本项目的 SharedStore 换掉了这一层:
- 粒度为单个 scope 文件的独占锁文件(
openSync(path, 'wx')/ O_EXCL); - 每个「读-改-写」事务都在锁内重新从磁盘读取该 scope 的全部记录;
- 写盘走 tmp +
renameSync原子替换; .vec向量边车走单行 append(append 语义天然跨进程安全),删除向量时才在锁内全量重写。
⚠️ 注意:DSH 插件本身仍然是「快照 + 全量重写」。当 DSH 进程和 MCP 进程同时写同一个 scope 时, 数据是双向互毁的,不是单方面碾压。scripts/acceptance-coexist.mjs实测:加锁写者 A 写入 24 条 → 磁盘剩 12 条 上游式写者 写入 12 条 → 磁盘剩 0 条 合计磁盘 12 条(写入前双方共 36 条)本项目的锁能保证自己这一侧的事务不丢(串行写 5/5、外部追加不被 flush 抹除均已验证), 但挡不住另一个进程的无锁全量重写 —— 这是上游
MemoryStore的病,本项目修不了它。实务纪律(唯一可靠的办法):
- 读随便读,两边同时读完全没问题(读路径不产生覆盖)。
- 写同一时刻只让一个进程写。日常用法是 DSH 与 MCP 交替着来, 不要在两边同一瞬间各写一条。
- MCP 侧的 3 个实例之间(多个 IDE 同时开)不会出问题 —— 它们共用同一把锁。
本项目基于 0.8.0 的源码(本机已升级到 0.8.0,2026-10-20)。
已核对 0.7.2 → 0.8.0 的差异:只改动了 6 个文件(client.js / panel.js /
provider/apply.js / provider/index.js / package.json / README.md),
store.js / scoring.js / scope.js / scope-names.js / tools/* 逐字节未变 ——
所以本项目复用这些模块的部分不受版本升级影响,升级后实测验收 13/13 全绿。
升级上游后仍建议比对这几个文件(它们才是与本项目耦合的部分)。
上游包无法被直接 import:
exports只声明了././client/./package.json(没有lib/*子路径), 裸包根入口又会因@deepseek-ai/cordispeer 依赖而ERR_MODULE_NOT_FOUND。 所以本项目复制了那几个零依赖纯模块的源码(scoring.js/scope.js逐字复制,store.js重写但保持字节级格式兼容),版权声明见third-party-notices.md。
键空间(与上游完全一致):
| 形式 | 含义 |
|---|---|
global |
所有项目共享 |
project:<绝对路径> |
一个工作区 |
session:<id> |
一个会话(同一套 projects/<sha1> 文件体系) |
| 自定义名 | 自由命名 |
自定义名规则:内部空白(含制表/换行)折叠为单个空格 → 转小写 → 长度 1~64 码点 →
禁 : / \ .. 控制字符、禁纯点/空格组成的名字、禁保留名 global/project/session/all。
任何不合法的作用域名一律报错,绝不静默回退。
MCP 里的默认行为(与 DSH 不同,原因见下):
| 工具 | 省略 scope 时 | 原因 |
|---|---|---|
memory_save |
global |
跨 IDE 共享的只有 global 与 project。 |
memory_search |
当前项目 + global | session 在 MCP 里没有共享意义。 |
memory_list |
当前项目 + global | 同上。 |
memory_forget(query) |
当前项目 | 比 session 安全,不会误删别人的。 |
为什么不用 session:DSH 的 session id 来自 DSH 自己的会话头,Trae / WorkBuddy 各有各的 id。
用 session 作用域等于每个 IDE 一份互不相通的记忆,跨 agent 共享就无从谈起。
所以本 MCP 把裸 session 落到当前工作区(与上游「拿不到会话就回退 project」同语义),
但显式 session:<id> 仍然原样保留,可以 pin 某个会话作用域。
路径归一化(必须归一化,否则同一个目录会分裂成两个 scope):
- 绝对路径 → 折叠
./..、去掉末尾分隔符、盘符大写(c:\x→C:\x); - 相对路径 → 按
DSH_MEMORY_PROJECT/--project解析(都没有才按process.cwd())。
默认开启,但完全可选。
- 检索是混合召回:
hybrid = max(lexical, cosine),不是加权求和。 词法通道用 IDF + CJK bigram(每个 scope 内部独立统计 IDF), 有强命中(共享拉丁词或 CJK bigram)用宽松阈值(global0.12 / 其余 0.1), 只有单字 CJK 命中则要求max(floor, 0.25)。 - 向量通道:
minScore0.5,recallN30,topK默认 5,score 保留 4 位小数。 - 排序:score 降序 → scope 层级(session 0 > project 1 > 自定义 2 > global 3)→
id.localeCompare。 - 过期记录(
expiredAt < now)跳过。
后端生命周期:
- 先
GET /health探活 —— 活着就复用,不接管生命周期、不杀进程 (所以可以和 DSH 的 8078 实例共存,本项目默认用 8199); - 没活才
spawnllama-server(windowsHide: true、detached: false), 日志重定向到<store-dir>/llama-server-mcp.log; - 进程退出时只杀自己 spawn 的那个。
熔断:连续失败后指数退避(1s → 2s → 4s … 上限 30s),不会每次检索都白等一次超时。 超时分级:交互检索 300ms 预算;写入 / 回填 10s 预算(整段内容在 CPU 上嵌入慢得多)。
关掉向量:
DSH_MEMORY_VECTOR=off node src/index.js退化后:memory_search 纯词法检索,memory_save 不再写 .vec 边车,完全不碰网络。
已存在的 .vec 文件不受影响。
复用 DSH 已算好的向量:本项目默认的模型名与维度是 qwen3-embedding-0.6b / 1024,
与现有 store 里的 .vec 头完全一致,所以 DSH 之前算好的向量可以直接拿来用,不必重新嵌入。
换了模型的话,头对不上,整个边车作废并重新嵌入(绝不拿旧模型的向量算相似度)。
llama-server 的 --pooling 决定向量落在哪个空间里。用错了,DSH 存好的向量就全部作废
(检索结果会变成噪声,而不是报错)。
上游 models.js 的原话是「BGE 系用 cls,Qwen3-Embedding 用 last」,本项目照此实现:
按模型名自动推断 —— bge-* → cls,nomic-* → mean,其余(Qwen3-Embedding 等)→ last。
实测(2026-10-20,拿 D:/dev/.dsh-memory-store/global.vec 里 DSH 存好的向量比):
| llama-server 参数 | 现算向量 vs DSH 已存向量 的余弦 |
|---|---|
--pooling last |
1.0000 —— 同一个空间,可直接复用 |
--pooling cls |
0.3522 —— 完全不同的空间,混用等于噪声 |
所以换模型时,除了
DSH_MEMORY_EMBED_MODEL,还要确认DSH_MEMORY_EMBED_POOLING。 换错了不会报错,只会静默地让向量检索失效 —— 这是本项目最容易踩的坑。
顺带一提:用 cls 时,不相关文本之间的余弦高达 0.97(几乎所有句子都"相似");
用 last 时降到 0.41,这才是正常嵌入空间该有的区分度。
- 与 DSH 并发写同一 scope 有丢数据风险(见上文「为什么不会互相覆盖」的注意事项)—— 本项目的存储层是并发安全的,但上游 DSH 插件不是。
- 触达计数:检索命中后
accessCount的刷新走存储层的touchMany(ids)—— 按 scope 分组,每个 scope 只取一次锁、只重写一次文件。 只对最终返回的 topK(≤5 条)做,不对全部候选做。 收益是打分公式里的accessBoost = 1 + 0.1·ln(1+accessCount); 代价是每次检索会有一次小规模写盘。这是本实现与上游的一处刻意差异 —— 上游的touch只是把 scope 标 dirty、靠 500ms 去抖 flush(看起来免费), 但那正是它跨进程会丢数据的那个 flush;这里没有免费的那一层。 - 没有 LLM rerank:上游 provider 有一个可选的第三阶段(LLM 重排,默认关闭)。 本项目只有词法 + 向量两路召回。
- 没有自动注入:DSH 插件会在 agent 回合开始时自动把相关记忆注入上下文;
MCP 侧没有这个机制 —— 其它 IDE 需要 agent 主动调用
memory_search。 session作用域在 MCP 里基本没用:跨 IDE 的 session id 不通用。 显式session:<id>仍然可用,但只有同一 IDE 内部才有意义。- Windows 路径大小写:归一化只统一盘符大小写(
c:→C:),路径其余部分的大小写按原样保留 (Windows 本身不区分,Linux 上可能区分)。 - 向量是 CPU 密集型:首次嵌入会有几秒延迟(模型加载)。
npm install
# 一条命令跑主力三项(约 7 秒,都不碰真实 store)
npm test
# 逐项跑
node scripts/selftest-cli.mjs # CLI 契约:--help/--version/未知参数/退出码/启动失败诊断/配置解析
node scripts/selftest-mcp.mjs # 完整 MCP 协议端到端(真起 stdio server)
node scripts/selftest.mjs # 存储层并发(真 fork 多进程)
node scripts/selftest-tools.mjs # 工具层:参数校验 / scope 决策 / 渲染(内存版假 store)
node scripts/selftest-protocol.mjs # 协议层:握手 / tools/list / tools/call(内存版假 store)
node scripts/selftest-vector.mjs # 向量后端:配置 / 探活 / 熔断 / 生命周期
node scripts/selftest-vector-e2e.mjs # 向量端到端:写 .vec 边车 + 语义召回
# 验收测试(会碰 store;真实库那一份需要你先指定 DSH_MEMORY_REAL_STORE,不设就跳过)
node scripts/acceptance-real.mjs # 端到端打真实 storeDir(只写/只删自己造的记录)
node scripts/acceptance-coexist.mjs # 与「上游式无锁写者」并存的实测数据(用临时目录)
node scripts/acceptance-standalone.mjs # 不开 DSH 的独立运行验证(纯词法可跑;自拉起后端需配环境变量)| 脚本 | 覆盖 | 检查项 | 碰真实 store? |
|---|---|---|---|
selftest-cli.mjs |
CLI 契约:--help/--version 的零副作用、未知参数 exit 2、启动失败诊断无裸栈、parseArgs/resolveConfig 纯函数 |
115(POSIX 114,有一条 Windows 专有断言) | 否 |
selftest-mcp.mjs |
真实 stdio 握手、5 个工具的 schema、保存、检索、幂等、列表/按前缀取回、删除后不再召回、错误码、向量开关对比、落盘检查、stderr 日志 | 40 | 否 |
selftest.mjs |
单进程语义、4 进程 × 20 条真并发、外部追加不被抹除、跨进程锁(超时/陈旧锁/粒度)、.vec 边车、别名映射 |
55 | 否 |
selftest-tools.mjs |
路径归一化、scope 决策、全部校验错误码、检索渲染、幂等、别名、自定义作用域、schema 自检 | 60(POSIX 59,盘符大小写折叠是 Windows 专有语义) | 否 |
selftest-protocol.mjs |
握手、tools/list、tools/call 正常/错误路径、stdout 纯净性 | 18 | 否 |
selftest-vector.mjs |
配置解析、探活、复用不 spawn、维度、L2 归一化、熔断退避、与已存向量的空间兼容性 | 27 | 只读 |
selftest-vector-e2e.mjs |
写 .vec 边车 → 读回 → 不同措辞的语义召回 |
6 | 否 |
acceptance-real.mjs |
对你指定的 storeDir 跑完整协议:写入(path ⇒ project: 作用域,并断言不是 global)→ 磁盘可见 → 跨作用域读回 → 幂等 → 按前缀取回 → 删除 → 尝试确认仍能读到 DSH 既有记忆 |
18 | 需 DSH_MEMORY_REAL_STORE(只删自己造的) |
acceptance-coexist.mjs |
与复刻上游「内存快照+全量重写」的无锁写者并发,量化双向互毁的损失 | 4 | 否(临时目录) |
acceptance-standalone.mjs |
不开 DSH:冷启动 → (可选)自拉起向量后端 → 读写 → 语义召回 → 退出后无孤儿进程 | 11 | 需 DSH_MEMORY_REAL_STORE(只删自己造的) |
检查项数在 Windows 与 Linux 上有 1~2 项差异,这是刻意的:被跳过的都是平台专有语义 (盘符不存在、盘符大小写折叠),在 POSIX 上断言它们反而是错的。CI 两个平台都跑,数字对不上是正常的。
selftest-mcp.mjs 用官方 SDK 的 Client + StdioClientTransport 起自己,
真实走一遍完整的 MCP 协议握手(initialize → tools/list → tools/call)。
- 默认
npm test不碰真实 store:它 =selftest-cli.mjs && selftest-mcp.mjs && selftest.mjs。 acceptance-real.mjs与acceptance-standalone.mjs会写 store,所以目标库必须由你显式给出:没设这个变量时它们只打印一行提示然后# PowerShell $env:DSH_MEMORY_REAL_STORE = "D:\dev\.dsh-memory-store"; node scripts/acceptance-real.mjs # bash / zsh DSH_MEMORY_REAL_STORE=~/.dsh-memory-store node scripts/acceptance-real.mjs
exit(0)跳过(退出码 0 表示"没测",不是"测过了")。 脚本不会猜路径、也不会回退到任何写死的机器路径;入口路径从脚本自身位置推导,换机器不用改。 它们只删自己造的记录,但半路 Ctrl-C 仍可能在库里留下几条带随机后缀标记的记录。 想先确认脚本逻辑通、又不碰真实库:把该变量指向os.tmpdir()下的临时目录即可。acceptance-standalone.mjs的向量后端也只从环境变量读:DSH_MEMORY_EMBED_EXE+DSH_MEMORY_EMBED_MODEL(自拉起)或DSH_MEMORY_EMBED_ENDPOINT(复用已有服务)。 都没配就打印提示,并把「自拉起后端 / 语义召回」标成跳过 —— 不报 FAIL,也不拿作者本机的 默认路径硬试。端口探活用node:net+ 原生fetch(),跨平台,不需要netstat/curl。⚠️ 真实 store 是「活的」:只要 DSH 开着,它的记忆插件就在往里写(记录、accessCount、scopes.json别名),MCP 侧检索命中也会刷accessCount。所以不要拿文件 mtime 判断"测试有没有污染真实库" —— 改动可能来自 DSH 自己的正常写入。要判断污染,看的是内容:global.jsonl里有没有测试专用标记字符串。acceptance-standalone.mjs要验证「自拉起向量后端」这一支时,该端口(默认 8199)必须空闲; 已被占用时它只会复用那个服务,并把「自拉起」标成跳过(不算失败)。 本机实测两台llama-server常驻(8078/8199 各一个),所以默认就落在"复用"这一支。selftest-vector.mjs会读(不写)真实 store 里的global.vec做向量空间比对; 该文件不存在时自动跳过那几项。selftest-vector-e2e.mjs在无可用向量后端时直接exit(0)跳过 —— 退出码 0 不代表真的测过。CI 里要当心这一点。acceptance-coexist.mjs本身是 flaky 的(实测 8 次里约 3 次 exit 1)。原因不是本项目的锁, 而是它故意复刻的那个"上游式写者"用固定${file}.tmp名 + rename,在 Windows 上并发 rename 同一个global.jsonl会EPERM。这恰好是"必须用唯一 tmp 名"那条不变量的活证据。看到 FAIL 重跑一次即可, 不是回归。- 所有 selftest 都在
os.tmpdir()下的随机目录里跑,selftest.mjs开头还有硬守卫 —— 测试路径一旦命中真实 store 就直接中止。加--keep可以保留临时目录便于排查。
本仓库除了给人看的这份 README,还准备了两个 agent 入口文件:
| 文件 | 给谁 | 内容 |
|---|---|---|
AGENTS.md |
接手/修改本仓库的 coding agent | 运行时事实、启动参数与环境变量全集、17 条改代码前必须知道的不变量、测试地图(含平台差异与 flaky 说明)、故障 → 定位命令、CI 说明 |
llms.txt |
想发现本仓库的 agent / 工具(遵循 llms.txt 约定) | 项目简介、快速开始、工具清单、配置、不变量摘要、源码结构,全部绝对 URL |
如果你正在用某个 agent 来部署这个服务,直接把 AGENTS.md 喂给它,比喂 README 更省 token 也更准 ——
README 里有大量"与上游的差异"「实测数据」这类对部署没用的内容。
MCP 服务本身也在运行时告诉客户端它能干什么:五个工具的 description 里写明了
"什么时候该用我 / 默认 scope 是什么 / 出错怎么改",initialize 时不需要额外文档。
存储层与评分算法复用自上游 @restregular/dsh-memory(MIT),
逐文件来源见 third-party-notices.md。
MIT License
Copyright (c) 2026 restregular
...