dsh-prime-memory 0.18.4 → 0.18.5
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 +28 -0
- package/dist/client.js +1297 -159
- package/dist/contract.d.ts +109 -1
- package/dist/hooks/capture.js +6 -2
- package/dist/hooks/recall.d.ts +4 -1
- package/dist/hooks/recall.js +10 -3
- package/dist/index.js +14 -4
- package/dist/metadata-validators.d.ts +5 -0
- package/dist/metadata-validators.js +11 -1
- package/dist/pipeline/ruminate.d.ts +11 -1
- package/dist/pipeline/ruminate.js +18 -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 +261 -13
- 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 +4 -0
- package/dist/store/sqlite.js +20 -0
- package/dist/tools/index.d.ts +4 -1
- package/dist/tools/index.js +224 -1
- package/dist/types.d.ts +4 -0
- package/package.json +13 -12
- 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/contract.d.ts
CHANGED
|
@@ -507,10 +507,91 @@ export interface WingOverviewResponse {
|
|
|
507
507
|
* 降级/无 tags 时 `rooms` 为空数组(面板显示"暂无"),不报错。
|
|
508
508
|
*/
|
|
509
509
|
export interface RoomsGetResponse {
|
|
510
|
-
/** Room 列表(
|
|
510
|
+
/** Room 列表(按记录数降序,同数按名升序;注册表条目 count=0 并带 source/label)。
|
|
511
|
+
* 条目可选字段 source/label 由端点层合并注册表后附带(客户端可选读,零断裂)。 */
|
|
511
512
|
rooms: RoomCount[];
|
|
512
513
|
/** Room 总数(= rooms.length,冗余只为前端少写一次 .length)。 */
|
|
513
514
|
total: number;
|
|
515
|
+
/** 孤儿记忆数:没有任何 Room(tags 为空/缺失)的记录条数(与 rooms 同口径含已退场)。 */
|
|
516
|
+
orphanCount: number;
|
|
517
|
+
/** 注册表条目(全量含 retired;分类管理面板用;未装配 = undefined)。 */
|
|
518
|
+
registry?: Array<{
|
|
519
|
+
slug: string;
|
|
520
|
+
label?: string;
|
|
521
|
+
description?: string;
|
|
522
|
+
source: 'pre-registered' | 'grown';
|
|
523
|
+
aliases?: string[];
|
|
524
|
+
status: 'active' | 'retired';
|
|
525
|
+
}>;
|
|
526
|
+
}
|
|
527
|
+
/** dsh-memory/room-register(预注册 Room;高权限:面板 memoryMutate 开启才生效)。 */
|
|
528
|
+
export interface RoomRegisterRequest {
|
|
529
|
+
slug: string;
|
|
530
|
+
label?: string;
|
|
531
|
+
description?: string;
|
|
532
|
+
/** 收编自生长 slug 时传 grown(缺省 pre-registered;已存在条目忽略此字段)。 */
|
|
533
|
+
source?: 'pre-registered' | 'grown';
|
|
534
|
+
}
|
|
535
|
+
export interface RoomRegisterResponse {
|
|
536
|
+
slug: string;
|
|
537
|
+
created: boolean;
|
|
538
|
+
/** 幂等补全时的提示(如「已存在,只补全缺失字段」)。 */
|
|
539
|
+
notice?: string;
|
|
540
|
+
}
|
|
541
|
+
/** dsh-memory/rooms-export(分类数据导出,CSV 直出)。 */
|
|
542
|
+
export interface RoomsExportRequest {
|
|
543
|
+
/** rooms = 词表+计数;orphans = 无 Room 绑定的记录清单;records = 按 Room 的记录清单(tag 必传)。 */
|
|
544
|
+
kind: 'rooms' | 'orphans' | 'records';
|
|
545
|
+
/** 各 kind 通用上限(1..10000,默认 2000)。 */
|
|
546
|
+
limit?: number;
|
|
547
|
+
/** kind=records 的目标 Room slug。 */
|
|
548
|
+
tag?: string;
|
|
549
|
+
}
|
|
550
|
+
export interface RoomsExportResponse {
|
|
551
|
+
kind: 'rooms' | 'orphans' | 'records';
|
|
552
|
+
/** CSV 文本(首行表头;
|
|
553
|
+
行尾;RFC 4180 转义)。 */
|
|
554
|
+
csv: string;
|
|
555
|
+
/** 数据行数(不含表头)。 */
|
|
556
|
+
total: number;
|
|
557
|
+
/** truncated = 达到 limit 上限,结果可能不完整。 */
|
|
558
|
+
truncated: boolean;
|
|
559
|
+
}
|
|
560
|
+
/** dsh-memory/room-admin(Room 破坏性面管理;高权限:面板 memoryMutate 开启才生效)。 */
|
|
561
|
+
export interface RoomAdminRequest {
|
|
562
|
+
/** merge/rename:from→to(mergeRoom/renameRoom 编排:预览→实跑+自动备份+入队场景重算);retire:slug 退役/恢复;update:改写 label/description。 */
|
|
563
|
+
action: 'merge' | 'rename' | 'retire' | 'update';
|
|
564
|
+
/** update 的显示名(可中文;非空才改写)。 */
|
|
565
|
+
label?: string;
|
|
566
|
+
/** update 的归类说明(喂标注器;非空才改写)。 */
|
|
567
|
+
description?: string;
|
|
568
|
+
/** merge/rename 的源 Room slug。 */
|
|
569
|
+
from?: string;
|
|
570
|
+
/** merge/rename 的目标 Room slug。 */
|
|
571
|
+
to?: string;
|
|
572
|
+
/** retire 的目标 Room slug。 */
|
|
573
|
+
slug?: string;
|
|
574
|
+
/** retire:true=恢复 active;缺省/false=退役。 */
|
|
575
|
+
active?: boolean;
|
|
576
|
+
/** retire:同时把该 room 下全部**活跃**记录退场(软删,可在「已退场」区恢复)。 */
|
|
577
|
+
withRecords?: boolean;
|
|
578
|
+
/** merge/rename:缺省 true 只返预览;实跑需显式 false。 */
|
|
579
|
+
dryRun?: boolean;
|
|
580
|
+
}
|
|
581
|
+
export interface RoomAdminResponse {
|
|
582
|
+
notice: string;
|
|
583
|
+
/** merge/rename 本次是否 dryRun(预览)。 */
|
|
584
|
+
dryRun?: boolean;
|
|
585
|
+
/** merge/rename 影响的记录条数(实跑 = 已改写条数)。 */
|
|
586
|
+
affected?: number;
|
|
587
|
+
/** 预览样例(受影响记录 id 的前几条)。 */
|
|
588
|
+
preview?: string[];
|
|
589
|
+
/** true = 超过单次上限,需续跑。 */
|
|
590
|
+
hasMore?: boolean;
|
|
591
|
+
/** 实跑时改写前备份文件名(rooms-merge-backups/)。 */
|
|
592
|
+
backupFile?: string;
|
|
593
|
+
/** retire withRecords 实际退场的记录条数(软删,可恢复)。 */
|
|
594
|
+
retiredRecords?: number;
|
|
514
595
|
}
|
|
515
596
|
/** dsh-memory/wing-backfill(一键回填,后台任务;端点立即返回,进度以 wing-overview 轮询)。 */
|
|
516
597
|
export interface WingBackfillResponse {
|
|
@@ -650,6 +731,11 @@ export interface ListRecordsRequest {
|
|
|
650
731
|
* Room 由 tags 派生(自生长),故这里传的是**具体 tag 名**而非枚举 id。
|
|
651
732
|
*/
|
|
652
733
|
tag?: string;
|
|
734
|
+
/**
|
|
735
|
+
* 孤儿记忆筛选:true = 只列**没有任何 Room(tags 为空/缺失)**的记录。
|
|
736
|
+
* 与 `tag` 互斥;同传时 `tag` 优先(客户端保证不同传)。
|
|
737
|
+
*/
|
|
738
|
+
untagged?: boolean;
|
|
653
739
|
/**
|
|
654
740
|
* 退场(软删)筛查:三态。省略 = 全部(活跃+已退场混排,靠 `UiRecord.retired`
|
|
655
741
|
* 徽标区分);`false` = 仅活跃;`true` = 仅已退场。
|
|
@@ -686,6 +772,15 @@ export interface UiRecord {
|
|
|
686
772
|
sourceAnchors: string[];
|
|
687
773
|
/** 检索相关度(列表路径无 score → null)。 */
|
|
688
774
|
score: number | null;
|
|
775
|
+
/** 老化权重:读路径衰减系数 max(0.5, 0.5^(Δ天/半衰期))(Δ=updatedAt;半衰期 0=关 → null)。 */
|
|
776
|
+
agingWeight?: number | null;
|
|
777
|
+
/** 召回使用统计:**进 topN(被注入)才算 used**;参与尝试未进 = 仅 attempts。缺省 = 未装配。 */
|
|
778
|
+
usage?: {
|
|
779
|
+
attempts: number;
|
|
780
|
+
used: number;
|
|
781
|
+
lastAttemptAt: number | null;
|
|
782
|
+
lastUsedAt: number | null;
|
|
783
|
+
} | null;
|
|
689
784
|
/**
|
|
690
785
|
* 是否已退场(软删)。`true` 时该记录在活动列表里仍以「已退场」态呈现(置灰 + 徽标),
|
|
691
786
|
* 可被 `records-restore` 找回——这正是设计要的「不静默消失、可恢复」,而非真删。
|
|
@@ -704,6 +799,13 @@ export interface ListRecordsResponse {
|
|
|
704
799
|
truncated: boolean;
|
|
705
800
|
/** 场景筛选下拉选项(仅 offset===0 时附带)。 */
|
|
706
801
|
scenes?: string[];
|
|
802
|
+
/** 召回使用汇总(老化权重效果口径:进 topN 才算 used)。未装配 = undefined。 */
|
|
803
|
+
usageSummary?: {
|
|
804
|
+
tracked: number;
|
|
805
|
+
attempted: number;
|
|
806
|
+
used: number;
|
|
807
|
+
rate: number;
|
|
808
|
+
} | null;
|
|
707
809
|
/**
|
|
708
810
|
* Wing 词表(R8 单一事实源:服务端随 list-records 下发,client 不再手抄;仅 offset===0 时附带)。
|
|
709
811
|
* 含 8 角 + `general`(跨域兜底,存量大 reserved 值仍可筛)。缺省(旧服务端)时 client
|
|
@@ -1201,6 +1303,9 @@ export interface DshMemoryRequestMap {
|
|
|
1201
1303
|
'dsh-memory/session-mode-set': SessionModeSetRequest;
|
|
1202
1304
|
'dsh-memory/wing-overview': Record<string, never>;
|
|
1203
1305
|
'dsh-memory/rooms-get': Record<string, never>;
|
|
1306
|
+
'dsh-memory/rooms-export': RoomsExportRequest;
|
|
1307
|
+
'dsh-memory/room-register': RoomRegisterRequest;
|
|
1308
|
+
'dsh-memory/room-admin': RoomAdminRequest;
|
|
1204
1309
|
'dsh-memory/wing-backfill': Record<string, never>;
|
|
1205
1310
|
'dsh-memory/session-stats': SessionStatsRequest;
|
|
1206
1311
|
'dsh-memory/settings-get': Record<string, never>;
|
|
@@ -1246,6 +1351,9 @@ export interface DshMemoryResponseMap {
|
|
|
1246
1351
|
'dsh-memory/session-mode-set': SessionModeSetResponse;
|
|
1247
1352
|
'dsh-memory/wing-overview': WingOverviewResponse;
|
|
1248
1353
|
'dsh-memory/rooms-get': RoomsGetResponse;
|
|
1354
|
+
'dsh-memory/rooms-export': RoomsExportResponse;
|
|
1355
|
+
'dsh-memory/room-register': RoomRegisterResponse;
|
|
1356
|
+
'dsh-memory/room-admin': RoomAdminResponse;
|
|
1249
1357
|
'dsh-memory/wing-backfill': WingBackfillResponse;
|
|
1250
1358
|
'dsh-memory/session-stats': SessionStatsResponse;
|
|
1251
1359
|
'dsh-memory/settings-get': SettingsGetResponse;
|
package/dist/hooks/capture.js
CHANGED
|
@@ -164,7 +164,9 @@ export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
|
164
164
|
// 补收 (L0 水位线, 末尾] 内 bracket 完整的尾轮(≤2 轮)——崩溃轮在缓冲丢失后
|
|
165
165
|
// 仍可从持久化层找回(persistence 会给孤儿轮补 interrupted 闭合,探针② 实证 98 处)。
|
|
166
166
|
// fail-open 纪律:任何异常只 warn,不阻塞会话启动;幂等 = 水位线 + 逐 turn 存在性检查。
|
|
167
|
-
|
|
167
|
+
// 0.1.7-rc.2 起 agent/session-start 并入 agent/created(serial:监听器需 async,
|
|
168
|
+
// 同 main 线 0.19.0 的 0.2.0 适配)。仍监听旧事件名的实现在 rc.2 宿主上永不触发。
|
|
169
|
+
ctx.on('agent/created', async (payload) => {
|
|
168
170
|
try {
|
|
169
171
|
if (payload.source !== 'resume')
|
|
170
172
|
return;
|
|
@@ -178,7 +180,9 @@ export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
|
178
180
|
const mode = modes.get(sid);
|
|
179
181
|
if (mode === 'off')
|
|
180
182
|
return; // off 档对记忆系统完全隐身,对齐现行为
|
|
181
|
-
|
|
183
|
+
// 0.1.7-rc.2 起 Session 弃用同步全量 .events,改 snapshotEvents()(同 main 线
|
|
184
|
+
// 0.19.0 的 0.2.0 适配);探针式读取,旧宿主形状走下方降级链。
|
|
185
|
+
let events = typeof session?.snapshotEvents === 'function' ? session.snapshotEvents() : undefined;
|
|
182
186
|
if (!events || events.length === 0) {
|
|
183
187
|
// task_8 降级链:events 不可得(宿主版本偏差)时依次试
|
|
184
188
|
// ctx.sessionQuery.readSession / ctx.sessionPersistence.readFrom(守卫探针式)
|
package/dist/hooks/recall.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { L1Store } from '../store/l1.js';
|
|
|
22
22
|
import type { PersonaStore } from '../store/persona.js';
|
|
23
23
|
import type { SceneStore } from '../store/scenes.js';
|
|
24
24
|
import type { SessionModeStore } from '../store/session-modes.js';
|
|
25
|
+
import type { RecallUsageStore } from '../store/recall-usage.js';
|
|
25
26
|
import type { MemoryLogger } from '../types.js';
|
|
26
27
|
import { type OccupancyLedger } from '../util/context-occupancy.js';
|
|
27
28
|
/**
|
|
@@ -89,4 +90,6 @@ export declare function registerRecall(ctx: Context, cfg: MemoryConfig, stores:
|
|
|
89
90
|
l1: L1Store;
|
|
90
91
|
scenes: Record<'chat' | 'work', SceneStore>;
|
|
91
92
|
persona: Record<'chat' | 'work', PersonaStore>;
|
|
92
|
-
}, logger: MemoryLogger, live: LiveSettingsHandle, modes: SessionModeStore, dataDir: string
|
|
93
|
+
}, logger: MemoryLogger, live: LiveSettingsHandle, modes: SessionModeStore, dataDir: string,
|
|
94
|
+
/** 召回使用统计(可选;未装配 = 零成本旁路):进 topN(注入)才算 used。 */
|
|
95
|
+
usage?: RecallUsageStore): RecallHooks;
|
package/dist/hooks/recall.js
CHANGED
|
@@ -112,7 +112,9 @@ export function emptyRecallStats(now = Date.now()) {
|
|
|
112
112
|
updatedAt: now,
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
-
export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir
|
|
115
|
+
export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir,
|
|
116
|
+
/** 召回使用统计(可选;未装配 = 零成本旁路):进 topN(注入)才算 used。 */
|
|
117
|
+
usage) {
|
|
116
118
|
/** 召回去重存储(同会话已注入的记忆不再重复注入;写穿持久化,重启不丢)。 */
|
|
117
119
|
const dedupe = new RecallDedupeStore(dataDir, logger);
|
|
118
120
|
/** 记忆占用流水(账本迁移写穿;重启后历史会话账目由此复生)。 */
|
|
@@ -218,7 +220,8 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
218
220
|
// 上下文压缩/清空 → 已注入内容从模型上下文丢失,重置该会话的去重压制
|
|
219
221
|
// (resume/startup 不重置:历史仍在,已注入的记忆模型还持有)。
|
|
220
222
|
// 占用账本同步全量归零(宁低勿高;轮级粒度近似)。
|
|
221
|
-
|
|
223
|
+
// 0.1.7-rc.2 起并入 agent/created(serial:监听器需 async)。
|
|
224
|
+
ctx.on('agent/created', async (payload) => {
|
|
222
225
|
if (payload.source === 'compact' || payload.source === 'clear') {
|
|
223
226
|
dedupe.reset(payload.agent.id);
|
|
224
227
|
const led = ledgerFor(payload.agent.id);
|
|
@@ -378,6 +381,7 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
378
381
|
injectedIds: [], suppressedCount: scoped.length - fresh.length,
|
|
379
382
|
durationMs: Date.now() - searchStart, outcome: 'suppressed',
|
|
380
383
|
});
|
|
384
|
+
usage?.recordAttempt(scoped.map((h) => h.id), []);
|
|
381
385
|
return decision;
|
|
382
386
|
}
|
|
383
387
|
const lines = applyRecallBudget(fresh.map((h) => `- [${h.scene_name ? `${h.type}|${h.scene_name}` : h.type}] ${h.content}`), { maxCharsPerMemory: cfg.recall.maxCharsPerMemory, maxTotalRecallChars: cfg.recall.maxTotalRecallChars });
|
|
@@ -389,6 +393,7 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
389
393
|
injectedIds: [], suppressedCount: scoped.length - fresh.length,
|
|
390
394
|
durationMs: Date.now() - searchStart, outcome: 'suppressed',
|
|
391
395
|
});
|
|
396
|
+
usage?.recordAttempt(fresh.map((h) => h.id), []);
|
|
392
397
|
return decision;
|
|
393
398
|
}
|
|
394
399
|
st.lastHits = lines.length;
|
|
@@ -421,6 +426,8 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
421
426
|
injectedIds: fresh.slice(0, lines.length).map((h) => h.id), suppressedCount: scoped.length - fresh.length,
|
|
422
427
|
durationMs: Date.now() - searchStart, outcome: 'injected',
|
|
423
428
|
});
|
|
429
|
+
// 使用统计:候选=scoped(尝试),进 topN(注入)=used
|
|
430
|
+
usage?.recordAttempt(scoped.map((h) => h.id), fresh.slice(0, lines.length).map((h) => h.id));
|
|
424
431
|
// 入账在成功构造注入消息之后、返回 enter 之前——任何前置抛错路径账目零扰动
|
|
425
432
|
const led = ledgerFor(payload.agent.id);
|
|
426
433
|
recordRecallInjection(led, text.length);
|
|
@@ -568,7 +575,7 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
568
575
|
for (const agent of agents.list())
|
|
569
576
|
registerForAgent(agent);
|
|
570
577
|
}
|
|
571
|
-
ctx.on('agent/created', (payload) => {
|
|
578
|
+
ctx.on('agent/created', async (payload) => {
|
|
572
579
|
registerForAgent(payload.agent);
|
|
573
580
|
});
|
|
574
581
|
ctx.effect(() => () => {
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,8 @@ import { SlotStore } from './store/slots.js';
|
|
|
36
36
|
import { registerMemoryTools } from './tools/index.js';
|
|
37
37
|
import { registerSlotTools } from './tools/slots.js';
|
|
38
38
|
import { registerSlotRecall } from './hooks/slot-recall.js';
|
|
39
|
+
import { RoomRegistryStore } from './store/rooms-registry.js';
|
|
40
|
+
import { RecallUsageStore } from './store/recall-usage.js';
|
|
39
41
|
import { registerSlotsProjection } from './projection/slots.js';
|
|
40
42
|
import { errDetail, withFileLog } from './util/filelog.js';
|
|
41
43
|
import { buildRouteChain, resolveModelRoute, invalidateEffortCache } from './llm.js';
|
|
@@ -158,6 +160,7 @@ export async function apply(ctx, config) {
|
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
162
|
const stores = {
|
|
163
|
+
usage: undefined, // 装配于下方(依赖 dataDir 就绪)
|
|
161
164
|
l0: new L0Store(dataDir, db, embed, logger),
|
|
162
165
|
l1: new L1Store(dataDir, db, embed, config.recall.strategy, logger, config.recall.decayHalfLifeDays,
|
|
163
166
|
// §D 第 3 路:图谱回链。图谱不可用时 searchNodes 自带 no-op,不影响双路。
|
|
@@ -350,16 +353,23 @@ export async function apply(ctx, config) {
|
|
|
350
353
|
backend = await createMemoryBackend({ dbPath: path.join(dataDir, 'memory.db'), dimensions: initial.dims, logger }, backend);
|
|
351
354
|
}
|
|
352
355
|
stores.backend = backend;
|
|
356
|
+
// Room 注册表(分类管理 beta.4;损坏降级为纯自生长目录)
|
|
357
|
+
const roomRegistry = new RoomRegistryStore(dataDir, logger);
|
|
358
|
+
await roomRegistry.init();
|
|
353
359
|
const ruminate = storageOk && !db.isDegraded() && ruminateFile
|
|
354
|
-
? new RuminateController(ctx, config, runner, stores, logger, live, ruminateFile)
|
|
360
|
+
? new RuminateController(ctx, config, runner, stores, logger, live, ruminateFile, roomRegistry)
|
|
355
361
|
: undefined;
|
|
356
362
|
let flushL0;
|
|
357
363
|
if (storageOk) {
|
|
358
364
|
flushL0 = registerCapture(ctx, config, runner, stores.l0, logger, live, modes);
|
|
359
365
|
}
|
|
360
|
-
|
|
366
|
+
// 召回使用统计(进 topN 才算 used;热路径只碰内存,去抖落盘)
|
|
367
|
+
const recallUsage = new RecallUsageStore(dataDir, logger);
|
|
368
|
+
await recallUsage.init();
|
|
369
|
+
stores.usage = recallUsage;
|
|
370
|
+
const recall = registerRecall(ctx, config, stores, logger, live, modes, dataDir, recallUsage);
|
|
361
371
|
runner.setAfterRun(recall.invalidateProfile);
|
|
362
|
-
registerMemoryTools(ctx, config, stores, logger, modes, live, ruminate);
|
|
372
|
+
registerMemoryTools(ctx, config, stores, logger, modes, live, ruminate, storageOk ? roomRegistry : undefined);
|
|
363
373
|
// 激活槽位(active slot):工具面 + 常驻注入 + 服务端投影(均走 ctx.effect,可撤销)
|
|
364
374
|
registerSlotTools(ctx, config, stores.slots, logger, modes, live, stores.l1);
|
|
365
375
|
const slotRecall = registerSlotRecall(ctx, config, stores.slots, logger, live);
|
|
@@ -378,7 +388,7 @@ export async function apply(ctx, config) {
|
|
|
378
388
|
runnerView: (sid, mode) => runner.sessionView(sid, mode),
|
|
379
389
|
l0Count: (sid) => stores.l0.countBySession(sid),
|
|
380
390
|
capabilities: () => db.getCapabilities(),
|
|
381
|
-
}, ruminate);
|
|
391
|
+
}, ruminate, storageOk ? roomRegistry : undefined);
|
|
382
392
|
// bench 控制服务(config.benchControl 门控,默认关):仅基准/调试部署注册,
|
|
383
393
|
// 供同进程的 bench-runner lifecycle 赛道触发 rebuild / 设置会话档位
|
|
384
394
|
// (宿主侧 RPC 无 call(),见 bench-control.ts)
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* 已是 string 的变量收窄成 `never`(该分支内无法再 `.slice`,已在 wing-backfill 踩过)。
|
|
11
11
|
*/
|
|
12
12
|
export declare function isWingId(v: unknown): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* 两级 Room slug(`major` 或 `major/minor`,各段满足 isTag;至多一个 slash)。
|
|
15
|
+
* Room 注册表/归类写回的 tags 域;与 rooms-registry.ts 同一事实源(就近定义避免依赖环)。
|
|
16
|
+
*/
|
|
17
|
+
export declare function isRoomSlug(v: unknown): boolean;
|
|
13
18
|
/** 同为布尔谓词(理由见 isWingId 注释)。 */
|
|
14
19
|
export declare function isTag(v: unknown): boolean;
|
|
15
20
|
/**
|
|
@@ -31,6 +31,16 @@ export function isWingId(v) {
|
|
|
31
31
|
}
|
|
32
32
|
/** 标签(Room 的构成单元)校验:小写字母数字连字符,1-32 字符。 */
|
|
33
33
|
const TAG_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
34
|
+
/**
|
|
35
|
+
* 两级 Room slug(`major` 或 `major/minor`,各段满足 isTag;至多一个 slash)。
|
|
36
|
+
* Room 注册表/归类写回的 tags 域;与 rooms-registry.ts 同一事实源(就近定义避免依赖环)。
|
|
37
|
+
*/
|
|
38
|
+
export function isRoomSlug(v) {
|
|
39
|
+
if (typeof v !== 'string')
|
|
40
|
+
return false;
|
|
41
|
+
const parts = v.split('/');
|
|
42
|
+
return parts.length <= 2 && parts.every((p) => TAG_RE.test(p));
|
|
43
|
+
}
|
|
34
44
|
/** 同为布尔谓词(理由见 isWingId 注释)。 */
|
|
35
45
|
export function isTag(v) {
|
|
36
46
|
return typeof v === 'string' && TAG_RE.test(v);
|
|
@@ -47,7 +57,7 @@ export function normTags(v, max = 3) {
|
|
|
47
57
|
if (typeof raw !== 'string')
|
|
48
58
|
continue;
|
|
49
59
|
const t = raw.trim().toLowerCase();
|
|
50
|
-
if (!isTag(t) || out.includes(t))
|
|
60
|
+
if (!(isTag(t) || isRoomSlug(t)) || out.includes(t))
|
|
51
61
|
continue;
|
|
52
62
|
out.push(t);
|
|
53
63
|
if (out.length >= max)
|
|
@@ -66,6 +66,8 @@ export declare class RuminateController {
|
|
|
66
66
|
private sessions;
|
|
67
67
|
private totalL1;
|
|
68
68
|
private pendingFile;
|
|
69
|
+
/** Room 注册表(可选;孤儿候选预标记的合并词表来源)。 */
|
|
70
|
+
private readonly roomRegistry;
|
|
69
71
|
/** 记忆后端:未注入时包 l1(进程内,行为与改造前等价)。 */
|
|
70
72
|
private backend;
|
|
71
73
|
constructor(ctx: Context, cfg: MemoryConfig, runner: Pick<MemoryRunner, 'enqueue' | 'states'>, stores: {
|
|
@@ -75,7 +77,15 @@ export declare class RuminateController {
|
|
|
75
77
|
scenes: Record<MemoryFamily, SceneStore>;
|
|
76
78
|
persona: Record<MemoryFamily, PersonaStore>;
|
|
77
79
|
state: StateStore;
|
|
78
|
-
}, logger: MemoryLogger, live: LiveSettingsHandle, pendingFile: string
|
|
80
|
+
}, logger: MemoryLogger, live: LiveSettingsHandle, pendingFile: string,
|
|
81
|
+
/** Room 注册表(可选;孤儿候选预标记的合并词表来源)。 */
|
|
82
|
+
roomRegistry?: {
|
|
83
|
+
listActive(): Array<{
|
|
84
|
+
slug: string;
|
|
85
|
+
label?: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
}>;
|
|
88
|
+
});
|
|
79
89
|
/** 状态快照 */
|
|
80
90
|
getStatus(): RuminateStatus;
|
|
81
91
|
/**
|
|
@@ -3,6 +3,7 @@ import { groupPendingBySession, loadPending, } from '../store/pending.js';
|
|
|
3
3
|
import { errDetail } from '../util/filelog.js';
|
|
4
4
|
import { runSceneConsolidation } from './l2.js';
|
|
5
5
|
import { relabelPass } from './relabel.js';
|
|
6
|
+
import { annotateOrphanCandidates } from '../room-review.js';
|
|
6
7
|
import { runPersona } from './l3.js';
|
|
7
8
|
const IDLE_STATUS = {
|
|
8
9
|
running: false,
|
|
@@ -44,11 +45,15 @@ export class RuminateController {
|
|
|
44
45
|
sessions = [];
|
|
45
46
|
totalL1 = 0;
|
|
46
47
|
pendingFile;
|
|
48
|
+
/** Room 注册表(可选;孤儿候选预标记的合并词表来源)。 */
|
|
49
|
+
roomRegistry;
|
|
47
50
|
/** 记忆后端:未注入时包 l1(进程内,行为与改造前等价)。 */
|
|
48
51
|
backend() {
|
|
49
52
|
return this.stores.backend ?? new InProcMemoryBackend(this.stores.l1);
|
|
50
53
|
}
|
|
51
|
-
constructor(ctx, cfg, runner, stores, logger, live, pendingFile
|
|
54
|
+
constructor(ctx, cfg, runner, stores, logger, live, pendingFile,
|
|
55
|
+
/** Room 注册表(可选;孤儿候选预标记的合并词表来源)。 */
|
|
56
|
+
roomRegistry) {
|
|
52
57
|
this.ctx = ctx;
|
|
53
58
|
this.cfg = cfg;
|
|
54
59
|
this.runner = runner;
|
|
@@ -56,6 +61,7 @@ export class RuminateController {
|
|
|
56
61
|
this.logger = logger;
|
|
57
62
|
this.live = live;
|
|
58
63
|
this.pendingFile = pendingFile;
|
|
64
|
+
this.roomRegistry = roomRegistry;
|
|
59
65
|
}
|
|
60
66
|
/** 状态快照 */
|
|
61
67
|
getStatus() {
|
|
@@ -222,6 +228,17 @@ export class RuminateController {
|
|
|
222
228
|
// 重标定失败不拖垮反刍整体(蒸馏/L2/L3 产物保留)
|
|
223
229
|
this.logger.warn(`[memory] 反刍重标定失败(不影响本次产物): ${errDetail(err)}`);
|
|
224
230
|
}
|
|
231
|
+
// 孤儿候选预标记(长尾治理):无 Room 的活跃孤儿对照现有 Room 词表产出
|
|
232
|
+
// 候选(roomReview='pending'),由 agent 用 memory_room_review 逐个复查;
|
|
233
|
+
// 每轮至多 40 条,吞错不拖垮反刍。
|
|
234
|
+
try {
|
|
235
|
+
const r = await annotateOrphanCandidates(this.ctx, this.cfg, { l1: this.stores.l1, logger: this.logger, registry: this.roomRegistry }, 40);
|
|
236
|
+
if (r.written > 0)
|
|
237
|
+
this.status.detail = `孤儿候选预标记 ${r.written} 条(待 agent 复查,工具 memory_room_review)`;
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
this.logger.warn(`[memory] 孤儿候选预标记失败(忽略): ${errDetail(err)}`);
|
|
241
|
+
}
|
|
225
242
|
this.status.phase = 'done';
|
|
226
243
|
this.finish(null);
|
|
227
244
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 孤儿记忆的 Room 候选预标记 + agent 逐个复查(长尾治理)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:Room 由 metadata.tags 自生长,但 tags 只在反刍 relabel 段随 wing 补打
|
|
5
|
+
* 顺带产出(每轮 ≤40 条、且只覆盖本轮补 wing 的记录)——「有 wing 无 tags」与
|
|
6
|
+
* 「预算用尽被 deferred」的记录成为无 Room 绑定的孤儿,且标注器不参考现有
|
|
7
|
+
* Room 词表,产出碎片化新 slug(81 个 Room 大多 1 条的成因)。
|
|
8
|
+
*
|
|
9
|
+
* 两段式:
|
|
10
|
+
* 1. **预标记(annotateOrphanCandidates)**:抽取活跃孤儿,LLM 对照**现有 Room
|
|
11
|
+
* 词表**产出候选(优先挂靠现有 Room,≤2 个新 slug 兜底),写
|
|
12
|
+
* `metadata.roomCandidates` + `roomReview:'pending'`——不动 tags、不进
|
|
13
|
+
* Room、不进召回,只是"待确认的建议"。
|
|
14
|
+
* 2. **逐个复查(memory_room_review 工具)**:agent 取下一条待复查记录
|
|
15
|
+
* (带候选 + 现有 Room 词表),确认后 confirm 写入 tags(进 Room,高权限
|
|
16
|
+
* 门控),或 skip 跳过。确认/跳过都是显式动作,预标记本身零行为面。
|
|
17
|
+
*
|
|
18
|
+
* 存储即 metadata 保留键(与 relabel 写 tags 同款 patchMetadata 合并纪律,
|
|
19
|
+
* 写前重读):`roomCandidates` / `roomReview:'pending'|'confirmed'|'skipped'`。
|
|
20
|
+
* 两键不在 rooms 聚合(rooms 只读 $.tags)、不在快照哈希投影关注的治理列——
|
|
21
|
+
* 与既有 metadata 键同层,无 schema 变更。
|
|
22
|
+
*/
|
|
23
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
24
|
+
import type { MemoryConfig } from './config.js';
|
|
25
|
+
import type { MemoryLogger, MemoryRecord } from './types.js';
|
|
26
|
+
export declare const ROOM_CANDIDATES_KEY = "roomCandidates";
|
|
27
|
+
export declare const ROOM_REVIEW_KEY = "roomReview";
|
|
28
|
+
export type RoomReviewState = 'pending' | 'confirmed' | 'skipped';
|
|
29
|
+
/** 复查与候选操作对 L1 的最小依赖面(便于测试注入)。 */
|
|
30
|
+
export interface RoomReviewStore {
|
|
31
|
+
list(opts: {
|
|
32
|
+
untagged?: boolean;
|
|
33
|
+
retired?: boolean;
|
|
34
|
+
limit: number;
|
|
35
|
+
offset: number;
|
|
36
|
+
}): {
|
|
37
|
+
items: MemoryRecord[];
|
|
38
|
+
total: number;
|
|
39
|
+
};
|
|
40
|
+
getByIds(ids: readonly string[]): MemoryRecord[];
|
|
41
|
+
patchMetadata(id: string, metadata: Record<string, unknown>): boolean;
|
|
42
|
+
listRooms(): Array<{
|
|
43
|
+
room: string;
|
|
44
|
+
count: number;
|
|
45
|
+
}>;
|
|
46
|
+
/** 确认写 tags 后失效 Room 计数缓存(30s TTL)。 */
|
|
47
|
+
invalidateRooms(): void;
|
|
48
|
+
}
|
|
49
|
+
/** merge/rename 的 store 面(RoomReviewStore + 游标重写与备份;L1Store 已实现)。 */
|
|
50
|
+
export interface RoomMergeStore extends RoomReviewStore {
|
|
51
|
+
listByTagAll(tag: string, cap?: number): MemoryRecord[];
|
|
52
|
+
backupTagRecords(tag: string): {
|
|
53
|
+
file: string;
|
|
54
|
+
count: number;
|
|
55
|
+
};
|
|
56
|
+
rewriteTag(from: string, to: string, cap?: number): {
|
|
57
|
+
rewritten: number;
|
|
58
|
+
scanned: number;
|
|
59
|
+
families: string[];
|
|
60
|
+
scenes: string[];
|
|
61
|
+
hasMore: boolean;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Room 注册表最小面(merge 收尾;RoomRegistryStore 已满足)。 */
|
|
65
|
+
export interface RoomRegistryMerge {
|
|
66
|
+
markMerged(fromSlug: string, toSlug: string): Promise<boolean>;
|
|
67
|
+
renameSlug(oldSlug: string, newSlug: string): Promise<boolean>;
|
|
68
|
+
/** rename 收尾恢复先前状态用(active 条目改名后不掉出标注词表)。 */
|
|
69
|
+
bySlug?(slug: string): {
|
|
70
|
+
status: 'active' | 'retired';
|
|
71
|
+
} | undefined;
|
|
72
|
+
setStatus?(slug: string, status: 'active' | 'retired'): Promise<boolean>;
|
|
73
|
+
}
|
|
74
|
+
export interface RoomCandidate {
|
|
75
|
+
id: string;
|
|
76
|
+
rooms: string[];
|
|
77
|
+
}
|
|
78
|
+
/** 解析标注器输出的逐条候选(容错:id 配对失败丢弃;isTag 校验;目录内优先,≤3)。 */
|
|
79
|
+
export declare function parseRoomCandidateItems(parsed: ReadonlyArray<{
|
|
80
|
+
id: string;
|
|
81
|
+
rooms: readonly string[];
|
|
82
|
+
}>, ids: ReadonlyArray<string>, roomCatalog: ReadonlyArray<{
|
|
83
|
+
room: string;
|
|
84
|
+
count: number;
|
|
85
|
+
}>): RoomCandidate[];
|
|
86
|
+
/** 候选标注系统 prompt:喂现有 Room 词表(带计数),优先挂靠、少量新建兜底。 */
|
|
87
|
+
export declare function roomCandidateSystemPrompt(roomCatalog: ReadonlyArray<{
|
|
88
|
+
room: string;
|
|
89
|
+
count: number;
|
|
90
|
+
}>): string;
|
|
91
|
+
/** 候选标注器:一批孤儿记录 → 候选 Room(优先现有词表;失败返回空,调用方零改动)。 */
|
|
92
|
+
export declare function roomCandidateChunk(ctx: Context, cfg: MemoryConfig, logger: MemoryLogger, chunk: MemoryRecord[], roomCatalog: ReadonlyArray<{
|
|
93
|
+
room: string;
|
|
94
|
+
count: number;
|
|
95
|
+
}>): Promise<Array<{
|
|
96
|
+
id: string;
|
|
97
|
+
rooms: string[];
|
|
98
|
+
}>>;
|
|
99
|
+
export interface OrphanAnnotateIO {
|
|
100
|
+
l1: RoomReviewStore;
|
|
101
|
+
logger: MemoryLogger;
|
|
102
|
+
/** Room 注册表(可选:未装配 = 词表只用自生长聚合,无注册条目)。 */
|
|
103
|
+
registry?: {
|
|
104
|
+
listActive(): Array<{
|
|
105
|
+
slug: string;
|
|
106
|
+
label?: string;
|
|
107
|
+
description?: string;
|
|
108
|
+
}>;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 预标记消费器:活跃孤儿(≤limit 条)批量产候选并写 pending。
|
|
113
|
+
* 写前重读合并(与 relabel 同纪律);失败单条跳过、原记录零改动。
|
|
114
|
+
*/
|
|
115
|
+
export interface OrphanAnnotateOverrides {
|
|
116
|
+
/** 测试注入口:替换 LLM 候选标注器(默认走真实 callLLM 路径);第二参为合并词表。 */
|
|
117
|
+
chunkAnnotator?: (chunk: MemoryRecord[], catalog: ReadonlyArray<{
|
|
118
|
+
room: string;
|
|
119
|
+
count: number;
|
|
120
|
+
label?: string;
|
|
121
|
+
description?: string;
|
|
122
|
+
}>) => Promise<Array<{
|
|
123
|
+
id: string;
|
|
124
|
+
rooms: string[];
|
|
125
|
+
}>>;
|
|
126
|
+
}
|
|
127
|
+
export declare function annotateOrphanCandidates(ctx: Context, cfg: MemoryConfig, io: OrphanAnnotateIO, limit?: number, overrides?: OrphanAnnotateOverrides): Promise<{
|
|
128
|
+
selected: number;
|
|
129
|
+
candidates: number;
|
|
130
|
+
written: number;
|
|
131
|
+
}>;
|
|
132
|
+
export interface MergeRoomResult {
|
|
133
|
+
dryRun: boolean;
|
|
134
|
+
/** 影响条数(dryRun=预览数;实跑=实际重写数)。 */
|
|
135
|
+
affected: number;
|
|
136
|
+
preview: string[];
|
|
137
|
+
applied: number;
|
|
138
|
+
hasMore: boolean;
|
|
139
|
+
backupFile?: string;
|
|
140
|
+
/** 实跑时回传:受影响的族与场景(调用方入队 recluster 'room-merge')。 */
|
|
141
|
+
families?: string[];
|
|
142
|
+
scenes?: string[];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Room merge/rename 编排(破坏性动作,分类管理 beta.5):
|
|
146
|
+
* - dryRun(缺省 true):只统计 affected + 前 6 条预览,**零写入**;
|
|
147
|
+
* - 实跑:执行前备份(matching rows → rooms-merge-backups/)→ 游标重写
|
|
148
|
+
* (写前重读-合并-写回,其余 metadata 键保全)→ 注册表 markMerged/renameSlug
|
|
149
|
+
* → 返回受影响 (family, scene) 供调用方入队 recluster('room-merge');
|
|
150
|
+
* - 单飞:并发 merge 直接抛错;与 relabel/ruminate 的时间错开由调用方保证。
|
|
151
|
+
* 抛错语义:非法参数/注册表冲突直接抛(调用方转 notice)。
|
|
152
|
+
*/
|
|
153
|
+
export declare function mergeRoom(io: {
|
|
154
|
+
l1: RoomMergeStore;
|
|
155
|
+
registry: RoomRegistryMerge;
|
|
156
|
+
logger: MemoryLogger;
|
|
157
|
+
}, from: string, to: string, opts?: {
|
|
158
|
+
dryRun?: boolean;
|
|
159
|
+
cap?: number;
|
|
160
|
+
}): Promise<MergeRoomResult>;
|
|
161
|
+
/** rename = merge 1:1 + 注册表改名(冲突时抛错提示走 merge)。
|
|
162
|
+
* 归类语义:改名**不是退役**——merge 编排的 markMerged 会把旧条目置 retired,
|
|
163
|
+
* 这里在改名后恢复其先前状态(active 条目改名后仍 active,不掉出标注词表)。 */
|
|
164
|
+
export declare function renameRoom(io: {
|
|
165
|
+
l1: RoomMergeStore;
|
|
166
|
+
registry: RoomRegistryMerge;
|
|
167
|
+
logger: MemoryLogger;
|
|
168
|
+
}, from: string, to: string, opts?: {
|
|
169
|
+
dryRun?: boolean;
|
|
170
|
+
cap?: number;
|
|
171
|
+
}): Promise<MergeRoomResult>;
|
|
172
|
+
/** 下一条待复查(供 agent 逐个过):记录摘要 + 候选 + 现有 Room 词表 + 剩余数。 */
|
|
173
|
+
export declare function nextReview(l1: RoomReviewStore): {
|
|
174
|
+
id: string;
|
|
175
|
+
content: string;
|
|
176
|
+
type: string;
|
|
177
|
+
sceneName: string;
|
|
178
|
+
candidates: string[];
|
|
179
|
+
roomCatalog: Array<{
|
|
180
|
+
room: string;
|
|
181
|
+
count: number;
|
|
182
|
+
}>;
|
|
183
|
+
remaining: number;
|
|
184
|
+
} | null;
|
|
185
|
+
/** 待复查剩余数(面板/工具兜底信息)。 */
|
|
186
|
+
export declare function pendingReviewCount(l1: RoomReviewStore): number;
|
|
187
|
+
/**
|
|
188
|
+
* 确认复查结果:候选(或 agent 改写的)写入 metadata.tags(**进 Room**),
|
|
189
|
+
* 清候选、标 confirmed。**高权限门控由调用方执行**(与 memory_delete 同款)。
|
|
190
|
+
* 返回写入的 tags;id 不存在/状态非法返回 null。
|
|
191
|
+
*/
|
|
192
|
+
export declare function confirmReview(l1: RoomReviewStore, id: string, rooms: readonly string[]): {
|
|
193
|
+
tags: string[];
|
|
194
|
+
} | null;
|
|
195
|
+
/** 跳过:标 skipped(不再进 pending 队列)。已 confirmed 的记录不受 skip 影响(挂了 Room 的不算长尾)。 */
|
|
196
|
+
export declare function skipReview(l1: RoomReviewStore, id: string): boolean;
|