@max-null/dsh-memory 0.6.1 → 0.7.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/dist/engine.d.ts CHANGED
@@ -6,6 +6,9 @@
6
6
  */
7
7
  import { Service } from '@deepseek-ai/cordis';
8
8
  import { type PromptFile } from './prompt-files.ts';
9
+ import type { AnchorProbes, MemoryAnchor } from './anchors.ts';
10
+ import type { MemorySource, SourceStat } from './query-log.ts';
11
+ export type { MemorySource, QueryLogEntry, SourceStat } from './query-log.ts';
9
12
  declare const memoryIdBrand: unique symbol;
10
13
  /** Opaque identity of one stored memory record. */
11
14
  export type MemoryId = string & {
@@ -15,8 +18,9 @@ export type MemoryId = string & {
15
18
  export declare function MemoryId(id: string): MemoryId;
16
19
  export type MemoryNamespace = 'global' | 'project';
17
20
  /**
18
- * 审核维度(0.3.0):`suggested` 待审核(模型写入);`approved` 已人工
19
- * 审核通过。注入与否由独立维度 `injected` 控制(见 MemoryRecord)。
21
+ * 审核维度(0.3.0):`suggested` 未生效;`approved` 已生效。2026-09-15 起写入默认
22
+ * `approved`(静默机制),`suggested` 主要留给被隔离的记录与人工回退。
23
+ * 注入与否由独立维度 `injected` 控制(见 MemoryRecord)。
20
24
  */
21
25
  export type MemoryStatus = 'suggested' | 'approved';
22
26
  /**
@@ -46,6 +50,28 @@ export interface MemoryRecord {
46
50
  status: MemoryStatus;
47
51
  /** 注入维度:true = 每轮全量注入 system prompt(常驻);false = 仅检索。 */
48
52
  injected: boolean;
53
+ /**
54
+ * 隔离位(2026-09-15):命中危险内容规则 → 不进注入、不进检索,等人工放行或删除。
55
+ * 与 `status` 双保险:隔离记录同时保持 `suggested`,即使某条路径漏了过滤也不会注入。
56
+ */
57
+ quarantined: boolean;
58
+ /** 隔离原因(命中的规则名,诊断与面板展示用)。 */
59
+ quarantineReason?: string;
60
+ /** 命中次数(2026-09-15 淘汰机制):被 memory_search 命中的累计次数。 */
61
+ hitCount: number;
62
+ /**
63
+ * 写入来源(2026-09-15 反例 5):'agent' = 主模型写入,'human' = 人工写入;
64
+ * 缺省 = 旧记录(来源未知,不猜测)。影响力统计按它聚合(见 sourceStats)。
65
+ */
66
+ source?: MemorySource;
67
+ /** 注入由信号自动开启(true)还是人工设置(缺省);只有自动开的会被「长期未命中」自动降级。 */
68
+ injectedAuto?: boolean;
69
+ /** 有效性锚点(2026-09-15):绑定到某个可探测的环境值;值变了即失效。 */
70
+ anchor?: MemoryAnchor;
71
+ /** 锚点已失效(由会话启动时的校验写入);失效不删除,只降权并在检索结果里标注。 */
72
+ stale?: boolean;
73
+ /** 失效原因(锚点名 + 声明值 vs 当前值,诊断用)。 */
74
+ staleReason?: string;
49
75
  content: string;
50
76
  keywords: string[];
51
77
  createdAt: number;
@@ -61,11 +87,20 @@ export interface MemoryWrite {
61
87
  content: string;
62
88
  namespace?: MemoryNamespace;
63
89
  keywords?: string[];
90
+ /** 写入来源(2026-09-15);缺省 'agent'(主模型写入)。面板等人工路径传 'human'。 */
91
+ source?: MemorySource;
92
+ /**
93
+ * 有效性锚点(可选,2026-09-15):把这条记忆绑定到某个可探测的环境值上,值变了即自动
94
+ * 失效。只用于「随环境变化的事实」(某工具版本下的行为、某环境变量决定的配置)。
95
+ */
96
+ anchor?: MemoryAnchor;
64
97
  }
65
98
  export interface MemoryFilter {
66
99
  namespace?: MemoryNamespace;
67
100
  status?: MemoryStatus;
68
101
  injected?: boolean;
102
+ /** 隔离过滤(2026-09-15):缺省排除隔离记录;传 true 只取隔离记录(面板审用)。 */
103
+ quarantined?: boolean;
69
104
  /** 记录类别过滤(0.6.0);缺省不过滤(工具层显式传 kind='fact' 保持旧行为)。 */
70
105
  kind?: MemoryKind;
71
106
  }
@@ -73,6 +108,36 @@ export interface MemoryHit {
73
108
  record: MemoryRecord;
74
109
  score: number;
75
110
  }
111
+ /** 反查候选({@link MemoryEngine.suggestKeywordGaps}):一条「本应命中」的现存记忆。 */
112
+ export interface KeywordGapCandidate {
113
+ id: string;
114
+ /** 正文摘要({@link GAP_EXCERPT_CHARS} 字符以内)。 */
115
+ excerpt: string;
116
+ /** 现有关键词;补写时在其基础上追加。 */
117
+ keywords: string[];
118
+ /** 精确词元命中数:> 0 表示「现在重搜就该命中」。 */
119
+ exact: number;
120
+ /** 前缀近似命中的查询词元(如 vue3 ↔ vue)。 */
121
+ relaxed: string[];
122
+ }
123
+ /** 一个空命中查询的反查结果;`candidates` 为空即知识缺口。 */
124
+ export interface KeywordGap {
125
+ query: string;
126
+ /** 该查询在日志里空命中的次数。 */
127
+ count: number;
128
+ /** 最近一次出现的时间。 */
129
+ lastAt: number;
130
+ candidates: KeywordGapCandidate[];
131
+ }
132
+ /** {@link MemoryEngine.suggestKeywordGaps} 的报告(只产出报告,不自动补写)。 */
133
+ export interface KeywordGapReport {
134
+ /** 查询日志总条数。 */
135
+ logSize: number;
136
+ /** 空命中条数(含被 `limit` 截掉的查询)。 */
137
+ misses: number;
138
+ /** 逐查询的候选(最多 `limit` 个查询,按最近出现排序)。 */
139
+ gaps: KeywordGap[];
140
+ }
76
141
  /** One durable memory change, emitted after the backend acknowledges the write. */
77
142
  export type MemoryChange = {
78
143
  operation: 'remembered';
@@ -88,6 +153,10 @@ export type MemoryChange = {
88
153
  operation: 'injected';
89
154
  id: MemoryId;
90
155
  injected: boolean;
156
+ } | {
157
+ operation: 'quarantined';
158
+ id: MemoryId;
159
+ quarantined: boolean;
91
160
  };
92
161
  declare module '@deepseek-ai/cordis' {
93
162
  interface Context {
@@ -130,6 +199,11 @@ export interface MemoryConfig {
130
199
  /** 全局提示词模板根目录(0.6.0);默认 `$DSH_HOME/prompt-library`。
131
200
  * project 模板随工作区(`<workspace>/.dsh/prompt-library`,与记忆同构)。 */
132
201
  promptGlobalRoot?: string;
202
+ /**
203
+ * 查询日志文件路径(2026-09-15 §六.6);默认为全局存储根下的 `query-log.json`
204
+ * ——即与 `memory.json` **同目录**(`$DSH_HOME/storages/query-log.json`),不落子目录。
205
+ */
206
+ queryLogPath?: string;
133
207
  }
134
208
  /**
135
209
  * Cross-session plaintext memory over the storage hub, split by namespace:
@@ -148,6 +222,12 @@ export declare class MemoryEngine extends Service {
148
222
  /** backend 只注册一次(registry 重名抛 duplicate;reload 清缓存后不得重复注册)。 */
149
223
  private registeredProjectBackends;
150
224
  private facility?;
225
+ /** 各存储文件上次被我们确认过的指纹;用于识别「另一个实例写过」。 */
226
+ private readonly storeStamps;
227
+ /** 写入串行链:「刷新—读—改—写」必须整体互斥,否则并发写会在其间穿插。 */
228
+ private writeChain;
229
+ /** 会话启动维护只跑一次的标记(2026-09-15)。 */
230
+ private maintenanceDone;
151
231
  constructor(ctx: import('@deepseek-ai/cordis').Context, config?: MemoryConfig);
152
232
  protected [Service.init](): Promise<void>;
153
233
  /** 打开全局存储域(init 与 reload 共用;backend 复用已注册实例)。 */
@@ -167,10 +247,44 @@ export declare class MemoryEngine extends Service {
167
247
  * (JsonStorageBackend 打开时加载一次,无 watch——2026-08-19 实测)。
168
248
  */
169
249
  reload(): Promise<void>;
170
- /** Create one record in `suggested` status — never self-promoting. */
250
+ /**
251
+ * Create one record. 低危内容直接 `approved`(静默生效,2026-09-15 静默记忆机制);
252
+ * 命中危险内容规则则隔离:`status` 留 `suggested`(天然不注入)**并且**置
253
+ * `quarantined`(不进检索),双保险,等待人工放行或删除。
254
+ */
255
+ /** 文件指纹:mtime + size(size 兜住同一毫秒内的两次写)。 */
256
+ private static fileStamp;
257
+ /** 全局记忆的存储文件(JsonStorageBackend 的 unit 文件名 = domain 名)。 */
258
+ private globalStoreFile;
259
+ /** 某工作区记忆的存储文件。 */
260
+ private projectStoreFile;
261
+ /** 记录当前指纹,作为「这就是我们造成的状态」的基准(写入完成后调用)。 */
262
+ private noteStoreStamps;
263
+ /**
264
+ * 写前新鲜度门:存储文件被外部改过就重载,避免整份覆盖吃掉对方的写入。
265
+ * 必须在取表**之前**调用——{@link reload} 重开 facility,此前取到的 table 引用会失效。
266
+ */
267
+ private refreshForWrite;
268
+ /**
269
+ * 一次写入的完整作用域:串行化 → 新鲜度门 → 执行 → 记指纹。
270
+ * 刷新与「读—改—写」必须同处一个互斥区,否则并发写会在两者之间穿插。
271
+ * @param projectCwd - 本次写入涉及的工作区;无则只覆盖全局存储。
272
+ * @param run - 实际的读改写;必须在内部重新取表(刷新后旧引用失效)。
273
+ * @returns `run` 的返回值。
274
+ */
275
+ private writeScope;
171
276
  remember(input: MemoryWrite, projectCwd?: string): Promise<MemoryRecord>;
277
+ /** {@link remember} 的实现体;必须经 `writeScope` 进入(多实例写入协调)。 */
278
+ private rememberInner;
172
279
  list(filter?: MemoryFilter, projectCwd?: string): Promise<MemoryRecord[]>;
280
+ /**
281
+ * 关键词检索。每次成功返回的检索都记一条查询日志(⑥ 2026-09-15)——命中与否都记:
282
+ * 漏检不产生任何事件,只有日志能事后回答「哪些查询本该命中谁」(反例 4)。日志是旁路:
283
+ * 写失败只记 warn,绝不改变本次检索结果。
284
+ */
173
285
  search(query: string, filter?: MemoryFilter, projectCwd?: string): Promise<MemoryHit[]>;
286
+ /** 检索打分本体(BM25 + 可选语义融合);查询日志由 {@link search} 记录。 */
287
+ private rank;
174
288
  /**
175
289
  * 语义排序(0.5.2):查询向量 + 候选向量的余弦相似度,仅在有
176
290
  * embeddings 配置时被调用。缺失向量(新增/更新过的记录)在此补生成
@@ -178,25 +292,136 @@ export declare class MemoryEngine extends Service {
178
292
  */
179
293
  private semanticRanking;
180
294
  /** 按已知 id 定位记录所在表(global 优先,其次当前工作区表)。 */
295
+ /**
296
+ * 会话启动维护(2026-09-15):一次扫描做完三件事——旧数据迁移、长期未命中的自动降级、
297
+ * 锚点批量校验。只跑一次、幂等、**失败不阻断**(记忆可用性优先于维护完成度)。
298
+ *
299
+ * 调用点在 `ensureProjectOpen`:它是 async,且每会话首次打开工作区表时必经。
300
+ * `memory:recall` 的 provider 是**同步**回调,不能在那里 await。
301
+ *
302
+ * @param projectCwd - 会话工作区:迁移/降级/校验都连它一起处理。
303
+ */
304
+ private runMaintenanceOnce;
305
+ /**
306
+ * 锚点探测上下文。`toolNames` 拿不到时返回 `undefined`——**不能给空数组**:那会被判成
307
+ * 「工具全没了」,让所有 `tool-list` 锚点误失效。
308
+ */
309
+ private anchorProbes;
181
310
  private tableOf;
182
- /** 命中标记:把 lastUsedAt 写回(命中记录不存在时跳过;不 emit 事件——统计性字段)。 */
311
+ /** 命中标记:写回 lastUsedAt 与命中次数,达标即自动升常驻(2026-09-15)。 */
183
312
  private markUsed;
313
+ /** {@link markUsed} 的实现体;必须经 `writeScope` 进入(`recordUse` 在此之内)。 */
314
+ private markUsedInner;
315
+ /**
316
+ * 命中记账 + 自动升级(2026-09-15 静默记忆机制):命中次数达阈值即自动打开常驻
317
+ * 注入——「被反复检索命中」是它值得每轮付费的唯一客观证据。统计性字段不 emit,
318
+ * 只有真的改变了注入状态才发 `memory/changed`。
319
+ */
320
+ private recordUse;
321
+ /** 查询日志(惰性创建;位置见 {@link queryLogPath})。 */
322
+ private queryLogInstance?;
323
+ private queryLog;
324
+ /** 查询日志文件路径:与 `memory.json` 同目录(不落子目录)。 */
325
+ private queryLogPath;
326
+ /**
327
+ * 记录一次检索:查询文本(截断到 120 字符)+ 命中条数 + 时间。**失败绝不影响检索**——
328
+ * 日志是旁路证据,不是检索的一部分。模板库检索不进日志:`prompt_search` 复用同一条
329
+ * `search` 通道,但模板是文件库、不属于「记忆漏检」的观察面。
330
+ *
331
+ * 查询文本是新的明文落盘点,因此先过危险内容硬拦(与写入路径同一套规则):命中就只记
332
+ * 规则名、不留原文——凭据不该因为「只是被搜了一下」而进另一个明文文件。
333
+ */
334
+ private recordQuery;
335
+ /**
336
+ * 影响力统计(⑥ / 反例 5):按来源聚合「写入量 / 被检索命中数 / 常驻注入数」。
337
+ * 只算 fact 记录(模板索引不是记忆),隔离记录不计(与 list 的默认口径一致:
338
+ * 它们不在流通里)。「被人工删除数」暂缺的原因见 `summarizeSources` 的注释。
339
+ */
340
+ sourceStats(projectCwd?: string): Promise<SourceStat[]>;
341
+ /**
342
+ * 低频反查(⑥,**只产出报告,不自动改写**):拿「命中 0 的查询」与既有记忆反查,
343
+ * 给出「本应命中却没命中」的候选,供人工或模型补 keywords。
344
+ *
345
+ * 不做自动补写:判断「这条记忆是否真该被那个查询找到」需要 LLM 语义判断,插件只提供
346
+ * 证据(候选为空即知识缺口——「这类知识你还没有」本身也是有用信息)。调用时机由人
347
+ * 或模型定,不在检索路径上、不产生每轮成本。
348
+ *
349
+ * @param projectCwd - 会话工作区;候选库连工作区记忆一起取。
350
+ * @param limit - 最多反查几个空命中查询(按最近出现排序)。
351
+ */
352
+ suggestKeywordGaps(projectCwd?: string, limit?: number): Promise<KeywordGapReport>;
353
+ /**
354
+ * 长期未命中的自动降级(2026-09-15 淘汰机制):**只撤自动开的常驻注入**——人工开
355
+ * 的开关不会被时间悄悄关掉(那是人的决定,不是信号的结论)。判定用 lastUsedAt,
356
+ * 从未命中过则回退 updatedAt。会话启动时调一次即可(见 index.ts 的预热路径)。
357
+ *
358
+ * @param projectCwd - 会话工作区;给了就连工作区表一起扫。
359
+ * @param now - 当前时间(测试注入用)。
360
+ * @returns 被撤下的条数(诊断与日志用)。
361
+ */
362
+ demoteStale(projectCwd?: string, now?: number): Promise<number>;
363
+ /** {@link demoteStale} 的实现体;必须经 `writeScope` 进入。 */
364
+ private demoteStaleInner;
365
+ /**
366
+ * 锚点批量校验(2026-09-15):会话启动时调一次,把失效的条目标成 `stale`。
367
+ *
368
+ * 失效同时**撤掉常驻注入**——内容可能已经错了,继续每轮注入就是在误导(反例 2 的
369
+ * 教训);但**不删除、也不排除检索**,它在结果里带标注,等模型核对后回写。
370
+ * 探测不到(`undefined`)按「未校验」处理,不动任何状态。
371
+ *
372
+ * @param probes - 探测上下文(工具清单 / 自身版本 / env 读取器)。
373
+ * @param projectCwd - 会话工作区;给了就连工作区表一起校验。
374
+ * @returns 本次状态发生变化的条数。
375
+ */
376
+ verifyAnchors(probes: AnchorProbes, projectCwd?: string): Promise<number>;
377
+ /** {@link verifyAnchors} 的实现体;必须经 `writeScope` 进入。 */
378
+ private verifyAnchorsInner;
379
+ /**
380
+ * 旧数据迁移(2026-09-15 静默记忆机制):一次性、幂等,会话启动维护时调用。
381
+ *
382
+ * 旧语义里 `suggested` 是「等人工审核」,新语义里它只剩「隔离」与「人工回退」两种含义——
383
+ * 不迁移的话那些记录会永久卡住:既不注入,也没人去放行。迁移逐条过危险内容检测:
384
+ * 安全则升 `approved`(写入即生效),命中则标 `quarantined` 进隔离(**必须先检测再放行**:
385
+ * 历史条目若含密钥,直接放行等于让它开始进注入)。
386
+ *
387
+ * 另外把「有 `lastUsedAt` 但无 `hitCount`」的老记录回填 `hitCount = 1`——它们可能早已被
388
+ * 反复用过,不该从零开始攒两次命中。旧 `injected: true` 且无 `injectedAuto` 的一律不动:
389
+ * 那是人工决定,不该被自动降级。
390
+ *
391
+ * @param projectCwd - 会话工作区;给了就连工作区表一起迁移。
392
+ * @returns 迁移统计(放行 / 隔离 / 回填计数)。
393
+ */
394
+ migrateLegacy(projectCwd?: string): Promise<{
395
+ approved: number;
396
+ quarantined: number;
397
+ counted: number;
398
+ }>;
399
+ /** {@link migrateLegacy} 的实现体;必须经 `writeScope` 进入。 */
400
+ private migrateLegacyInner;
184
401
  forget(id: MemoryId, projectCwd?: string): Promise<boolean>;
402
+ /** {@link forget} 的实现体;必须经 `writeScope` 进入。 */
403
+ private forgetInner;
185
404
  setStatus(id: MemoryId, status: MemoryStatus, projectCwd?: string): Promise<MemoryRecord>;
405
+ /** {@link setStatus} 的实现体;必须经 `writeScope` 进入。 */
406
+ private setStatusInner;
186
407
  /**
187
408
  * 注入维度开关(0.3.0):只改 injected,不动审核状态。供 UI 面板
188
409
  * 「常驻注入」开关调用(remote.setInjected)。
189
410
  */
190
411
  setInjected(id: MemoryId, injected: boolean, projectCwd?: string): Promise<MemoryRecord>;
412
+ /** {@link setInjected} 的实现体;必须经 `writeScope` 进入。 */
413
+ private setInjectedInner;
191
414
  /**
192
- * 修改一条记忆的内容/关键词(0.3.1 整理记忆用)。内容被模型改动后
193
- * 必须重新人工审核:status 重置为 suggested(自然停止注入——注入仅对
194
- * approved 生效);injected 保留原值(审核通过后注入开关原样恢复)。
415
+ * 修改一条记忆的内容/关键词(0.3.1 整理记忆用)。2026-09-15 起:改动**不再**
416
+ * 退回待审核——静默机制下记忆由系统与模型自行维护,更新即保持原有审核状态;
417
+ * `injected` 保留原值。若新内容命中危险规则则转为隔离(与写入路径同一判定)。
195
418
  */
196
419
  update(id: MemoryId, patch: {
197
420
  content?: string;
198
421
  keywords?: string[];
199
422
  }, projectCwd?: string): Promise<MemoryRecord>;
423
+ /** {@link update} 的实现体;必须经 `writeScope` 进入。 */
424
+ private updateInner;
200
425
  /**
201
426
  * 预热某工作区的 project 表(异步打开并缓存)。system prompt 的
202
427
  * `memory:recall` provider 是同步回调——只读已打开的表;预热让
@@ -253,4 +478,3 @@ export declare class MemoryEngine extends Service {
253
478
  /** 删除模板:md 文件与索引一并删除;未命中返回 false。 */
254
479
  promptRemove(idOrName: string, projectCwd?: string): Promise<boolean>;
255
480
  }
256
- export {};