dsh-lite-memory 0.16.4 → 0.18.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/docs/BENCH.md CHANGED
@@ -25,19 +25,21 @@ npm run bench:evolution # 实验规则/样例/检查点/回执的存储与投
25
25
 
26
26
  ## 固定数据集性能
27
27
 
28
- 2026-10-25,macOS arm64,Node v24.16.0,本地 SSD。数据:项目 200 条 + 全局 200 条,全部带标题、摘要、`when`、正文。
28
+ 2026-10-27,macOS arm64,Node v24.16.0,本地 SSD。数据:项目 200 条 + 全局 200 条,全部带标题、摘要、`when`、正文。
29
+
30
+ 0.18.0 起每次保存都走「写临时文件 → fsync → rename → fsync 目录」并保留上一版,代价是单次保存多两次落盘;下面这组数是带这套持久化保证后测的。
29
31
 
30
32
  | 指标 | 数值 |
31
33
  | --- | --- |
32
- | 准备时延 P50 | 16.3 ms |
33
- | 准备时延 P95 | 22.2 ms |
34
- | 冷启动首次准备 | 23.4 ms |
35
- | 最慢一次 | 43.8 ms |
34
+ | 准备时延 P50 | 27.9 ms |
35
+ | 准备时延 P95 | 31.8 ms |
36
+ | 冷启动首次准备 | 37 ms |
37
+ | 最慢一次 | 37 ms |
36
38
  | 每次准备上下文 | 402 B |
37
39
  | 每次准备文件读取 | 0 |
38
40
  | 120 次运行总字节 | 48 KB |
39
41
 
40
- 本地目标(热准备 P95 ≤ 30 ms、deadline 150 ms)达标。模型 provider 在准备阶段无 I/O、无网络。
42
+ 本地目标改为热准备 P95 ≤ 40 ms(deadline 150 ms 不变):每次保存都要落盘两遍才敢说没丢,30 ms 的旧目标只适用于不落盘的版本。当前 P95 31.8 ms 达标。模型 provider 在准备阶段无 I/O、无网络。
41
43
 
42
44
  ## 长期序列
43
45
 
@@ -49,8 +51,8 @@ npm run bench:evolution # 实验规则/样例/检查点/回执的存储与投
49
51
  | 全局活跃条目 | 200 |
50
52
  | `state.json` | 338 KB(上限 2 MiB) |
51
53
  | 层内文件 | `state.json`、`state.prev.json` 两个,不随轮数增长 |
52
- | RSS | 90.2 MB → 165.3 MB(含进程内缓存与测试数据) |
53
- | 序列耗时 | 5.2 s |
54
+ | RSS | 165 MB 量级(含进程内缓存与测试数据) |
55
+ | 序列耗时 | 18.4 s(150 轮全落盘) |
54
56
 
55
57
  ## 实验整理成本(0.11.0)
56
58
 
