dsh-plugin-xujie-memory 0.5.0 → 0.6.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0(内核 0.52.0)
4
+
5
+ ### 发布前修复
6
+ - 修正三个工具的输出 schema,使其符合 DSH 的原始 JSON Schema 校验。
7
+ - 工具使用实际会话标识与账户,删除后刷新画像投影并清除注入缓存。
8
+ - 移除包内 Python 字节码缓存;新增删除后画像回归测试。
9
+ - 暂无记忆编辑工具和图形化管理界面。
10
+
11
+ ### 新增:主动工具(三个)
12
+ - `memory_search`:按字面/错字模糊检索记忆原话,返回内容、`trace_id` 与权重。只读。
13
+ - `memory_forget`:删一条记忆——软删(内容清空、FTS 移除、依赖它的事实失效并重算,`erased_at` 留审计)。用户说「忘掉这条」时用。
14
+ - `memory_status`:原话/已删/状态/锚点/未确认计数 + 内核健康信息。只读。
15
+ - 内核新增 `POST /memory/forget`、`POST /memory/stats`;`memory.py` 增加 `forget()` 与 `stats()`。
16
+ - 宿主没有 `tools` 服务时静默跳过注册,不影响原本的注入。
17
+
18
+ ### 修复:回声
19
+ - 当前这句不再被当成「相关的旧事」注回去:`/turn` 用 `trace_id` 挡,`/context` 只拿得到文本,改为按内容挡(忽略空白与标点)。两条路都经过 `_recall`,挡在源头。
20
+ - `_recent` 兜底同样过滤当前句,避免同一句被记两笔时漏网。
21
+
22
+ ### 测试
23
+ - 新增 `test/echo.test.mjs`:同句、同句不同标点都不出现;换成无关问句时旧记忆照常出现(消回声不能消记忆)。
24
+ - 新增 `test/tools.test.mjs`:三个工具对真内核跑通——检索 → 删除 → 搜不到 → 已删计数 +1;不存在的 `trace_id` 返回错误。
25
+ - 修 `test/smoke.test.mjs` 的测试替身:插件现在有两组 effect,替身只存最后一个 disposer 会漏掉内核,进程留成孤儿占端口和 sqlite 文件。
26
+
27
+ ### 说明
28
+ - Windows x64 默认使用包内运行时;其他平台使用系统 Python。测试在仓库的内核目录中运行。
29
+
3
30
  ## 0.5.0(内核 0.52.0)
4
31
 
5
32
  ### 性能
@@ -21,7 +48,7 @@
21
48
  - 默认 `scope: user`:画像、偏好、锚点、账跨会话共享;追问熔断仍然按会话计算。`scope: session` 可恢复旧行为。
22
49
  - 去掉 session 模式下的跨会话 `lastContext` 兜底。
23
50
 
24
- ### 准确率(回归集 `test/kernel_regression.py`,31/31)
51
+ ### 历史准确率(原回归集 31/31,因沙箱重置丢失,目前无法复现)
25
52
  - 问句不再生成事实(「你还记得我欠谁钱吗」)。
26
53
  - 「还/本/书/瓶」等单字不再触发记账;日期里的数字(周五、四月)不再当成数量。
27
54
  - 无领域词的闲聊(「哈哈」「我女朋友叫小美」)不再落成偏好。
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Shoren-Syi
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shoren-Syi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -39,10 +39,9 @@ flowchart LR
39
39
  2. 把"这个人是谁、我现在记得什么、他专门交代过要记住什么"注入提示词
40
40
  3. 你没说清楚的地方,**最多问一次**,问过就不再提
41
41
 
42
- 它不是"给模型一个 `memory_search` 工具"——工具要模型自己想起来用,
43
- 而记忆的价值恰恰在于**不用想**。
42
+ 记忆会自动注入;需要主动查找或删除时,也可以使用下方工具。
44
43
 
45
- ### 三层记忆(当前版本 0.5.0)
44
+ ### 三层记忆(当前版本 0.6.0)
46
45
 
47
46
  | 层 | 内容 | 你怎么用 |
48
47
  |---|---|---|
@@ -61,7 +60,26 @@ flowchart LR
61
60
  **模糊搜索**:`POST /memory/search`,两字词、错一个字都能捞;
