@max-null/dsh-memory 0.9.2 → 0.11.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
@@ -39,8 +39,8 @@ npm install @max-null/dsh-memory
39
39
 
40
40
  ## 提供的服务与工具
41
41
 
42
- - **服务** `ctx.memory`:`remember` / `list` / `search` / `forget` / `setStatus`
43
- - **工具**:`memory_save`、`memory_list`、`memory_search`、`memory_confirm`、`memory_forget`、`memory_update`
42
+ - **服务** `ctx.memory`:`remember` / `list` / `search` / `forget` / `setStatus` / `move`
43
+ - **工具**:`memory_save`、`memory_list`、`memory_search`、`memory_confirm`、`memory_forget`、`memory_update`、`memory_move`
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
 
@@ -55,6 +55,26 @@ npm install @max-null/dsh-memory
55
55
 
56
56
  两个根都可用 config 覆盖(`globalRoot` / `projectRoot`)。`memory_list` / `memory_search` 不带 `namespace` 过滤时会同时查两层。旧版双重前缀文件名(`memory_project_memory_project_<hash>.json`)在打开时自动迁移为规范名。
57
57
 
58
+ ## 跨工作区可见性(0.10.0 / 0.11.0)
59
+
60
+ project 记忆按**会话工作区**分文件存放,所以「工作区」就是可见性的边界。0.10.0 起补了三个方向——针对的是同一个盲区:**不知道存在**(检索是有意图的动作,搜不出自己不知道存在的东西)。
61
+
62
+ | 方向 | 机制 | 效果 |
63
+ |---|---|---|
64
+ | 向上 | **祖先链** | 在子目录里开会话能检索到父目录的记忆(反向不行,链是单向向上的),结果带 `..` / `../..` 来源标记 |
65
+ | 向下 | **子项目索引行** | 注入里多一行「本工作区下另有 N 个子项目带记忆:xxx(M 条)——用 memory_search 检索」 |
66
+ | 全局 | **记忆索引行** | 注入里多一行「索引:当前可见 N 条记忆,主题集中在 X(14)、Y(12)…——用 memory_search 检索」 |
67
+
68
+ 三条边界:**写入永远只落当前工作区**(「我在这个项目里记的东西」不该被推理到别处);但**检索得到的记录就改得动**——`memory_update` / `memory_forget` 同样沿链定位;**注入路径只读当前工作区**(远处的记忆进检索、不进每轮成本)。
69
+
70
+ **0.11.0 起,「放错了层」有了显式的修正通道**:`memory_move(id, to)` 把一条记忆移到 `global`、`self`(当前工作区)或相对祖先(`..` / `../..`)。它是**例外动作**——默认写入仍然只落当前工作区,只有当你判定「这条其实属于上一层、或上一层的那一支」时才动它。移动**保持 id**、**从源层删除**(不是复制),并报告移动后源层是否仍看得见它:向上移动源层不失明(继承只向上,`global` 人人可见),向下移动则会让源层及其下层从此看不见它。典型用途是**把放高了的记忆降回它真正属于的工作区**——比如只属于某一个生态的规矩躺在 `global` 里、漏进了无关项目。判据与边界见设计文档 §3.2。
71
+
72
+ 后两行**不占注入预算**、自消除(没有内容时整行不出现),且**长度常数级**——不随记忆增长。索引行里的主题取自各条的关键词词频,与检索用的是同一套词汇,所以**索引里出现的词就是能搜到的词**。
73
+
74
+ **孤儿文件清点**:`node scripts/scan-orphans.mjs [根目录]` 报出「哪些记忆文件当前代码已经打不开」(历史命名遗留:早期无哈希后缀、双重 `memory_project_` 前缀)。**只读,不做任何处置**——旧文件可能含未过隔离检查的内容,处置需人工拍板。
75
+
76
+ **祖先链诊断与探针**:`node scripts/ancestor-probe.mjs plan [--cwd <会话 cwd>]` 只读地复算该 cwd 的祖先链(哪些层级带记忆文件、引擎会纳入几级),并回答「这个 cwd 上祖先链是否**可观测**」——若以上祖先层都没有记忆文件,祖先链生效与否行为逐字相同,那是个零差异的观测点。要实测检索侧跨链,用 `seed` / `check` / `clean` 在祖先层造一条临时探针再撤销。**前提**:探针只对**没打开过该层**的会话有效——引擎的表打开即缓存,而写路径的新鲜度门(`refreshForWrite`)只比对 global 与当前 cwd,**不含祖先层**,被缓存过的层会用陈旧内存态覆盖掉外部写入。
77
+
58
78
  ## 多实例共存(0.7.1)
