dsh-memory-ledger 0.0.0-stage → 1.0.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/docs/DETAIL.md ADDED
@@ -0,0 +1,151 @@
1
+ # 记忆册(dsh-memory-ledger)
2
+
3
+ 一个本地、可读、可手改的长期记忆台账,外加一个左侧栏面板。它解决两件在长会话之外反复花掉 token 的事:
4
+
5
+ 1. **用户设定每次都要重说**:身份、笔名、署名、语言、技术栈、格式偏好。
6
+ 2. **同一个坑反复踩**:删错目录、同一命令在同一环境下失败、同一工具用错参数——每次都要重新诊断一遍。
7
+
8
+ ## 工作原理
9
+
10
+ 插件在每一次提示词组装时读取台账,渲染成**两段**提示词注入系统提示词:
11
+
12
+ 1. **必须遵守的用户设定**(高优先级,`dsh-memory:constraints`)——只包含「硬约束」:置顶(pinned)的设定,以及 `key` 在 `constraintKeys` 名单里的设定(默认 `pen_name,signature`)。写成命令式、只放标题不放正文,紧跟人格段之后,明确声明「与它冲突的默认署名/风格/模板一律作废」。
13
+ 2. **长期记忆**(`dsh-memory:ledger`)——用户设定明细 + 已验证经验 + 重复失败预警。
14
+
15
+ 同一件事为什么出现两次:约束段是**防止被忽略**(很短、命令式、位置最靠前),记忆段是**提供上下文**(含正文、成因、解法)。约束段只放标题,所以正文不会重复计费。
16
+
17
+ 台账由两部分数据构成:
18
+
19
+ - **fact(设定)**:长期有效的稳定事实,例如「用户是作者」「笔名是墨白,写作类产出统一署名」。
20
+ - **lesson(经验)**:已经验证成功的问题→解法,例如「删除目录前先用 Resolve-Path 打印目标绝对路径,再 Remove-Item -Recurse」。
21
+
22
+ ## 两级作用域:用户 / 本项目
23
+
24
+ | 作用域 | 文件 | 适合放什么 |
25
+ | --- | --- | --- |
26
+ | `user` | `$DSH_HOME/memory/memory.json` | 跟着人走的东西:身份、笔名、语言偏好、通用工作流 |
27
+ | `project` | `$DSH_HOME/memory/projects/<目录名>-<hash>.json` | 只属于某个工作目录的东西:本仓的技术栈坑、本项目的排版约定 |
28
+
29
+ 文件名的 hash 取自工作目录绝对路径(Windows 下先转小写),所以两个不同目录不会互相污染,同一目录无论从哪开会话都落在同一份台账上。
30
+
31
+ - `memory_save` 默认:`fact` 进 user,`lesson` 进 project;可用 `scope="user"` / `scope="project"` 显式指定。
32
+ - `memory_search` 默认同时搜 user + 当前项目;`scope="all"` 搜所有项目。
33
+ - 注入段落里项目条目会标注「(本项目)」,模型知道哪些只在当前目录成立。
34
+
35
+ ## 检索:BM25 + TF-IDF 向量(混合)
36
+
37
+ `memory_search` 与面板搜索共用同一套排序,三种模式:
38
+
39
+ | 模式 | 打分 | 行为 |
40
+ | --- | --- | --- |
41
+ | `hybrid`(默认) | 0.6 × 向量余弦 + 0.4 × BM25(归一化) | 关键词命中 + 近义描述都能召回 |
42
+ | `keyword` | BM25 + 精确子串加分 | 要求查询里每个词都出现,结果可预测 |
43
+ | `vector` | TF-IDF 稀疏向量余弦 | 纯向量空间模型,不要求字面重合 |
44
+
45
+ 实现要点(都是经典 IR 做法,没有黑箱):
46
+
47
+ - **分词**:ASCII 词 + 中文相邻字二元组(bigram)+ 单个汉字串兜底。中文不需要分词器。
48
+ - **字段加权**:标题与 key 各重复三份、标签两份、正文一份,再建语料统计——标题命中远比正文里蹭到一个词重要。
49
+ - **停用词**:中英文功能词表先剔除(「一个」「怎么」「the」「and」……),否则极小语料里 IDF 不起作用。
50
+ - **跨库统一语料**:user 与 project 的候选条目放进同一个语料建索引再排名,所以两边的分数可比较;不会出现「每个文件各自归一化、跨文件排序失真」。
51
+ - 索引是**每次查询临时构建**的(几十到几百条都很快),不落盘、不占用启动时间。
52
+
53
+ ## 左侧栏面板
54
+
55
+ 插件在 root 作用域的 `sidebar.panellist` 注册一个图标行(id `dsh-memory`),并在 keyed slot `main` 注册同 id 的面板作为主区域内容——这是侧栏「按条目 id 切换主面板」的约定。面板里可以:
56
+
57
+ - 看到用户台账与所有项目台账的条目、统计、待处理失败预警;
58
+ - 用搜索框做混合检索(可切换 关键词 / 向量);
59
+ - 按范围过滤(用户 / 本项目 / 全部项目);
60
+ - **逐条编辑**:点「编辑」就地改标题、类型、槽位键、标签、正文、**记录原因**与置顶,保存后立即写回所属台账文件,并马上反映到下一轮提示词(不需要重启,也不需要重新问 agent);
61
+ - **归档前确认**:点「归档」不会立刻执行,而是展开一行「确认归档这条?」+说明(归档不删除,只是不再进入提示词)+「确认归档 / 取消」两个按钮,避免误点;
62
+ - **可恢复**:勾上工具栏的「显示已归档」,归档条目会带「已归档」标记出现,右侧按钮变成「恢复」,一键放回提示词。
63
+
64
+ ### 每条记忆都带「为什么记」与「来源项目」
65
+
66
+ 两个字段在**写入时自动产生**,面板里每行都会显示:
67
+
68
+ | 字段 | 含义 | 谁写入 |
69
+ | --- | --- | --- |
70
+ | `reason` | 记录原因,一行短文本(如「用户在对话中明确指定」「任务中验证后写入」) | `memory_save` 的 `reason` 参数;agent 不传时按类型给默认值;面板里可随时改 |
71
+ | `origin` | 来源项目:`{ cwd, label, sessionId, at }`——第一次被记录时所在的工作目录、会话与时间 | 宿主自动从会话上下文取得,面板里只读 |
72
+
73
+ `origin` 记录的是**第一次**记录的位置:之后在别的项目里改这条记忆,原因可以更新,但来源项目不会被改写(谁最早记的、在哪个项目记的,这条历史保持真实)。
74
+
75
+ 数据走 Host 自己的路由(默认前缀 `/dsh-memory/api/v1`):
76
+
77
+ - `GET <prefix>/ledger?q=&mode=&scope=&cwd=&archived=1`
78
+ - `POST <prefix>/update` body `{ id, title?, body?, key?, tags?, pinned?, kind?, reason?, archived? }`
79
+ - `POST <prefix>/forget` body `{ id, hard? }`
80
+
81
+ 编辑是**部分更新**:只改请求里出现的字段,其余字段原样保留;标题为空、类型非法会返回 400,未知 id 返回 404。编辑不改变条目所属作用域(要换作用域请用 `memory_save` 带 `scope` 重新写入)。
82
+
83
+ 路由有同源闸口:`Host` 必须指向回环地址、拒绝 `sec-fetch-site: cross-site`、带 `Origin` 时要求与 `Host` 一致。它没有接入 Host 自身的登录闸口(那需要一个未公开的请求准入契约),所以**在反向代理/隧道暴露 dsh web 的部署里,这条路由只受同源检查保护**;只是本机使用时不受影响。
84
+
85
+ ## 三个模型可见的工具
86
+
87
+ | 工具 | 作用 |
88
+ | --- | --- |
89
+ | `memory_save` | 写入或更新一条记忆,附带 `reason`(为什么记);同一 `key` 再保存会**覆盖旧值**,这是用户改设定后保持同步的方式 |
90
+ | `memory_search` | 混合检索(标题/键/标签/正文),结果带来源项目与记录原因 |
91
+ | `memory_forget` | 按 id 归档(默认)或彻底删除,自动定位所在作用域 |
92
+
93
+ ## 何时自动同步
94
+
95
+ - 段落文本**每次组装现算**,模型在同一会话里保存记忆后,下一步就能看到新内容。
96
+ - 用户直接编辑台账文件也会被识别(按修改时间重新读取)。
97
+ - 用户改设定时,模型按注入段落里的维护约定用同一 `key` 覆盖,而不是再堆一条。
98
+
99
+ ## 主动提醒:没记下来的设定
100
+
101
+ 光靠提示词里的「维护约定」,agent 仍可能忘了写。所以插件在回合边界做一道**零模型开销**的检查:
102
+
103
+ 1. 用户消息进入回合时(`agent/inbox/claimed`),用一组中英文规则判断它像不像「稳定设定」(身份、笔名、署名、语言、技术栈、格式偏好、以后/统一/不要…)。纯提问不会触发。
104
+ 2. 回合即将结束时(`agent/turn-stopping`),如果这一轮**没有成功的 `memory_save`**,就用 `agent.inject()` 往会话里排一条提醒,内容是那句话的引用 + 「如果它确实是稳定设定,现在调用 memory_save 并在 reason 里写明原因;如果是临时要求就忽略」。
105
+ 3. 一条待办只提醒一次;保存成功后待办清空,不会反复唠叨。
106
+
107
+ 因为是**规则判断**(正则),它不花任何 token,代价是:可能误报(多一条 30 字左右的提醒,无害),也可能漏报(换了完全没见过的说法)。
108
+
109
+ 提醒通过 `agent.inject()` 在回合结束时排队,实测会**让 agent 多走一个回合**去执行保存(这也是它起作用的方式),所以它省下的不是「零成本」,而是「不用你手动再提醒一次」。嫌吵可以把 `remindUnrecorded` 设为 `false`。
110
+
111
+ ## 重复失败自动统计
112
+
113
+ 监听 `tools/result`:失败的调用归一化成稳定**签名**(十六进制 id、数字、路径、引号内容替换成占位符)后计数。同一签名累计到阈值(默认 2 次)就出现在提示词的「重复失败预警」里,模型据此先查台账再决定是否重试。
114
+
115
+ 保存一条 `lesson` 时,若正文或标签里出现了该失败工具的名字,对应预警标记为已处理。失败统计落在该会话的工作目录台账里(与经验同作用域)。
116
+
117
+ ## 配置项
118
+
119
+ 在 profile 层按 id `dsh-memory-ledger` 覆盖(覆盖整块替换 config,需重述所有键):
120
+
121
+ | 键 | 默认 | 说明 |
122
+ | --- | --- | --- |
123
+ | `dataDir` | `''` | 台账目录;空表示 `$DSH_HOME/memory` |
124
+ | `projectMemory` | `true` | 是否按工作目录分库;关掉则只有一份全局台账 |
125
+ | `maxPromptChars` | `1600` | 注入段落硬字数上限 |
126
+ | `maxLessonsInPrompt` | `8` | 段落里最多展示多少条经验 |
127
+ | `searchMode` | `hybrid` | 默认检索模式 |
128
+ | `captureFailures` | `true` | 是否机械统计失败 |
129
+ | `incidentThreshold` | `2` | 同一签名出现几次后进入预警 |
130
+ | `maxIncidents` | `40` | 每份台账最多保留多少条签名 |
131
+ | `incidentMaxAgeDays` | `60` | 超过天数的签名丢弃 |
132
+ | `sectionOrder` | `100` | 段落排序位置 |
133
+ | `includeUsageHint` | `true` | 是否附加维护约定那一行 |
134
+ | `constraintKeys` | `pen_name,signature` | 哪些 `key` 的设定升级成硬约束(置顶的设定自动算) |
135
+ | `maxConstraintChars` | `300` | 硬约束段落的字数上限 |
136
+ | `remindUnrecorded` | `true` | 回合结束时若发现「像设定但没记录」,排一条提醒 |
137
+ | `ui` | `true` | 是否注册面板路由与左侧栏入口 |
138
+ | `routePrefix` | `/dsh-memory/api/v1` | 面板路由前缀 |
139
+
140
+ ## 斜杠命令
141
+
142
+ `/memory` 列出用户与当前项目台账;`/memory search <关键词>` 检索;`/memory forget <id>` **只提示确认**,要真正归档需再输入 `/memory forget <id> --yes`。
143
+
144
+ ## 已知限制(请如实对待)
145
+
146
+ - 「向量检索」是 **TF-IDF 稀疏向量**(经典向量空间模型),**不是神经语义嵌入**。它靠字面与字二元组重合工作:完全换一套说法、没有任何共字的问题可能召回不到。要真正的语义嵌入,需要接一个 embedding 提供方,本插件目前不联网、不需要任何 API key。
147
+ - 中文按二元组切分,对长句里单个字的改写不敏感;停用词表是通用中英文清单,可能误删极少数有意义的词。
148
+ - 台账是明文 JSON,不加密:**不要存密钥、令牌、证件号**。
149
+ - 项目作用域以**工作目录绝对路径**为键:同一仓库换一个路径(比如换个盘符或 clone 到别处)就是另一份台账。
150
+ - 面板通过 Host 路由取数,路由只做同源检查(见上文),不接入 Host 登录闸口。
151
+ - 同时启用 OpenViking 记忆插件时,两套记忆会各自注入一段内容。
package/icon.svg ADDED
@@ -0,0 +1,17 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Memory ledger">
2
+ <defs>
3
+ <linearGradient id="board" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#2f6df6"/>
5
+ <stop offset="1" stop-color="#16309b"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect x="3" y="3" width="58" height="58" rx="14" fill="url(#board)"/>
9
+ <rect x="14" y="12" width="30" height="38" rx="5" fill="#ffffff" opacity="0.28"/>
10
+ <rect x="19" y="16" width="30" height="38" rx="5" fill="#ffffff"/>
11
+ <rect x="25" y="24" width="18" height="3" rx="1.5" fill="#2f6df6"/>
12
+ <rect x="25" y="32" width="13" height="3" rx="1.5" fill="#8fa9f2"/>
13
+ <rect x="25" y="40" width="16" height="3" rx="1.5" fill="#8fa9f2"/>
14
+ <path d="M43 16h6v16l-3-3-3 3z" fill="#f6b73c"/>
15
+ <circle cx="20" cy="52" r="5" fill="#f6b73c"/>
16
+ <path d="M17.8 52l1.6 1.7 3-3.4" fill="none" stroke="#16309b" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
17
+ </svg>