62
61
  精确检索落空时,自动注入也会降级到模糊匹配。
63
62
 
64
- 本仓库保留 0.5.0 发布包中的内核;上表概括了三层记忆。
63
+ 本仓库保留 0.6.0 发布包中的内核;上表概括了三层记忆。
64
+
65
+ **当前这句不会被当成旧事注回去(0.6 修复)**:`/turn` 用 trace_id 挡,`/context` 只拿得到文本、按内容挡(忽略空白与标点)。换成问别的事,旧记忆照常出现。
66
+
67
+ ---
68
+
69
+ ## 主动工具(0.6 新增)
70
+
71
+ 注入是自动的,但**主动回忆**和**删掉记错的东西**需要有手。0.6 给模型注册了三个工具:
72
+
73
+ | 工具 | 作用 | 副作用 |
74
+ |---|---|---|
75
+ | `memory_search` | 按字面/错字模糊检索原话,返回内容、`trace_id`、权重 | 只读 |
76
+ | `memory_forget` | 删一条记忆(软删:内容清空、FTS 移除、依赖它的事实失效并重算) | 写,`erased_at` 留审计 |
77
+ | `memory_status` | 原话/已删/状态/锚点/未确认计数 + 内核健康 | 只读 |
78
+
79
+ 用户说「忘掉这条」时,模型可以自己搜到 `trace_id` 再删,不需要你手动清库。
80
+ 对应内核接口:`POST /memory/search`、`POST /memory/forget`、`POST /memory/stats`。
81
+
82
+ 宿主若没有 `tools` 服务(老版本),这三个工具静默跳过,注入照常。目前尚无编辑记忆工具或图形化记忆管理界面。
65
83
 
66
84
  ---
67
85
 
@@ -79,10 +97,16 @@ dsh plugin --profile <你的profile> add <解压后的 dsh-plugin-xujie-memory
79
97
  # 新开一个会话即可
80
98
  ```
81
99
 
82
- 仓库公开后,也可以从 GitHub 安装指定版本:
100
+ 也可以在 DSH 的「添加插件」中输入包名 `dsh-plugin-xujie-memory` 安装,或通过命令安装:
83
101
 
84
102
  ```bash
85
- dsh plugin --profile <你的profile> add github:Shoren-Syi/dsh-plugin-xujie-memory#v0.5.0
103
+ dsh plugin --profile <你的profile> add dsh-plugin-xujie-memory
104
+ ```
105
+
106
+ 也可以从 GitHub 安装指定版本:
107
+
108
+ ```bash
109
+ dsh plugin --profile <你的profile> add github:Shoren-Syi/dsh-plugin-xujie-memory#v0.6.0
86
110
  ```
87
111
 
88
112
  不需要 `pip install`,不需要填 `repoPath`,不需要另外起服务:
@@ -207,7 +231,7 @@ curl -s --noproxy '*' -X POST http://127.0.0.1:8765/turn \
207
231
 
208
232
  ```
209
233
  dsh-plugin-xujie-memory/
210
- ├── lib/index.js # 薄壳:挂 section / 收事件 / 起内核
234
+ ├── lib/index.js # 薄壳:挂 section / 注册工具 / 收事件 / 起内核
211
235
  ├── python/xujie_memory # Python 内核(零第三方依赖)
212
236
  ├── python/specs # 判定规则
213
237
  ├── runtime/win32-x64/ # 自带解释器(Windows)
@@ -219,6 +243,7 @@ dsh-plugin-xujie-memory/
219
243
 
220
244
  ## 开发验证
221
245
 
222
- `npm test` 运行插件与内核的集成冒烟测试,覆盖会话隔离和注入消息过滤;Windows x64 默认使用包内 Python,其他平台需要 Python 3.10+,也可通过 `PYTHON` 环境变量指定解释器。执行 `npm pack` 可打包当前仓库自带的内核。
246
+ `npm test` 运行冒烟、回声、工具及删除后画像对账测试。Windows x64 默认使用包内 Python;其他平台需要 Python 3.10+,也可通过 `PYTHON` 指定解释器。执行 `npm pack` 可打包当前仓库自带的内核。
223
247
 
