dsh-prime-memory 0.20.0-beta.2 → 0.20.0-beta.21

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +192 -3
  2. package/ENGINEERING-NOTES.md +17 -0
  3. package/dist/client.js +1158 -405
  4. package/dist/config.d.ts +0 -120
  5. package/dist/config.js +1 -21
  6. package/dist/contract.d.ts +111 -56
  7. package/dist/hooks/recall.d.ts +4 -1
  8. package/dist/hooks/recall.js +7 -1
  9. package/dist/index.d.ts +0 -78
  10. package/dist/index.js +14 -21
  11. package/dist/metadata-validators.d.ts +5 -0
  12. package/dist/metadata-validators.js +11 -1
  13. package/dist/pipeline/recluster.js +1 -1
  14. package/dist/pipeline/ruminate.d.ts +11 -1
  15. package/dist/pipeline/ruminate.js +18 -1
  16. package/dist/room-review.d.ts +196 -0
  17. package/dist/room-review.js +281 -0
  18. package/dist/stats.d.ts +23 -34
  19. package/dist/stats.js +268 -201
  20. package/dist/store/l1.d.ts +34 -0
  21. package/dist/store/l1.js +68 -1
  22. package/dist/store/recall-usage.d.ts +43 -0
  23. package/dist/store/recall-usage.js +149 -0
  24. package/dist/store/rooms-registry.d.ts +66 -0
  25. package/dist/store/rooms-registry.js +250 -0
  26. package/dist/store/session-modes.d.ts +0 -8
  27. package/dist/store/session-modes.js +0 -52
  28. package/dist/store/sqlite.d.ts +4 -0
  29. package/dist/store/sqlite.js +20 -0
  30. package/dist/tools/index.d.ts +4 -1
  31. package/dist/tools/index.js +232 -1
  32. package/dist/types.d.ts +4 -0
  33. package/dsh.plugin.json +1 -1
  34. package/package.json +1 -1
  35. package/dist/hooks/long-task.d.ts +0 -31
  36. package/dist/hooks/long-task.js +0 -93
  37. package/dist/store/context-usage.d.ts +0 -28
  38. package/dist/store/context-usage.js +0 -47
  39. package/dist/store/todo-ref.d.ts +0 -41
  40. package/dist/store/todo-ref.js +0 -65
@@ -44,14 +44,6 @@ export declare class SessionModeStore {
44
44
  resolvedRecall(sessionId: string, globalRecall: boolean): boolean;
45
45
  /** 设置会话级注入覆盖(undefined = 清除覆盖跟随全局。写穿持久化)。 */
46
46
  setRecall(sessionId: string, recall: boolean | undefined): void;
47
- /** 长任务模式(缺省 = 关)。 */
48
- getLongTask(sessionId: string): boolean;
49
- /** 切换长任务模式(写穿持久化)。 */
50
- setLongTask(sessionId: string, on: boolean): void;
51
- /** 尾部压缩水位线(undefined = 尚未压缩过)。 */
52
- getTailWatermark(sessionId: string): number | undefined;
53
- /** 推进尾部压缩水位线(写穿持久化;只前进不回退)。 */
54
- setTailWatermark(sessionId: string, turn: number): void;
55
47
  /** 会话级域锁定(多选):空数组 = 中心(智能档,无锁域)。 */
56
48
  getWings(sessionId: string): string[];
57
49
  /** 兼容读取(单选口径,取第一个锁定域):undefined = 中心。 */
@@ -92,8 +92,6 @@ export class SessionModeStore {
92
92
  : undefined,
93
93
  hallIncludeUnlabeled: typeof entry.hallIncludeUnlabeled === 'boolean' ? entry.hallIncludeUnlabeled : undefined,
94
94
  hallIncludeGeneral: typeof entry.hallIncludeGeneral === 'boolean' ? entry.hallIncludeGeneral : undefined,
95
- longTask: typeof entry.longTask === 'boolean' ? entry.longTask : undefined,
96
- lastTailCompressedTurn: typeof entry.lastTailCompressedTurn === 'number' ? entry.lastTailCompressedTurn : undefined,
97
95
  updatedAt: entry.updatedAt ?? now,
98
96
  });
99
97
  count++;
