dsh-prime-memory 0.18.5 → 0.18.7
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 +11 -28
- package/dist/client.js +159 -1297
- package/dist/contract.d.ts +1 -109
- package/dist/hooks/recall.d.ts +1 -4
- package/dist/hooks/recall.js +1 -7
- package/dist/hooks/slot-recall.js +24 -1
- package/dist/index.js +4 -14
- package/dist/metadata-validators.d.ts +0 -5
- package/dist/metadata-validators.js +1 -11
- package/dist/pipeline/ruminate.d.ts +1 -11
- package/dist/pipeline/ruminate.js +1 -18
- package/dist/projection/slots.js +9 -14
- package/dist/stats.d.ts +2 -26
- package/dist/stats.js +13 -261
- package/dist/store/l1.d.ts +0 -34
- package/dist/store/l1.js +1 -68
- package/dist/store/sqlite.d.ts +0 -4
- package/dist/store/sqlite.js +0 -20
- package/dist/tools/index.d.ts +1 -4
- package/dist/tools/index.js +2 -224
- package/dist/types.d.ts +0 -4
- package/package.json +2 -1
- package/dist/room-review.d.ts +0 -196
- package/dist/room-review.js +0 -281
- package/dist/store/recall-usage.d.ts +0 -43
- package/dist/store/recall-usage.js +0 -149
- package/dist/store/rooms-registry.d.ts +0 -66
- package/dist/store/rooms-registry.js +0 -250
package/dist/stats.js
CHANGED
|
@@ -18,7 +18,6 @@ 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';
|
|
22
21
|
import { projectDistillChain, validateDistillChain } from './settings.js';
|
|
23
22
|
import { RECEIPTS_QUERY_LIMIT_MAX, dimensionOf, toReceiptView } from './store/receipts.js';
|
|
24
23
|
import { getTraceStore } from './store/trace.js';
|
|
@@ -27,7 +26,7 @@ import { sourceAnchorLabels } from './pipeline/anchors.js';
|
|
|
27
26
|
import { readSupersedeMarker } from './store/supersede.js';
|
|
28
27
|
import { isSnapshotName } from './store/l1-snapshot.js';
|
|
29
28
|
import { WING_CATALOG, WING_FALLBACK } from './types.js';
|
|
30
|
-
import {
|
|
29
|
+
import { isTag } from './metadata-validators.js';
|
|
31
30
|
import { InProcMemoryBackend } from './store/memory-backend.js';
|
|
32
31
|
import { isWingCorner } from './store/session-modes.js';
|
|
33
32
|
import { startWingBackfill } from './wing-backfill.js';
|
|
@@ -36,7 +35,7 @@ import { snapshotTokenCost } from './token-cost.js';
|
|
|
36
35
|
const require = createRequire(import.meta.url);
|
|
37
36
|
export const PLUGIN_VERSION = require('../package.json').version;
|
|
38
37
|
/**
|
|
39
|
-
* 端点全集运行时清单(
|
|
38
|
+
* 端点全集运行时清单(36 个,与 tests/contract-keys.test.ts 的 ENDPOINTS 及
|
|
40
39
|
* contract.ts 类型映射表三方对齐,漂移由键集 diff 测试暴露)。
|
|
41
40
|
* 注意:本清单同时是 HTTP 前缀路由 `/dsh-memory/rpc/<短名>` 的**放行白名单**
|
|
42
41
|
* (见下方 SHORT_ENDPOINTS),漏一条 = 该端点在面板里静默消失(404 被客户端
|
|
@@ -53,9 +52,6 @@ export const MEMORY_ENDPOINTS = [
|
|
|
53
52
|
'dsh-memory/session-mode-set',
|
|
54
53
|
'dsh-memory/wing-overview',
|
|
55
54
|
'dsh-memory/rooms-get',
|
|
56
|
-
'dsh-memory/rooms-export',
|
|
57
|
-
'dsh-memory/room-register',
|
|
58
|
-
'dsh-memory/room-admin',
|
|
59
55
|
'dsh-memory/wing-backfill',
|
|
60
56
|
'dsh-memory/session-stats',
|
|
61
57
|
'dsh-memory/settings-get',
|
|
@@ -172,15 +168,12 @@ export function buildEndpointDeps(base, sources, controller) {
|
|
|
172
168
|
embedManager: sources.embedManager,
|
|
173
169
|
sessionInfo: sources.sessionInfo,
|
|
174
170
|
ruminate: controller,
|
|
175
|
-
roomRegistry: sources.roomRegistry,
|
|
176
171
|
};
|
|
177
172
|
return { ...base, ...injected };
|
|
178
173
|
}
|
|
179
174
|
export function registerMemoryRpc(ctx, cfg, stores, logger, status, live, modes, dataDir, rebuild, embedManager, sessionInfo,
|
|
180
175
|
/** 反刍控制器(存储降级时为 undefined):经 buildEndpointDeps 落入 deps.ruminate。 */
|
|
181
|
-
ruminate
|
|
182
|
-
/** Room 分类管理依赖(注册表;缺省 = rooms-get 不带 registry、rooms-export 恒 404 语义)。 */
|
|
183
|
-
roomRegistry) {
|
|
176
|
+
ruminate) {
|
|
184
177
|
/** 当前是否持有一段有效注册(dispose 完成后清空,允许服务重上线时重注册)。 */
|
|
185
178
|
let holding = false;
|
|
186
179
|
/** 当前 handle 绑定的 connection 实例(internal/service 第二参;用于识别实例替换)。 */
|
|
@@ -198,7 +191,7 @@ roomRegistry) {
|
|
|
198
191
|
// 端点处理器:HTTP 层与旧 connection.rpc 的 handler 共用同一分发。
|
|
199
192
|
const rpcHandler = async (endpoint, payload) => {
|
|
200
193
|
try {
|
|
201
|
-
const value = await handleEndpoint(endpoint, payload, buildEndpointDeps({ ctx, cfg, stores, logger }, { status, live, modes, dataDir, rebuild, embedManager, sessionInfo
|
|
194
|
+
const value = await handleEndpoint(endpoint, payload, buildEndpointDeps({ ctx, cfg, stores, logger }, { status, live, modes, dataDir, rebuild, embedManager, sessionInfo }, ruminate));
|
|
202
195
|
return { ok: true, value };
|
|
203
196
|
}
|
|
204
197
|
catch (err) {
|
|
@@ -346,7 +339,7 @@ function sanitizeSettings(s) {
|
|
|
346
339
|
}
|
|
347
340
|
/** 端点分发表(导出供测试直调:可精确注入 rebuild/ruminate 等可选控制器,验证 deps 接线)。 */
|
|
348
341
|
export async function handleEndpoint(endpoint, payload, deps) {
|
|
349
|
-
const { cfg, stores, status, live, modes, dataDir, rebuild, ruminate, embedManager, sessionInfo
|
|
342
|
+
const { cfg, stores, status, live, modes, dataDir, rebuild, ruminate, embedManager, sessionInfo } = deps;
|
|
350
343
|
switch (endpoint) {
|
|
351
344
|
case 'dsh-memory/stats':
|
|
352
345
|
return buildStats(cfg, stores, status);
|
|
@@ -463,223 +456,10 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
463
456
|
// ── Room 分类计数(标签自生长分类;展开 metadata.tags 聚合,零 schema) ──
|
|
464
457
|
case 'dsh-memory/rooms-get': {
|
|
465
458
|
const rooms = stores.l1.listRooms();
|
|
466
|
-
|
|
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 };
|
|
459
|
+
const v = { rooms, total: rooms.length };
|
|
681
460
|
return v;
|
|
682
461
|
}
|
|
462
|
+
// ── 一键回填(task_15):后台任务,单飞;端点立即返回,进度看 wing-overview ──
|
|
683
463
|
case 'dsh-memory/wing-backfill': {
|
|
684
464
|
const r = startWingBackfill({
|
|
685
465
|
ctx: deps.ctx,
|
|
@@ -973,10 +753,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
973
753
|
const retiredSel = typeof p.retired === 'boolean' ? p.retired : undefined;
|
|
974
754
|
// Room 过滤:tag 是**自生长**的 slug(无枚举),只做形状与长度校验(SQL 侧参数化)
|
|
975
755
|
const tagSel = typeof p.tag === 'string' ? p.tag.trim().slice(0, 64) : '';
|
|
976
|
-
if (tagSel && !
|
|
977
|
-
throw new Error('tag 非法(
|
|
978
|
-
// 孤儿记忆筛选(与 tag 互斥,tag 优先):只认显式布尔,其余视为缺省。
|
|
979
|
-
const untaggedSel = p.untagged === true && !tagSel;
|
|
756
|
+
if (tagSel && !isTag(tagSel))
|
|
757
|
+
throw new Error('tag 非法(需小写字母数字连字符,1-32 字符)');
|
|
980
758
|
// R13 多值归一: halls 数组只留非空字符串(≤40 字符),去重,上限 8(角数);
|
|
981
759
|
// wing 单值保留兼容,归一后与 halls 合并
|
|
982
760
|
const wingSel = Array.from(new Set([
|
|
@@ -1017,46 +795,29 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1017
795
|
if (!Array.isArray(tags) || !tags.some((t) => t === tagSel))
|
|
1018
796
|
return false;
|
|
1019
797
|
}
|
|
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
|
-
}
|
|
1027
798
|
return true;
|
|
1028
799
|
});
|
|
1029
800
|
}
|
|
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]));
|
|
1032
801
|
const resp = {
|
|
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
|
-
})),
|
|
802
|
+
items: filtered.slice(offset, offset + limit).map((h) => hitToUiRecord({ ...h, metadata: metaById?.get(h.id) })),
|
|
1037
803
|
hasMore: filtered.length > offset + limit,
|
|
1038
804
|
total: null,
|
|
1039
805
|
truncated: wanted > SEARCH_CAP,
|
|
1040
806
|
scenes: offset === 0 ? stores.l1.distinctScenes() : undefined,
|
|
1041
807
|
wingCatalog,
|
|
1042
|
-
usageSummary: stores.usage?.summary() ?? null,
|
|
1043
808
|
};
|
|
1044
809
|
return resp;
|
|
1045
810
|
}
|
|
1046
811
|
// 退场筛查仅浏览路径透传;检索路径不接——FTS/向量里本就没有已退场行,
|
|
1047
812
|
// 接了只会让「仅退场+关键词」永远空结果,不如如实不筛。
|
|
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,
|
|
813
|
+
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 });
|
|
1049
814
|
const resp = {
|
|
1050
|
-
items: items.map(
|
|
1051
|
-
agingWeight: agingWeightOf(r.updatedAt, stores.l1.decayHalfLife),
|
|
1052
|
-
usage: stores.usage?.get(r.id) ?? null,
|
|
1053
|
-
})),
|
|
815
|
+
items: items.map(hitToUiRecord),
|
|
1054
816
|
hasMore: offset + items.length < total,
|
|
1055
817
|
total,
|
|
1056
818
|
truncated: false,
|
|
1057
819
|
scenes: offset === 0 ? stores.l1.distinctScenes() : undefined,
|
|
1058
820
|
wingCatalog,
|
|
1059
|
-
usageSummary: stores.usage?.summary() ?? null,
|
|
1060
821
|
};
|
|
1061
822
|
return resp;
|
|
1062
823
|
}
|
|
@@ -1663,14 +1424,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1663
1424
|
}
|
|
1664
1425
|
}
|
|
1665
1426
|
/** 浏览器卡片字段(比 MemoryRecord 精简,去掉大 metadata;Hall 从 metadata 提取)。 */
|
|
1666
|
-
|
|
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) {
|
|
1427
|
+
export function hitToUiRecord(r) {
|
|
1674
1428
|
const retired = r.validTo !== undefined;
|
|
1675
1429
|
const mark = retired ? readSupersedeMarker(r.metadata) : undefined;
|
|
1676
1430
|
return {
|
|
@@ -1696,8 +1450,6 @@ export function hitToUiRecord(r, extra) {
|
|
|
1696
1450
|
// 但必须给出可见差异。
|
|
1697
1451
|
retired,
|
|
1698
1452
|
retiredReason: retired ? (mark?.reason ?? 'unknown') : null,
|
|
1699
|
-
agingWeight: extra?.agingWeight ?? null,
|
|
1700
|
-
usage: extra?.usage ?? null,
|
|
1701
1453
|
};
|
|
1702
1454
|
}
|
|
1703
1455
|
/**
|
package/dist/store/l1.d.ts
CHANGED
|
@@ -61,8 +61,6 @@ 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;
|
|
66
64
|
/** Room 计数缓存(见 `listRooms()`:tags 仅随反刍变化,不必每次敲库)。 */
|
|
67
65
|
private roomCache;
|
|
68
66
|
constructor(dataDir: string, db: MemoryDb, embed?: EmbeddingService, strategy?: RecallStrategy, logger?: MemoryLogger,
|
|
@@ -70,8 +68,6 @@ export declare class L1Store {
|
|
|
70
68
|
decayHalfLifeDays?: number,
|
|
71
69
|
/** 图谱路提供者(§D 第 3 路);不传则该路不存在,融合退回双路。 */
|
|
72
70
|
graphLane?: GraphLaneProvider);
|
|
73
|
-
/** 读路径时效半衰期(天;0=关)。老化权重展示与召回衰减同一事实源。 */
|
|
74
|
-
get decayHalfLife(): number;
|
|
75
71
|
init(): Promise<void>;
|
|
76
72
|
/** 旧版单文件 records.jsonl 一次性导入检索库,成功后改名 .imported。 */
|
|
77
73
|
private importLegacy;
|
|
@@ -92,34 +88,6 @@ export declare class L1Store {
|
|
|
92
88
|
* 返回 false 表示 id 不存在或写入失败——调用方须记账,不许静默。
|
|
93
89
|
*/
|
|
94
90
|
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
|
-
};
|
|
123
91
|
/** 按 id 精确取记录(去重决策的版本号查询用,避免全表扫描)。 */
|
|
124
92
|
getByIds(ids: string[]): MemoryRecord[];
|
|
125
93
|
/**
|
|
@@ -323,8 +291,6 @@ export declare class L1Store {
|
|
|
323
291
|
halls?: readonly string[];
|
|
324
292
|
/** Room 过滤(metadata.tags 含该 slug)。 */
|
|
325
293
|
tag?: string;
|
|
326
|
-
/** 孤儿记忆筛选(true = 只列没有 Room 的记录;与 tag 互斥,tag 优先)。 */
|
|
327
|
-
untagged?: boolean;
|
|
328
294
|
workspaceId?: string;
|
|
329
295
|
/** 退场筛查:三态(`undefined` 全部 / `false` 仅活跃 / `true` 仅已退场)。 */
|
|
330
296
|
retired?: boolean;
|
package/dist/store/l1.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 去重/合并的更新记录走 upsert(新 record id + 版本递增),不再全量重写文件。
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync,
|
|
9
|
+
import { existsSync, promises as fs } 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,10 +34,6 @@ 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
|
-
}
|
|
41
37
|
/** Room 计数缓存(见 `listRooms()`:tags 仅随反刍变化,不必每次敲库)。 */
|
|
42
38
|
roomCache = null;
|
|
43
39
|
constructor(dataDir, db, embed = new NoopEmbeddingService(), strategy = 'hybrid', logger,
|
|
@@ -56,10 +52,6 @@ export class L1Store {
|
|
|
56
52
|
this.decayHalfLifeDays = decayHalfLifeDays ?? 30;
|
|
57
53
|
this.graphLaneProvider = graphLane;
|
|
58
54
|
}
|
|
59
|
-
/** 读路径时效半衰期(天;0=关)。老化权重展示与召回衰减同一事实源。 */
|
|
60
|
-
get decayHalfLife() {
|
|
61
|
-
return this.decayHalfLifeDays;
|
|
62
|
-
}
|
|
63
55
|
async init() {
|
|
64
56
|
await ensureDir(this.recordsDir);
|
|
65
57
|
await this.importLegacy();
|
|
@@ -121,65 +113,6 @@ export class L1Store {
|
|
|
121
113
|
patchMetadata(id, metadata) {
|
|
122
114
|
return this.db.patchL1Metadata(id, metadata);
|
|
123
115
|
}
|
|
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
|
-
}
|
|
183
116
|
/** 按 id 精确取记录(去重决策的版本号查询用,避免全表扫描)。 */
|
|
184
117
|
getByIds(ids) {
|
|
185
118
|
return this.db.getL1ByIds(ids);
|
package/dist/store/sqlite.d.ts
CHANGED
|
@@ -384,8 +384,6 @@ export declare class MemoryDb {
|
|
|
384
384
|
halls?: readonly string[];
|
|
385
385
|
/** Room 过滤(metadata.tags 含该 slug)。 */
|
|
386
386
|
tag?: string;
|
|
387
|
-
/** 孤儿记忆筛选(true = 只列没有 Room 的记录;与 tag 互斥,tag 优先)。 */
|
|
388
|
-
untagged?: boolean;
|
|
389
387
|
workspaceId?: string;
|
|
390
388
|
/**
|
|
391
389
|
* 退场(软删)筛查:三态。
|
|
@@ -431,8 +429,6 @@ export declare class MemoryDb {
|
|
|
431
429
|
room: string;
|
|
432
430
|
count: number;
|
|
433
431
|
}>;
|
|
434
|
-
/** 孤儿记忆计数:没有任何 Room(tags 为空/缺失)的记录条数(含已退场,与 l1RoomCounts 同口径)。 */
|
|
435
|
-
untaggedL1Count(): number;
|
|
436
432
|
/** Hall 域计数(八边形角数据源):按 metadata.hall 分组计数 + 未打标行数。失败返回空。 */
|
|
437
433
|
wingL1Counts(): {
|
|
438
434
|
counts: Record<string, number>;
|
package/dist/store/sqlite.js
CHANGED
|
@@ -1486,11 +1486,6 @@ export class MemoryDb {
|
|
|
1486
1486
|
where.push(`EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(metadata_json) THEN metadata_json ELSE '{}' END, '$.tags') j WHERE j.value = ?)`);
|
|
1487
1487
|
params.push(opts.tag);
|
|
1488
1488
|
}
|
|
1489
|
-
else if (opts.untagged) {
|
|
1490
|
-
// 孤儿记忆:没有任何 Room(tags 数组缺失/为空/字段非法)。与 room 计数同一
|
|
1491
|
-
// json_valid 守卫口径;与 l1RoomCounts() 计数互补(rooms 计数 + 孤儿数 = 全量)。
|
|
1492
|
-
where.push(`NOT EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(metadata_json) THEN metadata_json ELSE '{}' END, '$.tags'))`);
|
|
1493
|
-
}
|
|
1494
1489
|
// 退场筛查(三态,见 opts.retired 注释):与 listRetiredL1 同一判据,保证
|
|
1495
1490
|
// 「仅退场」视图与「已退场」区看到同一批行。
|
|
1496
1491
|
if (opts.retired === false) {
|
|
@@ -1606,21 +1601,6 @@ export class MemoryDb {
|
|
|
1606
1601
|
return [];
|
|
1607
1602
|
}
|
|
1608
1603
|
}
|
|
1609
|
-
/** 孤儿记忆计数:没有任何 Room(tags 为空/缺失)的记录条数(含已退场,与 l1RoomCounts 同口径)。 */
|
|
1610
|
-
untaggedL1Count() {
|
|
1611
|
-
if (this.degraded)
|
|
1612
|
-
return 0;
|
|
1613
|
-
try {
|
|
1614
|
-
const row = this.db
|
|
1615
|
-
.prepare(`SELECT COUNT(*) AS n FROM l1_records r
|
|
1616
|
-
WHERE NOT EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(r.metadata_json) THEN r.metadata_json ELSE '{}' END, '$.tags'))`)
|
|
1617
|
-
.get();
|
|
1618
|
-
return Number(row?.n ?? 0);
|
|
1619
|
-
}
|
|
1620
|
-
catch {
|
|
1621
|
-
return 0;
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
1604
|
/** Hall 域计数(八边形角数据源):按 metadata.hall 分组计数 + 未打标行数。失败返回空。 */
|
|
1625
1605
|
wingL1Counts() {
|
|
1626
1606
|
if (this.degraded)
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -22,7 +22,6 @@ 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';
|
|
26
25
|
export declare function registerMemoryTools(ctx: Context, cfg: MemoryConfig, stores: {
|
|
27
26
|
l0: L0Store;
|
|
28
27
|
l1: L1Store;
|
|
@@ -32,6 +31,4 @@ export declare function registerMemoryTools(ctx: Context, cfg: MemoryConfig, sto
|
|
|
32
31
|
graph?: GraphStore;
|
|
33
32
|
}, logger: MemoryLogger, modes: SessionModeStore, live: LiveSettingsHandle,
|
|
34
33
|
/** 反刍控制器(可选:未装配时 ruminate 工具返回未启用提示)。 */
|
|
35
|
-
ruminate?: RuminateController
|
|
36
|
-
/** Room 注册表(分类管理 beta.4;缺省 = memory_room_admin 返回未装配提示)。 */
|
|
37
|
-
roomRegistry?: RoomRegistryStore): void;
|
|
34
|
+
ruminate?: RuminateController): void;
|