224
248
  本仓库由 0.5.0 发布包恢复。原先的回归测试集因之前的沙箱重置而丢失,发布包中也没有包含原来的 `test/kernel_regression.py` 和内核同步脚本。CHANGELOG 中的 31/31 是历史记录,当前仓库无法复现该回归集;冒烟测试不代表这 31 项回归已通过。
249
+
package/README_EN.md CHANGED
@@ -39,6 +39,22 @@ This compares common design approaches; individual tool-based systems can differ
39
39
 
40
40
  Importance scoring affects how slowly memories fade. Explicit requests such as “remember this” pin an anchor. Retrieval does not reset decay. The kernel also exposes `/memory/mark`, `/memory/curve`, and `/memory/search` endpoints.
41
41
 
42
+ ## Active tools (new in 0.6)
43
+
44
+ Injection is automatic, but **actively recalling** and **removing what was remembered wrong** need a hand. 0.6 registers three tools:
45
+
46
+ | Tool | What it does | Side effect |
47
+ |---|---|---|
48
+ | `memory_search` | Literal/fuzzy search over stored wording; returns content, `trace_id`, weight | read-only |
49
+ | `memory_forget` | Deletes one memory (soft delete: content cleared, FTS row removed, dependent facts invalidated and recomputed) | write; `erased_at` kept for audit |
50
+ | `memory_status` | Counts for stored/erased entries, states, anchors, open items, plus kernel health | read-only |
51
+
52
+ When the user says “forget that”, the model can search for the `trace_id` itself and delete it — no manual database surgery. Kernel endpoints: `POST /memory/search`, `POST /memory/forget`, `POST /memory/stats`.
53
+
54
+ Hosts without a `tools` service skip registration silently; injection keeps working. Editing memories and a graphical memory-management interface are not yet available.
55
+
56
+ The current sentence is no longer injected back as a past event (0.6 fix): `/turn` excludes it by `trace_id`, `/context` by content (whitespace and punctuation ignored). Ask about something else and older memories still appear.
57
+
42
58
  ## Installation
43
59
 
44
60
  The package targets the DSH 0.2 preview bundle mechanism and Node.js `^22.19 || >=24`. Windows x64 includes Python and the kernel; no separate Python installation or pip dependencies are needed. On macOS and Linux it falls back to system Python 3.10 or newer.
@@ -47,10 +63,16 @@ The package targets the DSH 0.2 preview bundle mechanism and Node.js `^22.19 ||
47
63
  dsh plugin --profile <your-profile> add <path-to-extracted-dsh-plugin-xujie-memory>
48
64
  ```
49
65
 
50
- When the repository is public, install the tagged version directly from GitHub:
66
+ Install by package name from npm:
67
+
68
+ ```bash
69
+ dsh plugin --profile <your-profile> add dsh-plugin-xujie-memory
70
+ ```
71
+
72
+ Or install the tagged version directly from GitHub:
51
73
 
52
74
  ```bash
53
- dsh plugin --profile <your-profile> add github:Shoren-Syi/dsh-plugin-xujie-memory#v0.5.0
75
+ dsh plugin --profile <your-profile> add github:Shoren-Syi/dsh-plugin-xujie-memory#v0.6.0
54
76
  ```
55
77
 
56
78
  Start a new session. The plugin starts the bundled kernel automatically. By default the database is stored at `~/.xujie-memory/memory.db`. Kernel failure skips memory for that turn rather than blocking the conversation.
@@ -80,13 +102,14 @@ Only `user/message` events are ingested. Runtime context and agent-instruction m
80
102
  ## Package contents
81
103
 
82
104
  ```text
83
- lib/index.js DSH integration and kernel lifecycle
105
+ lib/index.js DSH integration, tool registration and kernel lifecycle
84
106
  python/xujie_memory/ Python memory kernel
85
107
  python/specs/ Interpretation rules
86
108
  runtime/win32-x64/ Bundled Windows x64 Python runtime
87
109
  cordis.patch.yml DSH bundle patch
88
110
  ```
89
111
 