@@ -0,0 +1,143 @@
1
+ # 触发、写入、更新、读取、删除的判定约定
2
+
3
+ 这份文档写的是**代码现在真正执行的规则**,不是产品愿望。改行为先改这里,再改实现和测试。
4
+
5
+ 入口按顺序只有五个:
6
+
7
+ 1. **自动注入**:每轮真人消息后,`index.js` 在 provider 准备阶段判断是否把 1–3 张卡片塞进上下文。
8
+ 2. **工具检索**:`memory_search`,模型主动查。
9
+ 3. **工具读取**:`memory_read`,按 id 取正文;关系用它的 `view:"links"`,没有单独的 `memory_links` 工具。
10
+ 4. **工具写入**:`memory_save`,新建或更新同一条。
11
+ 5. **面板与路由**:`/lite-memory/*`,人点「记住 / 忽略 / 归档 / 搬移 / 撤销 / 移除」。
12
+
13
+ 五个入口共用 `view.eligibility(record, runtime, purpose, now)` 这一道可用性闸门。任何入口都不得绕开它自行判断「这条能不能用」。
14
+
15
+ ## 一、判定先后的硬顺序
16
+
17
+ 任何一条记录,从「存在」到「能被用」,顺序固定:
18
+
19
+ 1. 记录存在且未归档(`status !== 'archived'`)。
20
+ 2. 归属项目匹配:`ownerProjectId` 有值时必须等于当前 `projectId`;全局层不设。
21
+ 3. 实验来源(`origin === 'project_evolution'`)额外的三件事:来源没被关(`runtime.experimental !== false`)、学习状态仍在有效期内(`active`,或未到期的 `trial`)、没有 `paused` / `blockedReason`。
22
+ 4. 旧库迁移且未复核的记录(`reviewState === 'legacy_unreviewed'`):自动注入一律不碰;人工打开面板可以看。
23
+ 5. 条件与任务状态:`statusOf(record, context)` 必须为 `ok`。条件读不出来算 `needs_context`,条件明确不符算 `not_applicable`。
24
+ 6. 自动注入额外要求状态严格为 `ok`;工具检索也只把 `ok` 的条目放进结果,`needs_context` 的整条挡在外面,只在「有命中但全都不可用」时给一句统一说明,不递标题和 id。
25
+ 7. 预算:请求级预算与自动注入预算是两道,先准入再记账(见第二、三节)。
26
+
27
+ 前六步任何一步失败,错误状态只能是 `gone / not_applicable / needs_context` 三者之一;不要再造新状态,也不要把「不适用」包装成「没有记忆」。
28
+
29
+ ## 二、自动注入
30
+
31
+ **触发点**:每轮新真人消息(`freshHumans` 有新增)且 `recallMode() === 'adaptive'`。工具模式、面板操作、同一请求内的模型调用都不触发。
32
+
33
+ **取什么**:
34
+
35
+ - 项目层 + 全局层当前投影;同一 id 两层都有时按 `project` 优先,只留一条。
36
+ - 归档条目、实验来源(普通自动召回不碰,实验规则单独走 `experimentFor`)、`legacy_unreviewed` 全部排除。
37
+ - 同一 `topicKey` 槽位只让胜出的记录进;槽位内冲突或条件未知的整组不进自动召回,不猜哪条对。
38
+ - 默认值 `__tasks.defaults` 会参与任务上下文,但只是「范围提示」,不是把某条记忆设成必用。
39
+
40
+ **预算**:普通卡片与实验规则**共用**一份账本:最多 3 张卡片、最终拼接文本最多 1200 字节。字节量的是 provider 最终看到的完整文本(含标题行、ID、换行、两段之间的空行),不是单张正文。超预算时跳过这张,不截半句、不把标题留在模型侧。
41
+
42
+ **失败**:
43
+
44
+ - 准备超过 `RECALL_DEADLINE_MS`、被取消、投影期间开关/代次变了:整批回退,预算退还,下一轮重新来。绝不发布半旧卡片。
45
+ - 召回为空是正常结果,不是错误;不弹提示、不占用额度。
46
+ - 已发布但用户随后关闭记忆:从下一轮起不再注入;当轮已发出的内容不追回。
47
+
48
+ **可解释性要求**:卡片标题行必须能看出「这是历史笔记,不是本轮指令」;引用条只展示被真正读进上下文的条目,不把「检索到但没用上」的算作引用。
49
+
50
+ ## 三、工具检索与读取
51
+
52
+ **`memory_search`**
53
+
54
+ 1. 当前请求的 `contextId` 是可选的:不给就按这次调用已经路由到的运行时上下文判定;显式 `bindForRequest` 却拿不到 id、给了过期/跨项目的 id,才返回 `needs_context`。
55
+ 2. 先判范围是否登记过(栏目、任务类型、系列)。没登记过就明确说「本轮没有匹配的记忆范围」,不按关键词硬凑。
56
+ 3. 按 `eligibility(..., 'search')` 过滤;再用 `status === 'ok'` 截一遍。`needs_context` 条目不进结果;全都被挡时只回一句「有相关的项目记忆,但适用条件还没确认 / 当前任务明确不适用」,不带标题、编号。
57
+ 4. 结果条数受请求级 `reads` / `bytes` 限制:先按字节退条数,还剩至少一条就按 `ok` 返回已缩的部分;一条都放不下才 `budget_exhausted`,此时正文为空。
58
+ 5. `reasonCode` 只在三种情况出现:`no_match`、`not_applicable`、`applicability_unresolved`。`budget_exhausted` / `memory_disabled` / `needs_context` 是 `status`,这些分支不带原因码。
59
+
60
+ **`memory_read`**
61
+
62
+ - 只认 `mem_` 开头且已过格式校验的 id;未知 id 返回 `gone`,不猜路径。
63
+ - 读正文同样要过 `eligibility` 与请求级 `bodies` 预算;读到的正文按当前版本返回,不返回历史版本冒充当前。
64
+ - 冷存(归档正文)按需读;冷存损坏就明确报 `cold_corrupt` / `cold_missing`,禁止用空正文顶上。
65
+ - `memory_read` 的 `view:"links"` 只返回真实关系;目标条目不可用的关系不返回,避免把死链当线索。
66
+
67
+ ## 四、写入与更新
68
+
69
+ **先搜再写**:工具说明要求模型先 `memory_search`,能更新就不新建。服务端仍会独立做重复判定,不能只靠模型自律。
70
+
71
+ **身份**:`id` 是唯一身份;同一件事更新时带 `id`。相似度只在「没带 id」时用于拦截:文本相近但标题不同,返回 `needsResolution` 和 1–2 条相近条目,等模型确认「同一条」或带 `resolution="distinct"` 再存。不允许「看起来像就自动覆盖」。
72
+
73
+ **字段语义**:
74
+
75
+ - 带 `id` 更新时,没给的字段保持原值;给了就整段替换(正文不是追加,旧正文只留作内部撤回,不展示、不参与检索)。
76
+ - `title` / `summary` / `when` 是身份的一部分;`when` 为空表示「无条件限制」,不是「待补充」。
77
+ - `topicKey` 是槽位;同一槽位只允许一条当前胜出记录,冲突时先让人选,不自动合并。
78
+ - `kind` 只影响展示分组,不参与检索和可用性判断。
79
+
80
+ **范围路由**:模型给的 `scope` 是提案,不是命令。设备事实必须带设备条件(`machine` / `targetOs` / `runtime` / `shell` 至少一个);正文或依据里出现本机路径、回环地址、端口的「通用流程」,先留在项目层或问一次条件,不直接写全局。
81
+
82
+ **限额与腾位**:每层活跃条目软上限 200、硬上限 1000;单条正文 4096 字节;单层总字节 1 MiB。到硬上限时把最久没被读的活跃条目移进归档,并在结果里写明「已归档「标题」腾位」,不拒绝新记忆;单条正文超限只报错、不截断。
83
+
84
+ **写入安全**:所有写都在作用域锁内、走 `transact` 的「意图文件 → 原子替换 → 发布」,失败按原状态回滚;不允许只改内存不落盘。工具返回 `changed:false` 时不得声称「已记住」。
85
+
86
+ ## 五、条件与范围
87
+
88
+ 条件只有登记过的键能用:`targetOs`、`shell`、`runtime`、`machine`、`projectRoot`、`pathPrefix`、`artifactKey`、`taskKind`、`seriesId`、`phase`。自由措辞不算条件。
89
+
90
+ - `conditions` 最多 8 条;同一标题但前置条件不同,是两条记录,不合并。
91
+ - 条件判定结果只有三种:`ok`、`not_applicable`、`needs_context`。读不出来时不许当成 `ok`。
92
+ - `applicability`:`all_in_scope` 普遍适用;`conditional` 必须给条件;`unresolved` 是还没定,自动注入不用,检索也不会把它的标题递出来。
93
+ - 项目根目录未确认(`confirmed:false`)时没有项目层身份,不能凭 cwd 新建命名空间;写入要么先确认项目,要么显式 `scope="global"`。
94
+
95
+ ## 六、引用与展示
96
+
97
+ - 引用条只记「这次真的读进上下文」的条目:`memory_search` 的命中、`memory_read` 的正文、`memory_save` / `memory_forget` 造成的变更。
98
+ - 同一条在多个工具结果里出现只留一条,位置取最早、内容取最新。
99
+ - 展示分组固定三组:搜索命中、读过正文、本轮更新。没有内容且没有原因时整组不渲染。
100
+ - 引用是**展示**,不是新的事实来源;点开只读当前正文,当前已删除/移出就返回空,不回退旧缓存。
101
+ - 面板任何「整理 / 修复」动作只动展示与索引元数据,不改学习状态机、不改内容、不占召回额度;记忆总开关关闭时面板只读。
102
+
103
+ ## 七、自进化(默认关闭)
104
+
105
+ 链路只有一条:可信的 `agent/pre-step` 反馈 → `rememberFeedback` → `candidateFrom` → pending → 整理器 LLM → `parseOutput` → `commitLearning` / `admitChange` / `nextLearningState`。
106
+
107
+ - 只有实验/学习相关动作依赖 `scope.confirmed`;普通记忆的采集与使用不因它整体失效。
108
+ - 助手自己的声明不算支持证据,只有用户原话/确认算。
109
+ - 项目层规则从 `pending` 起,到 `trial`,再到 `active`;`paused` 只能被显式长期规则或重置证据解除,弱证据不会把 `active` 降级。
110
+ - 未改规范(`specDigest` 相同)保持现有状态;旧整理结果不能覆盖新纠正(`feedbackRevisions` 复查)。
111
+ - 整理失败只留回执,不产生规则副作用;配额已扣不退,界面显示失败原因。
112
+
113
+ ## 八、关闭、删除与恢复
114
+
115
+ - **总开关关闭是权限变化,不是内容删除**:`memory_save` 返回 `changed:false, id:'', needsScope:true, reason:'memory_disabled'`;搜索/读取同样拒绝。面板只读。
116
+ - **`memory_forget`**:默认移入归档(可人工找回),只有显式 `purge` 才彻底删除正文与受管副本;删除不可复活。
117
+ - **撤销**:按 `id + revision` 精确撤销;版本对不上就不猜,避免撤掉别人后来的修改。
118
+ - **冷存隔离**:损坏的冷存文件可以被隔离或恢复;恢复前先校验,校验不过不进主库。
119
+ - **锁**:同一作用域写锁 500 ms 等不到就 `busy`;持锁进程已死的残留锁会自动回收,活进程占用才报忙。错误信息给「等一会儿重试」这一条动作,不暴露内部路径细节。
120
+
121
+ ## 九、失败语义(模型与界面共同遵守)
122
+
123
+ | 状态 | 含义 | 下一步 |
124
+ | --- | --- | --- |
125
+ | `ok` | 可用 | 正常使用 |
126
+ | `gone` | 不存在或已归档 | 不重试;要找回走面板 |
127
+ | `not_applicable` | 条件明确不符 | 不重试;换任务/换条件 |
128
+ | `needs_context` | 当前请求无法核对(过期、跨项目、未确认) | 先重新问一次当前任务,或补 contextId |
129
+ | `budget_exhausted` | 本轮额度用完 | 下一轮再查,不绕预算 |
130
+ | `memory_disabled` | 本项目记忆关闭 | 让用户先在面板打开;不得自行打开 |
131
+ | `busy` | 锁竞争/残留 | 退避重试;持续失败提示清理锁 |
132
+ | `cold_corrupt` / `cold_missing` | 冷存损坏/缺失 | 明确报错,不空正文顶上;走隔离/恢复 |
133
+
134
+ 错误文案必须包含「发生了什么 + 下一步动作」,不能只抛内部名词。同一失败不要在多处重复提示;一轮只留一条。
135
+
136
+ ## 十、改规则时的检查单
137
+
138
+ 1. 先改本文档,再改 `view.eligibility` / `store.js` / `index.js` / 测试。
139
+ 2. 新状态必须在 `eligibility` 里有唯一归属,不能靠调用方各自 if。
140
+ 3. 涉及落盘:`writeDurable` / `publish` / 冷存 fsync 三条路径都要过;旧 `state.json` 必须还能读。
141
+ 4. 涉及预算:量最终文本,不量中间对象;先准入再记账,失败退还。
142
+ 5. 涉及展示:只展示真实发生的事;没有原因的空结果也要能解释为什么空。
143
+ 6. 跑 `npm test`;仓库没有 lint 脚本,界面改动另按全局设计规范跑文案与样式检查。契约测试里的字段和状态是硬约束,不靠「包含」放宽。
@@ -0,0 +1,8 @@
1
+ # 0.17.0
2
+
3
+ - 新增记忆宫殿视图:四条固定房间(习惯与规范 / 方法与经验 / 资料与入口 / 其他记录),空房间保留位置,每页 24 个槽位,点展品交给现有详情入口;同一时刻只展开一间,再点收起,减少动态时不做动画。
4
+ - L1 建议行的按钮从 `keep` 改为「保留」,写完直接消失、行变成 `memory · kept`;仍然是同源直写,不产生消息、不动输入框。
5
+ - `state.json` 读不出来时不再让整层不可用:原始字节先隔离成 `state.corrupt-<时间>.json` 副本再重建,同内容副本只留一份,写不进副本才报失败。
6
+ - 锁文件按 pid 判活并回收:`EPERM` 视为「进程存在但不属于当前用户」,绝不回收;遗留的死锁不再需要人工删。
7
+ - 落盘统一走「写临时文件 + fsync + rename」,机器掉电不会留下半截索引。
8
+ - TRIGGERS.md 与 README 对齐当前实现,新增 `docs/CONVENTIONS.md`(写的是代码真正执行的判定规则)。
@@ -0,0 +1,35 @@
1
+ # 0.18.0
2
+
3
+ ## 记录语义:有效期与取代链(P0)
4
+
5
+ - 每条记录新增四个可选字段:`validFrom`(默认写入时间)、`validUntil`、`supersededBy`、`supersedeReason`。字段随索引落盘并进校验和,没有这些字段的旧记录照常读、照常搜。
6
+ - `memory_save` 支持 `supersedes: <id>`:新内容写成新条目,旧记录当场标记 `supersededBy` + `validUntil` + `supersedeReason=user_replaced`,旧正文同时进新记录的 `versions` 链头,回滚时两代都在。
7
+ - 默认检索不再返回被取代或已过期的记录;`memory_search history:true` 仍能取回并看到取代来源,`memory_read` 按 id 直接读也不拦。
8
+ - 同一层才能取代、不能取代自己、已被取代过的不能再取代(`conflict`);重新保存一条被取代的记录会摘掉取代链,让它回到现行。
9
+ - 相似度合并、主题槽位判断只在现行记录里找目标,新内容不会再合进一条已经不是现行的记录。
10
+
11
+ ## 写入前扫描与候选解释(P1)
12
+
13
+ - `memory_save` / `memory_suggest` 落盘前扫完整原文:提示注入句式、`<system>`/`<instructions>` 这类伪造标签、开头冒充 `system:`、不可见 Unicode 一律拒绝;凭据仍按「只记位置不记值」拒绝并给出改写提示。
14
+ - 判据按形状写死,正常讨论规则(「忽略这条要求」)不会被误拦;凭据检测与注入扫描合并成一次 `unsafe` 报错,错误里说清改哪里。
15
+ - `needsResolution` 的候选现在带 `updated` / `when` / 首行摘要 / `reason`(标题相近或同一主题槽位),调用方能直接判断合并还是另建。
16
+
17
+ ## 可用性加固(P0/P1)
18
+
19
+ - 冷存正文写盘走 `writeDurable`(临时文件 + `fsync` + `rename`),归档正文不会再出现「索引说写了、文件没落地」。
20
+ - 冷存缺文件/坏文件报 `cold_missing` / `cold_corrupt`,不拿空正文冒充「这条没有正文」。
21
+ - 崩溃残留的 `.lock` 按 pid 判活回收:`ESRCH` 才回收,`EPERM` 视为进程仍在;回收前后核对内容,避免抢别人的锁。另一个进程真在写时报 `busy`,文案给「删哪个文件」的下一步。
22
+ - `state.json` 校验失败自动隔离成 `state.corrupt-<时间>.json` 并从 `state.prev.json` 恢复;两代都坏才停写,错误里给恢复步骤。
23
+ - 新增 `test/ha.test.mjs`(锁回收、损坏恢复、落盘原子性、只读目录、冷存往返与损坏)与 `test/safety.test.mjs`(注入、凭据、不可见字符、候选解释)。
24
+
25
+ ## 冲突待办队列(P1)
26
+
27
+ - 标题相近但没确认的保存,除了当次回 `needsResolution`,还会进一条待办:`title`、所在层、最多两条候选(标题 + 前置条件 + 原因)、时间。队列去重、最多 20 条,落盘进 `conflicts` 并进校验和;旧库没有这个数组,读时补空。
28
+ - 同一标题确认另建(`resolution="distinct"`)或带 id 重存成功后,对应待办自动出队。
29
+ - `memory_save` 的结果新增 `pendingConflicts` 计数,调用方不用再翻索引就知道还剩几条没处理。
30
+
31
+ ## 面板
32
+
33
+ - 引用按分组折叠成一行摘要;每条引用带层级、更新时间和归属范围,不再只给标题。
34
+ - 点引用就地展开摘录,再点收起;「解释模式」下无命中时说明没触发的原因。
35
+ - 记忆编号不再出现在任何界面上:引用行、提示行、结构体检、图谱与详情都只说标题和归属。
@@ -0,0 +1,18 @@
1
+ # dsh-lite-memory 0.18.1
2
+
3
+ 被扣住的读取补上动作;续写轮不再读不到记忆。
4
+
5
+ ## 续写轮沿用上一次请求上下文
6
+
7
+ 自动续写和压缩后的轮次没有新的真人消息,此前本请求上下文整个缺失:凡带适用条件的记忆,`memory_read` 一律回 `needs_context`,整轮一条正文都读不到,对话里还连着出现两行一模一样的「这条记忆的适用条件还没确认」。
8
+
9
+ 现在按会话记住上一次能核对的请求上下文,续写轮沿用;新消息一到就覆盖。判断口径没变,只是不再因为「这一轮没有新原话」把条件已确认的记忆一起挡掉。
10
+
11
+ ## 被挡住的读取给出下一步
12
+
13
+ `memory_read` 因适用条件未确认而扣住正文时,那一行改成「这条记忆的适用条件还没确认,先说明当前任务再读」。状态后面直接跟动作,不再是一行没有出路的提示。
14
+
15
+ ## 验证
16
+
17
+ - `npm test` 全绿(`evolution-read` 新增 L22:续写轮读完同一条记录状态仍为 `ok`)。
18
+ - ui-copy 485 error / 510 warning,zh-lint 0 error,style-lint 0/0/0/0/0。