@max-null/dsh-memory 0.15.0 → 0.15.1
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 +6 -8
- package/dist/index.d.ts +11 -0
- package/dist/index.js +31 -12
- package/dist/self.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ This plugin belongs to the **`@max-null/*` family** — a set of plugins that to
|
|
|
11
11
|
1. **写入即生效,人是例外干预者**:模型写入的记忆直接生效(`approved`),不再逐条等人放行;人保留随时查看、改写、删除、钉住或回滚的能力——人不在场不等于失控。
|
|
12
12
|
2. **可观测先于精准**:每条记忆是明文,`memory_list` 随时可见、`memory_forget` 随时删除——不存在"静默暗礁"。
|
|
13
13
|
3. **明文是人机共享的审计窗口**:记忆是可读文本,模型可自检其是否过期(有效性锚点会在所绑环境值变化后标记 `stale`),人可随时查看与改写。
|
|
14
|
-
4. **确定性且缓存安全**:BM25 关键词检索是存储的纯函数、无 LLM 调用;固定指引进 system-prompt section,`approved + injected` 记忆进 recall context(global 全量 +
|
|
14
|
+
4. **确定性且缓存安全**:BM25 关键词检索是存储的纯函数、无 LLM 调用;固定指引进 system-prompt section,`approved + injected` 记忆进 recall context(global 全量 + 当前会话工作区),逐条为单行摘要并按注入预算截断(超预算按最近更新优先取舍,省略数在面板可见)。
|
|
15
15
|
|
|
16
16
|
## 截图
|
|
17
17
|
|
|
@@ -39,8 +39,8 @@ npm install @max-null/dsh-memory
|
|
|
39
39
|
|
|
40
40
|
## 提供的服务与工具
|
|
41
41
|
|
|
42
|
-
- **服务** `ctx.memory`:`remember` / `list` / `search` / `forget` / `
|
|
43
|
-
-
|
|
42
|
+
- **服务** `ctx.memory`:`remember` / `list` / `search` / `forget` / `update` / `move` / `setStatus` / `setInjected` / `supersede` / `retract` / `sweep` / `demoteStale` / `verifyAnchors` / `reload` 等
|
|
43
|
+
- **工具**(12 个):`memory_save`、`memory_list`、`memory_search`、`memory_update`、`memory_confirm`、`memory_forget`、`memory_move`、`memory_sweep`,加提示词模板四件套 `prompt_search`、`prompt_get`、`prompt_list`、`prompt_add`
|
|
44
44
|
- **注入**:`tool:memory` 指引 section(工具用法 + 常驻注入判据)+ `memory:self` 机制自述 + `memory:recall` 召回 context(global 的 `approved + injected` + 当前会话工作区的 `approved + injected`,带 `[memory:<id>:<namespace>]` 来源标记;摘要化 + 预算截断)
|
|
45
45
|
- **检索**:BM25(CJK 单字 + 2-gram,content 与 keywords 字段分离加权;中文多字查询精度显著优于单字切分);可选语义融合(见「可选配置」)
|
|
46
46
|
|
|
@@ -94,17 +94,15 @@ md 文件是唯一事实源(`~/.dsh/prompt-library/*.md` 为 global;`<worksp
|
|
|
94
94
|
|
|
95
95
|
```
|
|
96
96
|
模型 memory_save → status: approved,立即生效;命中密钥/凭据规则则隔离(不进检索也不进注入)
|
|
97
|
-
给 injected 则同时钉住常驻(0.8.0
|
|
97
|
+
给 injected 则同时钉住常驻(0.8.0),不受自动规则影响
|
|
98
98
|
memory_search → 关键词/语义召回(只搜记忆;模板走 prompt_search 通道,不会混进候选池)
|
|
99
|
-
命中累计 2 次 → injected: true(自动打开常驻:global + 当前会话工作区,摘要化 + 预算截断)
|
|
100
|
-
30 天未再命中 → 自动撤下常驻(只撤自动开的;人工或模型显式动过的开关双向豁免)
|
|
101
99
|
人(面板 / 开关) → 钉住 / 删除 / 放行隔离记录 / 回滚
|
|
102
100
|
memory_forget → 随时删除(删除始终是人的动作)
|
|
103
101
|
```
|
|
104
102
|
|
|
105
|
-
|
|
103
|
+
**常驻开关只由人(或模型显式传 `injected`)决定。** 两条自动规则都已在 0.12.0 / 0.13.0 停用:命中次数不再自动升常驻(它只喂体检报告的候选提示),30 天未命中的自动撤下也改为**只计量不生效**——撤下来就基本不会自己回去,那种动作不该由一个没有分布依据的阈值决定(阈值来历见 `docs/设计/2026-09-29-借阅-dsh-mneme-后的自查.md` §二)。两者都不删除任何内容——记忆不会无限累积,也不会被系统自行清空。
|
|
106
104
|
|
|
107
|
-
|
|
105
|
+
**低频但关键**的记忆(长期约定 / 判据 / 委托)本来就够不着旧阈值,而自动规则停用之后,问题从「够不着」变成了「必须显式说」。这类走显式路径——在 `memory_save` / `memory_update` 里给 `injected` 即钉住,写的是与面板开关同一套语义(`injectedAuto: false`)。未审核与已隔离的记录不接受注入。
|
|
108
106
|
|
|
109
107
|
注入有**字符预算**(默认 1500,config `injectionBudget`):装不下的条目**整条丢弃**(不截内容),按「最近更新优先」取舍——所以 `injected: true` 不等于「每轮真的在场」,它只保证有资格排队。0.9.0 起这种出局不再无声:注入末尾会追加一行 `(另有 N 条常驻因预算未注入:…)`,0.9.1 起列出的是**每条的短摘要**而不是 id——查 id 是什么的那一步最容易省略,省略了就等于没报;**清理干净即自行消失**。面板的注入预览里也能展开看明细。诊断行本身不占预算。
|
|
110
108
|
|
package/dist/index.d.ts
CHANGED
|
@@ -12,4 +12,15 @@ export { MemoryId } from './engine.ts';
|
|
|
12
12
|
export { bm25FieldScores, bm25Scores, cosineSimilarity, rrfFuse, tokenize } from './bm25.ts';
|
|
13
13
|
export declare const name = "dsh-memory";
|
|
14
14
|
export declare const inject: string[];
|
|
15
|
+
/**
|
|
16
|
+
* 本次挂载实际注册的工具名,按注册顺序。
|
|
17
|
+
*
|
|
18
|
+
* 存在的理由:**自述(`memory:self`)与 README 都要列工具清单,而清单会漏**——0.11.0 加了
|
|
19
|
+
* `memory_move` 之后,自述的数字与清单一直停在 11,直到 0.14.0 收尾核对才被翻出来(README
|
|
20
|
+
* 那边更早,只列了 7 个)。`ToolRegistry` 只提供 `register` / `get`,**没有列举 API**,所以
|
|
21
|
+
* 清单没法从注册表反查;改成注册时登记,清单就有了唯一来源。
|
|
22
|
+
*
|
|
23
|
+
* @see test/memory.spec.ts 的自述覆盖用例——它遍历这个数组,不手抄名字。
|
|
24
|
+
*/
|
|
25
|
+
export declare const registeredToolNames: string[];
|
|
15
26
|
export declare function apply(ctx: Context, config?: MemoryConfig): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,22 @@ export { MemoryId } from "./engine.js";
|
|
|
16
16
|
export { bm25FieldScores, bm25Scores, cosineSimilarity, rrfFuse, tokenize } from "./bm25.js";
|
|
17
17
|
export const name = 'dsh-memory';
|
|
18
18
|
export const inject = ['storage', 'systemPrompt', 'tools', 'webServer', 'webRuntime'];
|
|
19
|
+
/**
|
|
20
|
+
* 本次挂载实际注册的工具名,按注册顺序。
|
|
21
|
+
*
|
|
22
|
+
* 存在的理由:**自述(`memory:self`)与 README 都要列工具清单,而清单会漏**——0.11.0 加了
|
|
23
|
+
* `memory_move` 之后,自述的数字与清单一直停在 11,直到 0.14.0 收尾核对才被翻出来(README
|
|
24
|
+
* 那边更早,只列了 7 个)。`ToolRegistry` 只提供 `register` / `get`,**没有列举 API**,所以
|
|
25
|
+
* 清单没法从注册表反查;改成注册时登记,清单就有了唯一来源。
|
|
26
|
+
*
|
|
27
|
+
* @see test/memory.spec.ts 的自述覆盖用例——它遍历这个数组,不手抄名字。
|
|
28
|
+
*/
|
|
29
|
+
export const registeredToolNames = [];
|
|
30
|
+
/** 登记一个工具名并原样返回——包在 `defineTool` 的 `name` 字段外面。 */
|
|
31
|
+
function registered(toolName) {
|
|
32
|
+
registeredToolNames.push(toolName);
|
|
33
|
+
return toolName;
|
|
34
|
+
}
|
|
19
35
|
/**
|
|
20
36
|
* 解析时间过滤参数(0.12.0):`7d` / `30d` 这类相对天数,或 `YYYY-MM-DD` 绝对日期。
|
|
21
37
|
*
|
|
@@ -321,6 +337,9 @@ function present(title, kind, rawInput) {
|
|
|
321
337
|
return { card: 'generic', title, kind, ...rawInput === undefined ? {} : { rawInput } };
|
|
322
338
|
}
|
|
323
339
|
export async function apply(ctx, config) {
|
|
340
|
+
// 每次挂载都是一次全新的注册——清空上一轮的登记。它是模块级数组(测试要能 import 它),
|
|
341
|
+
// 不清的话同一进程里挂载两次会累积成双份,而消费方(自述覆盖用例)拿到的数量就对不上了。
|
|
342
|
+
registeredToolNames.length = 0;
|
|
324
343
|
await ctx.plugin(MemoryEngine, config);
|
|
325
344
|
await ctx.plugin(MemoryGateway);
|
|
326
345
|
// 0.3.6:自带面板数据通道(HTTP,独立包无需 Typert 构建产物)
|
|
@@ -388,7 +407,7 @@ export async function apply(ctx, config) {
|
|
|
388
407
|
void memory.ensureProjectOpen(cwd).catch(() => { });
|
|
389
408
|
});
|
|
390
409
|
ctx.tools.register(defineTool({
|
|
391
|
-
name: 'memory_save',
|
|
410
|
+
name: registered('memory_save'),
|
|
392
411
|
description: 'Save one cross-session memory. It takes effect immediately; credential-like content is quarantined instead. Record durable preferences, corrections and reusable conclusions — not transient state, guesses, or facts readable from the repo (record where to look instead). 保存一条跨会话记忆:写入即生效;命中密钥/凭据规则的写入会被隔离。只记长期偏好、纠正与可复用结论——不记临时状态、推测、或能从仓库读到的事实(那类记「去哪查」)。',
|
|
393
412
|
parameters: {
|
|
394
413
|
content: { type: 'string', required: true, description: 'Plaintext memory content.' },
|
|
@@ -425,7 +444,7 @@ export async function apply(ctx, config) {
|
|
|
425
444
|
presentCall: args => present('Save memory', 'other', args.content),
|
|
426
445
|
}));
|
|
427
446
|
ctx.tools.register(defineTool({
|
|
428
|
-
name: 'memory_list',
|
|
447
|
+
name: registered('memory_list'),
|
|
429
448
|
description: 'List every stored memory, optionally filtered by namespace, status, injected switch, or update time. Every memory is plaintext and inspectable. 列出全部记忆,可按 namespace/status/injected/更新时间过滤;均为明文可查。',
|
|
430
449
|
parameters: {
|
|
431
450
|
namespace: { type: 'string', enum: ['global', 'project'], description: 'Restrict to one namespace. 限定单个命名空间.' },
|
|
@@ -453,7 +472,7 @@ export async function apply(ctx, config) {
|
|
|
453
472
|
presentCall: () => present('List memories', 'read'),
|
|
454
473
|
}));
|
|
455
474
|
ctx.tools.register(defineTool({
|
|
456
|
-
name: 'memory_search',
|
|
475
|
+
name: registered('memory_search'),
|
|
457
476
|
description: 'Recall stored memories by keyword. Deterministic literal matching — a miss means no stored term matched the query. 按关键词检索记忆(中文 2-gram + 关键词加权;配置嵌入时含语义融合;含待审核条目,被隔离的除外)。',
|
|
458
477
|
parameters: {
|
|
459
478
|
query: { type: 'string', required: true, description: 'Keyword query. 关键词查询.' },
|
|
@@ -475,7 +494,7 @@ export async function apply(ctx, config) {
|
|
|
475
494
|
presentCall: args => present('Search memory', 'read', args.query),
|
|
476
495
|
}));
|
|
477
496
|
ctx.tools.register(defineTool({
|
|
478
|
-
name: 'prompt_search',
|
|
497
|
+
name: registered('prompt_search'),
|
|
479
498
|
description: 'Search prompt templates by keyword plus dimension/difficulty/tag filters. Returns matching summaries; call prompt_get for the full text. 按关键词+维度/难度/标签检索提示词模板,返回匹配摘要;取全文用 prompt_get。',
|
|
480
499
|
parameters: {
|
|
481
500
|
query: { type: 'string', required: true, description: 'Keyword query (name/tag/body). 关键词查询(名称/标签/正文).' },
|
|
@@ -517,7 +536,7 @@ export async function apply(ctx, config) {
|
|
|
517
536
|
presentCall: args => present('Search prompt library', 'read', args.query),
|
|
518
537
|
}));
|
|
519
538
|
ctx.tools.register(defineTool({
|
|
520
|
-
name: 'prompt_get',
|
|
539
|
+
name: registered('prompt_get'),
|
|
521
540
|
description: 'Fetch a prompt template full text by id or name (name matches the file stem without the seq prefix). 按 id 或名称取模板全文(名称 = 文件名去序号部分)。',
|
|
522
541
|
parameters: {
|
|
523
542
|
nameOrId: { type: 'string', required: true, description: 'Template id or name. 模板 id 或名称.' },
|
|
@@ -546,7 +565,7 @@ export async function apply(ctx, config) {
|
|
|
546
565
|
presentCall: args => present('Get prompt template', 'read', args.nameOrId),
|
|
547
566
|
}));
|
|
548
567
|
ctx.tools.register(defineTool({
|
|
549
|
-
name: 'prompt_list',
|
|
568
|
+
name: registered('prompt_list'),
|
|
550
569
|
description: 'List every prompt template index (no full text), optionally filtered by dimension/difficulty/tag. 列出全部模板索引(不含全文),可按维度/难度/标签过滤。',
|
|
551
570
|
parameters: {
|
|
552
571
|
dimension: { type: 'string', description: '按维度过滤.' },
|
|
@@ -575,7 +594,7 @@ export async function apply(ctx, config) {
|
|
|
575
594
|
presentCall: () => present('List prompt templates', 'read'),
|
|
576
595
|
}));
|
|
577
596
|
ctx.tools.register(defineTool({
|
|
578
|
-
name: 'prompt_add',
|
|
597
|
+
name: registered('prompt_add'),
|
|
579
598
|
description: 'Add one prompt template to the library (writes a metadata-marked md file; source=agent is surfaced in the UI). 新增一条提示词模板(写元数据 md 文件;source=agent 在 UI 角标提示)。',
|
|
580
599
|
parameters: {
|
|
581
600
|
name: { type: 'string', required: true, description: 'Template name (also the file stem). 模板名称(也是文件名).' },
|
|
@@ -605,7 +624,7 @@ export async function apply(ctx, config) {
|
|
|
605
624
|
presentCall: args => present('Add prompt template', 'other', args.name),
|
|
606
625
|
}));
|
|
607
626
|
ctx.tools.register(defineTool({
|
|
608
|
-
name: 'memory_forget',
|
|
627
|
+
name: registered('memory_forget'),
|
|
609
628
|
description: 'Delete one stored memory by id. The human owner may remove any memory.',
|
|
610
629
|
parameters: {
|
|
611
630
|
id: { type: 'string', required: true, description: 'Exact memory id from memory_list or memory_search.' },
|
|
@@ -620,7 +639,7 @@ export async function apply(ctx, config) {
|
|
|
620
639
|
presentCall: args => present('Forget memory', 'other', args.id),
|
|
621
640
|
}));
|
|
622
641
|
ctx.tools.register(defineTool({
|
|
623
|
-
name: 'memory_move',
|
|
642
|
+
name: registered('memory_move'),
|
|
624
643
|
description: 'Move one memory to another layer: `global`, `self` (the current workspace), or a relative ancestor (`..` / `../..`). Use it when a memory sits at the wrong level — a project-specific rule parked in `global` that leaks into unrelated workspaces, or a fact that turned out to hold for sibling projects too. The record keeps its id, and the result reports whether the source layer still sees it. 把一条记忆移到另一层:`global`、`self`(当前工作区)或相对祖先(`..` / `../..`)。用于记忆待错了层——只属于某个项目的规矩躺在 `global` 里、漏进无关工作区,或某条事实其实对兄弟项目也成立。id 保持不变,结果里会报告移动后源层是否仍看得见它。',
|
|
625
644
|
parameters: {
|
|
626
645
|
id: { type: 'string', required: true, description: 'Exact memory id from memory_list or memory_search. 要移动的记忆 id。' },
|
|
@@ -645,7 +664,7 @@ export async function apply(ctx, config) {
|
|
|
645
664
|
presentCall: args => present('Move memory', 'other', `${args.id} → ${args.to}`),
|
|
646
665
|
}));
|
|
647
666
|
ctx.tools.register(defineTool({
|
|
648
|
-
name: 'memory_update',
|
|
667
|
+
name: registered('memory_update'),
|
|
649
668
|
description: 'Update the content, keywords, persistent-injection switch or validity anchor of one stored memory (e.g. correcting stale facts, or re-anchoring a memory whose anchor no longer holds). The record keeps its review status; content that looks like a credential quarantines it instead. 修改一条记忆的内容、关键词、常驻注入开关或有效性锚点(如修正过时信息、把失效的锚点改对)。改动后记忆保持原有审核状态;若新内容命中危险规则则转为隔离。',
|
|
650
669
|
parameters: {
|
|
651
670
|
id: { type: 'string', required: true, description: 'Exact memory id from memory_list. 记忆 id(来自 memory_list).' },
|
|
@@ -696,7 +715,7 @@ export async function apply(ctx, config) {
|
|
|
696
715
|
presentCall: args => present('Update memory', 'other', args.id),
|
|
697
716
|
}));
|
|
698
717
|
ctx.tools.register(defineTool({
|
|
699
|
-
name: 'memory_confirm',
|
|
718
|
+
name: registered('memory_confirm'),
|
|
700
719
|
description: 'Release a quarantined memory: marks it human-reviewed (`approved`) and clears its quarantine. Under the silent mechanism ordinary writes already take effect by themselves, so call this only when the human explicitly asks to release a quarantined memory. 放行一条被隔离的记忆:标记为已审核并解除隔离。静默机制下普通写入已自行生效,因此仅在用户明确要求放行隔离记忆时调用。',
|
|
701
720
|
parameters: {
|
|
702
721
|
id: { type: 'string', required: true, description: 'Exact memory id from memory_list. 记忆 id(来自 memory_list).' },
|
|
@@ -711,7 +730,7 @@ export async function apply(ctx, config) {
|
|
|
711
730
|
presentCall: args => present('Confirm memory', 'other', args.id),
|
|
712
731
|
}));
|
|
713
732
|
ctx.tools.register(defineTool({
|
|
714
|
-
name: 'memory_sweep',
|
|
733
|
+
name: registered('memory_sweep'),
|
|
715
734
|
description: 'Read-only health check: lists stale memories (with the reason), long-unused non-resident ones, candidates that keep being retrieved without being pinned, and how much of the injection budget the resident set takes. Changes nothing. 只读体检:列出失效的记忆(带原因)、久未被检索命中的非常驻记录、被反复检索却未常驻的候选,以及常驻占用了多少注入预算。不修改任何状态——最坏情况是报告不准,而不是误改内容。',
|
|
716
735
|
parameters: {},
|
|
717
736
|
output: {
|
package/dist/self.js
CHANGED
|
@@ -23,7 +23,7 @@ export const SELF_VERSION = (() => {
|
|
|
23
23
|
}
|
|
24
24
|
})();
|
|
25
25
|
export const SELF_DESCRIPTION = `[记忆系统自述] 本环境内置跨会话记忆服务(dsh-memory v${SELF_VERSION})。`
|
|
26
|
-
+ '你有
|
|
26
|
+
+ '你有 12 个工具:**记忆工具** memory_save / memory_search / memory_list / memory_update / memory_confirm / memory_forget / memory_move / memory_sweep(`memory_confirm` 仅在用户明确要求放行隔离记录时用;`memory_move` 把放错了层的记忆移到 global、当前工作区或相对祖先;`memory_sweep` 是只读体检——失效清单、久未命中、候选与预算占用,不修改任何状态);**提示词模板工具** prompt_search / prompt_get / prompt_list / prompt_add(模板是 md 文件、**永不注入**,走这条通道,不会混进记忆检索)。'
|
|
27
27
|
+ '记忆分两层:`global` 跨项目(偏好 / 习惯 / 环境知识),`project` 跟随工作区(落在 `<cwd>/.dsh/storages/`,随 git 提交分享)。'
|
|
28
28
|
+ '**何时写入**(不必刻意找机会,遇到下面这些就动手):'
|
|
29
29
|
+ '① 用户表达长期偏好或约定(「以后都…」「记住…」「我们的规矩是…」);'
|
|
@@ -34,7 +34,7 @@ export const SELF_DESCRIPTION = `[记忆系统自述] 本环境内置跨会话
|
|
|
34
34
|
+ '**锚点(可选)**:若这条记忆描述的是「随某个环境值变化的事实」(某工具版本下的行为、某环境变量决定的配置),用 anchor 参数把它绑到那个值上——值变了它会自动失效并标 stale,不会再被当成仍然正确;绑错了用 memory_update 改(当场重新判定,成立即复活),不要了就传 `anchor: null` 清除。当前锚点随记录一起返回。'
|
|
35
35
|
+ '写入即生效(`approved`),人工只在例外时介入;命中密钥/凭据规则的写入会被隔离(不进注入与检索)。'
|
|
36
36
|
+ '**常驻注入**:每轮注入「global + 当前会话工作区」里 approved 且开着常驻开关的记忆单行摘要(形如 `- [memory:id:namespace] 摘要`);全文要靠 memory_search 取。'
|
|
37
|
-
+ '
|
|
37
|
+
+ '常驻开关由人(或模型显式传 `injected`)决定:**自动改变注入状态的规则已全部停用**——命中次数不再自动升常驻(它只喂体检报告的候选提示),30 天未命中的自动撤下也已改为**只计量不生效**(撤下来就基本不会自己回去,那种动作不该由一个没有分布依据的阈值决定)。**给 memory_save / memory_update 传 `injected` 即显式接管**。'
|
|
38
38
|
+ '**钉的判据是「失效可察觉性」**:这条记忆若没被想起来,你会不会根本意识不到自己漏了它?会 → 钉常驻(规则 / 委托 / 判据);不会 → 留检索(事实 / 参考 / 案例)。'
|
|
39
39
|
+ '**注入有预算**(默认 1500 字符,约装 11 条),装不下的整条丢弃、按**最近更新**优先取舍(不是最近被检索——检索是「需要时才用」的行为,拿它决定「谁每轮在场」会让查得多的挤掉钉得牢的)——所以「钉了常驻」只等于「有资格排队」。'
|
|
40
40
|
+ '装不下时注入末尾会出现一行 `(另有 N 条常驻因预算未注入:…短摘要…)`——**那是「有常驻记忆并不在场」的唯一信号**,看到它就该逐条评估撤下。'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max-null/dsh-memory",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Cross-session plaintext memory plugin for the DeepSeek Harness — deterministic BM25 recall, writes take effect immediately, signal-driven injection, no embeddings",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|