@modusensus/dsh-mneme 0.3.8 → 0.4.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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
6
6
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
7
  [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
8
- [![tests](https://img.shields.io/badge/tests-443%20passed-success)](https://github.com/modusensus/dsh-mneme)
8
+ [![tests](https://img.shields.io/badge/tests-450%20passed-success)](https://github.com/modusensus/dsh-mneme)
9
9
 
10
10
  > 给 DeepSeek Harness 的跨会话记忆插件:让 Agent 记住你、记住项目、自动整理记忆。**Mneme**(Μνήμη)——希腊记忆女神 Mnemosyne 之名,掌管记忆与梦境,正如 autoDream 在后台巩固记忆。
11
11
 
@@ -18,6 +18,11 @@
18
18
  - **SQLite 主存储**:`~/.dsh/memory/memory.db`,`node:sqlite` 内置,零原生依赖
19
19
  - **Markdown 镜像**:`preferences.md` / `projects.md` / `decisions.md` / `history.md` / `summary.md`,人类可读、可手工编辑(**人工修改优先**合并回库)
20
20
  - **4+1 种记忆类型**:`preference`(偏好)/ `project`(项目)/ `decision`(决策)/ `history`(历史)/ `summary`(总览)
21
+ - **镜像同步状态机(v0.3.6+)**:mirror 与主库强一致,用 `generation`(期望轮次)/ `applied_generation`(已应用轮次)建模同步债务
22
+ - 业务写操作在**自身事务内原子递增** desired generation——崩溃在 COMMIT 后、渲染前,重启也能凭 durable 债务恢复,绝不静默跳过(v0.3.8)
23
+ - `generation` 用 SQLite 原子语句递增,多进程并发零丢失;带 `CHECK` 上界,负数/溢出拒绝
24
+ - 逐 type 记录 `committed / failed / pending` 回执,健康端点区分 `ok / degraded / unknown`
25
+ - 状态写失败不静默:同步失败落日志并留债务,重启自动收敛
21
26
 
22
27
  ### 模型工具(7 个)
23
28
 
@@ -49,6 +54,22 @@
49
54
  - **Fail-safe**:非法 LLM 输出(未知 id / 非法 action / 跨类型合并 / 越界 importance)拒绝整单,绝不破坏记忆库
50
55
  - **裁决审计**:每次运行写入 `dream_runs` 审计表(输入快照 sha256 digest + 完整输入快照 + 决策清单 + 逐 id 去向 + receipt),可离线回放;merge / conflict / update 幂等应用,重放/并发重复执行无累积副作用;update 记录 `_before` 快照
51
56
 
57
+ ### Sleep Mode 系统级睡眠 💤(v0.4.0,opt-in)
58
+
59
+ 从 autoDream 的"被动阈值触发"升级为"主动定时维护 + 分层压缩"。系统空闲 `sleepIdleMinutes` 分钟自动执行深度维护,**默认关闭**(`sleepModeEnabled: false`),开启后行为:
60
+
61
+ - **可中断**:AbortController 实现,用户恢复活动即中止当前周期(`noteWrite` 重置空闲计时 + 中断信号)
62
+ - **串行安全**:睡眠周期走 `service.enqueue` 串行队列,与 autoDream 严格不重叠;`minRefTimeMs` 防止快照后被召回的记忆被误降级
63
+ - **四阶段深度维护**:
64
+ 1. `conflict_resolution`:全库冲突消解,strictness 三级可配(gentle 0.92 / normal 0.85 / aggressive 0.75)
65
+ 2. `archival_demotion`:按 `last_accessed_at` 分层——30 天未召回压成摘要(原文进 `_full_content`,可无损恢复)、90 天完全归档
66
+ 3. `pattern_discovery`:LLM 扫描近期记忆提炼规律,产出 `type=pattern` 记忆,evidence 强校验防伪造
67
+ 4. `relation_completion`:检测孤立实体并补全隐含关系(共现 `related_to` / 项目 `part_of` / 技术 `depends_on`)
68
+ - **Fail-safe**:每阶段独立 try/catch,LLM 故障只跳过对应阶段;无 LLM 路由时纯规则降级(demotion/relations)照常执行
69
+ - **审计延续**:睡眠周期写入 `dream_runs`,`run_type='sleep'`,与 autoDream 共用审计表可追溯
70
+
71
+ > 配置详见 `docs/SLEEP.md`;迁移说明见 `docs/MIGRATION.md`。
72
+
52
73
  ### Web 记忆面板
53
74
 
54
75
  官方设置面板 → 「记忆库设置」→「记忆」标签:按类型浏览、全文搜索;启用向量搜索后可用「语义」切换做向量召回。
@@ -106,6 +127,31 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
106
127
 
107
128
  > 📖 详见 [实体结构化记忆设计](docs/ENTITIES.md) · [语义增强架构](docs/SEMANTIC.md) · [本地模型部署指南](docs/LOCAL_MODEL.md) · [从 v0.1 升级说明](docs/MIGRATION.md)
108
129
 
130
+ ## 🆕 最近版本亮点
131
+
132
+ | 版本 | 亮点 |
133
+ |------|------|
134
+ | **v0.4.0** | 系统级睡眠 Sleep Mode:空闲触发的四阶段深度维护(冲突消解 / 归档降级 / 模式发现 / 关系补全),可中断、串行安全、fail-safe,分层压缩释放冷记忆;471 测试全绿 |
135
+ | **v0.3.9** | 修复第三方审计 4 项 FAIL:CAS 同事务原子化、Mirror 降级回执透传、逐 type 物理终态收敛、Generation 强整数校验与并发初始化稳定化 |
136
+ | **v0.3.8** | audit peer 复验 6 项运行时阻断全部修复:desired generation 同事务原子递增(崩溃窗口不再静默跳过)、同步失败不静默、原子 generation 增量(多进程零丢失)、逐 type committed/failed/pending 回执、读取失败显式 unknown、generation 上界/负数 CHECK |
137
+ | **v0.3.7** | 启动竞态修复:人工编辑 md 镜像后重启向量重建失败(回灌移入 init 就绪后 + scheduleEmbed 就绪门) |
138
+ | **v0.3.6** | mirror 同步状态机:generation/applied_generation 债务建模、F-NEW-03 mirror 健康状态、持久 dirty + 启动 recoverMirror |
139
+ | **v0.3.0** | 记忆基因:实体/属性/关系三表 + 时间轴 + 实体搜索 + autoDream supersedes |
140
+
141
+ ## 🗺️ 进化路线图
142
+
143
+ | 版本 | 状态 | 主题 | 说明 |
144
+ |------|------|------|------|
145
+ | v0.2.x | ✅ 完成 | 语义增强 + 反思更新 | 本地 Embedding/Rerank/聚类、`failure_memories` 失败追踪 |
146
+ | v0.3.0 | ✅ 完成 | 记忆基因 | entities/attrs/relations 三表 + 时间轴 + 实体搜索 |
147
+ | v0.3.6–0.3.8 | ✅ 完成 | 镜像一致性 + 审计加固 | generation 同步状态机、audit peer 6 项运行时阻断修复、450 测试全绿 |
148
+ | v0.3.9 | ✅ 完成 | 审计加固 A/B/D/F | compareAndUpdate 同事务原子性、degraded 回执、逐 type 物理终态、整数 fail-closed、并发初始化稳定 |
149
+ | **v0.4.0** | 🚧 开发中 | 系统级睡眠 Sleep Mode | 空闲触发的四阶段深度维护(冲突消解 / 归档降级 / 模式发现 / 关系补全)、分层压缩、可中断串行 fail-safe;471 测试全绿 |
150
+ | **v0.4.1** | 📋 规划 | 反思性成长 | 纠错双向回流(改记忆同时反思"为什么记错/召回错")+ 规则演进(从 failure 提炼规律注入系统提示)+ 自适应参数 |
151
+ | **v0.5.0+** | 🚀 远期 | 自进化记忆 | 兴趣漂移跟踪 + 跨 workspace 记忆共享(等 DSH 支持) |
152
+
153
+ > 新能力一律做成**可开关的功能**(配置启用/关闭),默认保守开启、不破坏现有行为。`failure_memories` 表与 autoDream 决策引擎已为后续反思性成长铺好路。
154
+
109
155
  ## 📦 安装
110
156
 
111
157
  ### 前置条件
@@ -243,7 +289,7 @@ src/
243
289
  lib/
244
290
  ├── client.js # Web 面板(手写 ModuleLoader bundle)
245
291
  └── *.js # src 的同步分发产物
246
- test/ # 443 个 node:test 测试(含审计与三轴线压测不变量)
292
+ test/ # 450 个 node:test 测试(含审计与三轴线压测不变量)
247
293
  scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步
248
294
  ```
249
295
 
@@ -252,7 +298,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
252
298
  ```bash
253
299
  cd dsh-mneme
254
300
  npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
255
- npm test # 运行 443 个测试
301
+ npm test # 运行 450 个测试
256
302
  npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
257
303
  npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
258
304
  ```
package/lib/api.js CHANGED
@@ -354,7 +354,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
354
354
  });
355
355
 
356
356
  return {
357
- routes: 7,
357
+ routes: 9,
358
358
  dispose: () => {
359
359
  for (const dispose of disposers) dispose();
360
360
  }
package/lib/config.js CHANGED
@@ -91,4 +91,42 @@ export const Config = z.object({
91
91
  entityExtractionMaxAttrs: z.natural().min(1).max(50).default(20),
92
92
  // Prefix/semantic search over entity names (used by recall).
93
93
  entitySearchEnabled: z.boolean().default(true),
94
+
95
+ // --- sleep mode: idle-triggered deep maintenance (v0.4.0) ---------------
96
+ // Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight)
97
+ // sleep fires when the store has been quiet for sleepIdleMinutes and deep-
98
+ // maintains the whole library: conflict resolution, archival demotion,
99
+ // pattern discovery and entity relation completion. Abortable on user
100
+ // activity, audited into dream_runs (run_type='sleep'), and serialized with
101
+ // autoDream so the two never overlap.
102
+ sleepModeEnabled: z.boolean().default(false),
103
+ // Quiet window before a cycle fires (minutes).
104
+ sleepIdleMinutes: z.natural().min(1).max(60).default(5),
105
+ // Minimum gap between two sleep runs (hours) — a second idle window within
106
+ // this interval does not retrigger.
107
+ sleepMinIntervalHours: z.natural().min(1).max(168).default(8),
108
+ // Conflict adjudication strictness:
109
+ // gentle only high-confidence conflicts (threshold 0.92) are resolved
110
+ // normal standard dream-level (threshold 0.85)
111
+ // aggressive low-confidence pairs are also adjudicated (threshold 0.75)
112
+ sleepConflictStrictness: z.union([
113
+ z.const("gentle"),
114
+ z.const("normal"),
115
+ z.const("aggressive")
116
+ ]).default("normal"),
117
+ // Archival demotion tiering (days since last access):
118
+ // >= sleepArchiveDays → shrink to summary, full body kept in _full_content
119
+ // >= sleepCompressDays → archived outright (entity relations preserved)
120
+ sleepArchiveDays: z.natural().min(7).max(365).default(30),
121
+ sleepCompressDays: z.natural().min(7).max(365).default(90),
122
+ // Pattern discovery scan window (most recent memories to scan).
123
+ sleepPatternMinMemories: z.natural().min(10).max(1000).default(100),
124
+ // How far back pattern discovery considers entity attr changes (days).
125
+ sleepPatternLookbackDays: z.natural().min(1).max(90).default(30),
126
+ // Max pattern memories minted per run (0 = disabled).
127
+ sleepMaxPatternPerRun: z.natural().min(0).max(10).default(3),
128
+ // Optional LLM route override for sleep's bulk passes (empty = use dream
129
+ // route / agent default model).
130
+ sleepProvider: z.string().default(""),
131
+ sleepModel: z.string().default(""),
94
132
  });
@@ -1,4 +1,4 @@
1
- const ACTIONS = new Set(["keep", "merge", "archive", "conflict", "update"]);
1
+ const ACTIONS = new Set(["keep", "merge", "archive", "conflict", "update", "create"]);
2
2
 
3
3
  /**
4
4
  * Validate a dream decision list against a snapshot of eligible memories.
@@ -26,6 +26,25 @@ export function validateDecisions(decisions, snapshot, options = {}) {
26
26
  errors.push(`${at}: conflict needs distinct winner and loser`);
27
27
  continue;
28
28
  }
29
+ } else if (d.action === "create") {
30
+ // Mint a fresh memory (sleep pattern discovery). Claims no existing id,
31
+ // so it skips the claiming loop below; evidence is optional provenance
32
+ // (already filtered to real ids by the caller) and is stored in content.
33
+ if (typeof d.title !== "string" || !d.title.trim()) {
34
+ errors.push(`${at}: create needs non-empty title`);
35
+ continue;
36
+ }
37
+ if (typeof d.content !== "string" || !d.content.trim()) {
38
+ errors.push(`${at}: create needs non-empty content`);
39
+ continue;
40
+ }
41
+ if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
42
+ errors.push(`${at}: create importance must be an integer 1-5 when provided`);
43
+ }
44
+ if (typeof d.type !== "string" || !d.type.trim()) {
45
+ errors.push(`${at}: create needs non-empty type`);
46
+ }
47
+ continue;
29
48
  } else if (!Array.isArray(d.ids) || d.ids.length === 0) {
30
49
  errors.push(`${at}: ${d.action} needs non-empty ids`);
31
50
  continue;
@@ -95,6 +114,12 @@ export function validateDecisions(decisions, snapshot, options = {}) {
95
114
  if (updateCount > maxUpdatePerRun) {
96
115
  errors.push(`too many update decisions: ${updateCount} > ${maxUpdatePerRun}`);
97
116
  }
117
+ // Cap pattern minting per run (sleepMaxPatternPerRun passes through here).
118
+ const createCount = decisions.filter((d) => d.action === "create").length;
119
+ const maxCreatePerRun = options.maxCreatePerRun ?? 5;
120
+ if (createCount > maxCreatePerRun) {
121
+ errors.push(`too many create decisions: ${createCount} > ${maxCreatePerRun}`);
122
+ }
98
123
  // Every snapshot id must appear in at least one decision
99
124
  for (const id of snapshot.keys()) {
100
125
  if (!claimed.has(id)) errors.push(`memory ${JSON.stringify(id)} missing from decisions`);
@@ -202,10 +227,33 @@ function applyOne(d, service, snapshot, config = {}) {
202
227
  case "archive": return applyArchive(d, service, snapshot);
203
228
  case "merge": return applyMerge(d, service, snapshot, config);
204
229
  case "conflict": return applyConflict(d, service, snapshot);
230
+ case "create": return applyCreate(d, service, config);
205
231
  default: return applyUpdate(d, service, snapshot, config);
206
232
  }
207
233
  }
208
234
 
235
+ /**
236
+ * Mint a fresh memory (pattern discovery). No existing target, so no CAS guard.
237
+ * Evidence ids ride in the content so a pattern stays traceable to its source
238
+ * memories. saveWithDedupe dedupes identical mints (idempotent replay-safe).
239
+ */
240
+ function applyCreate(d, service, config = {}) {
241
+ const title = String(d.title ?? "").trim();
242
+ const content = String(d.content ?? "").trim();
243
+ const importance = Number.isInteger(d.importance) ? d.importance : 3;
244
+ const type = typeof d.type === "string" ? d.type : "pattern";
245
+ const evidence = Array.isArray(d.evidence)
246
+ ? d.evidence.filter((id) => typeof id === "string")
247
+ : [];
248
+ const body = evidence.length > 0
249
+ ? `${content}\n\n[证据: ${evidence.join(", ")}]`
250
+ : content;
251
+ const created = service.saveWithDedupe({ type, title, content: body, importance });
252
+ const memory = created?.memory;
253
+ if (!memory) return "skipped"; // deduped/subsumed: nothing minted, clean no-op
254
+ return { applied: 1, committed: { action: "create", id: memory.id, type } };
255
+ }
256
+
209
257
  function applyArchive(d, service, snapshot) {
210
258
  const targets = d.ids.filter((id) => {
211
259
  const mem = service.getById(id);