90
- This repository was reconstructed from the distributed 0.5.0 package. Run `npm test` for the integration smoke test and `npm pack` to package the bundled kernel. Windows x64 uses the bundled Python runtime; other platforms need Python 3.10 or newer. Set `PYTHON` to use a specific interpreter for the smoke test.
112
+ The repository was reconstructed from the 0.5.0 package and updated for 0.6.0. Run `npm test` for the smoke, echo, tools, and deletion projection tests; run `npm pack` to package the bundled kernel. Windows x64 uses the bundled Python runtime; other platforms need Python 3.10 or newer. Set `PYTHON` to select an interpreter.
91
113
 
92
114
  The original regression suite was lost when the previous sandbox was reset. The distribution did not include `test/kernel_regression.py` or the kernel synchronization script. The 31/31 result in the changelog is historical and cannot be reproduced from this repository; the smoke test does not replace that regression suite.
115
+
package/cordis.patch.yml CHANGED
@@ -1,49 +1,49 @@
1
- # 这个 bundle 往配置树里插的一行。
2
- #
3
- # `name` 必须是包的**名字**(和 package.json 的 name 一致),
4
- # `id` 是用户之后覆盖配置时的定位锚点——要稳定,不要和别的插件重名。
5
- #
6
- # 注意:DSH 0.2.1-rc.2 要求 patch 文件是**顶层数组**,所以整份内容放在
7
- # 一个 `- insert:` 列表项里(映射形式会被插件安装校验拒绝)。
8
- - insert:
9
- - id: xujie-memory
10
- name: dsh-plugin-xujie-memory
11
- config:
12
- # Python 内核监听地址;宿主的 section 注入和写记忆都走这条本地 HTTP。
13
- endpoint: "http://127.0.0.1:8765"
14
-
15
- # 留空 = 用插件包内自带的 python/ 内核(推荐,装上即用)。
16
- # 只有开发时想指向仓库才填绝对路径。
17
- repoPath: ""
18
-
19
- # 由插件拉起内核进程。设成 false 就要求你另外手动起服务。
20
- autoStart: true
21
-
22
- # 留空 = 用插件包内自带的解释器(runtime/<平台>-<架构>/)。
23
- # 只有想换成本机 Python 时才填。
24
- pythonPath: ""
25
-
26
- # 留空 = ~/.xujie-memory/memory.db;想换位置或跑测试时填绝对路径。
27
- dbPath: ""
28
-
29
- # 一个会话最多主动追问几次。超过之后材料里只剩一句总数。
30
- maxAskedTotal: 6
31
-
32
- # 记忆范围(v0.5):
33
- # user 人物卡/偏好/锚点/账跨会话有效(默认,长期记忆)
34
- # session 每个会话一份、互不相通(0.4 的旧行为)
35
- scope: "user"
36
-
37
- # 内核日志;留空 = 与记忆库同目录的 kernel.log(超过 2MB 自动轮转)。
38
- logFile: ""
39
-
40
- # 插件与内核共享的鉴权 token 文件;留空 = 与记忆库同目录的 token。
41
- tokenFile: ""
42
-
43
- # 宿主若会 await section.text 返回的 Promise,可设为 true:
44
- # 组装提示词时最多等 waitMs 毫秒拿这一轮的记忆。宿主不支持就别开。
45
- asyncSection: false
46
- waitMs: 400
47
-
48
- # 可选诊断文件:宿主 logger 把 info 过滤掉时,用它确认插件挂载/内核就绪。
49
- statusFile: ""
1
+ # 这个 bundle 往配置树里插的一行。
2
+ #
3
+ # `name` 必须是包的**名字**(和 package.json 的 name 一致),
4
+ # `id` 是用户之后覆盖配置时的定位锚点——要稳定,不要和别的插件重名。
5
+ #
6
+ # 注意:DSH 0.2.1-rc.2 要求 patch 文件是**顶层数组**,所以整份内容放在
7
+ # 一个 `- insert:` 列表项里(映射形式会被插件安装校验拒绝)。
8
+ - insert:
9
+ - id: xujie-memory
10
+ name: dsh-plugin-xujie-memory
11
+ config:
12
+ # Python 内核监听地址;宿主的 section 注入和写记忆都走这条本地 HTTP。
13
+ endpoint: "http://127.0.0.1:8765"
14
+
15
+ # 留空 = 用插件包内自带的 python/ 内核(推荐,装上即用)。
16
+ # 只有开发时想指向仓库才填绝对路径。
17
+ repoPath: ""
18
+
19
+ # 由插件拉起内核进程。设成 false 就要求你另外手动起服务。
20
+ autoStart: true
21
+
22
+ # 留空 = 用插件包内自带的解释器(runtime/<平台>-<架构>/)。
23
+ # 只有想换成本机 Python 时才填。
24
+ pythonPath: ""
25
+
26
+ # 留空 = ~/.xujie-memory/memory.db;想换位置或跑测试时填绝对路径。
27
+ dbPath: ""
28
+
29
+ # 一个会话最多主动追问几次。超过之后材料里只剩一句总数。
30
+ maxAskedTotal: 6
31
+
32
+ # 记忆范围(v0.5):
33
+ # user 人物卡/偏好/锚点/账跨会话有效(默认,长期记忆)
34
+ # session 每个会话一份、互不相通(0.4 的旧行为)
35
+ scope: "user"
36
+
37
+ # 内核日志;留空 = 与记忆库同目录的 kernel.log(超过 2MB 自动轮转)。
38
+ logFile: ""
39
+
40
+ # 插件与内核共享的鉴权 token 文件;留空 = 与记忆库同目录的 token。
41
+ tokenFile: ""
42
+
43
+ # 宿主若会 await section.text 返回的 Promise,可设为 true:
44
+ # 组装提示词时最多等 waitMs 毫秒拿这一轮的记忆。宿主不支持就别开。
45
+ asyncSection: false
46
+ waitMs: 400
47
+
48
+ # 可选诊断文件:宿主 logger 把 info 过滤掉时,用它确认插件挂载/内核就绪。
49
+ statusFile: ""
package/lib/index.js CHANGED
@@ -34,6 +34,7 @@ const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
34
34
  const BUNDLED_KERNEL = join(PACKAGE_ROOT, 'python')