59
79
 
60
80
  两个 DSH 实例(例如 DSH web 与 SSiD 桌面壳)可以同时运行、共用同一个 `DSH_HOME`,**记忆不再互相抹掉**。
@@ -127,6 +147,9 @@ npm run typecheck # tsc 严格类型检查
127
147
  npm test # vitest 单测
128
148
  npm run build # 产出 dist/
129
149
  node scripts/verify-loader.mjs # 用 Loader 端到端验证插件可加载
150
+ node scripts/scan-orphans.mjs [根目录] # 清点「当前代码打不开」的记忆文件(只读)
151
+ node scripts/unzstd-frames.mjs <源> <目标> # 解多帧 zstd(读 DSH 会话日志用)
152
+ node scripts/ancestor-probe.mjs plan [--cwd <cwd>] # 复算祖先链(只读);seed/check/clean 验 ④-A
130
153
  ```
131
154
 
132
155
  ## 依赖(peerDependencies,由宿主提供)
package/dist/engine.d.ts CHANGED
@@ -82,6 +82,11 @@ export interface MemoryRecord {
82
82
  kind?: MemoryKind;
83
83
  /** prompt 记录元数据(仅 kind==='prompt')。 */
84
84
  meta?: PromptMetaIndex;
85
+ /**
86
+ * 来源工作区(④-A 2026-09-19):仅在跨工作区召回时出现——`undefined` = 当前会话
87
+ * 工作区,`..` / `../..` = 上级工作区。检索结果与注入行据此标出来源,避免「记忆串味」。
88
+ */
89
+ scope?: string;
85
90
  }
86
91
  export interface MemoryWrite {
87
92
  content: string;
@@ -173,6 +178,11 @@ export type MemoryChange = {
173
178
  operation: 'quarantined';
174
179
  id: MemoryId;
175
180
  quarantined: boolean;
181
+ } | {
182
+ operation: 'moved';
183
+ id: MemoryId;
184
+ from: string;
185
+ to: string;
176
186
  };
177
187
  declare module '@deepseek-ai/cordis' {
178
188
  interface Context {
@@ -234,6 +244,10 @@ export declare class MemoryEngine extends Service {
234
244
  private globalTable?;
235
245
  /** project 域按工作区 cwd 懒打开 + 缓存(多会话并发各工作区独立)。 */
236
246
  private projectTables;
247
+ /** 子项目发现缓存(④-B 2026-09-19):key = 工作区路径;值 = 其直接子目录中带记忆的(按条数降序)。 */
248
+ private readonly neighborCache;
249
+ /** 进行中的子项目发现(key → promise):预热是 fire-and-forget,之后的显式调用与它合流而非重复扫描。 */
250
+ private readonly neighborOpenings;
237
251
  /** 进行中的工作区打开(key → promise):并发调用复用同一次,见 {@link projectTableFor}。 */
238
252
  private readonly projectOpenings;
239
253
  private projectFacilities;
@@ -295,6 +309,75 @@ export declare class MemoryEngine extends Service {
295
309
  private globalStoreFile;
296
310
  /** 某工作区记忆的存储文件。 */
297
311
  private projectStoreFile;
312
+ /**
313
+ * 工作区链(④-A 2026-09-19):当前 cwd 起向上,收集**记忆文件已存在**的层级。
314
+ *
315
+ * 两条约束:
316
+ * ① **自身始终纳入**(depth 0)——即使文件还不存在,也保持既有行为(`projectTableFor`
317
+ * 会创建它,会话预热路径同样如此)。
318
+ * ② **祖先只收文件已存在的**——对不存在的层级打开表会让 JsonStorageBackend **创建**空
319
+ * 文件,等于在用户每个祖先目录里留一个空记忆文件。
320
+ *
321
+ * `depth` 是真实的上级层数(每向上一步 +1,无论该级是否有文件),供来源标记渲染成
322
+ * `..` / `../..`。路径一律先 `join()` 规范化再算哈希——`projectBackendName` 对字符串
323
+ * 敏感,`H:\a\b` 与 `H:/a/b` 会得出**不同**的文件名(设计文档 §2.3)。
324
+ */
325
+ private projectChain;
326
+ /**
327
+ * 打开工作区链上的表(由近及远)。复用 {@link projectTableFor} 的缓存与并发去重——
328
+ * 链上每级各自一张表,重复调用不会重复打开。
329
+ */
330
+ private projectChainTables;
331
+ /** 来源标记:depth 0(当前工作区)不带标记;祖先渲染为 `..` / `../..`。 */
332
+ private static scopeLabel;
333
+ /**
334
+ * 子项目发现(④-B 2026-09-19):扫描 cwd 的**直接子目录**(不递归——实测一层即覆盖全部
335
+ * 实际情况:19 个子目录里 3 个带记忆),找出带记忆文件、且其中有非隔离记录的。
336
+ *
337
+ * 结果缓存进 {@link neighborCache}——注入 provider 是同步的,只能读缓存;扫描本身在这个
338
+ * 异步方法里做,由 `ensureProjectOpen` 预热路径触发。
339
+ *
340
+ * 跳过点开头的目录(`.dsh` / `.git` 等不是子项目);只报条数、不搬运内容——索引行的用途
341
+ * 是「告诉你去搜」,不是把子项目的记忆变成当前会话的持续成本。
342
+ */
343
+ discoverNeighbors(projectCwd: string): Promise<Array<{
344
+ name: string;
345
+ count: number;
346
+ }>>;
347
+ /** {@link discoverNeighbors} 的实际扫描;并发去重由调用方负责。 */
348
+ private scanNeighbors;
349
+ /**
350
+ * 同步读取已发现的子项目(④-B):注入 provider 是同步回调,只能读缓存。
351
+ * 缓存由 {@link discoverNeighbors} 在预热路径填充;未跑过时返回空数组——该轮索引行
352
+ * 缺席、下一轮补上,与工作区表预热同一套取舍。
353
+ */
354
+ neighborsOf(projectCwd?: string): Array<{
355
+ name: string;
356
+ count: number;
357
+ }>;
358
+ /**
359
+ * 同步读取索引候选(① 2026-09-19):global + 工作区链上**已打开**的表里的全部记录。
360
+ *
361
+ * 与 {@link recallRecords} 的分工:后者只取 `injected` 的(进注入行),前者取全部
362
+ * (供索引行统计规模与主题)。同样是同步只读已缓存的表——注入 provider 是同步回调,
363
+ * 未打开的层级本轮不计入,影响可忽略(链上祖先在检索路径打开后即缓存)。
364
+ */
365
+ indexCandidates(projectCwd?: string): MemoryRecord[];
366
+ /**
367
+ * 按 id 定位记录所在表:global 优先,其次工作区链(自身 → 祖先,由近及远)。
368
+ * **异步版**——写路径必须用它:`writeScope` 可能在本次进入前刚 `reload()`,
369
+ * 那时同步缓存已被清空(同 {@link markUsedInner} 的注释)。
370
+ */
371
+ private locateRecord;
372
+ /**
373
+ * 写入协调覆盖的存储文件:global + **全部已打开的工作区表** + 本次 cwd。
374
+ *
375
+ * 记录({@link noteStoreStamps})与比对({@link refreshForWrite})必须共用这一个定义——
376
+ * 两边各写一份正是漂移的来源:在此之前比对侧只列 global + cwd,而 ④-A 把**读**路径扩到了
377
+ * 祖先链(`projectChain`),于是被缓存过的祖先层「指纹记下了却从不被检查」——外部改动看不见,
378
+ * 下一次写回把它**整份覆盖**。**读扩到哪,门就得跟着扩到哪。**
379
+ */
380
+ private storeScopeFiles;
298
381
  /** 记录当前指纹,作为「这就是我们造成的状态」的基准(写入完成后调用)。 */
299
382
  private noteStoreStamps;
300
383
  /**
@@ -344,6 +427,7 @@ export declare class MemoryEngine extends Service {
344
427
  * 「工具全没了」,让所有 `tool-list` 锚点误失效。
345
428
  */
346
429
  private anchorProbes;
430
+ /** 同步版定位:只查**已打开**的表(global + 链上已缓存的工作区表)。 */
347
431
  private tableOf;
348
432
  /** 命中标记:写回 lastUsedAt 与命中次数,达标即自动升常驻(2026-09-15)。 */
349
433
  private markUsed;
@@ -438,6 +522,34 @@ export declare class MemoryEngine extends Service {
438
522
  forget(id: MemoryId, projectCwd?: string): Promise<boolean>;
439
523
  /** {@link forget} 的实现体;必须经 `writeScope` 进入。 */
440
524
  private forgetInner;
525
+ /**
526
+ * 把一条记忆移到另一层(迭代设计 §3.2)。**两个方向共用一个操作**:向上提到祖先层或
527
+ * `global`,向下把误放高层的降回它真正属于的工作区。
528
+ *
529
+ * 原设计(`memory_promote`)只允许向上,理由是「往下或横向会让原本能看见的人失明」。
530
+ * 但**移动的本意就是改变谁能看见**——那不是副作用,是目的。所以约束从「禁止方向」
531
+ * 换成「让后果可见」:返回值里的 `sourceStillSees` 就是那个后果。
532
+ */
533
+ move(id: MemoryId, to: string, projectCwd?: string): Promise<{
534
+ id: MemoryId;
535
+ from: string;
536
+ to: string;
537
+ sourceStillSees: boolean;
538
+ }>;
539
+ /** {@link move} 的实现体;必须经 `writeScope` 进入。 */
540
+ private moveInner;
541
+ /** 按 id 定位记录**并带上它所在的层**(`locateRecord` 只返回表,而移动必须知道来源层)。 */
542
+ private locateWithKey;
543
+ /**
544
+ * 解析 `memory_move` 的目标:`global`、`self` / `.`(当前 cwd),或相对祖先(`..`、`../..`)。
545
+ *
546
+ * 用**相对深度**而不是绝对路径,是为了和来源标记 `scope`(`..` / `../..`)说同一套话。
547
+ * 允许落在**还没有记忆文件**的祖先层——移动是显式动作,在那儿创建文件正是目的,
548
+ * 不是 `projectChain` 要避免的那种副作用。
549
+ */
550
+ private static resolveTarget;
551
+ /** `ancestor` 是否就是 `node` 本身或它的祖先——用来判断移动后源层是否仍看得见。 */
552
+ private static isAncestorOrSelf;
441
553
  setStatus(id: MemoryId, status: MemoryStatus, projectCwd?: string): Promise<MemoryRecord>;
442
554
  /** {@link setStatus} 的实现体;必须经 `writeScope` 进入。 */
443
555
  private setStatusInner;