@@ -137,52 +135,6 @@ export class SessionModeStore {
137
135
  halls: entry?.halls,
138
136
  hallIncludeUnlabeled: entry?.hallIncludeUnlabeled,
139
137
  hallIncludeGeneral: entry?.hallIncludeGeneral,
140
- longTask: entry?.longTask,
141
- lastTailCompressedTurn: entry?.lastTailCompressedTurn,
142
- updatedAt: Date.now(),
143
- });
144
- this.writeChain = this.writeChain.then(() => this.persist());
145
- }
146
- /** 长任务模式(缺省 = 关)。 */
147
- getLongTask(sessionId) {
148
- return this.entries.get(sessionId)?.longTask ?? false;
149
- }
150
- /** 切换长任务模式(写穿持久化)。 */
151
- setLongTask(sessionId, on) {
152
- const entry = this.entries.get(sessionId);
153
- this.entries.set(sessionId, {
154
- mode: entry?.mode ?? this.loaded,
155
- recall: entry?.recall,
156
- hall: entry?.hall,
157
- halls: entry?.halls,
158
- hallIncludeUnlabeled: entry?.hallIncludeUnlabeled,
159
- hallIncludeGeneral: entry?.hallIncludeGeneral,
160
- longTask: on,
161
- lastTailCompressedTurn: entry?.lastTailCompressedTurn,
162
- updatedAt: Date.now(),
163
- });
164
- this.writeChain = this.writeChain.then(() => this.persist());
165
- }
166
- /** 尾部压缩水位线(undefined = 尚未压缩过)。 */
167
- getTailWatermark(sessionId) {
168
- const t = this.entries.get(sessionId)?.lastTailCompressedTurn;
169
- return typeof t === 'number' && Number.isFinite(t) ? t : undefined;
170
- }
171
- /** 推进尾部压缩水位线(写穿持久化;只前进不回退)。 */
172
- setTailWatermark(sessionId, turn) {
173
- const entry = this.entries.get(sessionId);
174
- const prev = entry?.lastTailCompressedTurn;
175
- if (typeof prev === 'number' && prev >= turn)
176
- return;
177
- this.entries.set(sessionId, {
178
- mode: entry?.mode ?? this.loaded,
179
- recall: entry?.recall,
180
- hall: entry?.hall,
181
- halls: entry?.halls,
182
- hallIncludeUnlabeled: entry?.hallIncludeUnlabeled,
183
- hallIncludeGeneral: entry?.hallIncludeGeneral,
184
- longTask: entry?.longTask,
185
- lastTailCompressedTurn: turn,
186
138
  updatedAt: Date.now(),
187
139
  });
188
140
  this.writeChain = this.writeChain.then(() => this.persist());
@@ -221,8 +173,6 @@ export class SessionModeStore {
221
173
  halls: locked === undefined ? entry?.halls : locked.length > 0 ? locked : undefined,
222
174
  hallIncludeUnlabeled: boundaries?.includeUnlabeled ?? entry?.hallIncludeUnlabeled,
223
175
  hallIncludeGeneral: boundaries?.includeGeneral ?? entry?.hallIncludeGeneral,
224
- longTask: entry?.longTask,
225
- lastTailCompressedTurn: entry?.lastTailCompressedTurn,
226
176
  updatedAt: Date.now(),
227
177
  });
228
178
  this.writeChain = this.writeChain.then(() => this.persist());
@@ -244,8 +194,6 @@ export class SessionModeStore {
244
194
  halls: entry?.halls,
245
195
  hallIncludeUnlabeled: entry?.hallIncludeUnlabeled,
246
196
  hallIncludeGeneral: entry?.hallIncludeGeneral,
247
- longTask: entry?.longTask,
248
- lastTailCompressedTurn: entry?.lastTailCompressedTurn,
249
197
  updatedAt: Date.now(),
250
198
  });
251
199
  this.writeChain = this.writeChain.then(() => this.persist());