35
35
  const DEFAULT_ENDPOINT = 'http://127.0.0.1:8765'
36
36
  const TURN_TIMEOUT_MS = 4000
37
+ const TOOL_TIMEOUT_MS = 5000
37
38
  const HEALTH_TIMEOUT_MS = 800
38
39
  const READY_WAIT_MS = 10000
39
40
  const WATCHDOG_MS = 5000
@@ -254,6 +255,126 @@ export function apply(ctx, config = {}) {
254
255
  })
255
256
  })
256
257
 
258
+ // ------------------------------------------------------------ 工具
259
+ // 注入全自动,但"主动回忆"和"记错了删掉"必须给模型一只手:
260
+ // 只会被动接收召回结果 = 只许它看,不许它查、不许它改。
261
+ // 三个都是可解释的低危操作:搜(只读)、忘(软删,可审计)、看账(只读)。
262
+ const toolDisposers = []
263
+ ctx.inject(['tools'], (toolCtx) => {
264
+ // 宿主没给 tools 服务(老版本 / 测试替身)时静默跳过:
265
+ // 工具是增强,不能因为它让整块记忆插件挂不上。
266
+ if (typeof toolCtx?.tools?.register !== 'function') {
267
+ log('warn', '宿主没有 tools 服务,检索/删除工具不会注册(注入照常)')
268
+ return
269
+ }
270
+ const sessionOf = (exec) => exec?.agent?.session ?? exec?.session
271
+ const call = async (path, payload, exec) => {
272
+ const resp = await fetchJson(
273
+ `${endpoint}${path}`,
274
+ { session: sessionKey(sessionOf(exec)), user: String(sessionOf(exec)?.userId ?? sessionOf(exec)?.account ?? 'user'), ...payload },
275
+ TOOL_TIMEOUT_MS,
276
+ token,
277
+ )
278
+ if (!resp || resp.ok !== true) {
279
+ throw new Error(`xujie-memory: ${resp?.detail ?? resp?.error ?? 'memory kernel did not respond'}`)
280
+ }
281
+ return resp
282
+ }
283
+ const text = (value) => [{ type: 'text', text: value }]
284
+
285
+ toolDisposers.push(toolCtx.tools.register({
286
+ name: 'memory_search',
287
+ description: '在用户的长期记忆里检索他说过的话(支持短词与错字模糊匹配)。只读。返回原话、trace_id 与权重点。要删除某条记忆前,先用它拿到 trace_id。',
288
+ parameters: {
289
+ type: 'object',
290
+ properties: {
291
+ query: { type: 'string', description: '要检索的内容;两个字也能搜,错一个字也能命中' },
292
+ limit: { type: 'number', description: '最多返回几条,默认 5(1-20)' },
293
+ },
294
+ required: ['query'],
295
+ },
296
+ output: {
297
+ schema: { type: 'object' },
298
+ render: (_args, value) => {
299
+ const hits = value?.hits ?? []
300
+ if (hits.length === 0) return text(`没有搜到与「${value?.query ?? ''}」相关的记忆。`)
301
+ const lines = hits.map((h) => {
302
+ const tags = [h.anchor ? '锚点' : '', h.importance ? `重要度 ${h.importance}` : ''].filter(Boolean)
303
+ return `- ${h.content}(trace_id: ${h.trace_id}${tags.length ? ',' + tags.join(',') : ''})`
304
+ })
305
+ return text(`搜到 ${hits.length} 条:\n${lines.join('\n')}`)
306
+ },
307
+ },
308
+ execute: async (args, exec) => {
309
+ const query = String(args?.query ?? '').trim().slice(0, MAX_TEXT_CHARS)
310
+ if (!query) throw new Error('memory_search: query 不能为空')
311
+ const limit = Math.max(1, Math.min(20, Math.round(Number(args?.limit) || 5)))
312
+ const resp = await call('/memory/search', { query, limit }, exec)
313
+ return { ok: true, query: resp.query ?? query, hits: resp.hits ?? [] }
314
+ },
315
+ }))
316
+
317
+ toolDisposers.push(toolCtx.tools.register({
318
+ name: 'memory_forget',
319
+ description: '删掉一条记忆(软删:内容清空、FTS 移除、由它推出的事实一起失效并重算;库里保留 erased_at 供审计,不可恢复)。trace_id 用 memory_search 取。用户说「忘掉这条」时用。',
320
+ parameters: {
321
+ type: 'object',
322
+ properties: {
323
+ trace_id: { type: 'string', description: '要删的记忆条目 id(memory_search 返回的 trace_id)' },
324
+ },
325
+ required: ['trace_id'],
326
+ },
327
+ output: {
328
+ schema: { type: 'object' },
329
+ render: (_args, value) => {
330
+ if (!value?.erased) return text('删除失败:内核没有确认。')
331
+ const tail = value.already_erased ? '(这条之前已经删过)' : ''
332
+ return text(`已删除 ${value.trace_id}${tail},连带失效 ${value.state_keys?.length ?? 0} 组状态。`)
333
+ },
334
+ },
335
+ execute: async (args, exec) => {
336
+ const traceId = String(args?.trace_id ?? '').trim()
337
+ if (!traceId) throw new Error('memory_forget: trace_id 不能为空')
338
+ await Promise.allSettled([...pending.values()])
339
+ const result = await call('/memory/forget', { trace_id: traceId }, exec)
340
+ contexts.clear()
341
+ lastContext = ''
342
+ return result
343
+ },
344
+ }))
345
+
346
+ toolDisposers.push(toolCtx.tools.register({
347
+ name: 'memory_status',
348
+ description: '看记忆库现状:原话条数、已删条数、锚点数、未确认项,以及内核健康信息。用户问「你记得我什么」「记忆正常吗」时用。只读。',
349
+ parameters: { type: 'object', properties: {} },
350
+ output: {
351
+ schema: { type: 'object' },
352
+ render: (_args, value) => {
353
+ const kernel = value?.kernel ?? {}
354
+ const head = `记忆库:原话 ${value?.traces ?? 0} 条(已删 ${value?.traces_erased ?? 0} 条),`
355
+ + `状态 ${value?.states ?? 0} 条,锚点 ${value?.anchors ?? 0} 个,`
356
+ + `未确认 ${value?.clarifications_open ?? 0} 个。`
357
+ const tail = kernel.ok
358
+ ? `内核:pid ${kernel.pid},版本 ${kernel.version},scope=${kernel.scope_mode},库 ${kernel.db}`
359
+ : '内核:当前连不上(注入会降级为「这一轮没有记忆」)。'
360
+ return text(`${head}\n${tail}`)
361
+ },
362
+ },
363
+ execute: async (_args, exec) => {
364
+ const [stats, kernel] = await Promise.all([
365
+ call('/memory/stats', {}, exec),
366
+ health(endpoint, token),
367
+ ])
368
+ return { ...stats, kernel }
369
+ },
370
+ }))
371
+ })
372
+ ctx.effect(() => () => {
373
+ for (const dispose of toolDisposers.splice(0)) {
374
+ try { dispose?.() } catch { /* 已释放 */ }
375
+ }
376
+ }, 'xujie-memory: tools')
377
+
257
378
  // ------------------------------------------------------------ 写记忆
