@max-null/dsh-memory 0.22.0 → 0.22.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 +5 -1
- package/dist/index.js +58 -50
- package/dist/self.d.ts +3 -0
- package/dist/self.js +30 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -144,10 +144,14 @@ DSH 存储层的写入是「读—改—写全量覆盖」,且官方两个后
|
|
|
144
144
|
- 只读文件、只复制——**绝不复用引擎的写入路径**。写路径会先 `reload()` 再合并,磁盘缺失时它会把内存覆盖掉,正是这次事故的成因;备份不能走同一条路。
|
|
145
145
|
- 日记按**逐字节内容**判断是否已备份过,不按 mtime:复制与编辑都会动 mtime,内容才是事实。
|
|
146
146
|
|
|
147
|
-
## 云同步(2026-11-11
|
|
147
|
+
## 云同步(2026-11-11)· **默认关闭**
|
|
148
148
|
|
|
149
149
|
把本机记忆镜像到一个 git 仓库,多个环境之间共享同一份记忆。
|
|
150
150
|
|
|
151
|
+
> **默认不启用。** 不配 `memory.sync.remote` 时,`memory_sync` 与 `memory_conflicts`
|
|
152
|
+
> **根本不注册**(模型看不到,也就不会去调),自述里的工具数也停在 14。只配 `mirrorDir`
|
|
153
|
+
> 不配 `remote` 同样不启用——本地镜像不解决任何问题。这是实验性功能,不该影响别人的安装。
|
|
154
|
+
|
|
151
155
|
**为什么是「一条记忆一个文件」。** 整份 `memory.json` 放进 git 会撞:2026-11-06 实测,两端各新增一条记忆后 merge,`git` 报 `CONFLICT (content)`——两条记录挤在同一个插入点,按「取一边」解决会**静默丢掉另一端那条**,而 git 仍报「合并成功」。改成一条一个文件后同一场景是 `Merge made by the 'ort' strategy`,0 冲突。
|
|
152
156
|
|
|
153
157
|
**项目身份用项目名,不用路径。** 本机 `H:\MaxNull\WorkStation` 与另一台机器 `D:\WorkStation` 是同一个项目——目录名(统一小写)决定它在 `records/project/` 下落到哪。用绝对路径哈希的话,换个盘符就成了另一个项目,记忆会静默分裂成两份互不相认的。
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { MemoryGateway } from "./remote.js";
|
|
|
13
13
|
import { runMemoryBackup } from "./backup.js";
|
|
14
14
|
import { ANCHOR_KINDS } from "./anchors.js";
|
|
15
15
|
import { mountMemoryApi } from "./routes.js";
|
|
16
|
-
import {
|
|
16
|
+
import { buildSelfDescription } from "./self.js";
|
|
17
17
|
import { appendEntry, diaryDir, lastWriteAt, pointerLine, readDay, recentDates } from "./diary.js";
|
|
18
18
|
import z from '@deepseek-ai/schemastery';
|
|
19
19
|
import { DEFAULT_INJECTION_BUDGET, DEFAULT_SUMMARY_CHARS, candidateNotice, indexNotice, neighborNotice, neutralizeBraces, omittedNotice, renderInjection, staleNotice } from "./injection.js";
|
|
@@ -508,12 +508,18 @@ export async function apply(ctx, config) {
|
|
|
508
508
|
order: 115,
|
|
509
509
|
text: GUIDANCE,
|
|
510
510
|
});
|
|
511
|
+
// 云同步是**默认关闭**的(2026-11-11):实验性功能不该影响别人的安装。判据是
|
|
512
|
+
// 「配了 remote」——只配 mirrorDir 没有意义(本地镜像不解决任何问题)。
|
|
513
|
+
// 关闭时那两个工具**根本不注册**,而不是注册了再报错:模型看不到就不会去调。
|
|
514
|
+
const syncRaw = (config?.sync ?? {});
|
|
515
|
+
const syncEnabled = typeof syncRaw.remote === 'string' && syncRaw.remote !== '';
|
|
511
516
|
// 0.3.2:记忆机制自述——常驻注入(不落用户存储),随发版更新。
|
|
512
517
|
// order 49 < recall(50):机制说明在具体记忆内容之前。
|
|
513
518
|
ctx.systemPrompt.context({
|
|
514
519
|
name: 'memory:self',
|
|
515
520
|
order: 49,
|
|
516
|
-
|
|
521
|
+
// 自述里的工具清单必须与实际注册的一致:漏一个,模型就按一份不存在的工具面行事。
|
|
522
|
+
text: buildSelfDescription(syncEnabled),
|
|
517
523
|
});
|
|
518
524
|
const injectionBudget = config?.injectionBudget ?? DEFAULT_INJECTION_BUDGET;
|
|
519
525
|
const summaryChars = config?.summaryChars ?? DEFAULT_SUMMARY_CHARS;
|
|
@@ -1032,56 +1038,58 @@ export async function apply(ctx, config) {
|
|
|
1032
1038
|
...typeof raw.envName === 'string' && raw.envName !== '' ? { envName: raw.envName } : {},
|
|
1033
1039
|
};
|
|
1034
1040
|
};
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1041
|
+
if (syncEnabled) {
|
|
1042
|
+
ctx.tools.register(defineTool({
|
|
1043
|
+
name: registered('memory_sync'),
|
|
1044
|
+
description: 'Push this machine\'s memories to the sync repo, pull other machines\' memories back, or inspect sync state. Conflicts are never auto-resolved — both versions are kept under conflicts/ for a human to settle. 把本机记忆推到同步仓库 / 拉回其他机器的记忆 / 查看同步状态。冲突不会自动裁决:两边版本都留在 conflicts/ 里等人处理。',
|
|
1045
|
+
parameters: {
|
|
1046
|
+
action: {
|
|
1047
|
+
type: 'string',
|
|
1048
|
+
required: true,
|
|
1049
|
+
enum: ['status', 'pull', 'push'],
|
|
1050
|
+
description: 'status = 只看状态(不读不写);pull = 拉取并导入;push = 镜像、提交并推送。',
|
|
1051
|
+
},
|
|
1044
1052
|
},
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
render: (_args, value) => renderJson(value),
|
|
1049
|
-
},
|
|
1050
|
-
async execute(args) {
|
|
1051
|
-
const cfg = syncConfig();
|
|
1052
|
-
const run = args.action === 'push'
|
|
1053
|
-
? await syncPush(memory, cfg)
|
|
1054
|
-
: args.action === 'pull'
|
|
1055
|
-
? await syncPull(memory, cfg)
|
|
1056
|
-
: await syncStatus(cfg);
|
|
1057
|
-
// 工具返回值要求 `Record<string, JsonValue>`;这几个结果都是普通可序列化对象,
|
|
1058
|
-
// 结构化类型对不上而已。走一遍 JSON 往返顺便保证它确实可序列化。
|
|
1059
|
-
return JSON.parse(JSON.stringify(run));
|
|
1060
|
-
},
|
|
1061
|
-
presentCall: args => present('Sync memory', 'other'),
|
|
1062
|
-
}));
|
|
1063
|
-
ctx.tools.register(defineTool({
|
|
1064
|
-
name: registered('memory_conflicts'),
|
|
1065
|
-
description: 'List memory conflicts left by sync. Sync never auto-resolves them: if two machines changed the same memory, both versions were kept. 列出同步留下的记忆冲突。同步不会自动裁决——两端改过同一条时,两个版本都留着。',
|
|
1066
|
-
parameters: {
|
|
1067
|
-
action: {
|
|
1068
|
-
type: 'string',
|
|
1069
|
-
required: true,
|
|
1070
|
-
enum: ['list'],
|
|
1071
|
-
description: 'list = 列出 conflicts/ 下的冲突文件。',
|
|
1053
|
+
output: {
|
|
1054
|
+
schema: { type: 'object', additionalProperties: true, properties: {} },
|
|
1055
|
+
render: (_args, value) => renderJson(value),
|
|
1072
1056
|
},
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1057
|
+
async execute(args) {
|
|
1058
|
+
const cfg = syncConfig();
|
|
1059
|
+
const run = args.action === 'push'
|
|
1060
|
+
? await syncPush(memory, cfg)
|
|
1061
|
+
: args.action === 'pull'
|
|
1062
|
+
? await syncPull(memory, cfg)
|
|
1063
|
+
: await syncStatus(cfg);
|
|
1064
|
+
// 工具返回值要求 `Record<string, JsonValue>`;这几个结果都是普通可序列化对象,
|
|
1065
|
+
// 结构化类型对不上而已。走一遍 JSON 往返顺便保证它确实可序列化。
|
|
1066
|
+
return JSON.parse(JSON.stringify(run));
|
|
1067
|
+
},
|
|
1068
|
+
presentCall: args => present('Sync memory', 'other'),
|
|
1069
|
+
}));
|
|
1070
|
+
ctx.tools.register(defineTool({
|
|
1071
|
+
name: registered('memory_conflicts'),
|
|
1072
|
+
description: 'List memory conflicts left by sync. Sync never auto-resolves them: if two machines changed the same memory, both versions were kept. 列出同步留下的记忆冲突。同步不会自动裁决——两端改过同一条时,两个版本都留着。',
|
|
1073
|
+
parameters: {
|
|
1074
|
+
action: {
|
|
1075
|
+
type: 'string',
|
|
1076
|
+
required: true,
|
|
1077
|
+
enum: ['list'],
|
|
1078
|
+
description: 'list = 列出 conflicts/ 下的冲突文件。',
|
|
1079
|
+
},
|
|
1080
|
+
},
|
|
1081
|
+
output: {
|
|
1082
|
+
schema: { type: 'object', additionalProperties: true, properties: {} },
|
|
1083
|
+
render: (_args, value) => renderJson(value),
|
|
1084
|
+
},
|
|
1085
|
+
async execute() {
|
|
1086
|
+
const cfg = syncConfig();
|
|
1087
|
+
const files = conflictFiles(cfg);
|
|
1088
|
+
return { count: files.length, files };
|
|
1089
|
+
},
|
|
1090
|
+
presentCall: () => present('List memory conflicts', 'other'),
|
|
1091
|
+
}));
|
|
1092
|
+
}
|
|
1085
1093
|
// 定时备份(2026-10-18 事故后新增):只读复制,绝不走引擎的写入路径。
|
|
1086
1094
|
// 首次延迟 1 分钟避开启动高峰;此后按 `backup.intervalMinutes` 重复;`unref` 不挡进程退出。
|
|
1087
1095
|
const backup = config?.backup;
|
package/dist/self.d.ts
CHANGED
package/dist/self.js
CHANGED
|
@@ -24,23 +24,33 @@ export const SELF_VERSION = (() => {
|
|
|
24
24
|
return 'unknown';
|
|
25
25
|
}
|
|
26
26
|
})();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
27
|
+
function buildDescription(syncEnabled) {
|
|
28
|
+
return (`[记忆系统自述] 本环境内置跨会话记忆服务(dsh-memory v${SELF_VERSION})。`
|
|
29
|
+
+ '你有 ' + (syncEnabled ? 16 : 14) + ' 个工具:**记忆工具** 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 文件、**永不注入**,走这条通道,不会混进记忆检索);'
|
|
30
|
+
+ (syncEnabled
|
|
31
|
+
? '**同步工具** memory_sync / memory_conflicts:跨环境同步;`memory_sync` 的 `status` 只看状态、`pull` 拉取并导入、`push` 镜像并推送,`memory_conflicts` 列出**未被自动裁决**的冲突,同步**不会**自动决定哪一边对——两边都改过同一条时两个版本都留着等人处理,记忆被静默覆盖就没了;'
|
|
32
|
+
: '')
|
|
33
|
+
+ '**日记工具** diary_read / diary_write:日记是 md 文件,落在 `<DSH_HOME>/diary`,与记忆**分开存、不进检索、不占注入预算**。记的是经历——发生了什么(有细节)、我怎么想(可以有情绪、可以没结论)、关于他(他说了什么、我注意到了什么);不是任务清单(那是 git log),也不是结论(那是 memory_save)。读它不吃预算,需要时整篇读。'
|
|
34
|
+
+ '记忆分两层:`global` 跨项目(偏好 / 习惯 / 环境知识),`project` 跟随工作区(落在 `<cwd>/.dsh/storages/`,随 git 提交分享)。'
|
|
35
|
+
+ '**何时写入**(不必刻意找机会,遇到下面这些就动手):'
|
|
36
|
+
+ '① 用户表达长期偏好或约定(「以后都…」「记住…」「我们的规矩是…」);'
|
|
37
|
+
+ '② 用户纠正了你的做法或理解——把正确结论记下来;'
|
|
38
|
+
+ '③ 得出跨会话仍然有用的结论、路径或坑,而非本次任务的临时细节。'
|
|
39
|
+
+ '**不要记**:临时状态、未经验证的推测、能从代码或文档直接读到的事实(那类记「去哪查」即可)。'
|
|
40
|
+
+ '**怎么写**:一条一件事,并给多角度关键词(同义词、缩写、中英),否则将来检索不到它。'
|
|
41
|
+
+ '**锚点(可选)**:若这条记忆描述的是「随某个环境值变化的事实」(某工具版本下的行为、某环境变量决定的配置),用 anchor 参数把它绑到那个值上——值变了它会自动失效并标 stale,不会再被当成仍然正确;绑错了用 memory_update 改(当场重新判定,成立即复活),不要了就传 `anchor: null` 清除。当前锚点随记录一起返回。'
|
|
42
|
+
+ '写入即生效(`approved`),人工只在例外时介入;命中密钥/凭据规则的写入会被隔离(不进注入与检索)。'
|
|
43
|
+
+ '**常驻注入**:每轮注入「global + 当前会话工作区」里 approved 且开着常驻开关的记忆单行摘要(形如 `- [memory:id:namespace] 摘要`);全文要靠 memory_search 取。'
|
|
44
|
+
+ '常驻开关由人(或模型显式传 `injected`)决定:**自动改变注入状态的规则已全部停用**——命中次数不再自动升常驻(它只喂体检报告的候选提示),30 天未命中的自动撤下也已改为**只计量不生效**(撤下来就基本不会自己回去,那种动作不该由一个没有分布依据的阈值决定)。**给 memory_save / memory_update 传 `injected` 即显式接管**。'
|
|
45
|
+
+ '**钉的判据是「失效可察觉性」**:这条记忆若没被想起来,你会不会根本意识不到自己漏了它?会 → 钉常驻(规则 / 委托 / 判据);不会 → 留检索(事实 / 参考 / 案例)。'
|
|
46
|
+
+ '**注入有预算**(默认 1500 字符,约装 11 条),装不下的整条丢弃、按**最近更新**优先取舍(不是最近被检索——检索是「需要时才用」的行为,拿它决定「谁每轮在场」会让查得多的挤掉钉得牢的)——所以「钉了常驻」只等于「有资格排队」。'
|
|
47
|
+
+ '装不下时注入末尾会出现一行 `(另有 N 条常驻因预算未注入:…短摘要…)`——**那是「有常驻记忆并不在场」的唯一信号**,看到它就该逐条评估撤下。'
|
|
48
|
+
+ '发现某条记忆与实际不符时用 memory_update 修正(保持生效);确认没用的用 memory_forget。'
|
|
49
|
+
+ '(English: dsh-memory provides cross-session memory. Writes take effect immediately; save durable preferences, corrections and reusable conclusions — not transient state, guesses, or facts readable from the repo (record where to look instead). Always add multi-angle keywords. Pass `injected: true` to keep one resident every turn — reserve that for rules and standing agreements; facts stay search-only. Resident injection runs on a budget, so a pinned memory is only queued, never guaranteed to be in context.)');
|
|
50
|
+
}
|
|
51
|
+
/** 能力全貌(面板 / 远端视图用;同步未启用时也展示它的存在)。 */
|
|
52
|
+
export const SELF_DESCRIPTION = buildDescription(true);
|
|
53
|
+
/** 按当前是否启用同步生成自述——系统提示那一路用这个。 */
|
|
54
|
+
export function buildSelfDescription(syncEnabled) {
|
|
55
|
+
return buildDescription(syncEnabled);
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max-null/dsh-memory",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.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": {
|