@@ -400,6 +400,8 @@ export declare class MemoryDb {
400
400
  halls?: readonly string[];
401
401
  /** Room 过滤(metadata.tags 含该 slug)。 */
402
402
  tag?: string;
403
+ /** 孤儿记忆筛选(true = 只列没有 Room 的记录;与 tag 互斥,tag 优先)。 */
404
+ untagged?: boolean;
403
405
  workspaceId?: string;
404
406
  /**
405
407
  * 退场(软删)筛查:三态。
@@ -445,6 +447,8 @@ export declare class MemoryDb {
445
447
  room: string;
446
448
  count: number;
447
449
  }>;
450
+ /** 孤儿记忆计数:没有任何 Room(tags 为空/缺失)的记录条数(含已退场,与 l1RoomCounts 同口径)。 */
451
+ untaggedL1Count(): number;
448
452
  /** Hall 域计数(八边形角数据源):按 metadata.hall 分组计数 + 未打标行数。失败返回空。 */
449
453
  wingL1Counts(): {
450
454
  counts: Record<string, number>;
@@ -1601,6 +1601,11 @@ export class MemoryDb {
1601
1601
  where.push(`EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(metadata_json) THEN metadata_json ELSE '{}' END, '$.tags') j WHERE j.value = ?)`);
1602
1602
  params.push(opts.tag);
1603
1603
  }
1604
+ else if (opts.untagged) {
1605
+ // 孤儿记忆:没有任何 Room(tags 数组缺失/为空/字段非法)。与 room 计数同一
1606
+ // json_valid 守卫口径;与 l1RoomCounts() 计数互补(rooms 计数 + 孤儿数 = 全量)。
1607
+ where.push(`NOT EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(metadata_json) THEN metadata_json ELSE '{}' END, '$.tags'))`);
1608
+ }
1604
1609
  // 退场筛查(三态,见 opts.retired 注释):与 listRetiredL1 同一判据,保证
1605
1610
  // 「仅退场」视图与「已退场」区看到同一批行。
1606
1611
  if (opts.retired === false) {
@@ -1716,6 +1721,21 @@ export class MemoryDb {
1716
1721
  return [];
1717
1722
  }
1718
1723
  }
1724
+ /** 孤儿记忆计数:没有任何 Room(tags 为空/缺失)的记录条数(含已退场,与 l1RoomCounts 同口径)。 */
1725
+ untaggedL1Count() {
1726
+ if (this.degraded)
1727
+ return 0;
1728
+ try {
1729
+ const row = this.db
1730
+ .prepare(`SELECT COUNT(*) AS n FROM l1_records r
1731
+ WHERE NOT EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(r.metadata_json) THEN r.metadata_json ELSE '{}' END, '$.tags'))`)
1732
+ .get();
1733
+ return Number(row?.n ?? 0);
1734
+ }
1735
+ catch {
1736
+ return 0;
1737
+ }
1738
+ }
1719
1739
  /** Hall 域计数(八边形角数据源):按 metadata.hall 分组计数 + 未打标行数。失败返回空。 */
1720
1740
  wingL1Counts() {
1721
1741
  if (this.degraded)
@@ -22,6 +22,7 @@ import type { PersonaStore } from '../store/persona.js';
22
22
  import type { SceneStore } from '../store/scenes.js';
23
23
  import type { SessionModeStore } from '../store/session-modes.js';
24
24
  import type { MemoryFamily, MemoryLogger } from '../types.js';
25
+ import type { RoomRegistryStore } from '../store/rooms-registry.js';
25
26
  export declare function registerMemoryTools(ctx: Context, cfg: MemoryConfig, stores: {
26
27
  l0: L0Store;
27
28
  l1: L1Store;
@@ -31,4 +32,6 @@ export declare function registerMemoryTools(ctx: Context, cfg: MemoryConfig, sto
31
32
  graph?: GraphStore;
32
33
  }, logger: MemoryLogger, modes: SessionModeStore, live: LiveSettingsHandle,
33
34
  /** 反刍控制器(可选:未装配时 ruminate 工具返回未启用提示)。 */
34
- ruminate?: RuminateController): void;
35
+ ruminate?: RuminateController,
36
+ /** Room 注册表(分类管理 beta.4;缺省 = memory_room_admin 返回未装配提示)。 */
37
+ roomRegistry?: RoomRegistryStore): void;
@@ -3,6 +3,7 @@ import { RECEIPTS_QUERY_LIMIT_MAX, dimensionOf, toReceiptView } from '../store/r
3
3
  import { listConflictPairs, renderConflictResolution, renderConflicts, resolveConflictPair } from '../conflict-service.js';
4
4
  import { WING_CATALOG, WING_FALLBACK, normPersistence, normScope, resolveRecordScope } from '../types.js';
5
5
  import { scopeFilterOf, workspaceIdOf } from '../workspace.js';
6
+ import { annotateOrphanCandidates, confirmReview, mergeRoom, nextReview, pendingReviewCount, renameRoom, skipReview, } from '../room-review.js';
6
7
  import { applyGovernanceWeights } from '../store/governance.js';
7
8
  import { resolveRepoScope } from '../repo-scope.js';
8
9
  import { GRAPH_STATUS_LABELS } from '../prompts/graph-projection.js';
@@ -32,7 +33,9 @@ function applyManualRedaction(record, cfg, logger) {
32
33
  }
33
34
  export function registerMemoryTools(ctx, cfg, stores, logger, modes, live,
34
35
  /** 反刍控制器(可选:未装配时 ruminate 工具返回未启用提示)。 */
35
- ruminate) {
36
+ ruminate,
37
+ /** Room 注册表(分类管理 beta.4;缺省 = memory_room_admin 返回未装配提示)。 */
38
+ roomRegistry) {
36
39
  if (!cfg.tools)
37
40
  return;
38
41
  /**
@@ -182,6 +185,153 @@ ruminate) {
182
185
  };
183
186
  },
184
187
  }));
188
+ // ── memory_room_admin: Room 粒度目录治理(分类管理 beta.4;beta.5 起 merge/rename/retire 已注册) ──
189
+ // list 免预算零风险;merge/rename/retire 走 dryRun 预览→实跑,执行前自动备份,完成入队场景重算。
190
+ ctx.tools.register(defineTool({
191
+ name: 'memory_room_admin',
192
+ description: 'Room 分类管理(目录粒度)。action="list" 列出注册表条目与自生长 Room 计数;' +
193
+ 'action="register" 预注册一个 Room(高权限;两级制——大类上限 200,小类用「大类/小类」细分不占额度,如 dsh-plugin/merge;' +
194
+ 'action="merge" 合并两个 Room(高权限;dryRun 默认 true 只返预览,实跑改写全部匹配记录的 tags 并入队场景重算,执行前自动备份);' +
195
+ 'action="rename" 改名(= merge 1:1 + 注册表改名);action="retire" 退役一个注册 Room(高权限;存量记录不动)。' +
196
+ 'merge/rename 与反刍并发会相互覆盖,请在反刍空闲时执行。',
197
+ parameters: {
198
+ action: { type: 'string', required: true, description: 'list | register(高权限) | merge(高权限) | rename(高权限) | retire(高权限)' },
199
+ slug: { type: 'string', description: 'register 的 Room slug(小写字母数字连字符)' },
200
+ label: { type: 'string', description: 'register 的人类可读名(可选,可中文)' },
201
+ description: { type: 'string', description: 'register 的归类说明(可选;喂给标注器帮助归类)' },
202
+ from: { type: 'string', description: 'merge/rename 的源 Room slug' },
203
+ to: { type: 'string', description: 'merge/rename 的目标 Room slug' },
204
+ dryRun: { type: 'string', description: 'merge/rename:默认 true 只返预览;"false" 才实际执行' },
205
+ },
206
+ output: {
207
+ schema: {
208
+ type: 'object',
209
+ properties: {
210
+ notice: { type: 'string' },
211
+ rooms: { type: 'string' },
212
+ slug: { type: 'string' },
213
+ created: { type: 'boolean' },
214
+ },
215
+ additionalProperties: false,
216
+ },
217
+ render: (_args, value) => [{ type: 'text', text: value.notice ?? JSON.stringify(value) }],
218
+ },
219
+ execute: async (args) => {
220
+ const action = String(args.action ?? '');
221
+ if (action === 'list') {
222
+ if (!roomRegistry)
223
+ return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
224
+ const registry = roomRegistry.list();
225
+ const counts = stores.l1.listRooms();
226
+ const countOf = (slug) => counts.find((r) => r.room === slug)?.count ?? 0;
227
+ const regLines = registry.map((e) => `- ${e.slug}${e.label ? `(${e.label})` : ''} [${e.source}/${e.status}] 现有 ${countOf(e.slug)} 条${e.description ? ` — ${e.description}` : ''}`);
228
+ const grownOnly = counts.filter((r) => !registry.some((e) => e.slug === r.room));
229
+ return {
230
+ notice: `注册表 ${registry.length} 条(上限 200),自生长未注册 ${grownOnly.length} 个。\n` +
231
+ (regLines.join('\n') || '(注册表为空——用 action=register 预注册)') +
232
+ `\n自生长未注册:${grownOnly.map((r) => `${r.room}(${r.count})`).join(", ") || "无"}`,
233
+ rooms: `${registry.length}+${grownOnly.length}`,
234
+ };
235
+ }
236
+ if (action === 'register') {
237
+ if (!roomRegistry)
238
+ return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
239
+ if (!live.get().memoryMutate)
240
+ return { notice: MUTATE_OFF_NOTICE };
241
+ const slug = String(args.slug ?? '').trim();
242
+ try {
243
+ const r = await roomRegistry.register({
244
+ slug,
245
+ label: typeof args.label === 'string' ? args.label : undefined,
246
+ description: typeof args.description === 'string' ? args.description : undefined,
247
+ });
248
+ return {
249
+ notice: r.created
250
+ ? `已预注册 Room:${r.entry.slug}${r.entry.label ? `(${r.entry.label})` : ''}。候选标注器将优先把相关记忆挂到它上面。`
251
+ : `Room 已存在:${r.entry.slug}(本次只补全了缺失的 label/description)。`,
252
+ slug: r.entry.slug,
253
+ created: r.created,
254
+ };
255
+ }
256
+ catch (err) {
257
+ return { notice: `注册失败: ${err instanceof Error ? err.message : String(err)}` };
258
+ }
259
+ }
260
+ if (action === 'update') {
261
+ if (!roomRegistry)
262
+ return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
263
+ if (!live.get().memoryMutate)
264
+ return { notice: MUTATE_OFF_NOTICE };
265
+ const slug = String(args.slug ?? '').trim();
266
+ if (!slug)
267
+ return { notice: 'update 需要 slug。' };
268
+ try {
269
+ const ok = await roomRegistry.update(slug, {
270
+ label: typeof args.label === 'string' ? args.label : undefined,
271
+ description: typeof args.description === 'string' ? args.description : undefined,
272
+ });
273
+ return { notice: ok ? `已更新:${slug}(显示名/归类说明)。` : `无变化:${slug} 未找到或未提供非空字段。`, slug };
274
+ }
275
+ catch (err) {
276
+ return { notice: `更新失败: ${err instanceof Error ? err.message : String(err)}` };
277
+ }
278
+ }
279
+ if (action === 'merge' || action === 'rename') {
280
+ if (!roomRegistry)
281
+ return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
282
+ if (!live.get().memoryMutate)
283
+ return { notice: MUTATE_OFF_NOTICE };
284
+ const from = String(args.from ?? '').trim();
285
+ const to = String(args.to ?? '').trim();
286
+ if (!from || !to)
287
+ return { notice: 'merge/rename 需要 from 与 to 两个 slug。' };
288
+ const dryRun = String(args.dryRun ?? 'true').toLowerCase() !== 'false';
289
+ try {
290
+ const fn = action === 'merge' ? mergeRoom : renameRoom;
291
+ const r = await fn({ l1: stores.l1, registry: roomRegistry, logger }, from, to, { dryRun });
292
+ if (r.dryRun) {
293
+ return {
294
+ notice: `[dryRun 预览] ${from} → ${to}:影响 ${r.affected} 条${r.hasMore ? '(超过单次上限,需续跑)' : ''}。` +
295
+ `样例:${r.preview.join(', ') || '无'}。确认无误后以 dryRun="false" 执行(执行前自动备份,重写含已退场行,完成后自动入队场景重算)。`,
296
+ affected: r.affected,
297
+ };
298
+ }
299
+ // 实跑:受影响场景入队重算(source='room-merge')
300
+ let requeued = 0;
301
+ for (const family of r.families ?? []) {
302
+ for (const scene of r.scenes ?? []) {
303
+ stores.l1.enqueueSceneRecluster(family, [scene], `room-merge:${Date.now()}`, 'room-merge');
304
+ requeued++;
305
+ }
306
+ }
307
+ return {
308
+ notice: `${action === 'merge' ? '合并' : '改名'}完成:${from} → ${to},重写 ${r.applied} 条` +
309
+ `(备份:${r.backupFile ?? '无'})${requeued > 0 ? `;已入队 ${requeued} 个场景重算` : ''}。`,
310
+ applied: r.applied,
311
+ id: to,
312
+ };
313
+ }
314
+ catch (err) {
315
+ return { notice: `${action} 失败: ${err instanceof Error ? err.message : String(err)}` };
316
+ }
317
+ }
318
+ if (action === 'retire') {
319
+ if (!roomRegistry)
320
+ return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
321
+ if (!live.get().memoryMutate)
322
+ return { notice: MUTATE_OFF_NOTICE };
323
+ const slug = String(args.slug ?? '').trim();
324
+ try {
325
+ const ok = await roomRegistry.setStatus(slug, 'retired');
326
+ return { notice: ok ? `已退役 Room:${slug}(存量记录的 tags 不动,仅词表不再推荐)。` : `未找到注册条目:${slug}(自生长 Room 无需退役,会随 tags 自然消失)。`, slug };
327
+ }
328
+ catch (err) {
329
+ return { notice: `退役失败: ${err instanceof Error ? err.message : String(err)}` };
330
+ }
331
+ }
332
+ return { notice: `非法 action:${action}(允许 list/register/merge/rename/retire)。` };
333
+ },
334
+ }));
185
335
  // ── conversation_search: L0 原始对话 ──
186
336
  ctx.tools.register(defineTool({
187
337
  name: 'conversation_search',
@@ -778,6 +928,87 @@ ruminate) {
778
928
  },
779
929
  }));
780
930
  // ── 反刍状态查询(查看当前反刍进度) ──
931
+ // ── memory_room_review:孤儿记忆的 Room 候选逐个复查(agent 长尾治理)──────
932
+ // next/annotate 只读或预算内标注(无需门);confirm/skip 改写 metadata,
933
+ // confirm 吃 live.memoryMutate 高权限门(与 memory_delete 同款)。
934
+ ctx.tools.register(defineTool({
935
+ name: 'memory_room_review',
936
+ description: '孤儿记忆(无 Room 绑定)的逐个复查:action="next" 取下一条待复查记录(带系统预标记的候选 Room 与现有词表,agent 依据记录内容判定);' +
937
+ 'action="confirm" 按 id 把 1-3 个 Room slug 写入该记忆(高权限);action="skip" 跳过(不再进队列);' +
938
+ 'action="annotate" 立即对一批孤儿做候选预标记(平时由反刍自动推进)。建议长会话空闲时逐个过,保持分类收敛——优先挂靠现有 Room。',
939
+ parameters: {
940
+ action: { type: 'string', required: true, description: 'next(取下一条)| confirm(确认写入)| skip(跳过)| annotate(批量预标记)' },
941
+ id: { type: 'string', description: 'confirm/skip 的目标记录 id(action=next 返回的 id)' },
942
+ rooms: { type: 'string', description: 'confirm 的 Room slug 列表(逗号分隔,1-3 个,小写字母数字连字符;可含词表外新 slug)' },
943
+ },
944
+ output: {
945
+ schema: {
946
+ type: 'object',
947
+ properties: {
948
+ notice: { type: 'string' },
949
+ id: { type: 'string' },
950
+ content: { type: 'string' },
951
+ candidates: { type: 'string' },
952
+ roomCatalog: { type: 'string' },
953
+ remaining: { type: 'number' },
954
+ confirmedTags: { type: 'string' },
955
+ annotated: { type: 'number' },
956
+ deferred: { type: 'number' },
957
+ },
958
+ additionalProperties: false,
959
+ },
960
+ render: (_args, value) => [{ type: 'text', text: value.notice ?? JSON.stringify(value) }],
961
+ },
962
+ execute: async (args) => {
963
+ const action = String(args.action ?? '');
964
+ const io = { l1: stores.l1, logger };
965
+ if (action === 'next') {
966
+ const item = nextReview(io.l1);
967
+ if (!item)
968
+ return { notice: '没有待复查的孤儿记忆(roomCandidates 队列为空)。可用 action=annotate 先做一轮候选预标记。' };
969
+ return {
970
+ id: item.id,
971
+ content: item.content,
972
+ candidates: item.candidates.join(', '),
973
+ roomCatalog: item.roomCatalog.map((r) => `${r.room}(${r.count})`).join(', '),
974
+ remaining: item.remaining,
975
+ };
976
+ }
977
+ if (action === 'annotate') {
978
+ const r = await annotateOrphanCandidates(ctx, cfg, { ...io, registry: roomRegistry }, 40);
979
+ return {
980
+ notice: `候选预标记完成:选中 ${r.selected} 条,产候选 ${r.candidates} 条,写 pending ${r.written} 条。用 action=next 逐个复查。`,
981
+ annotated: r.candidates,
982
+ deferred: r.selected - r.written,
983
+ };
984
+ }
985
+ if (action === 'confirm') {
986
+ if (!live.get().memoryMutate)
987
+ return { notice: MUTATE_OFF_NOTICE };
988
+ const id = String(args.id ?? '').trim();
989
+ if (!id)
990
+ return { notice: 'confirm 需要 id(action=next 返回的 id)。' };
991
+ const rooms = String(args.rooms ?? '')
992
+ .split(/[,,、]/)
993
+ .map((x) => x.trim())
994
+ .filter(Boolean);
995
+ if (rooms.length === 0)
996
+ return { notice: 'confirm 需要 rooms(逗号分隔的 1-3 个 Room slug)。' };
997
+ const r = confirmReview(io.l1, id, rooms);
998
+ if (!r)
999
+ return { notice: `确认失败:id=${id} 不存在、已被确认过、或 rooms 全部非法(需 1-3 个合法 slug)。` };
1000
+ return { notice: `已挂到 Room:${r.tags.join(', ')}(面板下一次刷新即可见)。`, confirmedTags: r.tags.join(', '), id };
1001
+ }
1002
+ if (action === 'skip') {
1003
+ const id = String(args.id ?? '').trim();
1004
+ if (!id)
1005
+ return { notice: 'skip 需要 id。' };
1006
+ const ok = skipReview(io.l1, id);
1007
+ return { notice: ok ? `已跳过 ${id}(不再进复查队列)。` : `跳过失败:id=${id} 不存在。`, id };
1008
+ }
1009
+ return { notice: `非法 action:${action}(允许 next/confirm/skip/annotate)。剩余待复查 ${pendingReviewCount(stores.l1)} 条。` };
1010
+ },
1011
+ }));
781
1012
  ctx.tools.register(defineTool({
782
1013
  name: 'memory_ruminate_status',
783
1014
  description: '查询当前记忆反刍的状态与进度(是否运行中、当前阶段、已完成/总会话数等)。\n\n注意:在 DSH Web GUI 的对话中无法直接调用此工具,因为模型可调用工具列表不包含 ruminate_status。如需查询进度,请在记忆库面板的蒸馏面板查看。',
package/dist/types.d.ts CHANGED
@@ -41,6 +41,10 @@ export interface RoomCount {
41
41
  room: string;
42
42
  /** 该 Room 下的记录数(与 `wingL1Counts()` 同口径:含 retired 行)。 */
43
43
  count: number;
44
+ /** 来源标示(rooms-get 端点层合并注册表后附带;'grown' = 自生长,可缺省)。 */
45
+ source?: 'pre-registered' | 'grown';
46
+ /** 人类可读名(注册表条目才有;面板 chip 可用 label 展示)。 */
47
+ label?: string;
44
48
  }
45
49
  /** 记录族标签推断:work_* 前缀 → work,其余(含 auto 档兜底)→ chat。 */
46
50
  export declare function familyForType(type: string): MemoryFamily;
package/dsh.plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dsh-prime-memory",
3
3
  "name": "dsh-prime-memory",
4
- "version": "0.20.0-beta.2-dsh0.2.0.1",
4
+ "version": "0.20.0-beta.6-dsh0.2.0.1",
5
5
  "description": {
6
6
  "en": "Layered distilled memory plugin (L0 capture → L1 atomic memory → L2 scenes → L3 persona) with automatic recall injection before each model step.",
7
7
  "zh": "L0~L3 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-prime-memory",
3
- "version": "0.20.0-beta.2",
3
+ "version": "0.20.0-beta.21",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public",
@@ -1,31 +0,0 @@
1
- import type { Context } from '@deepseek-ai/cordis';
2
- import type { MemoryConfig } from '../config.js';
3
- import type { ContextUsageStore } from '../store/context-usage.js';
4
- import type { SlotStore } from '../store/slots.js';
5
- import type { TodoRefStore } from '../store/todo-ref.js';
6
- import type { SessionModeStore } from '../store/session-modes.js';
7
- import type { MemoryLogger } from '../types.js';
8
- /** todo 参考槽位的受管标题(精确匹配;refreshByTitle 按它原地刷新)。 */
9
- export declare const TODO_REF_SLOT_TITLE = "\u957F\u4EFB\u52A1 \u00B7 \u4EFB\u52A1\u53C2\u8003(todo)";
10
- export interface LongTaskMonitorDeps {
11
- cfg: MemoryConfig;
12
- modes: SessionModeStore;
13
- slots: SlotStore;
14
- contextUsage: ContextUsageStore;
15
- todoRef: TodoRefStore;
16
- logger: MemoryLogger;
17
- }
18
- /**
19
- * 长任务模式的事件监听(总开关 cfg.longTask.enabled 关闭时不注册):
20
- * - assistant/message 的官方 usage 输入侧 → 上下文使用量(最近值语义);
21
- * - compaction/summary → 上下文坍缩归零;
22
- * - todo/write → 仅长任务模式下:记录快照 + 漂移检测,并把任务清单刷进
23
- * pinned `todo` 槽位(slot-recall 常驻注入,压缩后任务北极星仍在)。
24
- *
25
- * fail-open 纪律:任何异常只 warn,绝不影响会话事件主流程。
26
- */
27
- export declare function registerLongTaskMonitor(ctx: Context, deps: LongTaskMonitorDeps): () => void;
28
- /** 任务清单 → pinned todo 槽位(body ≤512,截断明示——截断不静默)。 */
29
- export declare function upsertTodoRefSlot(deps: LongTaskMonitorDeps, text: string): Promise<void>;
30
- /** 关闭 todo 参考槽位(长任务关闭时撤下注入;无槽位 = 幂等 no-op)。 */
31
- export declare function closeTodoRefSlot(deps: LongTaskMonitorDeps): Promise<boolean>;
@@ -1,93 +0,0 @@
1
- /** todo 参考槽位的受管标题(精确匹配;refreshByTitle 按它原地刷新)。 */
2
- export const TODO_REF_SLOT_TITLE = '长任务 · 任务参考(todo)';
3
- /**
4
- * 长任务模式的事件监听(总开关 cfg.longTask.enabled 关闭时不注册):
5
- * - assistant/message 的官方 usage 输入侧 → 上下文使用量(最近值语义);
6
- * - compaction/summary → 上下文坍缩归零;
7
- * - todo/write → 仅长任务模式下:记录快照 + 漂移检测,并把任务清单刷进
8
- * pinned `todo` 槽位(slot-recall 常驻注入,压缩后任务北极星仍在)。
9
- *
10
- * fail-open 纪律:任何异常只 warn,绝不影响会话事件主流程。
11
- */
12
- export function registerLongTaskMonitor(ctx, deps) {
13
- if (!deps.cfg.longTask.enabled)
14
- return () => { };
15
- const off = ctx.on('session/event', (session, event) => {
16
- try {
17
- const sid = String(session.id ?? session);
18
- if (event.type === 'assistant/message') {
19
- const usage = event.data
20
- .usage;
21
- if (usage && typeof usage === 'object') {
22
- const total = num(usage.inputTokens) + num(usage.cacheReadTokens) + num(usage.cacheWriteTokens);
23
- if (total > 0)
24
- deps.contextUsage.record(sid, total);
25
- }
26
- return;
27
- }
28
- if (event.type === 'compaction/summary') {
29
- // 压缩坍缩:上下文规模归零,建议阈值从新基数重新逼近。
30
- deps.contextUsage.reset(sid);
31
- return;
32
- }
33
- if (event.type === 'todo/write') {
34
- // todo 参考只在长任务模式可用(用户显式开启才消耗注入预算)。
35
- if (!deps.modes.getLongTask(sid))
36
- return;
37
- const raw = event.data.todos;
38
- if (!Array.isArray(raw))
39
- return;
40
- const items = [];
41
- for (const t of raw) {
42
- if (t === null || typeof t !== 'object')
43
- continue;
44
- const content = String(t.content ?? '').trim();
45
- if (!content)
46
- continue;
47
- const status = String(t.status ?? 'pending');
48
- items.push({ content, status });
49
- }
50
- if (items.length === 0)
51
- return;
52
- const entry = deps.todoRef.record(sid, items);
53
- void upsertTodoRefSlot(deps, entry.text).catch((err) => deps.logger.warn(`[memory] todo 参考槽位刷新失败(不影响事件流): ${msg(err)}`));
54
- }
55
- }
56
- catch (err) {
57
- deps.logger.warn(`[memory] 长任务监听处理失败: ${msg(err)}`);
58
- }
59
- });
60
- return off;
61
- }
62
- /** 任务清单 → pinned todo 槽位(body ≤512,截断明示——截断不静默)。 */
63
- export async function upsertTodoRefSlot(deps, text) {
64
- const MAX = 512;
65
- let body = text;
66
- if (byteLen(text) > MAX) {
67
- let cut = text;
68
- while (cut.length > 0 && byteLen(cut) + byteLen('\n…(已截断,完整清单见 todo 面板)') > MAX) {
69
- cut = cut.slice(0, -1);
70
- }
71
- body = `${cut}\n…(已截断,完整清单见 todo 面板)`;
72
- }
73
- await deps.slots.refreshByTitle(TODO_REF_SLOT_TITLE, { body, pinned: true });
74
- }
75
- /** 关闭 todo 参考槽位(长任务关闭时撤下注入;无槽位 = 幂等 no-op)。 */
76
- export async function closeTodoRefSlot(deps) {
77
- const found = deps.slots
78
- .list()
79
- .find((s) => s.title === TODO_REF_SLOT_TITLE && s.status === 'open');
80
- if (!found)
81
- return false;
82
- await deps.slots.close(found.id, 'dropped');
83
- return true;
84
- }
85
- function num(v) {
86
- return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : 0;
87
- }
88
- function msg(err) {
89
- return err instanceof Error ? err.message : String(err);
90
- }
91
- function byteLen(s) {
92
- return new TextEncoder().encode(s).length;
93
- }
@@ -1,28 +0,0 @@
1
- import type { MemoryLogger } from '../types.js';
2
- /**
3
- * 会话上下文使用量(长任务模式 §上下文估计)。
4
- *
5
- * 数据源:assistant/message 官方 usage 的输入侧(inputTokens + cache 读/写)——
6
- * 最新一条 assistant 消息的输入侧 ≈ 该时刻的上下文规模,取"最近值"而非累加
7
- * (每轮输入侧已含全部历史)。compaction/summary 后上下文坍缩,归零重计。
8
- *
9
- * 纯内存 per-session Map(热路径,session/event 每事件触达;不落盘——
10
- * 重启后首条 assistant 消息即可重建,丢失窗口无建议价值)。
11
- */
12
- export declare class ContextUsageStore {
13
- private readonly logger?;
14
- private readonly latest;
15
- /** 上一次归零时间(compaction 诊断透出;可省)。 */
16
- private readonly compactedAt;
17
- constructor(logger?: MemoryLogger | undefined);
18
- /** 记录最近一次官方输入侧 token 数(只增语义由调用方保证传"最新一轮")。 */
19
- record(sessionId: string, inputTokens: number): void;
20
- /** 压缩坍缩:归零并记录时刻。 */
21
- reset(sessionId: string): void;
22
- /** 最近输入侧 token 数(无样本 = undefined)。 */
23
- latestTokens(sessionId: string): number | undefined;
24
- /** 上下文占窗口百分比(窗口未知或无样本 = null)。 */
25
- percentOfWindow(sessionId: string, contextWindowTokens: number | undefined): number | null;
26
- /** 测试与停机清理。 */
27
- clear(): void;
28
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * 会话上下文使用量(长任务模式 §上下文估计)。
3
- *
4
- * 数据源:assistant/message 官方 usage 的输入侧(inputTokens + cache 读/写)——
5
- * 最新一条 assistant 消息的输入侧 ≈ 该时刻的上下文规模,取"最近值"而非累加
6
- * (每轮输入侧已含全部历史)。compaction/summary 后上下文坍缩,归零重计。
7
- *
8
- * 纯内存 per-session Map(热路径,session/event 每事件触达;不落盘——
9
- * 重启后首条 assistant 消息即可重建,丢失窗口无建议价值)。
10
- */
11
- export class ContextUsageStore {
12
- logger;
13
- latest = new Map();
14
- /** 上一次归零时间(compaction 诊断透出;可省)。 */
15
- compactedAt = new Map();
16
- constructor(logger) {
17
- this.logger = logger;
18
- }
19
- /** 记录最近一次官方输入侧 token 数(只增语义由调用方保证传"最新一轮")。 */
20
- record(sessionId, inputTokens) {
21
- if (!Number.isFinite(inputTokens) || inputTokens < 0)
22
- return;
23
- this.latest.set(sessionId, Math.round(inputTokens));
24
- }
25
- /** 压缩坍缩:归零并记录时刻。 */
26
- reset(sessionId) {
27
- if (this.latest.delete(sessionId))
28
- this.compactedAt.set(sessionId, Date.now());
29
- }
30
- /** 最近输入侧 token 数(无样本 = undefined)。 */
31
- latestTokens(sessionId) {
32
- return this.latest.get(sessionId);
33
- }
34
- /** 上下文占窗口百分比(窗口未知或无样本 = null)。 */
35
- percentOfWindow(sessionId, contextWindowTokens) {
36
- const used = this.latest.get(sessionId);
37
- if (used === undefined || !Number.isFinite(contextWindowTokens) || (contextWindowTokens ?? 0) <= 0) {
38
- return null;
39
- }
40
- return Math.min(100, Math.round((used / contextWindowTokens) * 100));
41
- }
42
- /** 测试与停机清理。 */
43
- clear() {
44
- this.latest.clear();
45
- this.compactedAt.clear();
46
- }
47
- }