dsh-memory-fast 0.0.0-stage → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -2
- package/cordis.patch.yml +3 -0
- package/index.js +200 -0
- package/package.json +30 -3
- package/prompt.md +99 -0
package/README.md
CHANGED
|
@@ -1,3 +1,211 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-memory-fast
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 的长期记忆插件——复用 DSH 原生的 `AGENTS.md` 加载链为 agent(智能体) 提供跨会话长期记忆,0.1.16 起内置 `memory_search` 检索工具。零存储层。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 🧠 **跨会话记忆**——通过 DSH 原生 `dsh-agent-instructions` 加载器,记忆在会话间自动持久化。
|
|
8
|
+
- 🛠 **写入零自定义工具**——agent 直接用系统 `read` / `write` / `edit` 工具维护记忆文件,不破坏 touch 热更新监听。
|
|
9
|
+
- 🔍 **`memory_search` 检索(0.1.16+)**——插件注册一个只读检索工具,对全局与各工作区记忆文件(热层+冷层)做关键字/正则搜索,休眠态同样可用。
|
|
10
|
+
- 🔄 **热更新**——`dsh-agent-instructions` 的 touch 监听器会捕获原生 `edit` 调用,当前会话立即收到 `Updated instructions`。
|
|
11
|
+
- 📦 **零存储层**——没有 JSON / SQLite / JSONL,没有解析 bug,没有并发写入问题。
|
|
12
|
+
- 🗂 **两级记忆**——全局 + 工作区;会话进入对应工作区时自动加载工作区层。
|
|
13
|
+
- ⚖️ **静态/动态分工**——插件只负责静态机制(加载链、模板、写入分流);所有操作规则放进 `~/.dsh/AGENTS.md`,改了立即热更新,不用重启。
|
|
14
|
+
- 🪪 **可回溯的变更日志**——工作区变更日志条目携带会话短ID,任何改动都能回溯到产生它的 `session.jsonl.zstd` 会话日志。
|
|
15
|
+
|
|
16
|
+
## 工作原理
|
|
17
|
+
|
|
18
|
+
DSH 内置的 `dsh-agent-instructions` 插件在每个会话开始时把 `~/.dsh/AGENTS.md` 与工作区根的 `AGENTS.md` 注入对话上下文。`dsh-memory` 寄生在这条加载链上:它只注入一段系统提示词,告诉模型**记忆放在哪里、两个文件如何组织**——模型随后直接用系统 `read` / `write` / `edit` 工具操作这些文件。
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
[会话 N] 模型调用 edit → 把记忆写入 AGENTS.md
|
|
22
|
+
[会话 N+1] (任意工作区,任意时刻)启动预步骤加载 AGENTS.md → 记忆进入上下文
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 插件 vs AGENTS.md——各放什么
|
|
26
|
+
|
|
27
|
+
插件提示词是**静态的**:只有发布新版本并重启 `dsh` 才会改变。因此它必须保持精简——只放那些缺失就会让整套记忆机制崩溃的部分。
|
|
28
|
+
|
|
29
|
+
| 类型 | 例子 | 放在哪 | 为什么 |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| **机制性**(不能热替换) | "AGENTS.md 存在"、加载链、模板骨架、写入分流、跨工作区检索流程 | 插件提示词 | 模型一旦遗忘,整套记忆系统就崩;全新工作区还没有 AGENTS.md 可读这些规则 |
|
|
32
|
+
| **经验性**(应该可热替换) | 索引同步规则、索引行格式、描述写作标准、维护规则、收尾检查、变更日志短ID | `~/.dsh/AGENTS.md`(`## 注意事项`) | 这些是使用中沉淀的经验;编辑后通过 touch 监听器立即热更新,无需重启 |
|
|
33
|
+
|
|
34
|
+
这个分工之所以成立,关键在于:**新工作区首次创建 AGENTS.md 时,模型同时正在读全局 `~/.dsh/AGENTS.md`**——全局「注意事项」里的规则全程在场,即使新工作区文件还没建好也不会漏。所以插件提示词无需重复这些规则。
|
|
35
|
+
|
|
36
|
+
## 安装
|
|
37
|
+
|
|
38
|
+
### 前置条件
|
|
39
|
+
|
|
40
|
+
- DSH(`@deepseek-ai/dsh`)已配好 profile(如 `web`)
|
|
41
|
+
- `pnpm`
|
|
42
|
+
|
|
43
|
+
### 步骤
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
# 1. 把插件放到 ~/.dsh/plugins/ 下
|
|
47
|
+
cp -r dsh-memory ~/.dsh/plugins/dsh-memory
|
|
48
|
+
|
|
49
|
+
# 2. 在 profile 的 package.json(~/.dsh/profiles/<your-profile>/package.json)里加上:
|
|
50
|
+
# dependencies: "dsh-memory": "file:../../plugins/dsh-memory"
|
|
51
|
+
# dsh.profile.bundles: ["...", "dsh-memory"]
|
|
52
|
+
|
|
53
|
+
# 3. 安装并重启
|
|
54
|
+
cd ~/.dsh/profiles/<your-profile> && pnpm install
|
|
55
|
+
dsh --profile <your-profile>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
若 `dsh-web` 由 systemd 管理,改完插件提示词后用:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
systemctl restart dsh-web
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
该服务配置了 `Restart=always`、`RestartSec=3`,以及会先杀掉占用 3080 端口进程的 `ExecStartPre`。重启会断开当前 Web GUI 会话——刷新页面即可重连新会话并加载新提示词。
|
|
65
|
+
|
|
66
|
+
## 记忆布局
|
|
67
|
+
|
|
68
|
+
两层结构,没有段落标记——整个文件都是记忆,按章节组织。
|
|
69
|
+
|
|
70
|
+
| 层 | 文件 | 加载时机 | 章节 |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| 全局 | `~/.dsh/AGENTS.md` | 每个会话 | 用户偏好 / 注意事项 / **工作区索引** / 待办 / 变更日志 |
|
|
73
|
+
| 工作区 | `<工作区根>/AGENTS.md`(根 = 会话当前工作目录) | 进入该工作区时 | 工作区注意事项 / 工作区变更日志 |
|
|
74
|
+
|
|
75
|
+
- **偏好**永远进全局——习惯不分工作区。
|
|
76
|
+
- **注意事项**(跨工作区操作规则)永远进全局——见上表。工作区不复制这些规则。
|
|
77
|
+
- **待办**永远进全局——汇总所有工作区,每条标注来源,便于在任何会话推进。
|
|
78
|
+
- **工作区索引**只在全局——每个工作区一行,含 ≤50 字描述 + 关键词 + 路径 + 规模,让模型按需 `read` 其他工作区的记忆。
|
|
79
|
+
|
|
80
|
+
### 工作区索引行格式
|
|
81
|
+
|
|
82
|
+
> **该规则存放位置:** `~/.dsh/AGENTS.md` → `## 注意事项` → `**工作区索引行格式**`。此处仅作参考,要改请去那里改。
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
- (YYYY-MM-DD) [workspace] 工作区标识 | 特征+用途描述(≤50字) | 关键词1、关键词2 | 路径: /绝对路径/AGENTS.md | 规模: N条
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**描述写作标准**
|
|
89
|
+
|
|
90
|
+
1. ≤50 字,结构固定:`[技术栈/形态] + [做什么] + [给谁/解决什么]`
|
|
91
|
+
2. 英文专有名词配中文解释,如 `GLM(智谱大模型)`、`Go(谷歌编程语言)`、`admin(管理后台)`
|
|
92
|
+
3. 避免空泛词("系统"、"服务"、"工具"),写具体形态("HTTP 转发服务"而非"一个服务")
|
|
93
|
+
4. 缩写一律补全含义
|
|
94
|
+
|
|
95
|
+
**示例**
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
- (2026-09-02) [workspace] glm-free-api | Go(谷歌编程语言) HTTP 转发服务,把智谱免费 GLM(智谱大模型) 接口包装成 OpenAI 兼容格式供下游客户端调用 | 鉴权、流式解析、admin(管理后台)、部署 | 路径: /opt/glm-free-api/server/AGENTS.md | 规模: 11条
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 索引同步(强制)
|
|
102
|
+
|
|
103
|
+
> **该规则存放位置:** `~/.dsh/AGENTS.md` → `## 注意事项` → `**索引同步(强制)**`。
|
|
104
|
+
|
|
105
|
+
只要模型用 write/edit 改过任意工作区的 `AGENTS.md`(新增、修改、删除任何条目),**必须在同一回合内**同步全局 `~/.dsh/AGENTS.md`「工作区索引」里对应那一行——行首日期改为今天、规模改为最新条目数、描述/关键词按需调整。收尾检查会在回合结束前再次核对。
|
|
106
|
+
|
|
107
|
+
### 工作区变更日志短ID
|
|
108
|
+
|
|
109
|
+
**工作区**的 `## 工作区变更日志` 每条末尾追加 ` #<短ID>`,用于回溯:
|
|
110
|
+
|
|
111
|
+
- 短ID = `$DSH_SESSION_ID` 去掉 `session-` 前缀后 UUID 的前 8 位。
|
|
112
|
+
- 示例:`- (2026-09-03) ... #8acbdac5`。
|
|
113
|
+
- **只加工作区变更日志**,全局变更日志不加。
|
|
114
|
+
- 历史条目无法回溯的不补。
|
|
115
|
+
|
|
116
|
+
## 会话日志与环境变量
|
|
117
|
+
|
|
118
|
+
DSH 通过环境变量向每次 `bash` 工具调用暴露当前会话信息:
|
|
119
|
+
|
|
120
|
+
| 变量 | 含义 |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `DSH_HOME` | DSH 根目录,通常 `/root/.dsh` |
|
|
123
|
+
| `DSH_SESSION_ID` | 当前会话 ID,如 `session-8acbdac5-d05c-4ce8-afbf-9243787338f5` |
|
|
124
|
+
| `DSH_SESSION_JSONL` | 完整聊天日志路径(zstd 压缩的 JSONL) |
|
|
125
|
+
| `DSH_WEB_URL` | Web GUI 地址,如 `http://127.0.0.1:3080` |
|
|
126
|
+
| `DSH_SHELL` | 在 harness shell 下为 `1` |
|
|
127
|
+
|
|
128
|
+
### 会话日志结构
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
/root/.dsh/sessions/ ← 根目录
|
|
132
|
+
└── --root-~8BB0~5FC6~63D2~4EF6--/ ← 编码后的工作区路径(此处为 /root/记忆插件)
|
|
133
|
+
└── session-8acbdac5-d05c-4ce8-afbf-9243787338f5/ ← 每个会话一个文件夹
|
|
134
|
+
└── session.jsonl.zstd ← 完整聊天日志,zstd 压缩的 JSONL
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
文件夹名编码规则:把工作区路径中每段非字母数字字符替换为 `~<十六进制码点>~`,所以 `记忆插件` 变成 `~8BB0~5FC6~63D2~4EF6`,结尾 `--` 标记路径结束。
|
|
138
|
+
|
|
139
|
+
查看会话日志:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
# 解压为普通 JSONL,然后用 read 工具或 jq 查看
|
|
143
|
+
zstd -d /root/.dsh/sessions/--root-~8BB0~5FC6~63D2~4EF6--/session-8acbdac5-.../session.jsonl.zstd -o session.jsonl
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
工作区变更日志的短ID 正好是会话文件夹名 UUID 的前缀,所以从变更日志条目回到产生它的会话日志只需一步。
|
|
147
|
+
|
|
148
|
+
## memory_search 检索工具(0.1.16+)
|
|
149
|
+
|
|
150
|
+
0.1.16 起插件通过 `inject: ["tools"]` 注册一个**只读**工具 `memory_search`,把"跨工作区检索"从纯提示词约定升级为机制兜底:
|
|
151
|
+
|
|
152
|
+
| 参数 | 说明 |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `query`(必填) | 关键字或正则,大小写不敏感;非法正则自动退回纯文本包含匹配 |
|
|
155
|
+
| `scope`(可选) | `all`(默认)= 全局 + 工作区索引登记的全部工作区;`global` = 仅 `~/.dsh` 两个全局文件;`workspaces` = 仅索引工作区文件 |
|
|
156
|
+
| `limit`(可选) | 单文件最多列出的匹配行数,默认 20(硬上限 100;`total_matches` 始终统计全量命中) |
|
|
157
|
+
|
|
158
|
+
- **搜索范围**:全局热/冷层(`~/.dsh/AGENTS.md`、`~/.dsh/AGENT-MEMORY.md`)+ 全局「工作区索引」行里 `路径:` 指向的各工作区 `AGENTS.md` 及同目录 `AGENT-MEMORY.md`;索引未登记的路径不猜测、不搜索。
|
|
159
|
+
- **返回**:`{query, files_searched, total_matches, matches:[{file, line, section, text}]}`,`section` 为匹配行所属的最近二级标题(`## `);模型拿到指针后用 `read` 跟进取上下文。
|
|
160
|
+
- **休眠态可用**:工具注册与 active/sleep 状态无关——休眠省的是提示词模板,检索兜底始终在场。
|
|
161
|
+
- **零新增依赖**:纯 Node 内置 `fs`/`path` 实现。
|
|
162
|
+
|
|
163
|
+
## 为什么写入不用自定义工具?
|
|
164
|
+
|
|
165
|
+
早期版本注册过 `memory_save` / `memory_search` / `memory_list` / `memory_delete` 一整套读写工具。其中**写入类**工具的三个问题,本设计仍然规避(只读的检索工具不受这些限制,故 0.1.16 补回了 `memory_search`):
|
|
166
|
+
|
|
167
|
+
1. **破坏热更新**——自定义工具不触发 `dsh-agent-instructions` 对原生 `read/write/edit` 的 touch 监听器,对 AGENTS.md 的编辑无法传播到当前会话。
|
|
168
|
+
2. **自建存储的 bug**——JSON 解析、原子写入、并发写入全都得自己处理(我们踩过好几个坑)。
|
|
169
|
+
3. **非原生 UX**——绕过了系统工具的审批/展示卡片。
|
|
170
|
+
|
|
171
|
+
写入仍直接用系统工具,得到的是完全原生、感知热更新的流水线;检索由插件注册的 `memory_search` 提供(只读,不触碰写入链路,touch 热更新不受影响)。插件本体约 200 行 JavaScript。
|
|
172
|
+
|
|
173
|
+
## 验证
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
node --check index.js
|
|
177
|
+
# Syntax check passed
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
安装后,确认配置树加载到了它:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
dsh --profile <your-profile> --dump-config | grep dsh-memory
|
|
184
|
+
# # == dsh-memory
|
|
185
|
+
# - id: dsh-memory
|
|
186
|
+
# name: dsh-memory
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
运行时注册系统提示词 section 需要重启 `dsh` 才生效。
|
|
190
|
+
|
|
191
|
+
## 局限
|
|
192
|
+
|
|
193
|
+
- **两级一致性是尽力而为**——全局索引与工作区文件是两次独立的 `edit`,中断会留下过时索引。强制的索引同步规则(在全局 `## 注意事项`)+ 收尾检查能缓解但无法消除。
|
|
194
|
+
- **跨层无自动去重**——同一内容写在两层会被注入两次。注意事项里禁止同一条信息写两个文件。
|
|
195
|
+
- **无文件锁**——多个会话并发编辑同一 AGENTS.md 可能竞态。个人单用户使用风险低。
|
|
196
|
+
- **软约束**——保存时机、章节归属、索引同步都依赖模型遵守提示词,没有硬性保证。
|
|
197
|
+
- **插件提示词不可热替换**——改 `index.js` 必须重启 `dsh` 才生效。操作规则放进 `~/.dsh/AGENTS.md` 正是为了规避这一限制。
|
|
198
|
+
|
|
199
|
+
## 文件结构
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
dsh-memory/
|
|
203
|
+
├── index.js # 插件入口——注入系统提示词 + 注册 memory_search 检索工具
|
|
204
|
+
├── package.json # 插件清单,声明 dsh.bundle.patch
|
|
205
|
+
├── cordis.patch.yml # patch 层,把插件注册到 profile
|
|
206
|
+
└── README.md # 本文件
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// dsh-memory:自足版 + memory_search 检索工具。
|
|
2
|
+
// 提示词注入:按状态注入系统提示词(prompt.md),宣告记忆文件体系并提供初始化模板。
|
|
3
|
+
// 工具注册:memory_search —— 对全局/各工作区记忆文件(热层+冷层)做关键字/正则检索,
|
|
4
|
+
// 作为跨工作区检索的机制化兜底;active/sleep 两态均注册(休眠省的是模板,不是检索能力)。
|
|
5
|
+
// 状态文件 ~/.dsh/.dsh-memory-state(active/sleep,缺省 active)每轮组装时现读——
|
|
6
|
+
// section 的 text 是函数(dsh-system-prompt 每次 assemble 求值),状态翻转即时生效、无需重启。
|
|
7
|
+
// 首次记忆初始化完成后模型自律写入 sleep;用户说「初始化记忆插件」时写回 active 唤醒。
|
|
8
|
+
//
|
|
9
|
+
// schema 纪律(沿用 dsh-anti-detect-browser v2 教训):
|
|
10
|
+
// 1. 输出 schema 每层 object 显式 additionalProperties:false
|
|
11
|
+
// 2. 输出字段固定全存在(无 undefined 抖动)
|
|
12
|
+
// 3. defineTool 必带 output.render(dsh 无条件调用 userRender)
|
|
13
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
18
|
+
|
|
19
|
+
const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const PROMPT_PATH = join(PLUGIN_DIR, "prompt.md");
|
|
21
|
+
const STATE_PATH = join(homedir(), ".dsh", ".dsh-memory-state");
|
|
22
|
+
const GLOBAL_AGENTS = join(homedir(), ".dsh", "AGENTS.md");
|
|
23
|
+
const GLOBAL_MEMORY = join(homedir(), ".dsh", "AGENT-MEMORY.md");
|
|
24
|
+
const MARK_START = "<!-- DSH-MEMORY:SLEEP_HOOK_START -->";
|
|
25
|
+
const MARK_END = "<!-- DSH-MEMORY:SLEEP_HOOK_END -->";
|
|
26
|
+
|
|
27
|
+
const raw = readFileSync(PROMPT_PATH, "utf8");
|
|
28
|
+
const startIdx = raw.indexOf(MARK_START);
|
|
29
|
+
const endIdx = raw.indexOf(MARK_END);
|
|
30
|
+
if (startIdx < 0 || endIdx < 0 || endIdx < startIdx) {
|
|
31
|
+
throw new Error("dsh-memory: prompt.md 缺少休眠钩子分隔标记 (" + MARK_START + " ... " + MARK_END + ")");
|
|
32
|
+
}
|
|
33
|
+
const FULL_TEXT = raw.slice(0, startIdx).trimEnd();
|
|
34
|
+
const SLEEP_TEXT = raw.slice(startIdx + MARK_START.length, endIdx).trim();
|
|
35
|
+
|
|
36
|
+
// 每轮 assemble 现读状态(一次几十字节 read,成本可忽略);
|
|
37
|
+
// 状态文件不存在视为 active——新机首装即完整加载,符合「第一次初始化」语义。
|
|
38
|
+
function readState() {
|
|
39
|
+
try {
|
|
40
|
+
return readFileSync(STATE_PATH, "utf8").trim() === "sleep" ? "sleep" : "active";
|
|
41
|
+
} catch {
|
|
42
|
+
return "active";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── memory_search:记忆文件检索 ──────────────────────────────
|
|
47
|
+
|
|
48
|
+
// 搜索范围。scope=all:全局热/冷层 + 工作区索引列出的各工作区热/冷层;
|
|
49
|
+
// scope=global:仅 ~/.dsh 两个全局文件;scope=workspaces:仅索引工作区文件。
|
|
50
|
+
// 工作区来源是全局热层「工作区索引」行里的 `路径: <abs>/AGENTS.md`——索引没登记的工作区
|
|
51
|
+
// 不在检索范围(不猜测路径);索引行解析失败静默跳过。
|
|
52
|
+
function listMemoryFiles(scope) {
|
|
53
|
+
const files = [];
|
|
54
|
+
if (scope === "global" || scope === "all") {
|
|
55
|
+
files.push(GLOBAL_AGENTS, GLOBAL_MEMORY);
|
|
56
|
+
}
|
|
57
|
+
if (scope === "workspaces" || scope === "all") {
|
|
58
|
+
try {
|
|
59
|
+
const text = readFileSync(GLOBAL_AGENTS, "utf8");
|
|
60
|
+
const re = /路径:\s*([^\s|]+AGENTS\.md)/g;
|
|
61
|
+
let m;
|
|
62
|
+
while ((m = re.exec(text)) !== null) {
|
|
63
|
+
const agents = m[1];
|
|
64
|
+
files.push(agents, join(dirname(agents), "AGENT-MEMORY.md"));
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// 全局热层读不到时 workspaces 部分为空,不致命
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// 去重 + 只保留真实存在的文件(冷层按设计允许不存在)
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const f of files) {
|
|
74
|
+
if (seen.has(f)) continue;
|
|
75
|
+
seen.add(f);
|
|
76
|
+
try {
|
|
77
|
+
readFileSync(f, "utf8");
|
|
78
|
+
out.push(f);
|
|
79
|
+
} catch {
|
|
80
|
+
// 文件不存在 → 不算 searched
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 优先按正则匹配(大小写不敏感);非法正则自动退回纯文本包含匹配。
|
|
87
|
+
function makeMatcher(query) {
|
|
88
|
+
try {
|
|
89
|
+
const re = new RegExp(query, "i");
|
|
90
|
+
return (line) => re.test(line);
|
|
91
|
+
} catch {
|
|
92
|
+
const q = query.toLowerCase();
|
|
93
|
+
return (line) => line.toLowerCase().includes(q);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function doMemorySearch(args) {
|
|
98
|
+
const scope = args.scope === "global" || args.scope === "workspaces" ? args.scope : "all";
|
|
99
|
+
const limit = Math.max(1, Math.min(100, Math.floor(args.limit) || 20));
|
|
100
|
+
const match = makeMatcher(args.query);
|
|
101
|
+
const files = listMemoryFiles(scope);
|
|
102
|
+
const matches = [];
|
|
103
|
+
let total = 0;
|
|
104
|
+
for (const file of files) {
|
|
105
|
+
const lines = readFileSync(file, "utf8").split(/\r?\n/);
|
|
106
|
+
let section = "(无小节)";
|
|
107
|
+
let inFile = 0;
|
|
108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
109
|
+
const line = lines[i];
|
|
110
|
+
const heading = /^##\s+/.exec(line);
|
|
111
|
+
if (heading) section = line.replace(/^##\s+/, "").trim() || "(无小节)";
|
|
112
|
+
if (match(line)) {
|
|
113
|
+
total++;
|
|
114
|
+
inFile++;
|
|
115
|
+
if (inFile <= limit) {
|
|
116
|
+
matches.push({ file, line: i + 1, section, text: line.trim().slice(0, 500) });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { query: args.query, files_searched: files.length, total_matches: total, matches };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const SEARCH_SCHEMA = {
|
|
125
|
+
type: "object",
|
|
126
|
+
additionalProperties: false,
|
|
127
|
+
properties: {
|
|
128
|
+
query: { type: "string", required: true },
|
|
129
|
+
files_searched: { type: "number", required: true },
|
|
130
|
+
total_matches: { type: "number", required: true },
|
|
131
|
+
matches: {
|
|
132
|
+
type: "array",
|
|
133
|
+
required: true,
|
|
134
|
+
items: {
|
|
135
|
+
type: "object",
|
|
136
|
+
additionalProperties: false,
|
|
137
|
+
properties: {
|
|
138
|
+
file: { type: "string", required: true },
|
|
139
|
+
line: { type: "number", required: true },
|
|
140
|
+
section: { type: "string", required: true },
|
|
141
|
+
text: { type: "string", required: true },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
function renderSearch(_args, v) {
|
|
149
|
+
const parts = [
|
|
150
|
+
`query: ${v.query}`,
|
|
151
|
+
`files searched: ${v.files_searched}, total matches: ${v.total_matches}`,
|
|
152
|
+
];
|
|
153
|
+
let lastFile = null;
|
|
154
|
+
for (const m of v.matches) {
|
|
155
|
+
if (m.file !== lastFile) {
|
|
156
|
+
parts.push("", `# ${m.file}`);
|
|
157
|
+
lastFile = m.file;
|
|
158
|
+
}
|
|
159
|
+
parts.push(`L${m.line} [${m.section}] ${m.text}`);
|
|
160
|
+
}
|
|
161
|
+
if (v.total_matches > v.matches.length) {
|
|
162
|
+
parts.push("", "(部分匹配因单文件 limit 未列出——缩小 query 或提高 limit)");
|
|
163
|
+
}
|
|
164
|
+
if (v.total_matches === 0) {
|
|
165
|
+
parts.push("(无匹配——记忆文件里可能没有这项内容,或该工作区未在全局索引登记)");
|
|
166
|
+
}
|
|
167
|
+
return [{ type: "text", text: parts.join("\n") }];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const memorySearchTool = defineTool({
|
|
171
|
+
name: "memory_search",
|
|
172
|
+
description:
|
|
173
|
+
"Search all dsh memory files (global AGENTS.md/AGENT-MEMORY.md plus every workspace listed in the global 工作区索引, hot+cold layers) for a keyword or regex (case-insensitive; invalid regex falls back to plain-text). Returns file, line number, section heading and matching line — follow up with read on the file for full context. Works even while the memory prompt is asleep.",
|
|
174
|
+
parameters: {
|
|
175
|
+
query: { type: "string", required: true, description: "Keyword or regex (case-insensitive)." },
|
|
176
|
+
scope: { type: "string", description: '"all" (default) = global + indexed workspaces; "global" = ~/.dsh files only; "workspaces" = indexed workspace files only.' },
|
|
177
|
+
limit: { type: "number", description: "Max matches listed per file, default 20 (hard cap 100; total_matches still counts all)." },
|
|
178
|
+
},
|
|
179
|
+
output: { schema: SEARCH_SCHEMA, render: renderSearch },
|
|
180
|
+
execute: doMemorySearch,
|
|
181
|
+
timeoutMs: 15000,
|
|
182
|
+
isConcurrencySafe: () => true,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ── Cordis 插件入口 ─────────────────────────────────────────
|
|
186
|
+
export const name = "dsh-memory";
|
|
187
|
+
export const inject = ["systemPrompt", "tools"];
|
|
188
|
+
|
|
189
|
+
export function apply(ctx) {
|
|
190
|
+
ctx.effect(() => ctx.systemPrompt.section({
|
|
191
|
+
name: "dsh-memory:memory-usage",
|
|
192
|
+
order: 90,
|
|
193
|
+
text: () => (readState() === "sleep" ? SLEEP_TEXT : FULL_TEXT),
|
|
194
|
+
}));
|
|
195
|
+
ctx.tools.register(memorySearchTool);
|
|
196
|
+
// 进程内加载证明:apply() 被 dsh 主进程真正执行时留下标记文件(0.2.x 验证手段)
|
|
197
|
+
try {
|
|
198
|
+
writeFileSync(new URL("./.loaded", import.meta.url), new Date().toISOString());
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-memory-fast",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.16",
|
|
4
|
+
"description": "DeepSeek Harness 记忆插件:AGENTS.md 三级冷热长期记忆 + memory_search 检索工具(全局+工作区、热/冷层、休眠态可用)。零存储层。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.js",
|
|
9
|
+
"prompt.md",
|
|
10
|
+
"cordis.patch.yml",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"harness:check": "node --check index.js"
|
|
15
|
+
},
|
|
16
|
+
"dsh": {
|
|
17
|
+
"bundle": {
|
|
18
|
+
"patch": "./cordis.patch.yml"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@deepseek-ai/cordis": "*",
|
|
23
|
+
"@deepseek-ai/dsh-tools": "*"
|
|
24
|
+
},
|
|
25
|
+
"peerDependenciesMeta": {
|
|
26
|
+
"@deepseek-ai/cordis": {
|
|
27
|
+
"optional": true
|
|
28
|
+
},
|
|
29
|
+
"@deepseek-ai/dsh-tools": {
|
|
30
|
+
"optional": true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
6
33
|
}
|
package/prompt.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
## 长期记忆(AGENTS.md)
|
|
2
|
+
|
|
3
|
+
你的长期记忆体系由三类文件构成,DSH 会在每个会话开始时自动加载前两类:
|
|
4
|
+
- `~/.dsh/AGENTS.md`:全局记忆,所有会话自动加载。文件不存在时用 write 照抄下方全局模板创建(该文件自身的规则已内嵌在模板「注意事项」中,原样复制即可)。
|
|
5
|
+
- 工作区根(会话当前所在的工作目录)的 `AGENTS.md`:该工作区的记忆,会话在该工作区时自动加载。文件不存在时用 write 照抄下方工作区模板创建。
|
|
6
|
+
- `AGENT-MEMORY.md`(~/.dsh/ 与各工作区根):记忆详情冷层文件,DSH 不自动注入,需要时按需 read/grep。
|
|
7
|
+
|
|
8
|
+
直接用系统自带的 read / write / edit 维护这些文件。写入方式不重要——无论手写还是自动追加都是记忆。
|
|
9
|
+
|
|
10
|
+
跨工作区检索时优先调用 `memory_search` 工具(本插件注册):对全局与各工作区的记忆文件(热层+冷层)做关键字/正则搜索,返回文件、行号与小节;搜到指针后再 read 冷层取详情。该工具在休眠态同样可用。
|
|
11
|
+
|
|
12
|
+
### 初始化模板(仅文件不存在时用 write 原样照抄创建,各文件自身的规则已内嵌在模板「注意事项」中)
|
|
13
|
+
|
|
14
|
+
模板仅为初始种子;已存在的记忆文件以其自身内容为唯一权威,与模板冲突时以记忆文件为准。
|
|
15
|
+
|
|
16
|
+
#### 全局记忆模板(`~/.dsh/AGENTS.md`)
|
|
17
|
+
|
|
18
|
+
```md
|
|
19
|
+
# 本文件是全局跨工作区持久记忆文件
|
|
20
|
+
|
|
21
|
+
> 若本文件内容因超预算未被全部注入,先 read 本文件;产生需长期记住的事项时写入对应章节。
|
|
22
|
+
> 只记录使用中沉淀的注意事项,不重复 README/项目文档已有的知识。
|
|
23
|
+
> 如果发现全局记忆与工作区记忆重复的地方请及时整理。
|
|
24
|
+
|
|
25
|
+
## 用户偏好
|
|
26
|
+
|
|
27
|
+
(跨工作区通用的偏好、习惯、约定)
|
|
28
|
+
|
|
29
|
+
## 注意事项
|
|
30
|
+
|
|
31
|
+
(跨工作区通用注意事项+本文件自身的记忆规则。标题以 **规则·** 开头的条目是本文件自身的规则,只可修改原文,严禁压缩、指针化或移入冷层;随本节持续维护:)
|
|
32
|
+
|
|
33
|
+
- **规则·记忆三级冷热架构**:全局本文件存跨工作区事项与各工作区索引;工作区根 AGENTS.md 存该工作区的注意事项与近期日志(单条日志硬上限 2 行);工作区根 `AGENT-MEMORY.md` 冷层存完整历史归档与长流程详情,长条目在 AGENTS.md 原位置留一行指针 `- (日期) 一句话结论 → 详见 AGENT-MEMORY.md#节名`。冷层文件名全网唯一化、终身不改(名字是检索契约)。
|
|
34
|
+
- **规则·写入分流**:跨工作区通用写全局本文件;工作区专属写该工作区根 AGENTS.md;偏好类一律写全局「用户偏好」,待办一律写全局「待办」并标注所属工作区;同一条信息严禁写入两个文件(DSH 不去重,重复写入会被重复注入)。
|
|
35
|
+
- **规则·变更日志维护**:所有变更日志(全局/工作区/冷层归档)一律按日期「新→旧」倒序,新条目插在标题下第一行,严禁追加到章节末尾;工作区日志只留近 10 条短条目,更早的整批移入冷层「变更日志归档」并留一行归档指针;全局日志只记跨工作区/架构级事项(架构升级、新工作区立项、dsh 本体变更),逐实例的部署/修复细节一律归工作区冷层,严禁在全局重复记录。
|
|
36
|
+
- **规则·跨工作区检索**:需要其他工作区的记忆而当前不在该工作区时,先看全局「工作区索引」的描述与关键词判断相关性,再按路径 read 对应 AGENTS.md;读到「→ 详见 AGENT-MEMORY.md#节名」指针必须跟进 read 冷层文件取详情。
|
|
37
|
+
- **规则·自我维护(防膨胀,强制)**:全局本文件 >30KB 触发已完成待办即删(细节进冷层)、日志只留近 20 条且仅跨工作区事项、巨型注意事项压缩+指针(仅限经验类条目);硬红线 55KB,任何记忆文件触线本轮立即只做压缩;各工作区文件的体积阈值见各文件自身规则。全局冷层 `~/.dsh/AGENT-MEMORY.md` 首次需要归档时创建,结构与工作区冷层同构(`## 变更日志归档` / `## 长流程与踩坑档案`)。
|
|
38
|
+
- **规则·索引同步与格式(强制)**:只要改过任何工作区的 AGENTS.md 或其冷层,同一回合内同步更新全局「工作区索引」对应行;行格式 `- (YYYY-MM-DD~YYYY-MM-DD) [workspace] 工作区标识 | 特征+用途描述(≤50字) | 关键词1、关键词2 | 路径: /绝对路径/AGENTS.md | 规模: N条`,行首日期=起止时间——起=该工作区最早记忆条目日期(热层+冷层最早,建区时确定后一般不变),止=最近更新日期,规模用热/冷分层口径(如:热层29条+冷层121条);关键词封顶 10 个且不得重复;描述≤50字,结构=技术栈/形态+做什么+给谁/解决什么,英文专有名词配中文解释、缩写补全、避免空泛词。
|
|
39
|
+
- **规则·收尾检查**:任务完成或用户示意结束时,回顾本次对话——若本次改过任何记忆文件,确认全局「工作区索引」对应行已同步;若有值得长期保留但尚未保存的信息,先保存再结束回复。
|
|
40
|
+
|
|
41
|
+
## 工作区索引
|
|
42
|
+
|
|
43
|
+
- (YYYY-MM-DD~YYYY-MM-DD) [workspace] 工作区标识 | 特征+用途描述(≤50字) | 关键词1、关键词2 | 路径: /绝对路径/AGENTS.md | 规模: 热层N条+冷层N条
|
|
44
|
+
|
|
45
|
+
## 待办
|
|
46
|
+
|
|
47
|
+
- [工作区标识] (YYYY-MM-DD) 待办内容
|
|
48
|
+
|
|
49
|
+
## 变更日志
|
|
50
|
+
|
|
51
|
+
- (YYYY-MM-DD) 重要改动记录
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
#### 工作区记忆模板(工作区根 `AGENTS.md`)
|
|
55
|
+
|
|
56
|
+
```md
|
|
57
|
+
# 本文件是DSH的工作区跨会话持久记忆文件
|
|
58
|
+
|
|
59
|
+
> 工作区跨会话持久记忆。若本文件内容因超预算未被自动注入,先 read 本文件;产生需长期记住的事项时写入对应章节。
|
|
60
|
+
> 只记录使用中沉淀的注意事项,不重复 README/项目文档已有的知识。
|
|
61
|
+
|
|
62
|
+
## 工作区注意事项
|
|
63
|
+
|
|
64
|
+
(该工作区专属的注意事项、操作流程、踩坑记录。本文件自身的记忆规则如下:)
|
|
65
|
+
|
|
66
|
+
- **单条日志硬上限 2 行**:长条目(多段流程/完整复现/部署记录)写本工作区冷层 `AGENT-MEMORY.md`,本文件原位置留一行指针 `- (日期) 一句话结论 → 详见 AGENT-MEMORY.md#节名`。
|
|
67
|
+
- **变更日志只留近 10 条**:更早的整批移入冷层「变更日志归档」章节(保持「新→旧」倒序),本文件日志尾部留一行「完整历史见 AGENT-MEMORY.md#变更日志归档」。所有变更日志按日期「新→旧」倒序,新条目插在标题下第一行,严禁追加到章节末尾。
|
|
68
|
+
- **变更日志短ID**:本文件每条变更日志末尾追加 ` #<短ID>`,短ID = $DSH_SESSION_ID 去掉 session- 前缀后的 UUID 前 8 位(会话日志路径见 $DSH_SESSION_JSONL)。
|
|
69
|
+
- **偏好/待办不存本文件**:一律写全局 `~/.dsh/AGENTS.md`(偏好进「用户偏好」,待办进「待办」并标注本工作区)。
|
|
70
|
+
- **写入即自检**:对本文件或本工作区冷层做过 edit 后顺手 bash `wc -c` 测体积,本文件 >32KB 触发压缩(日志超限移冷层、长条目指针化)。
|
|
71
|
+
|
|
72
|
+
## 工作区变更日志
|
|
73
|
+
|
|
74
|
+
- (YYYY-MM-DD) 重要改动记录
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### 工作区详情文件模板(工作区根 `AGENT-MEMORY.md`,冷层)
|
|
78
|
+
|
|
79
|
+
```md
|
|
80
|
+
# 本文件是DSH的工作区记忆详情文件(冷层)
|
|
81
|
+
|
|
82
|
+
> 本文件存完整历史归档与长流程详情。AGENTS.md 里的「→ 详见 AGENT-MEMORY.md#节名」指针指向本文件的对应二级标题。
|
|
83
|
+
|
|
84
|
+
## 变更日志归档
|
|
85
|
+
|
|
86
|
+
(完整历史条目归档,按「新→旧」倒序,新条目插顶;AGENTS.md 只保留近期短条目)
|
|
87
|
+
|
|
88
|
+
## 长流程与踩坑档案
|
|
89
|
+
|
|
90
|
+
(多段操作流程、完整复现过程、部署记录等长条目,一节一事,标题即锚点)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 插件状态自管(首次休眠)
|
|
94
|
+
|
|
95
|
+
本提示词由 dsh-memory 插件注入。当全局 `~/.dsh/AGENTS.md` 与当前工作区记忆文件均已初始化完毕后,在首次会话收尾时用 write 把 `~/.dsh/.dsh-memory-state` 写为 `sleep`——此后新会话只注入休眠钩子,省下本段提示词的上下文开销;用户说「初始化记忆插件」时把该文件写回 `active` 即恢复完整注入(下一轮对话生效,无需重启)。
|
|
96
|
+
|
|
97
|
+
<!-- DSH-MEMORY:SLEEP_HOOK_START -->
|
|
98
|
+
当用户说「初始化记忆插件」(或明确要求重启记忆插件)时:用 write 把 `~/.dsh/.dsh-memory-state` 写为 `active`——下一轮对话起自动恢复本插件的完整注入,无需重启。
|
|
99
|
+
<!-- DSH-MEMORY:SLEEP_HOOK_END -->
|