258
379
  ctx.on('session/event', (session, event) => {
259
380
  const type = event?.type
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-xujie-memory",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Long-term memory for DeepSeek Harness: facts, anchors, decay and a person card, injected every turn without the model having to ask. Bundles the Python kernel and runtime — install and it works.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -1,14 +1,14 @@
1
- {
2
- "name": "aloof",
3
- "persona": "高冷",
4
- "anger_sensitivity": 0.8,
5
- "anger_half_life_days": 0.5,
6
- "sadness_half_life_days": 5.0,
7
- "trust_damage": 1.2,
8
- "trust_repair": 1.0,
9
- "speak_first": false,
10
- "hold_words": false,
11
- "demand_explanation": true,
12
- "intensity": "normal",
13
- "note": "不容易被激怒(0.8),但一旦掉信任就掉得更多(1.2)。不主动开口,也不需要嘴硬——隔阂本身就是态度。"
14
- }
1
+ {
2
+ "name": "aloof",
3
+ "persona": "高冷",
4
+ "anger_sensitivity": 0.8,
5
+ "anger_half_life_days": 0.5,
6
+ "sadness_half_life_days": 5.0,
7
+ "trust_damage": 1.2,
8
+ "trust_repair": 1.0,
9
+ "speak_first": false,
10
+ "hold_words": false,
11
+ "demand_explanation": true,
12
+ "intensity": "normal",
13
+ "note": "不容易被激怒(0.8),但一旦掉信任就掉得更多(1.2)。不主动开口,也不需要嘴硬——隔阂本身就是态度。"
14
+ }
@@ -1,14 +1,14 @@
1
- {
2
- "name": "gentle",
3
- "persona": "温柔",
4
- "anger_sensitivity": 0.5,
5
- "anger_half_life_days": 1.0,
6
- "sadness_half_life_days": 2.0,
7
- "trust_damage": 0.8,
8
- "trust_repair": 1.3,
9
- "speak_first": true,
10
- "hold_words": false,
11
- "demand_explanation": false,
12
- "intensity": "soft",
13
- "note": "同样被冒犯,气只有别人一半(0.5);修复时回得更多(1.3)。会主动开口把话说开。"
14
- }
1
+ {
2
+ "name": "gentle",
3
+ "persona": "温柔",
4
+ "anger_sensitivity": 0.5,
5
+ "anger_half_life_days": 1.0,
6
+ "sadness_half_life_days": 2.0,
7
+ "trust_damage": 0.8,
8
+ "trust_repair": 1.3,
9
+ "speak_first": true,
10
+ "hold_words": false,
11
+ "demand_explanation": false,
12
+ "intensity": "soft",
13
+ "note": "同样被冒犯,气只有别人一半(0.5);修复时回得更多(1.3)。会主动开口把话说开。"
14
+ }
@@ -1,14 +1,14 @@
1
- {
2
- "name": "tsundere",
3
- "persona": "傲娇",
4
- "anger_sensitivity": 1.3,
5
- "anger_half_life_days": 0.5,
6
- "sadness_half_life_days": 3.0,
7
- "trust_damage": 1.0,
8
- "trust_repair": 0.8,
9
- "speak_first": false,
10
- "hold_words": true,
11
- "demand_explanation": true,
12
- "intensity": "sharp",
13
- "note": "气来得快去得也快(半衰期半天),但嘴上不肯先软。信任修得慢——0.8 的修复乘数意味着同样的和解,她只回八成。"
14
- }
1
+ {
2
+ "name": "tsundere",
3
+ "persona": "傲娇",
4
+ "anger_sensitivity": 1.3,
5
+ "anger_half_life_days": 0.5,
6
+ "sadness_half_life_days": 3.0,
7
+ "trust_damage": 1.0,
8
+ "trust_repair": 0.8,
9
+ "speak_first": false,
10
+ "hold_words": true,
11
+ "demand_explanation": true,
12
+ "intensity": "sharp",
13
+ "note": "气来得快去得也快(半衰期半天),但嘴上不肯先软。信任修得慢——0.8 的修复乘数意味着同样的和解,她只回八成。"
14
+ }
@@ -1,31 +1,31 @@
1
- {
2
- "relation": "debt",
3
- "version": "1",
4
- "risk_tier": "R2",
5
- "required_slots": [
6
- "debtor",
7
- "creditor",
8
- "item",
9
- "quantity"
10
- ],
11
- "allowed_modalities": [
12
- "asserted",
13
- "request"
14
- ],
15
- "writers": [
16
- "debtor",
17
- "creditor"
18
- ],
19
- "effect_kind": "delta",
20
- "admission_mode": "assisted",
21
- "value_type": "number",
22
- "unit": null,
23
- "allow_negative": false,
24
- "confirmation": "explicit",
25
- "validity": "permanent",
26
- "conflict_policy": "dispute",
27
- "feeds": [],
28
- "derived_from": [],
29
- "note": "游戏内债务。建立用 set,偿还与免除用 delta(-)。余额不得为负;累计偿付量参与修正校验。unit 不是必填,也不固定在规格里:债务可以是米、书、金币,单位由对话决定(eval 时通过上下文 unit_hint 提供)。口语里「三十」不说单位,把它当必填只会把正常句子挂起。给了但与规格不符时,由内核判 disputed。 允许 request:免除在语用上多是请求(「给我免三碗」),不是陈述事实。请求仍走 explicit 确认。 【准入决策】永久 assisted,不是\"等指标变好再升\":(1)实测准确率约 48%(EXPERIMENT §36–39),(2)R2 的错误代价不对称——多问一句是打扰,少问一句是记错钱,(3)错误集中在\"承诺 vs 已发生\"和\"疑问句确认\"两类高频语用(XM-21/22),那是能力边界,不是调参能解决的。",
30
- "conservation": "repayment"
31
- }
1
+ {
2
+ "relation": "debt",
3
+ "version": "1",
4
+ "risk_tier": "R2",
5
+ "required_slots": [
6
+ "debtor",
7
+ "creditor",
8
+ "item",
9
+ "quantity"
10
+ ],
11
+ "allowed_modalities": [
12
+ "asserted",
13
+ "request"
14
+ ],
15
+ "writers": [
16
+ "debtor",
17
+ "creditor"
18
+ ],
19
+ "effect_kind": "delta",
20
+ "admission_mode": "assisted",
21
+ "value_type": "number",
22
+ "unit": null,
23
+ "allow_negative": false,
24
+ "confirmation": "explicit",
25
+ "validity": "permanent",
26
+ "conflict_policy": "dispute",
27
+ "feeds": [],
28
+ "derived_from": [],
29
+ "note": "游戏内债务。建立用 set,偿还与免除用 delta(-)。余额不得为负;累计偿付量参与修正校验。unit 不是必填,也不固定在规格里:债务可以是米、书、金币,单位由对话决定(eval 时通过上下文 unit_hint 提供)。口语里「三十」不说单位,把它当必填只会把正常句子挂起。给了但与规格不符时,由内核判 disputed。 允许 request:免除在语用上多是请求(「给我免三碗」),不是陈述事实。请求仍走 explicit 确认。 【准入决策】永久 assisted,不是\"等指标变好再升\":(1)实测准确率约 48%(EXPERIMENT §36–39),(2)R2 的错误代价不对称——多问一句是打扰,少问一句是记错钱,(3)错误集中在\"承诺 vs 已发生\"和\"疑问句确认\"两类高频语用(XM-21/22),那是能力边界,不是调参能解决的。",
30
+ "conservation": "repayment"
31
+ }