dsh-prime-memory 0.16.2 → 0.16.4
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/CHANGELOG.md +44 -0
- package/cordis.patch.yml +6 -0
- package/dist/client.js +1333 -168
- package/dist/config.d.ts +37 -0
- package/dist/config.js +7 -0
- package/dist/contract.d.ts +155 -1
- package/dist/hooks/capture-recovery.d.ts +64 -0
- package/dist/hooks/capture-recovery.js +105 -0
- package/dist/hooks/capture.js +123 -1
- package/dist/hooks/recall-ack.d.ts +48 -0
- package/dist/hooks/recall-ack.js +79 -0
- package/dist/hooks/recall.d.ts +23 -1
- package/dist/hooks/recall.js +121 -6
- package/dist/index.d.ts +22 -0
- package/dist/index.js +18 -4
- package/dist/metadata-validators.d.ts +5 -0
- package/dist/metadata-validators.js +11 -1
- package/dist/pipeline/l1.d.ts +4 -0
- package/dist/pipeline/l1.js +9 -2
- package/dist/pipeline/reconcile.js +4 -0
- package/dist/pipeline/ruminate.d.ts +11 -1
- package/dist/pipeline/ruminate.js +18 -1
- package/dist/pipeline/runner.js +62 -0
- package/dist/prompts/boundary.d.ts +17 -0
- package/dist/prompts/boundary.js +20 -0
- package/dist/prompts/graph-projection.js +4 -1
- package/dist/prompts/l1-dedup.d.ts +3 -3
- package/dist/prompts/l1-dedup.js +9 -1
- package/dist/prompts/l1-extraction.d.ts +3 -3
- package/dist/prompts/l1-extraction.js +12 -0
- package/dist/prompts/persona.js +6 -0
- package/dist/prompts/scene.js +7 -1
- package/dist/room-review.d.ts +196 -0
- package/dist/room-review.js +281 -0
- package/dist/stats.d.ts +26 -2
- package/dist/stats.js +270 -13
- package/dist/store/l0.d.ts +4 -0
- package/dist/store/l0.js +8 -0
- package/dist/store/l1.d.ts +34 -0
- package/dist/store/l1.js +68 -1
- package/dist/store/recall-usage.d.ts +43 -0
- package/dist/store/recall-usage.js +149 -0
- package/dist/store/rooms-registry.d.ts +66 -0
- package/dist/store/rooms-registry.js +250 -0
- package/dist/store/sqlite.d.ts +16 -0
- package/dist/store/sqlite.js +57 -0
- package/dist/store/trace.d.ts +39 -0
- package/dist/store/trace.js +149 -0
- package/dist/tools/index.d.ts +4 -1
- package/dist/tools/index.js +249 -2
- package/dist/types.d.ts +4 -0
- package/dist/util/redact.d.ts +27 -0
- package/dist/util/redact.js +228 -0
- package/dsh.plugin.json +1 -1
- package/package.json +3 -3
- package/dist/hall-backfill.d.ts +0 -33
- package/dist/hall-backfill.js +0 -95
- package/dist/hall-migrate.d.ts +0 -32
- package/dist/hall-migrate.js +0 -88
package/dist/stats.js
CHANGED
|
@@ -18,14 +18,16 @@ import { EFFORT_CHOICES, resolveDataDir } from './config.js';
|
|
|
18
18
|
import { effectiveCfg } from './pipeline/runner.js';
|
|
19
19
|
import { emptyRecallStats } from './hooks/recall.js';
|
|
20
20
|
import { buildRouteChain, decideSendableEffort, LAYER_DEFAULT_BUDGETS, layerChainOrNull, resolveModelContextWindow, resolveModelEfforts, resolveModelRoute } from './llm.js';
|
|
21
|
+
import { mergeRoom, renameRoom } from './room-review.js';
|
|
21
22
|
import { projectDistillChain, validateDistillChain } from './settings.js';
|
|
22
23
|
import { RECEIPTS_QUERY_LIMIT_MAX, dimensionOf, toReceiptView } from './store/receipts.js';
|
|
24
|
+
import { getTraceStore } from './store/trace.js';
|
|
23
25
|
import { resolveConflictPair, listConflictPairs } from './conflict-service.js';
|
|
24
26
|
import { sourceAnchorLabels } from './pipeline/anchors.js';
|
|
25
27
|
import { readSupersedeMarker } from './store/supersede.js';
|
|
26
28
|
import { isSnapshotName } from './store/l1-snapshot.js';
|
|
27
29
|
import { WING_CATALOG, WING_FALLBACK } from './types.js';
|
|
28
|
-
import {
|
|
30
|
+
import { isRoomSlug } from './metadata-validators.js';
|
|
29
31
|
import { InProcMemoryBackend } from './store/memory-backend.js';
|
|
30
32
|
import { isWingCorner } from './store/session-modes.js';
|
|
31
33
|
import { startWingBackfill } from './wing-backfill.js';
|
|
@@ -34,7 +36,7 @@ import { snapshotTokenCost } from './token-cost.js';
|
|
|
34
36
|
const require = createRequire(import.meta.url);
|
|
35
37
|
export const PLUGIN_VERSION = require('../package.json').version;
|
|
36
38
|
/**
|
|
37
|
-
* 端点全集运行时清单(
|
|
39
|
+
* 端点全集运行时清单(与 tests/contract-keys.test.ts 的 ENDPOINTS 及
|
|
38
40
|
* contract.ts 类型映射表三方对齐,漂移由键集 diff 测试暴露)。
|
|
39
41
|
* 注意:本清单同时是 HTTP 前缀路由 `/dsh-memory/rpc/<短名>` 的**放行白名单**
|
|
40
42
|
* (见下方 SHORT_ENDPOINTS),漏一条 = 该端点在面板里静默消失(404 被客户端
|
|
@@ -51,6 +53,9 @@ export const MEMORY_ENDPOINTS = [
|
|
|
51
53
|
'dsh-memory/session-mode-set',
|
|
52
54
|
'dsh-memory/wing-overview',
|
|
53
55
|
'dsh-memory/rooms-get',
|
|
56
|
+
'dsh-memory/rooms-export',
|
|
57
|
+
'dsh-memory/room-register',
|
|
58
|
+
'dsh-memory/room-admin',
|
|
54
59
|
'dsh-memory/wing-backfill',
|
|
55
60
|
'dsh-memory/session-stats',
|
|
56
61
|
'dsh-memory/settings-get',
|
|
@@ -66,6 +71,7 @@ export const MEMORY_ENDPOINTS = [
|
|
|
66
71
|
'dsh-memory/scenes',
|
|
67
72
|
'dsh-memory/persona',
|
|
68
73
|
'dsh-memory/log-tail',
|
|
74
|
+
'dsh-memory/trace-tail',
|
|
69
75
|
'dsh-memory/rebuild-status',
|
|
70
76
|
'dsh-memory/rebuild-start',
|
|
71
77
|
'dsh-memory/rebuild-cancel',
|
|
@@ -166,12 +172,15 @@ export function buildEndpointDeps(base, sources, controller) {
|
|
|
166
172
|
embedManager: sources.embedManager,
|
|
167
173
|
sessionInfo: sources.sessionInfo,
|
|
168
174
|
ruminate: controller,
|
|
175
|
+
roomRegistry: sources.roomRegistry,
|
|
169
176
|
};
|
|
170
177
|
return { ...base, ...injected };
|
|
171
178
|
}
|
|
172
179
|
export function registerMemoryRpc(ctx, cfg, stores, logger, status, live, modes, dataDir, rebuild, embedManager, sessionInfo,
|
|
173
180
|
/** 反刍控制器(存储降级时为 undefined):经 buildEndpointDeps 落入 deps.ruminate。 */
|
|
174
|
-
ruminate
|
|
181
|
+
ruminate,
|
|
182
|
+
/** Room 分类管理依赖(注册表;缺省 = rooms-get 不带 registry、rooms-export 恒 404 语义)。 */
|
|
183
|
+
roomRegistry) {
|
|
175
184
|
/** 当前是否持有一段有效注册(dispose 完成后清空,允许服务重上线时重注册)。 */
|
|
176
185
|
let holding = false;
|
|
177
186
|
/** 当前 handle 绑定的 connection 实例(internal/service 第二参;用于识别实例替换)。 */
|
|
@@ -189,7 +198,7 @@ ruminate) {
|
|
|
189
198
|
// 端点处理器:HTTP 层与旧 connection.rpc 的 handler 共用同一分发。
|
|
190
199
|
const rpcHandler = async (endpoint, payload) => {
|
|
191
200
|
try {
|
|
192
|
-
const value = await handleEndpoint(endpoint, payload, buildEndpointDeps({ ctx, cfg, stores, logger }, { status, live, modes, dataDir, rebuild, embedManager, sessionInfo }, ruminate));
|
|
201
|
+
const value = await handleEndpoint(endpoint, payload, buildEndpointDeps({ ctx, cfg, stores, logger }, { status, live, modes, dataDir, rebuild, embedManager, sessionInfo, roomRegistry }, ruminate));
|
|
193
202
|
return { ok: true, value };
|
|
194
203
|
}
|
|
195
204
|
catch (err) {
|
|
@@ -337,7 +346,7 @@ function sanitizeSettings(s) {
|
|
|
337
346
|
}
|
|
338
347
|
/** 端点分发表(导出供测试直调:可精确注入 rebuild/ruminate 等可选控制器,验证 deps 接线)。 */
|
|
339
348
|
export async function handleEndpoint(endpoint, payload, deps) {
|
|
340
|
-
const { cfg, stores, status, live, modes, dataDir, rebuild, ruminate, embedManager, sessionInfo } = deps;
|
|
349
|
+
const { cfg, stores, status, live, modes, dataDir, rebuild, ruminate, embedManager, sessionInfo, roomRegistry } = deps;
|
|
341
350
|
switch (endpoint) {
|
|
342
351
|
case 'dsh-memory/stats':
|
|
343
352
|
return buildStats(cfg, stores, status);
|
|
@@ -454,10 +463,223 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
454
463
|
// ── Room 分类计数(标签自生长分类;展开 metadata.tags 聚合,零 schema) ──
|
|
455
464
|
case 'dsh-memory/rooms-get': {
|
|
456
465
|
const rooms = stores.l1.listRooms();
|
|
457
|
-
|
|
466
|
+
// 端点层合并注册表(Agent A 设计):注册表 active 条目 count=0 补齐并带
|
|
467
|
+
// source/label(source 用条目真实来源——收编 grown ≠ 预注册,⭐角标靠它区分);
|
|
468
|
+
// 自生长条目标 source='grown'。不动 listRooms() 的缓存语义。
|
|
469
|
+
const merged = rooms.map((r) => ({ ...r, source: 'grown' }));
|
|
470
|
+
const registry = roomRegistry?.listActive() ?? [];
|
|
471
|
+
for (const e of registry) {
|
|
472
|
+
const idx = merged.findIndex((r) => r.room === e.slug);
|
|
473
|
+
if (idx === -1) {
|
|
474
|
+
merged.push({ room: e.slug, count: 0, source: e.source, label: e.label });
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
merged[idx] = { ...merged[idx], source: e.source, label: e.label ?? merged[idx].label };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const v = {
|
|
481
|
+
rooms: merged,
|
|
482
|
+
total: merged.length,
|
|
483
|
+
orphanCount: stores.l1.untaggedL1Count(),
|
|
484
|
+
registry: roomRegistry?.list().map((e) => ({
|
|
485
|
+
slug: e.slug,
|
|
486
|
+
label: e.label,
|
|
487
|
+
description: e.description,
|
|
488
|
+
source: e.source,
|
|
489
|
+
aliases: e.aliases,
|
|
490
|
+
status: e.status,
|
|
491
|
+
})),
|
|
492
|
+
};
|
|
493
|
+
return v;
|
|
494
|
+
}
|
|
495
|
+
case 'dsh-memory/room-register': {
|
|
496
|
+
if (!roomRegistry)
|
|
497
|
+
throw new Error('Room 注册表未装配(需要插件重载以初始化)。');
|
|
498
|
+
if (!live?.get().memoryMutate)
|
|
499
|
+
throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后,模型才能预注册 Room。');
|
|
500
|
+
const p = (payload ?? {});
|
|
501
|
+
const slug = String(p.slug ?? '').trim();
|
|
502
|
+
if (!slug)
|
|
503
|
+
throw new Error('slug 不能为空');
|
|
504
|
+
const r = await roomRegistry.register({
|
|
505
|
+
slug,
|
|
506
|
+
label: typeof p.label === 'string' ? p.label : undefined,
|
|
507
|
+
description: typeof p.description === 'string' ? p.description : undefined,
|
|
508
|
+
source: p.source === 'grown' ? 'grown' : undefined,
|
|
509
|
+
});
|
|
510
|
+
const v = {
|
|
511
|
+
slug: r.entry.slug,
|
|
512
|
+
created: r.created,
|
|
513
|
+
notice: r.created ? '已预注册。' : '已存在(只补全了缺失的 label/description)。',
|
|
514
|
+
};
|
|
515
|
+
return v;
|
|
516
|
+
}
|
|
517
|
+
// ── Room 破坏性面管理(merge/rename/retire;与 memory_room_admin 工具同编排) ──
|
|
518
|
+
case 'dsh-memory/room-admin': {
|
|
519
|
+
if (!roomRegistry)
|
|
520
|
+
throw new Error('Room 注册表未装配(需要插件重载以初始化)。');
|
|
521
|
+
if (!live?.get().memoryMutate)
|
|
522
|
+
throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后,才能执行 Room 管理。');
|
|
523
|
+
const p = (payload ?? {});
|
|
524
|
+
if (p.action === 'update') {
|
|
525
|
+
const slug = String(p.slug ?? '').trim();
|
|
526
|
+
if (!slug)
|
|
527
|
+
throw new Error('update 需要 slug');
|
|
528
|
+
const changed = await roomRegistry.update(slug, {
|
|
529
|
+
label: typeof p.label === 'string' ? p.label : undefined,
|
|
530
|
+
description: typeof p.description === 'string' ? p.description : undefined,
|
|
531
|
+
});
|
|
532
|
+
const v = {
|
|
533
|
+
notice: changed
|
|
534
|
+
? `已更新:${slug}(显示名/归类说明,标注器下次即用新词)`
|
|
535
|
+
: `无变化:${slug} 未找到或未提供任何非空字段`,
|
|
536
|
+
};
|
|
537
|
+
return v;
|
|
538
|
+
}
|
|
539
|
+
if (p.action === 'retire') {
|
|
540
|
+
const slug = String(p.slug ?? '').trim();
|
|
541
|
+
if (!slug)
|
|
542
|
+
throw new Error('retire 需要 slug');
|
|
543
|
+
const active = p.active === true;
|
|
544
|
+
// 自生长未注册 slug 退场:自动补一条 grown 注册条目再置 retired——
|
|
545
|
+
// 否则词表无物可变,面板按钮无法切换为「恢复」(用户实测反馈)
|
|
546
|
+
if (!active && p.withRecords === true && !roomRegistry.bySlug(slug)) {
|
|
547
|
+
await roomRegistry.register({ slug, source: 'grown' });
|
|
548
|
+
}
|
|
549
|
+
const changed = await roomRegistry.setStatus(slug, active ? 'active' : 'retired');
|
|
550
|
+
// 带记录退场(软删,可恢复):该 room 下全部**活跃**记录分批 retire(每批 ≤200,与 records-delete 同原语)
|
|
551
|
+
let retiredRecords = 0;
|
|
552
|
+
if (p.withRecords === true && !active) {
|
|
553
|
+
for (let offset = 0;; offset += 200) {
|
|
554
|
+
const page = stores.l1.list({ tag: slug, retired: false, limit: 200, offset });
|
|
555
|
+
if (page.items.length === 0)
|
|
556
|
+
break;
|
|
557
|
+
const n = stores.l1.retire(page.items.map((r) => r.id), { at: new Date().toISOString(), reason: 'manual' });
|
|
558
|
+
retiredRecords += n;
|
|
559
|
+
if (page.items.length < 200)
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
deps.logger.info(`[memory] Room 退役带记录:${slug},退场 ${retiredRecords} 条(软删,可恢复)`);
|
|
563
|
+
}
|
|
564
|
+
const v = {
|
|
565
|
+
retiredRecords: retiredRecords > 0 ? retiredRecords : undefined,
|
|
566
|
+
notice: changed
|
|
567
|
+
? active
|
|
568
|
+
? `已恢复:${slug}`
|
|
569
|
+
: `已退役:${slug}(存量记录的 tags 不动,仅词表不再推荐)。${retiredRecords > 0 ? `已连同退场 ${retiredRecords} 条记录(软删,可在「已退场」区恢复)。` : ''}`
|
|
570
|
+
: `未找到注册条目或状态无变化:${slug}。${retiredRecords > 0 ? `已退场 ${retiredRecords} 条记录(软删,可恢复)。` : ''}`,
|
|
571
|
+
};
|
|
572
|
+
return v;
|
|
573
|
+
}
|
|
574
|
+
if (p.action === 'merge' || p.action === 'rename') {
|
|
575
|
+
const from = String(p.from ?? '').trim();
|
|
576
|
+
const to = String(p.to ?? '').trim();
|
|
577
|
+
if (!from || !to)
|
|
578
|
+
throw new Error('merge/rename 需要 from 与 to 两个 slug');
|
|
579
|
+
const dryRun = p.dryRun !== false;
|
|
580
|
+
const r = await (p.action === 'merge' ? mergeRoom : renameRoom)({ l1: stores.l1, registry: roomRegistry, logger: deps.logger }, from, to, { dryRun });
|
|
581
|
+
if (r.dryRun) {
|
|
582
|
+
const v = {
|
|
583
|
+
notice: `[dryRun 预览] ${from} → ${to}:影响 ${r.affected} 条${r.hasMore ? '(超过单次上限,需续跑)' : ''}。` +
|
|
584
|
+
`样例:${r.preview.join(', ') || '无'}。确认无误后以 dryRun=false 执行(执行前自动备份,完成后自动入队场景重算)。`,
|
|
585
|
+
dryRun: true,
|
|
586
|
+
affected: r.affected,
|
|
587
|
+
preview: r.preview,
|
|
588
|
+
hasMore: r.hasMore,
|
|
589
|
+
};
|
|
590
|
+
return v;
|
|
591
|
+
}
|
|
592
|
+
const v = {
|
|
593
|
+
notice: `${p.action === 'merge' ? '合并' : '改名'}完成:${from} → ${to},重写 ${r.applied} 条` +
|
|
594
|
+
`(备份:${r.backupFile ?? '无'})。`,
|
|
595
|
+
dryRun: false,
|
|
596
|
+
affected: r.applied,
|
|
597
|
+
backupFile: r.backupFile,
|
|
598
|
+
};
|
|
599
|
+
return v;
|
|
600
|
+
}
|
|
601
|
+
throw new Error(`非法 action:${String(p.action)}(允许 merge/rename/retire)`);
|
|
602
|
+
}
|
|
603
|
+
case 'dsh-memory/rooms-export': {
|
|
604
|
+
const p = (payload ?? {});
|
|
605
|
+
const kind = p.kind === 'orphans' ? 'orphans' : p.kind === 'rooms' ? 'rooms' : p.kind === 'records' ? 'records' : '';
|
|
606
|
+
if (!kind)
|
|
607
|
+
throw new Error(`非法 kind: ${String(p.kind)}(允许 rooms/orphans/records)`);
|
|
608
|
+
const limit = Math.min(Math.max(Number(p.limit) || 2000, 1), 10_000);
|
|
609
|
+
const esc = (v) => {
|
|
610
|
+
const t = String(v ?? '');
|
|
611
|
+
return /[,\r\n"]/.test(t) ? '"' + t.replace(/"/g, '""') + '"' : t;
|
|
612
|
+
};
|
|
613
|
+
if (kind === 'rooms') {
|
|
614
|
+
const registry = roomRegistry?.list() ?? [];
|
|
615
|
+
const counts = new Map(stores.l1.listRooms().map((r) => [r.room, r.count]));
|
|
616
|
+
const lines = ['slug,count,source,label,description,status,aliases'];
|
|
617
|
+
let n = 0;
|
|
618
|
+
const seen = new Set();
|
|
619
|
+
for (const e of registry) {
|
|
620
|
+
lines.push([e.slug, String(counts.get(e.slug) ?? 0), e.source, e.label ?? '', e.description ?? '', e.status, (e.aliases ?? []).join('|')].map(esc).join(','));
|
|
621
|
+
seen.add(e.slug);
|
|
622
|
+
n++;
|
|
623
|
+
}
|
|
624
|
+
for (const r of stores.l1.listRooms()) {
|
|
625
|
+
if (seen.has(r.room))
|
|
626
|
+
continue;
|
|
627
|
+
lines.push([r.room, String(r.count), 'grown', '', '', 'active', ''].map(esc).join(','));
|
|
628
|
+
n++;
|
|
629
|
+
}
|
|
630
|
+
const v = { kind, csv: lines.join('\r\n'), total: n, truncated: false };
|
|
631
|
+
return v;
|
|
632
|
+
}
|
|
633
|
+
if (kind === 'records') {
|
|
634
|
+
const tag = String(p.tag ?? '').trim();
|
|
635
|
+
if (!tag)
|
|
636
|
+
throw new Error('kind=records 需要 tag(目标 Room slug)');
|
|
637
|
+
const header = 'id,type,scene_name,updated_at,tags,content';
|
|
638
|
+
const lines = [header];
|
|
639
|
+
let n = 0;
|
|
640
|
+
let truncated2 = false;
|
|
641
|
+
for (let offset = 0; offset < limit; offset += 200) {
|
|
642
|
+
const page = stores.l1.list({ tag, retired: undefined, limit: 200, offset });
|
|
643
|
+
for (const r of page.items) {
|
|
644
|
+
if (n >= limit) {
|
|
645
|
+
truncated2 = true;
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
const meta = (r.metadata ?? {});
|
|
649
|
+
const tags = Array.isArray(meta.tags) ? meta.tags.join('|') : '';
|
|
650
|
+
lines.push([r.id, r.type, r.scene_name, new Date(r.updatedAt).toISOString(), tags, r.content].map(esc).join(','));
|
|
651
|
+
n++;
|
|
652
|
+
}
|
|
653
|
+
if (truncated2 || page.items.length < 200)
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
const v = { kind, csv: lines.join('\r\n'), total: n, truncated: truncated2 };
|
|
657
|
+
return v;
|
|
658
|
+
}
|
|
659
|
+
// orphans:无 Room 绑定的活跃记录清单(id/type/scene/updated/content/候选/状态)
|
|
660
|
+
const header = 'id,type,scene_name,updated_at,content,room_candidates,room_review';
|
|
661
|
+
const lines = [header];
|
|
662
|
+
let n = 0;
|
|
663
|
+
let truncated = false;
|
|
664
|
+
for (let offset = 0; offset < limit; offset += 200) {
|
|
665
|
+
const page = stores.l1.list({ untagged: true, retired: false, limit: 200, offset });
|
|
666
|
+
for (const r of page.items) {
|
|
667
|
+
if (n >= limit) {
|
|
668
|
+
truncated = true;
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
const meta = (r.metadata ?? {});
|
|
672
|
+
lines.push([r.id, r.type, r.scene_name, new Date(r.updatedAt).toISOString(), r.content,
|
|
673
|
+
Array.isArray(meta.roomCandidates) ? meta.roomCandidates.join('|') : '',
|
|
674
|
+
String(meta.roomReview ?? '')].map(esc).join(','));
|
|
675
|
+
n++;
|
|
676
|
+
}
|
|
677
|
+
if (truncated || page.items.length < 200)
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
const v = { kind, csv: lines.join('\r\n'), total: n, truncated };
|
|
458
681
|
return v;
|
|
459
682
|
}
|
|
460
|
-
// ── 一键回填(task_15):后台任务,单飞;端点立即返回,进度看 wing-overview ──
|
|
461
683
|
case 'dsh-memory/wing-backfill': {
|
|
462
684
|
const r = startWingBackfill({
|
|
463
685
|
ctx: deps.ctx,
|
|
@@ -751,8 +973,10 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
751
973
|
const retiredSel = typeof p.retired === 'boolean' ? p.retired : undefined;
|
|
752
974
|
// Room 过滤:tag 是**自生长**的 slug(无枚举),只做形状与长度校验(SQL 侧参数化)
|
|
753
975
|
const tagSel = typeof p.tag === 'string' ? p.tag.trim().slice(0, 64) : '';
|
|
754
|
-
if (tagSel && !
|
|
755
|
-
throw new Error('tag 非法(
|
|
976
|
+
if (tagSel && !isRoomSlug(tagSel))
|
|
977
|
+
throw new Error('tag 非法(两级制:major 或 major/minor,各段小写字母数字连字符)');
|
|
978
|
+
// 孤儿记忆筛选(与 tag 互斥,tag 优先):只认显式布尔,其余视为缺省。
|
|
979
|
+
const untaggedSel = p.untagged === true && !tagSel;
|
|
756
980
|
// R13 多值归一: halls 数组只留非空字符串(≤40 字符),去重,上限 8(角数);
|
|
757
981
|
// wing 单值保留兼容,归一后与 halls 合并
|
|
758
982
|
const wingSel = Array.from(new Set([
|
|
@@ -793,29 +1017,46 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
793
1017
|
if (!Array.isArray(tags) || !tags.some((t) => t === tagSel))
|
|
794
1018
|
return false;
|
|
795
1019
|
}
|
|
1020
|
+
if (untaggedSel) {
|
|
1021
|
+
// 孤儿:tags 缺失/非数组/全部空串均算无 Room
|
|
1022
|
+
const tags = m?.tags;
|
|
1023
|
+
const hasRoom = Array.isArray(tags) && tags.some((t) => typeof t === 'string' && t !== '');
|
|
1024
|
+
if (hasRoom)
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
796
1027
|
return true;
|
|
797
1028
|
});
|
|
798
1029
|
}
|
|
1030
|
+
const pageIds = filtered.slice(offset, offset + limit).map((h) => h.id);
|
|
1031
|
+
const updatedById = new Map(stores.l1.getByIds(pageIds).map((r) => [r.id, r.updatedAt]));
|
|
799
1032
|
const resp = {
|
|
800
|
-
items: filtered.slice(offset, offset + limit).map((h) => hitToUiRecord({ ...h, metadata: metaById?.get(h.id) }
|
|
1033
|
+
items: filtered.slice(offset, offset + limit).map((h) => hitToUiRecord({ ...h, metadata: metaById?.get(h.id) }, {
|
|
1034
|
+
agingWeight: agingWeightOf(updatedById.get(h.id), stores.l1.decayHalfLife),
|
|
1035
|
+
usage: stores.usage?.get(h.id) ?? null,
|
|
1036
|
+
})),
|
|
801
1037
|
hasMore: filtered.length > offset + limit,
|
|
802
1038
|
total: null,
|
|
803
1039
|
truncated: wanted > SEARCH_CAP,
|
|
804
1040
|
scenes: offset === 0 ? stores.l1.distinctScenes() : undefined,
|
|
805
1041
|
wingCatalog,
|
|
1042
|
+
usageSummary: stores.usage?.summary() ?? null,
|
|
806
1043
|
};
|
|
807
1044
|
return resp;
|
|
808
1045
|
}
|
|
809
1046
|
// 退场筛查仅浏览路径透传;检索路径不接——FTS/向量里本就没有已退场行,
|
|
810
1047
|
// 接了只会让「仅退场+关键词」永远空结果,不如如实不筛。
|
|
811
|
-
const { items, total } = stores.l1.list({ type: p.type || undefined, scene: p.scene || undefined, hall: p.hall || undefined, halls: wingSel.length > 0 ? wingSel : undefined, tag: tagSel || undefined, retired: retiredSel, limit, offset });
|
|
1048
|
+
const { items, total } = stores.l1.list({ type: p.type || undefined, scene: p.scene || undefined, hall: p.hall || undefined, halls: wingSel.length > 0 ? wingSel : undefined, tag: tagSel || undefined, untagged: untaggedSel || undefined, retired: retiredSel, limit, offset });
|
|
812
1049
|
const resp = {
|
|
813
|
-
items: items.map(hitToUiRecord
|
|
1050
|
+
items: items.map((r) => hitToUiRecord(r, {
|
|
1051
|
+
agingWeight: agingWeightOf(r.updatedAt, stores.l1.decayHalfLife),
|
|
1052
|
+
usage: stores.usage?.get(r.id) ?? null,
|
|
1053
|
+
})),
|
|
814
1054
|
hasMore: offset + items.length < total,
|
|
815
1055
|
total,
|
|
816
1056
|
truncated: false,
|
|
817
1057
|
scenes: offset === 0 ? stores.l1.distinctScenes() : undefined,
|
|
818
1058
|
wingCatalog,
|
|
1059
|
+
usageSummary: stores.usage?.summary() ?? null,
|
|
819
1060
|
};
|
|
820
1061
|
return resp;
|
|
821
1062
|
}
|
|
@@ -1152,6 +1393,13 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1152
1393
|
const p = (payload ?? {});
|
|
1153
1394
|
return { lines: readLogTail(join(dataDir, 'memory.log'), Math.min(Math.max(Number(p.lines) || 200, 1), 1000)) };
|
|
1154
1395
|
}
|
|
1396
|
+
case 'dsh-memory/trace-tail': {
|
|
1397
|
+
// §F 追踪尾读:未初始化(trace.enabled=false)回空集;坏行已在 store 侧跳过
|
|
1398
|
+
const p = (payload ?? {});
|
|
1399
|
+
const store = getTraceStore();
|
|
1400
|
+
const events = store ? await store.tail(Math.min(Math.max(Number(p.lines) || 100, 1), 500), p.kind) : [];
|
|
1401
|
+
return { events };
|
|
1402
|
+
}
|
|
1155
1403
|
case 'dsh-memory/rebuild-status': {
|
|
1156
1404
|
if (!rebuild) {
|
|
1157
1405
|
const v = { supported: false, running: false, phase: 'idle' };
|
|
@@ -1415,7 +1663,14 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1415
1663
|
}
|
|
1416
1664
|
}
|
|
1417
1665
|
/** 浏览器卡片字段(比 MemoryRecord 精简,去掉大 metadata;Hall 从 metadata 提取)。 */
|
|
1418
|
-
|
|
1666
|
+
/** 老化权重(与读路径 applyDecayWeight 同式):max(0.5, 0.5^(Δ天/半衰期));半衰期 ≤0 = 关 → null。 */
|
|
1667
|
+
export function agingWeightOf(updatedAtMs, halfLifeDays, now = Date.now()) {
|
|
1668
|
+
if (!(halfLifeDays > 0))
|
|
1669
|
+
return null;
|
|
1670
|
+
const ageDays = Math.max(0, (now - (updatedAtMs ?? 0)) / 86_400_000);
|
|
1671
|
+
return Math.max(0.5, Math.pow(0.5, ageDays / halfLifeDays));
|
|
1672
|
+
}
|
|
1673
|
+
export function hitToUiRecord(r, extra) {
|
|
1419
1674
|
const retired = r.validTo !== undefined;
|
|
1420
1675
|
const mark = retired ? readSupersedeMarker(r.metadata) : undefined;
|
|
1421
1676
|
return {
|
|
@@ -1441,6 +1696,8 @@ export function hitToUiRecord(r) {
|
|
|
1441
1696
|
// 但必须给出可见差异。
|
|
1442
1697
|
retired,
|
|
1443
1698
|
retiredReason: retired ? (mark?.reason ?? 'unknown') : null,
|
|
1699
|
+
agingWeight: extra?.agingWeight ?? null,
|
|
1700
|
+
usage: extra?.usage ?? null,
|
|
1444
1701
|
};
|
|
1445
1702
|
}
|
|
1446
1703
|
/**
|
package/dist/store/l0.d.ts
CHANGED
|
@@ -19,6 +19,10 @@ export declare class L0Store {
|
|
|
19
19
|
countBySession(sessionId: string): Promise<number>;
|
|
20
20
|
/** 该会话最近 n 条消息(时间升序;蒸馏背景参考用,按会话现查——ADR-0003)。 */
|
|
21
21
|
recentBySession(sessionId: string, limit: number): Promise<ConversationMessage[]>;
|
|
22
|
+
/** §A 水位线:该会话 L0 已落盘的最大 turn(空会话 undefined;委托 MemoryDb)。 */
|
|
23
|
+
maxCapturedTurn(sessionId: string): number | undefined;
|
|
24
|
+
/** §A 幂等检查:该 (session, turn) 是否已有任意 L0 行(委托 MemoryDb)。 */
|
|
25
|
+
hasAnyL0Message(sessionId: string, turn: number): boolean;
|
|
22
26
|
/** 检索:FTS + 向量 hybrid(RRF 融合),返回按相关性排序的消息。 */
|
|
23
27
|
search(query: string, limit: number): Promise<L0MessageRecord[]>;
|
|
24
28
|
/** 活切换嵌入源:同步换底层服务(嵌入源三态切换用)。 */
|
package/dist/store/l0.js
CHANGED
|
@@ -128,6 +128,14 @@ export class L0Store {
|
|
|
128
128
|
async recentBySession(sessionId, limit) {
|
|
129
129
|
return this.db.recentL0BySession(sessionId, limit);
|
|
130
130
|
}
|
|
131
|
+
/** §A 水位线:该会话 L0 已落盘的最大 turn(空会话 undefined;委托 MemoryDb)。 */
|
|
132
|
+
maxCapturedTurn(sessionId) {
|
|
133
|
+
return this.db.maxCapturedTurn(sessionId);
|
|
134
|
+
}
|
|
135
|
+
/** §A 幂等检查:该 (session, turn) 是否已有任意 L0 行(委托 MemoryDb)。 */
|
|
136
|
+
hasAnyL0Message(sessionId, turn) {
|
|
137
|
+
return this.db.hasAnyL0Message(sessionId, turn);
|
|
138
|
+
}
|
|
131
139
|
/** 检索:FTS + 向量 hybrid(RRF 融合),返回按相关性排序的消息。 */
|
|
132
140
|
async search(query, limit) {
|
|
133
141
|
const caps = this.db.getCapabilities();
|
package/dist/store/l1.d.ts
CHANGED
|
@@ -61,6 +61,8 @@ export declare class L1Store {
|
|
|
61
61
|
private readonly decayHalfLifeDays;
|
|
62
62
|
/** §D 第 3 路(图谱回链);缺省 = 不接,恰为 2 路。 */
|
|
63
63
|
private readonly graphLaneProvider?;
|
|
64
|
+
/** 孤儿记忆计数(没有任何 Room 的记录;rooms-get 面板「无绑定」chip 用)。 */
|
|
65
|
+
untaggedL1Count(): number;
|
|
64
66
|
/** Room 计数缓存(见 `listRooms()`:tags 仅随反刍变化,不必每次敲库)。 */
|
|
65
67
|
private roomCache;
|
|
66
68
|
constructor(dataDir: string, db: MemoryDb, embed?: EmbeddingService, strategy?: RecallStrategy, logger?: MemoryLogger,
|
|
@@ -68,6 +70,8 @@ export declare class L1Store {
|
|
|
68
70
|
decayHalfLifeDays?: number,
|
|
69
71
|
/** 图谱路提供者(§D 第 3 路);不传则该路不存在,融合退回双路。 */
|
|
70
72
|
graphLane?: GraphLaneProvider);
|
|
73
|
+
/** 读路径时效半衰期(天;0=关)。老化权重展示与召回衰减同一事实源。 */
|
|
74
|
+
get decayHalfLife(): number;
|
|
71
75
|
init(): Promise<void>;
|
|
72
76
|
/** 旧版单文件 records.jsonl 一次性导入检索库,成功后改名 .imported。 */
|
|
73
77
|
private importLegacy;
|
|
@@ -88,6 +92,34 @@ export declare class L1Store {
|
|
|
88
92
|
* 返回 false 表示 id 不存在或写入失败——调用方须记账,不许静默。
|
|
89
93
|
*/
|
|
90
94
|
patchMetadata(id: string, metadata: Record<string, unknown>): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* 收集某 Room 的全部匹配记录(含已退场行,口径与 Room 计数一致)。
|
|
97
|
+
* 内部分页拉取,上限 cap 防跑飞;返回记录数组供备份与干跑预览。
|
|
98
|
+
*/
|
|
99
|
+
listByTagAll(tag: string, cap?: number): MemoryRecord[];
|
|
100
|
+
/**
|
|
101
|
+
* 备份某 Room 的匹配记录到 `<dataDir>/rooms-merge-backups/<ts>-<tag>.json`
|
|
102
|
+
* (执行前快照;破坏性重写前的唯一回滚物料)。返回备份文件路径与条数。
|
|
103
|
+
*/
|
|
104
|
+
backupTagRecords(tag: string): {
|
|
105
|
+
file: string;
|
|
106
|
+
count: number;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* 游标重写:把 metadata.tags 里的 `from` 全部替换为 `to`(**含已退场行**,
|
|
110
|
+
* 口径与 Room 计数一致)。写前重读-合并-写回(relabel 同款):只动 tags,
|
|
111
|
+
* roomCandidates/roomReview/hall/cogHall/sourceAnchors 等其余键原样保全。
|
|
112
|
+
*
|
|
113
|
+
* 单次上限 cap(500)可续跑:再次调用同一 from 即继续处理剩余行。
|
|
114
|
+
* 返回重写条数 + 受影响的 (family, scene) 集(供 recluster 'room-merge' 入队)。
|
|
115
|
+
*/
|
|
116
|
+
rewriteTag(from: string, to: string, cap?: number): {
|
|
117
|
+
rewritten: number;
|
|
118
|
+
scanned: number;
|
|
119
|
+
families: string[];
|
|
120
|
+
scenes: string[];
|
|
121
|
+
hasMore: boolean;
|
|
122
|
+
};
|
|
91
123
|
/** 按 id 精确取记录(去重决策的版本号查询用,避免全表扫描)。 */
|
|
92
124
|
getByIds(ids: string[]): MemoryRecord[];
|
|
93
125
|
/**
|
|
@@ -291,6 +323,8 @@ export declare class L1Store {
|
|
|
291
323
|
halls?: readonly string[];
|
|
292
324
|
/** Room 过滤(metadata.tags 含该 slug)。 */
|
|
293
325
|
tag?: string;
|
|
326
|
+
/** 孤儿记忆筛选(true = 只列没有 Room 的记录;与 tag 互斥,tag 优先)。 */
|
|
327
|
+
untagged?: boolean;
|
|
294
328
|
workspaceId?: string;
|
|
295
329
|
/** 退场筛查:三态(`undefined` 全部 / `false` 仅活跃 / `true` 仅已退场)。 */
|
|
296
330
|
retired?: boolean;
|
package/dist/store/l1.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 去重/合并的更新记录走 upsert(新 record id + 版本递增),不再全量重写文件。
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync, promises as fs } from 'node:fs';
|
|
9
|
+
import { existsSync, mkdirSync, promises as fs, writeFileSync } from 'node:fs';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import { familyForType, isScopeVisible } from '../types.js';
|
|
12
12
|
import { graphHitRecordIds } from '../graph/search.js';
|
|
@@ -34,6 +34,10 @@ export class L1Store {
|
|
|
34
34
|
decayHalfLifeDays;
|
|
35
35
|
/** §D 第 3 路(图谱回链);缺省 = 不接,恰为 2 路。 */
|
|
36
36
|
graphLaneProvider;
|
|
37
|
+
/** 孤儿记忆计数(没有任何 Room 的记录;rooms-get 面板「无绑定」chip 用)。 */
|
|
38
|
+
untaggedL1Count() {
|
|
39
|
+
return this.db.untaggedL1Count();
|
|
40
|
+
}
|
|
37
41
|
/** Room 计数缓存(见 `listRooms()`:tags 仅随反刍变化,不必每次敲库)。 */
|
|
38
42
|
roomCache = null;
|
|
39
43
|
constructor(dataDir, db, embed = new NoopEmbeddingService(), strategy = 'hybrid', logger,
|
|
@@ -52,6 +56,10 @@ export class L1Store {
|
|
|
52
56
|
this.decayHalfLifeDays = decayHalfLifeDays ?? 30;
|
|
53
57
|
this.graphLaneProvider = graphLane;
|
|
54
58
|
}
|
|
59
|
+
/** 读路径时效半衰期(天;0=关)。老化权重展示与召回衰减同一事实源。 */
|
|
60
|
+
get decayHalfLife() {
|
|
61
|
+
return this.decayHalfLifeDays;
|
|
62
|
+
}
|
|
55
63
|
async init() {
|
|
56
64
|
await ensureDir(this.recordsDir);
|
|
57
65
|
await this.importLegacy();
|
|
@@ -113,6 +121,65 @@ export class L1Store {
|
|
|
113
121
|
patchMetadata(id, metadata) {
|
|
114
122
|
return this.db.patchL1Metadata(id, metadata);
|
|
115
123
|
}
|
|
124
|
+
// ── Room merge/rename 的游标重写(分类管理 beta.5;破坏性面)──────────────
|
|
125
|
+
/**
|
|
126
|
+
* 收集某 Room 的全部匹配记录(含已退场行,口径与 Room 计数一致)。
|
|
127
|
+
* 内部分页拉取,上限 cap 防跑飞;返回记录数组供备份与干跑预览。
|
|
128
|
+
*/
|
|
129
|
+
listByTagAll(tag, cap = 500) {
|
|
130
|
+
const out = [];
|
|
131
|
+
for (let offset = 0; offset < cap; offset += 200) {
|
|
132
|
+
const page = this.list({ tag, retired: undefined, limit: Math.min(200, cap - out.length), offset });
|
|
133
|
+
out.push(...page.items);
|
|
134
|
+
if (page.items.length < 200 || out.length >= cap)
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 备份某 Room 的匹配记录到 `<dataDir>/rooms-merge-backups/<ts>-<tag>.json`
|
|
141
|
+
* (执行前快照;破坏性重写前的唯一回滚物料)。返回备份文件路径与条数。
|
|
142
|
+
*/
|
|
143
|
+
backupTagRecords(tag) {
|
|
144
|
+
const records = this.listByTagAll(tag, 2000);
|
|
145
|
+
const backupDir = path.join(this.dataDir, 'rooms-merge-backups');
|
|
146
|
+
if (!existsSync(backupDir))
|
|
147
|
+
mkdirSync(backupDir, { recursive: true });
|
|
148
|
+
const file = path.join(backupDir, `${Date.now()}-${tag.replace(/[^a-z0-9-]/gi, '_')}.json`);
|
|
149
|
+
writeFileSync(file, JSON.stringify({ tag, exportedAt: new Date().toISOString(), records }, null, 2), 'utf8');
|
|
150
|
+
return { file, count: records.length };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 游标重写:把 metadata.tags 里的 `from` 全部替换为 `to`(**含已退场行**,
|
|
154
|
+
* 口径与 Room 计数一致)。写前重读-合并-写回(relabel 同款):只动 tags,
|
|
155
|
+
* roomCandidates/roomReview/hall/cogHall/sourceAnchors 等其余键原样保全。
|
|
156
|
+
*
|
|
157
|
+
* 单次上限 cap(500)可续跑:再次调用同一 from 即继续处理剩余行。
|
|
158
|
+
* 返回重写条数 + 受影响的 (family, scene) 集(供 recluster 'room-merge' 入队)。
|
|
159
|
+
*/
|
|
160
|
+
rewriteTag(from, to, cap = 500) {
|
|
161
|
+
const rows = this.listByTagAll(from, cap + 1);
|
|
162
|
+
const hasMore = rows.length > cap;
|
|
163
|
+
const batch = rows.slice(0, cap);
|
|
164
|
+
let rewritten = 0;
|
|
165
|
+
const families = new Set();
|
|
166
|
+
const scenes = new Set();
|
|
167
|
+
for (const r of batch) {
|
|
168
|
+
const meta = { ...(r.metadata ?? {}) };
|
|
169
|
+
const tags = Array.isArray(meta.tags) ? meta.tags.map(String) : [];
|
|
170
|
+
if (!tags.includes(from))
|
|
171
|
+
continue; // 命中是 json_each 判等,数组缺失即无可改
|
|
172
|
+
const next = tags.map((t) => (t === from ? to : t));
|
|
173
|
+
meta.tags = [...new Set(next)];
|
|
174
|
+
if (this.patchMetadata(r.id, meta)) {
|
|
175
|
+
rewritten++;
|
|
176
|
+
families.add(r.family ?? 'chat');
|
|
177
|
+
if (r.scene_name)
|
|
178
|
+
scenes.add(r.scene_name);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { rewritten, scanned: batch.length, families: [...families], scenes: [...scenes], hasMore };
|
|
182
|
+
}
|
|
116
183
|
/** 按 id 精确取记录(去重决策的版本号查询用,避免全表扫描)。 */
|
|
117
184
|
getByIds(ids) {
|
|
118
185
|
return this.db.getL1ByIds(ids);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { MemoryLogger } from '../types.js';
|
|
2
|
+
export declare const RECALL_USAGE_FILE_VERSION: 1;
|
|
3
|
+
export interface RecallUsageEntry {
|
|
4
|
+
attempts: number;
|
|
5
|
+
used: number;
|
|
6
|
+
lastAttemptAt: number | null;
|
|
7
|
+
lastUsedAt: number | null;
|
|
8
|
+
}
|
|
9
|
+
export interface RecallUsageSummary {
|
|
10
|
+
/** 有统计的记忆条数。 */
|
|
11
|
+
tracked: number;
|
|
12
|
+
/** 尝试总次数(Σ attempts)。 */
|
|
13
|
+
attempted: number;
|
|
14
|
+
/** 使用总次数(Σ used)。 */
|
|
15
|
+
used: number;
|
|
16
|
+
/** 使用率(百分比整数;attempted=0 时 0)。 */
|
|
17
|
+
rate: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class RecallUsageStore {
|
|
20
|
+
private readonly logger?;
|
|
21
|
+
private readonly file;
|
|
22
|
+
private entries;
|
|
23
|
+
private loaded;
|
|
24
|
+
private degraded;
|
|
25
|
+
private degradedLogged;
|
|
26
|
+
private writeTimer;
|
|
27
|
+
private writeChain;
|
|
28
|
+
constructor(dataDir: string, logger?: MemoryLogger | undefined);
|
|
29
|
+
init(): Promise<void>;
|
|
30
|
+
private ensureLoaded;
|
|
31
|
+
/** 一轮召回:候选集 attempts++/lastAttemptAt=now;used 子集额外 used++/lastUsedAt=now
|
|
32
|
+
* (used 不在候选内 = 防御路径,同样算一次尝试——用了就是被尝试过)。 */
|
|
33
|
+
recordAttempt(candidateIds: readonly string[], usedIds: readonly string[]): void;
|
|
34
|
+
get(id: string): RecallUsageEntry | undefined;
|
|
35
|
+
summary(): RecallUsageSummary;
|
|
36
|
+
/** 立即落盘(去抖窗口内的兜底;测试/停机用)。 */
|
|
37
|
+
flush(): Promise<void>;
|
|
38
|
+
/** 同步等待在途写(测试用;无在途写时立即返回)。 */
|
|
39
|
+
flushSync(): void;
|
|
40
|
+
private prune;
|
|
41
|
+
private schedulePersist;
|
|
42
|
+
private persist;
|
|
43
|
+
}
|