dsh-prime-memory 0.20.0-beta.3 → 0.20.0-beta.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 +12 -1
- package/dist/client.js +36 -2
- package/dist/contract.d.ts +30 -1
- package/dist/index.js +6 -2
- package/dist/pipeline/ruminate.js +12 -0
- package/dist/room-review.d.ts +118 -0
- package/dist/room-review.js +199 -0
- package/dist/stats.d.ts +9 -2
- package/dist/stats.js +87 -5
- package/dist/store/rooms-registry.d.ts +52 -0
- package/dist/store/rooms-registry.js +204 -0
- package/dist/tools/index.d.ts +4 -1
- package/dist/tools/index.js +155 -1
- package/dist/types.d.ts +4 -0
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,7 +17,18 @@
|
|
|
17
17
|
- **建议状态(`longtask-hint-get`)+ 浮动球(`client/src/longtask-fab.ts`)**:ego-browser 式 vanilla overlay(FAB+面板,拖拽/位置持久化/视口夹紧),**建议不阻断**——仅当「上下文占用达 `contextThresholdPct`」或「todo 漂移达 `driftThreshold`」且长任务未开启时点亮呼吸光晕;面板内:长任务开关(临时,per-session,**不带 mode**——切开关不重置用户档位)、占用/漂移进度条、手动尾部压缩按钮(长任务开启才可用)、任务计数。轮询忙 2s/闲 5s,RPC 失败静默;座位契约不变(settings.section 恰一个,body 单例不占任何座位)。
|
|
18
18
|
- **`session-mode-set` 的 `mode` 转为可选**:与 `longTask` 二选一必传——纯切长任务开关不再被迫发送档位(旧客户端恒传 mode,行为不变)。响应 `mode` 改回当前生效值。
|
|
19
19
|
|
|
20
|
-
## [0.20.0-beta.
|
|
20
|
+
## [0.20.0-beta.4] — 2026-10-01
|
|
21
|
+
|
|
22
|
+
> Room 分类管理第一船(**零漂移面**):注册表空 = 与 beta.3 行为逐字一致。merge/rename/retire 破坏性面将随 beta.5(dryRun 默认 true 保留一版)。
|
|
23
|
+
|
|
24
|
+
### 新增
|
|
25
|
+
|
|
26
|
+
- **Room 注册表(`rooms-registry.json` sidecar)**:预先注册策划好的 Room(slug/label/description);损坏/为空 → 降级回退纯自生长目录,绝不阻塞面板与标注器;上限 200 防刷。`rooms-get` 端点层合并:注册条目 count=0 也进词表并带 `source`/`label`,响应新增 `registry` 全量字段(客户端可选读)。
|
|
27
|
+
- **导出端点 `dsh-memory/rooms-export`**(kind=rooms|orphans,CSV 直出):rooms = 注册表∪自生长词表带计数;orphans = 无 Room 绑定的活跃记录清单(含候选/复查状态列)。
|
|
28
|
+
- **工具 `memory_room_admin`**(action=list|register):list 列注册表+自生长总览;register 预注册 Room(高权限门,幂等补全)——配合 `memory_room_review` 的逐个复查,agent 有了「注册→预标记→逐个确认」的完整分类管理入口。
|
|
29
|
+
- 客户端:Room chip 带「预注册/自生长」来源标示;孤儿 chip 带待复查语义。
|
|
30
|
+
|
|
31
|
+
## [0.20.0-beta.3] — 2026-10-01## [0.20.0-beta.3] — 2026-10-01
|
|
21
32
|
|
|
22
33
|
### 新增
|
|
23
34
|
|
package/dist/client.js
CHANGED
|
@@ -3298,6 +3298,7 @@ var __defProp = Object.defineProperty;
|
|
|
3298
3298
|
});
|
|
3299
3299
|
const [roomPage, setRoomPage] = (0, import_react15.useState)(0);
|
|
3300
3300
|
const [orphanCount, setOrphanCount] = (0, import_react15.useState)(0);
|
|
3301
|
+
const [roomRegistry, setRoomRegistry] = (0, import_react15.useState)([]);
|
|
3301
3302
|
const [retiredFilter, setRetiredFilter] = (0, import_react15.useState)("");
|
|
3302
3303
|
const [rooms, setRooms] = (0, import_react15.useState)([]);
|
|
3303
3304
|
const [wingCatalog, setHallCatalog] = (0, import_react15.useState)(null);
|
|
@@ -3356,6 +3357,7 @@ var __defProp = Object.defineProperty;
|
|
|
3356
3357
|
if (r && r.ok) {
|
|
3357
3358
|
setRooms(r.value.rooms ?? []);
|
|
3358
3359
|
setOrphanCount(r.value.orphanCount ?? 0);
|
|
3360
|
+
setRoomRegistry(r.value.registry ?? []);
|
|
3359
3361
|
}
|
|
3360
3362
|
}).catch(() => {
|
|
3361
3363
|
});
|
|
@@ -3562,6 +3564,35 @@ var __defProp = Object.defineProperty;
|
|
|
3562
3564
|
]
|
|
3563
3565
|
}
|
|
3564
3566
|
),
|
|
3567
|
+
rooms.length > 0 || orphanCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
3568
|
+
"button",
|
|
3569
|
+
{
|
|
3570
|
+
type: "button",
|
|
3571
|
+
title: "导出分类数据 CSV(Room 词表+计数)",
|
|
3572
|
+
onClick: () => {
|
|
3573
|
+
rpc("dsh-memory/rooms-export", { kind: "rooms" }).then((r) => {
|
|
3574
|
+
if (!r || !r.ok || !r.value?.csv) return;
|
|
3575
|
+
const blob = new Blob([r.value.csv], { type: "text/csv;charset=utf-8" });
|
|
3576
|
+
const url = URL.createObjectURL(blob);
|
|
3577
|
+
const a = document.createElement("a");
|
|
3578
|
+
a.href = url;
|
|
3579
|
+
a.download = "dsh-memory-rooms.csv";
|
|
3580
|
+
a.click();
|
|
3581
|
+
URL.revokeObjectURL(url);
|
|
3582
|
+
}).catch(() => void 0);
|
|
3583
|
+
},
|
|
3584
|
+
style: {
|
|
3585
|
+
cursor: "pointer",
|
|
3586
|
+
fontSize: 12,
|
|
3587
|
+
padding: "2px 8px",
|
|
3588
|
+
borderRadius: 6,
|
|
3589
|
+
border: "1px solid var(--dsh-mem-border)",
|
|
3590
|
+
background: "transparent",
|
|
3591
|
+
color: "var(--dsh-mem-text-2)"
|
|
3592
|
+
},
|
|
3593
|
+
children: "导出 CSV"
|
|
3594
|
+
}
|
|
3595
|
+
) : null,
|
|
3565
3596
|
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
3566
3597
|
"button",
|
|
3567
3598
|
{
|
|
@@ -3612,7 +3643,7 @@ var __defProp = Object.defineProperty;
|
|
|
3612
3643
|
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center", marginBottom: roomsOpen ? 6 : 0 }, children: [
|
|
3613
3644
|
(roomsOpen ? rooms.slice(roomPage * ROOM_PAGE_SIZE, (roomPage + 1) * ROOM_PAGE_SIZE) : rooms.slice(0, 8)).map((r) => {
|
|
3614
3645
|
const on = tagFilter === r.room && !untaggedOnly;
|
|
3615
|
-
return /* @__PURE__ */ (0, import_jsx_runtime15.
|
|
3646
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
3616
3647
|
"button",
|
|
3617
3648
|
{
|
|
3618
3649
|
type: "button",
|
|
@@ -3634,7 +3665,10 @@ var __defProp = Object.defineProperty;
|
|
|
3634
3665
|
background: on ? "var(--dsh-mem-bg-inset)" : "transparent",
|
|
3635
3666
|
color: on ? "var(--dsh-mem-accent)" : "var(--dsh-mem-text-2)"
|
|
3636
3667
|
},
|
|
3637
|
-
children:
|
|
3668
|
+
children: [
|
|
3669
|
+
r.label ? r.label + " · " + r.count : r.room + " · " + r.count,
|
|
3670
|
+
r.source === "pre-registered" ? " ⭐" : ""
|
|
3671
|
+
]
|
|
3638
3672
|
},
|
|
3639
3673
|
r.room
|
|
3640
3674
|
);
|
package/dist/contract.d.ts
CHANGED
|
@@ -557,12 +557,39 @@ export interface WingOverviewResponse {
|
|
|
557
557
|
* 降级/无 tags 时 `rooms` 为空数组(面板显示"暂无"),不报错。
|
|
558
558
|
*/
|
|
559
559
|
export interface RoomsGetResponse {
|
|
560
|
-
/** Room 列表(
|
|
560
|
+
/** Room 列表(按记录数降序,同数按名升序;注册表条目 count=0 并带 source/label)。
|
|
561
|
+
* 条目可选字段 source/label 由端点层合并注册表后附带(客户端可选读,零断裂)。 */
|
|
561
562
|
rooms: RoomCount[];
|
|
562
563
|
/** Room 总数(= rooms.length,冗余只为前端少写一次 .length)。 */
|
|
563
564
|
total: number;
|
|
564
565
|
/** 孤儿记忆数:没有任何 Room(tags 为空/缺失)的记录条数(与 rooms 同口径含已退场)。 */
|
|
565
566
|
orphanCount: number;
|
|
567
|
+
/** 注册表条目(全量含 retired;分类管理面板用;未装配 = undefined)。 */
|
|
568
|
+
registry?: Array<{
|
|
569
|
+
slug: string;
|
|
570
|
+
label?: string;
|
|
571
|
+
description?: string;
|
|
572
|
+
source: 'pre-registered' | 'grown';
|
|
573
|
+
aliases?: string[];
|
|
574
|
+
status: 'active' | 'retired';
|
|
575
|
+
}>;
|
|
576
|
+
}
|
|
577
|
+
/** dsh-memory/rooms-export(分类数据导出,CSV 直出)。 */
|
|
578
|
+
export interface RoomsExportRequest {
|
|
579
|
+
/** rooms = 词表+计数;orphans = 无 Room 绑定的记录清单。 */
|
|
580
|
+
kind: 'rooms' | 'orphans';
|
|
581
|
+
/** orphans 上限(1..10000,默认 2000)。 */
|
|
582
|
+
limit?: number;
|
|
583
|
+
}
|
|
584
|
+
export interface RoomsExportResponse {
|
|
585
|
+
kind: 'rooms' | 'orphans';
|
|
586
|
+
/** CSV 文本(首行表头;
|
|
587
|
+
行尾;RFC 4180 转义)。 */
|
|
588
|
+
csv: string;
|
|
589
|
+
/** 数据行数(不含表头)。 */
|
|
590
|
+
total: number;
|
|
591
|
+
/** truncated = 达到 limit 上限,结果可能不完整。 */
|
|
592
|
+
truncated: boolean;
|
|
566
593
|
}
|
|
567
594
|
/** dsh-memory/wing-backfill(一键回填,后台任务;端点立即返回,进度以 wing-overview 轮询)。 */
|
|
568
595
|
export interface WingBackfillResponse {
|
|
@@ -1260,6 +1287,7 @@ export interface DshMemoryRequestMap {
|
|
|
1260
1287
|
'dsh-memory/longtask-compress-tail': LongTaskCompressTailRequest;
|
|
1261
1288
|
'dsh-memory/wing-overview': Record<string, never>;
|
|
1262
1289
|
'dsh-memory/rooms-get': Record<string, never>;
|
|
1290
|
+
'dsh-memory/rooms-export': RoomsExportRequest;
|
|
1263
1291
|
'dsh-memory/wing-backfill': Record<string, never>;
|
|
1264
1292
|
'dsh-memory/session-stats': SessionStatsRequest;
|
|
1265
1293
|
'dsh-memory/settings-get': Record<string, never>;
|
|
@@ -1307,6 +1335,7 @@ export interface DshMemoryResponseMap {
|
|
|
1307
1335
|
'dsh-memory/session-mode-set': SessionModeSetResponse;
|
|
1308
1336
|
'dsh-memory/wing-overview': WingOverviewResponse;
|
|
1309
1337
|
'dsh-memory/rooms-get': RoomsGetResponse;
|
|
1338
|
+
'dsh-memory/rooms-export': RoomsExportResponse;
|
|
1310
1339
|
'dsh-memory/wing-backfill': WingBackfillResponse;
|
|
1311
1340
|
'dsh-memory/session-stats': SessionStatsResponse;
|
|
1312
1341
|
'dsh-memory/settings-get': SettingsGetResponse;
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ import { registerSlotRecall } from './hooks/slot-recall.js';
|
|
|
39
39
|
import { registerLongTaskMonitor } from './hooks/long-task.js';
|
|
40
40
|
import { ContextUsageStore } from './store/context-usage.js';
|
|
41
41
|
import { TodoRefStore } from './store/todo-ref.js';
|
|
42
|
+
import { RoomRegistryStore } from './store/rooms-registry.js';
|
|
42
43
|
import { registerSlotsProjection } from './projection/slots.js';
|
|
43
44
|
import { errDetail, withFileLog } from './util/filelog.js';
|
|
44
45
|
import { buildRouteChain, resolveModelRoute, invalidateEffortCache } from './llm.js';
|
|
@@ -364,6 +365,9 @@ export async function apply(ctx, config) {
|
|
|
364
365
|
// todo/write 快照参考(仅长任务会话;写 pinned 槽位走 slot-recall 常驻注入)。
|
|
365
366
|
const contextUsage = new ContextUsageStore(logger);
|
|
366
367
|
const todoRef = new TodoRefStore();
|
|
368
|
+
// Room 注册表(分类管理 beta.4;损坏降级为纯自生长目录)
|
|
369
|
+
const roomRegistry = new RoomRegistryStore(dataDir, logger);
|
|
370
|
+
await roomRegistry.init();
|
|
367
371
|
if (storageOk) {
|
|
368
372
|
registerLongTaskMonitor(ctx, {
|
|
369
373
|
cfg: config,
|
|
@@ -376,7 +380,7 @@ export async function apply(ctx, config) {
|
|
|
376
380
|
}
|
|
377
381
|
const recall = registerRecall(ctx, config, stores, logger, live, modes, dataDir);
|
|
378
382
|
runner.setAfterRun(recall.invalidateProfile);
|
|
379
|
-
registerMemoryTools(ctx, config, stores, logger, modes, live, ruminate);
|
|
383
|
+
registerMemoryTools(ctx, config, stores, logger, modes, live, ruminate, storageOk ? roomRegistry : undefined);
|
|
380
384
|
// 激活槽位(active slot):工具面 + 常驻注入 + 服务端投影(均走 ctx.effect,可撤销)
|
|
381
385
|
registerSlotTools(ctx, config, stores.slots, logger, modes, live, stores.l1);
|
|
382
386
|
const slotRecall = registerSlotRecall(ctx, config, stores.slots, logger, live);
|
|
@@ -395,7 +399,7 @@ export async function apply(ctx, config) {
|
|
|
395
399
|
runnerView: (sid, mode) => runner.sessionView(sid, mode),
|
|
396
400
|
l0Count: (sid) => stores.l0.countBySession(sid),
|
|
397
401
|
capabilities: () => db.getCapabilities(),
|
|
398
|
-
}, ruminate, storageOk ? { runner, slots: stores.slots, contextUsage, todoRef } : undefined);
|
|
402
|
+
}, ruminate, storageOk ? { runner, slots: stores.slots, contextUsage, todoRef } : undefined, storageOk ? roomRegistry : undefined);
|
|
399
403
|
// bench 控制服务(config.benchControl 门控,默认关):仅基准/调试部署注册,
|
|
400
404
|
// 供同进程的 bench-runner lifecycle 赛道触发 rebuild / 设置会话档位
|
|
401
405
|
// (宿主侧 RPC 无 call(),见 bench-control.ts)
|
|
@@ -4,6 +4,7 @@ import { errDetail } from '../util/filelog.js';
|
|
|
4
4
|
import { runSceneConsolidation } from './l2.js';
|
|
5
5
|
import { relabelPass } from './relabel.js';
|
|
6
6
|
import { processSceneReclusterJobs } from './recluster.js';
|
|
7
|
+
import { annotateOrphanCandidates } from '../room-review.js';
|
|
7
8
|
import { runPersona } from './l3.js';
|
|
8
9
|
const IDLE_STATUS = {
|
|
9
10
|
running: false,
|
|
@@ -233,6 +234,17 @@ export class RuminateController {
|
|
|
233
234
|
catch (err) {
|
|
234
235
|
this.logger.warn(`[memory] 场景重聚类调度失败(忽略): ${errDetail(err)}`);
|
|
235
236
|
}
|
|
237
|
+
// 孤儿候选预标记(长尾治理):无 Room 的活跃孤儿对照现有 Room 词表产出
|
|
238
|
+
// 候选(roomReview='pending'),由 agent 用 memory_room_review 逐个复查;
|
|
239
|
+
// 每轮至多 40 条,吞错不拖垮反刍。
|
|
240
|
+
try {
|
|
241
|
+
const r = await annotateOrphanCandidates(this.ctx, this.cfg, { l1: this.stores.l1, logger: this.logger }, 40);
|
|
242
|
+
if (r.written > 0)
|
|
243
|
+
this.status.detail = `孤儿候选预标记 ${r.written} 条(待 agent 复查,工具 memory_room_review)`;
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
this.logger.warn(`[memory] 孤儿候选预标记失败(忽略): ${errDetail(err)}`);
|
|
247
|
+
}
|
|
236
248
|
this.status.phase = 'done';
|
|
237
249
|
this.finish(null);
|
|
238
250
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
export interface RoomCandidate {
|
|
50
|
+
id: string;
|
|
51
|
+
rooms: string[];
|
|
52
|
+
}
|
|
53
|
+
/** 解析标注器输出的逐条候选(容错:id 配对失败丢弃;isTag 校验;目录内优先,≤3)。 */
|
|
54
|
+
export declare function parseRoomCandidateItems(parsed: ReadonlyArray<{
|
|
55
|
+
id: string;
|
|
56
|
+
rooms: readonly string[];
|
|
57
|
+
}>, ids: ReadonlyArray<string>, roomCatalog: ReadonlyArray<{
|
|
58
|
+
room: string;
|
|
59
|
+
count: number;
|
|
60
|
+
}>): RoomCandidate[];
|
|
61
|
+
/** 候选标注系统 prompt:喂现有 Room 词表(带计数),优先挂靠、少量新建兜底。 */
|
|
62
|
+
export declare function roomCandidateSystemPrompt(roomCatalog: ReadonlyArray<{
|
|
63
|
+
room: string;
|
|
64
|
+
count: number;
|
|
65
|
+
}>): string;
|
|
66
|
+
/** 候选标注器:一批孤儿记录 → 候选 Room(优先现有词表;失败返回空,调用方零改动)。 */
|
|
67
|
+
export declare function roomCandidateChunk(ctx: Context, cfg: MemoryConfig, logger: MemoryLogger, chunk: MemoryRecord[], roomCatalog: ReadonlyArray<{
|
|
68
|
+
room: string;
|
|
69
|
+
count: number;
|
|
70
|
+
}>): Promise<Array<{
|
|
71
|
+
id: string;
|
|
72
|
+
rooms: string[];
|
|
73
|
+
}>>;
|
|
74
|
+
export interface OrphanAnnotateIO {
|
|
75
|
+
l1: RoomReviewStore;
|
|
76
|
+
logger: MemoryLogger;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 预标记消费器:活跃孤儿(≤limit 条)批量产候选并写 pending。
|
|
80
|
+
* 写前重读合并(与 relabel 同纪律);失败单条跳过、原记录零改动。
|
|
81
|
+
*/
|
|
82
|
+
export interface OrphanAnnotateOverrides {
|
|
83
|
+
/** 测试注入口:替换 LLM 候选标注器(默认走真实 callLLM 路径)。 */
|
|
84
|
+
chunkAnnotator?: (chunk: MemoryRecord[]) => Promise<Array<{
|
|
85
|
+
id: string;
|
|
86
|
+
rooms: string[];
|
|
87
|
+
}>>;
|
|
88
|
+
}
|
|
89
|
+
export declare function annotateOrphanCandidates(ctx: Context, cfg: MemoryConfig, io: OrphanAnnotateIO, limit?: number, overrides?: OrphanAnnotateOverrides): Promise<{
|
|
90
|
+
selected: number;
|
|
91
|
+
candidates: number;
|
|
92
|
+
written: number;
|
|
93
|
+
}>;
|
|
94
|
+
/** 下一条待复查(供 agent 逐个过):记录摘要 + 候选 + 现有 Room 词表 + 剩余数。 */
|
|
95
|
+
export declare function nextReview(l1: RoomReviewStore): {
|
|
96
|
+
id: string;
|
|
97
|
+
content: string;
|
|
98
|
+
type: string;
|
|
99
|
+
sceneName: string;
|
|
100
|
+
candidates: string[];
|
|
101
|
+
roomCatalog: Array<{
|
|
102
|
+
room: string;
|
|
103
|
+
count: number;
|
|
104
|
+
}>;
|
|
105
|
+
remaining: number;
|
|
106
|
+
} | null;
|
|
107
|
+
/** 待复查剩余数(面板/工具兜底信息)。 */
|
|
108
|
+
export declare function pendingReviewCount(l1: RoomReviewStore): number;
|
|
109
|
+
/**
|
|
110
|
+
* 确认复查结果:候选(或 agent 改写的)写入 metadata.tags(**进 Room**),
|
|
111
|
+
* 清候选、标 confirmed。**高权限门控由调用方执行**(与 memory_delete 同款)。
|
|
112
|
+
* 返回写入的 tags;id 不存在/状态非法返回 null。
|
|
113
|
+
*/
|
|
114
|
+
export declare function confirmReview(l1: RoomReviewStore, id: string, rooms: readonly string[]): {
|
|
115
|
+
tags: string[];
|
|
116
|
+
} | null;
|
|
117
|
+
/** 跳过:标 skipped(不再进 pending 队列)。已 confirmed 的记录不受 skip 影响(挂了 Room 的不算长尾)。 */
|
|
118
|
+
export declare function skipReview(l1: RoomReviewStore, id: string): boolean;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { isTag } from './metadata-validators.js';
|
|
2
|
+
import { callLLM, parseJsonLogged } from './llm.js';
|
|
3
|
+
export const ROOM_CANDIDATES_KEY = 'roomCandidates';
|
|
4
|
+
export const ROOM_REVIEW_KEY = 'roomReview';
|
|
5
|
+
/** 解析标注器输出的逐条候选(容错:id 配对失败丢弃;isTag 校验;目录内优先,≤3)。 */
|
|
6
|
+
export function parseRoomCandidateItems(parsed, ids, roomCatalog) {
|
|
7
|
+
const idSet = new Set(ids);
|
|
8
|
+
const catalogSet = new Set(roomCatalog.map((r) => r.room));
|
|
9
|
+
const byId = new Map();
|
|
10
|
+
for (const item of parsed) {
|
|
11
|
+
if (!idSet.has(item.id))
|
|
12
|
+
continue;
|
|
13
|
+
const cleaned = [];
|
|
14
|
+
for (const r of item.rooms) {
|
|
15
|
+
const slug = typeof r === 'string' ? r.trim().toLowerCase() : '';
|
|
16
|
+
if (!slug || !isTag(slug))
|
|
17
|
+
continue;
|
|
18
|
+
if (!cleaned.includes(slug))
|
|
19
|
+
cleaned.push(slug);
|
|
20
|
+
}
|
|
21
|
+
// 目录内优先排前,其余兜底;截断到 3
|
|
22
|
+
cleaned.sort((a, b) => Number(catalogSet.has(b)) - Number(catalogSet.has(a)));
|
|
23
|
+
byId.set(item.id, cleaned.slice(0, 3));
|
|
24
|
+
}
|
|
25
|
+
return [...byId.entries()].map(([id, rooms]) => ({ id, rooms }));
|
|
26
|
+
}
|
|
27
|
+
/** 候选标注系统 prompt:喂现有 Room 词表(带计数),优先挂靠、少量新建兜底。 */
|
|
28
|
+
export function roomCandidateSystemPrompt(roomCatalog) {
|
|
29
|
+
const catalog = roomCatalog.length > 0
|
|
30
|
+
? roomCatalog
|
|
31
|
+
.slice(0, 40)
|
|
32
|
+
.map((r) => `- ${r.room}(现有 ${r.count} 条)`)
|
|
33
|
+
.join('\n')
|
|
34
|
+
: '(暂无现有 Room)';
|
|
35
|
+
return ('你是记忆库的 Room 归类器。给你若干条记忆(带 id)与现有 Room 词表(带计数),' +
|
|
36
|
+
'为每条记忆选出它最可能归属的 1-3 个 Room slug。规则:\n' +
|
|
37
|
+
'1. **优先从现有 Room 词表里选**(保持分类收敛,避免碎片化);\n' +
|
|
38
|
+
'2. 词表确实没有合适主题时,才提议最多 2 个新 slug(小写字母/数字/连字符);\n' +
|
|
39
|
+
'3. 完全无法判断时返回空数组(宁缺毋滥,不硬猜)。\n' +
|
|
40
|
+
`现有 Room 词表:\n${catalog}\n\n` +
|
|
41
|
+
'输出 JSON 数组:[{"id":"<原id>","rooms":["slug1","slug2"]}]。只输出 JSON,不要多余文字。');
|
|
42
|
+
}
|
|
43
|
+
/** 候选标注器:一批孤儿记录 → 候选 Room(优先现有词表;失败返回空,调用方零改动)。 */
|
|
44
|
+
export async function roomCandidateChunk(ctx, cfg, logger, chunk, roomCatalog) {
|
|
45
|
+
const list = chunk.map((r, i) => `${i + 1}. id=${r.id}\n${r.content.slice(0, 300)}`).join('\n\n');
|
|
46
|
+
try {
|
|
47
|
+
const raw = await callLLM(ctx, cfg, {
|
|
48
|
+
system: roomCandidateSystemPrompt(roomCatalog),
|
|
49
|
+
user: `记忆列表:\n${list}`,
|
|
50
|
+
maxTokens: 4096,
|
|
51
|
+
layer: 'l1-extract',
|
|
52
|
+
logger,
|
|
53
|
+
});
|
|
54
|
+
const parsed = parseJsonLogged(raw, 'Room 候选标注', logger);
|
|
55
|
+
if (!Array.isArray(parsed))
|
|
56
|
+
return [];
|
|
57
|
+
const shape = parsed
|
|
58
|
+
.map((item) => ({
|
|
59
|
+
id: typeof item?.id === 'string' ? item.id : '',
|
|
60
|
+
rooms: Array.isArray(item?.rooms) ? item.rooms : [],
|
|
61
|
+
}))
|
|
62
|
+
.filter((x) => x.id !== '');
|
|
63
|
+
return parseRoomCandidateItems(shape, chunk.map((r) => r.id), roomCatalog);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
logger.warn(`[memory] Room 候选标注块失败(跳过 ${chunk.length} 条,原记录未改): ${err instanceof Error ? err.message : String(err)}`);
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** 活跃孤儿的选择器:untagged + 未退场,跳过已 skipped 的。 */
|
|
71
|
+
function selectPendingOrphans(l1, limit) {
|
|
72
|
+
const out = [];
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
for (let offset = 0; offset < 2000; offset += 200) {
|
|
75
|
+
const page = l1.list({ untagged: true, retired: false, limit: 200, offset });
|
|
76
|
+
for (const r of page.items) {
|
|
77
|
+
if (seen.has(r.id))
|
|
78
|
+
continue;
|
|
79
|
+
seen.add(r.id);
|
|
80
|
+
if (r.metadata?.[ROOM_REVIEW_KEY] === 'skipped')
|
|
81
|
+
continue;
|
|
82
|
+
out.push(r);
|
|
83
|
+
if (out.length >= limit)
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
if (page.items.length < 200)
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
export async function annotateOrphanCandidates(ctx, cfg, io, limit = 40, overrides = {}) {
|
|
92
|
+
const selected = selectPendingOrphans(io.l1, limit);
|
|
93
|
+
if (selected.length === 0)
|
|
94
|
+
return { selected: 0, candidates: 0, written: 0 };
|
|
95
|
+
// 全库 Room 词表(计数降序)。整批共享一张词表(一次 LLM 调用),
|
|
96
|
+
// 同仓优先的精化在逐条候选写入后由 repo-change 重算消费,此处保持简单。
|
|
97
|
+
const rooms = io.l1.listRooms();
|
|
98
|
+
const rows = overrides.chunkAnnotator !== undefined
|
|
99
|
+
? await overrides.chunkAnnotator(selected)
|
|
100
|
+
: await roomCandidateChunk(ctx, cfg, io.logger, selected, rooms);
|
|
101
|
+
let written = 0;
|
|
102
|
+
for (const { id, rooms: candidates } of rows) {
|
|
103
|
+
if (candidates.length === 0)
|
|
104
|
+
continue;
|
|
105
|
+
const fresh = io.l1.getByIds([id])[0];
|
|
106
|
+
if (!fresh)
|
|
107
|
+
continue;
|
|
108
|
+
const meta = { ...(fresh.metadata ?? {}) };
|
|
109
|
+
meta[ROOM_CANDIDATES_KEY] = candidates;
|
|
110
|
+
meta[ROOM_REVIEW_KEY] = 'pending';
|
|
111
|
+
if (io.l1.patchMetadata(id, meta))
|
|
112
|
+
written++;
|
|
113
|
+
}
|
|
114
|
+
io.logger.info(`[memory] 孤儿候选预标记:选中 ${selected.length},产候选 ${rows.length},写 pending ${written} 条(待 agent 复查)`);
|
|
115
|
+
return { selected: selected.length, candidates: rows.length, written };
|
|
116
|
+
}
|
|
117
|
+
/** 待复查队列:pending 且有候选的活跃孤儿,按更新时间升序(最旧的先复查)。 */
|
|
118
|
+
function pendingQueue(l1) {
|
|
119
|
+
const out = [];
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
for (let offset = 0; offset < 2000; offset += 200) {
|
|
122
|
+
const page = l1.list({ untagged: true, retired: false, limit: 200, offset });
|
|
123
|
+
for (const r of page.items) {
|
|
124
|
+
if (seen.has(r.id))
|
|
125
|
+
continue;
|
|
126
|
+
seen.add(r.id);
|
|
127
|
+
const meta = (r.metadata ?? {});
|
|
128
|
+
if (meta[ROOM_REVIEW_KEY] === 'pending' && Array.isArray(meta[ROOM_CANDIDATES_KEY]))
|
|
129
|
+
out.push(r);
|
|
130
|
+
}
|
|
131
|
+
if (page.items.length < 200)
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
return out.sort((a, b) => a.updatedAt - b.updatedAt);
|
|
135
|
+
}
|
|
136
|
+
/** 下一条待复查(供 agent 逐个过):记录摘要 + 候选 + 现有 Room 词表 + 剩余数。 */
|
|
137
|
+
export function nextReview(l1) {
|
|
138
|
+
const queue = pendingQueue(l1);
|
|
139
|
+
if (queue.length === 0)
|
|
140
|
+
return null;
|
|
141
|
+
const r = queue[0];
|
|
142
|
+
const meta = (r.metadata ?? {});
|
|
143
|
+
return {
|
|
144
|
+
id: r.id,
|
|
145
|
+
content: r.content.slice(0, 600),
|
|
146
|
+
type: r.type,
|
|
147
|
+
sceneName: r.scene_name,
|
|
148
|
+
candidates: meta[ROOM_CANDIDATES_KEY] ?? [],
|
|
149
|
+
roomCatalog: l1.listRooms().slice(0, 40),
|
|
150
|
+
remaining: queue.length,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** 待复查剩余数(面板/工具兜底信息)。 */
|
|
154
|
+
export function pendingReviewCount(l1) {
|
|
155
|
+
return pendingQueue(l1).length;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 确认复查结果:候选(或 agent 改写的)写入 metadata.tags(**进 Room**),
|
|
159
|
+
* 清候选、标 confirmed。**高权限门控由调用方执行**(与 memory_delete 同款)。
|
|
160
|
+
* 返回写入的 tags;id 不存在/状态非法返回 null。
|
|
161
|
+
*/
|
|
162
|
+
export function confirmReview(l1, id, rooms) {
|
|
163
|
+
const found = l1.getByIds([id])[0];
|
|
164
|
+
if (!found)
|
|
165
|
+
return null;
|
|
166
|
+
const meta = { ...(found.metadata ?? {}) };
|
|
167
|
+
// 已确认/已跳过的不允许重复确认(防止 agent 循环空转)
|
|
168
|
+
if (meta[ROOM_REVIEW_KEY] === 'confirmed')
|
|
169
|
+
return null;
|
|
170
|
+
const cleaned = [];
|
|
171
|
+
for (const r of rooms) {
|
|
172
|
+
if (typeof r !== 'string')
|
|
173
|
+
continue;
|
|
174
|
+
const slug = r.trim().toLowerCase();
|
|
175
|
+
if (!slug || !isTag(slug) || cleaned.includes(slug))
|
|
176
|
+
continue;
|
|
177
|
+
cleaned.push(slug);
|
|
178
|
+
}
|
|
179
|
+
if (cleaned.length === 0)
|
|
180
|
+
return null;
|
|
181
|
+
meta.tags = cleaned;
|
|
182
|
+
delete meta[ROOM_CANDIDATES_KEY];
|
|
183
|
+
meta[ROOM_REVIEW_KEY] = 'confirmed';
|
|
184
|
+
if (!l1.patchMetadata(id, meta))
|
|
185
|
+
return null;
|
|
186
|
+
l1.invalidateRooms();
|
|
187
|
+
return { tags: cleaned };
|
|
188
|
+
}
|
|
189
|
+
/** 跳过:标 skipped(不再进 pending 队列)。已 confirmed 的记录不受 skip 影响(挂了 Room 的不算长尾)。 */
|
|
190
|
+
export function skipReview(l1, id) {
|
|
191
|
+
const found = l1.getByIds([id])[0];
|
|
192
|
+
if (!found)
|
|
193
|
+
return false;
|
|
194
|
+
const meta = { ...(found.metadata ?? {}) };
|
|
195
|
+
if (meta[ROOM_REVIEW_KEY] === 'confirmed')
|
|
196
|
+
return false;
|
|
197
|
+
meta[ROOM_REVIEW_KEY] = 'skipped';
|
|
198
|
+
return l1.patchMetadata(id, meta);
|
|
199
|
+
}
|
package/dist/stats.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type MemoryConfig } from './config.js';
|
|
|
3
3
|
import { type RecallSessionStats } from './hooks/recall.js';
|
|
4
4
|
import type { RebuildController } from './pipeline/rebuild.js';
|
|
5
5
|
import type { RuminateController } from './pipeline/ruminate.js';
|
|
6
|
+
import type { RoomRegistryStore } from './store/rooms-registry.js';
|
|
6
7
|
import { type LiveSettingsHandle } from './settings.js';
|
|
7
8
|
import type { GraphStore } from './store/graph-store.js';
|
|
8
9
|
import type { L0Store } from './store/l0.js';
|
|
@@ -27,7 +28,7 @@ export interface MemoryStatusSource {
|
|
|
27
28
|
pending(): number;
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
|
-
* 端点全集运行时清单(
|
|
31
|
+
* 端点全集运行时清单(与 tests/contract-keys.test.ts 的 ENDPOINTS 及
|
|
31
32
|
* contract.ts 类型映射表三方对齐,漂移由键集 diff 测试暴露)。
|
|
32
33
|
* 注意:本清单同时是 HTTP 前缀路由 `/dsh-memory/rpc/<短名>` 的**放行白名单**
|
|
33
34
|
* (见下方 SHORT_ENDPOINTS),漏一条 = 该端点在面板里静默消失(404 被客户端
|
|
@@ -105,6 +106,8 @@ interface MemoryRpcSources {
|
|
|
105
106
|
sessionInfo?: SessionInfoSource;
|
|
106
107
|
/** 长任务端点依赖(未装配 = 端点恒 disabled,面板不挂载)。 */
|
|
107
108
|
longTask?: LongTaskEndpointDeps;
|
|
109
|
+
/** Room 注册表(未装配 = rooms-get 不带 registry,rooms-export 不可用)。 */
|
|
110
|
+
roomRegistry?: RoomRegistryStore;
|
|
108
111
|
}
|
|
109
112
|
/** 端点 deps 的注入面(ctx/cfg/stores/logger 由调用方绑定,其余由此处决定)。 */
|
|
110
113
|
export type EndpointDepsInput = Omit<EndpointDeps, 'ctx' | 'cfg' | 'stores' | 'logger'>;
|
|
@@ -128,7 +131,9 @@ export declare function registerMemoryRpc(ctx: Context, cfg: MemoryConfig, store
|
|
|
128
131
|
/** 反刍控制器(存储降级时为 undefined):经 buildEndpointDeps 落入 deps.ruminate。 */
|
|
129
132
|
ruminate?: RuminateController,
|
|
130
133
|
/** 长任务端点依赖(cfg.longTask.enabled 门控;缺省 = 端点恒 disabled)。 */
|
|
131
|
-
longTaskDeps?: LongTaskEndpointDeps
|
|
134
|
+
longTaskDeps?: LongTaskEndpointDeps,
|
|
135
|
+
/** Room 分类管理依赖(注册表;缺省 = rooms-get 不带 registry、rooms-export 恒 404 语义)。 */
|
|
136
|
+
roomRegistry?: RoomRegistryStore): void;
|
|
132
137
|
export interface EndpointDeps {
|
|
133
138
|
ctx: Context;
|
|
134
139
|
cfg: MemoryConfig;
|
|
@@ -153,6 +158,8 @@ export interface EndpointDeps {
|
|
|
153
158
|
sessionInfo?: SessionInfoSource;
|
|
154
159
|
/** 长任务端点依赖(未装配 = 端点恒 disabled,面板不挂载)。 */
|
|
155
160
|
longTask?: LongTaskEndpointDeps;
|
|
161
|
+
/** Room 注册表(未装配 = rooms-get 不带 registry,rooms-export 不可用)。 */
|
|
162
|
+
roomRegistry?: RoomRegistryStore;
|
|
156
163
|
}
|
|
157
164
|
/** 端点分发表(导出供测试直调:可精确注入 rebuild/ruminate 等可选控制器,验证 deps 接线)。 */
|
|
158
165
|
export declare function handleEndpoint(endpoint: string, payload: unknown, deps: EndpointDeps): Promise<unknown>;
|
package/dist/stats.js
CHANGED
|
@@ -36,7 +36,7 @@ import { snapshotTokenCost } from './token-cost.js';
|
|
|
36
36
|
const require = createRequire(import.meta.url);
|
|
37
37
|
export const PLUGIN_VERSION = require('../package.json').version;
|
|
38
38
|
/**
|
|
39
|
-
* 端点全集运行时清单(
|
|
39
|
+
* 端点全集运行时清单(与 tests/contract-keys.test.ts 的 ENDPOINTS 及
|
|
40
40
|
* contract.ts 类型映射表三方对齐,漂移由键集 diff 测试暴露)。
|
|
41
41
|
* 注意:本清单同时是 HTTP 前缀路由 `/dsh-memory/rpc/<短名>` 的**放行白名单**
|
|
42
42
|
* (见下方 SHORT_ENDPOINTS),漏一条 = 该端点在面板里静默消失(404 被客户端
|
|
@@ -55,6 +55,7 @@ export const MEMORY_ENDPOINTS = [
|
|
|
55
55
|
'dsh-memory/longtask-compress-tail',
|
|
56
56
|
'dsh-memory/wing-overview',
|
|
57
57
|
'dsh-memory/rooms-get',
|
|
58
|
+
'dsh-memory/rooms-export',
|
|
58
59
|
'dsh-memory/wing-backfill',
|
|
59
60
|
'dsh-memory/session-stats',
|
|
60
61
|
'dsh-memory/settings-get',
|
|
@@ -217,6 +218,7 @@ export function buildEndpointDeps(base, sources, controller) {
|
|
|
217
218
|
sessionInfo: sources.sessionInfo,
|
|
218
219
|
ruminate: controller,
|
|
219
220
|
longTask: sources.longTask,
|
|
221
|
+
roomRegistry: sources.roomRegistry,
|
|
220
222
|
};
|
|
221
223
|
return { ...base, ...injected };
|
|
222
224
|
}
|
|
@@ -224,7 +226,9 @@ export function registerMemoryRpc(ctx, cfg, stores, logger, status, live, modes,
|
|
|
224
226
|
/** 反刍控制器(存储降级时为 undefined):经 buildEndpointDeps 落入 deps.ruminate。 */
|
|
225
227
|
ruminate,
|
|
226
228
|
/** 长任务端点依赖(cfg.longTask.enabled 门控;缺省 = 端点恒 disabled)。 */
|
|
227
|
-
longTaskDeps
|
|
229
|
+
longTaskDeps,
|
|
230
|
+
/** Room 分类管理依赖(注册表;缺省 = rooms-get 不带 registry、rooms-export 恒 404 语义)。 */
|
|
231
|
+
roomRegistry) {
|
|
228
232
|
/** 当前是否持有一段有效注册(dispose 完成后清空,允许服务重上线时重注册)。 */
|
|
229
233
|
let holding = false;
|
|
230
234
|
/** 当前 handle 绑定的 connection 实例(internal/service 第二参;用于识别实例替换)。 */
|
|
@@ -390,7 +394,7 @@ function sanitizeSettings(s) {
|
|
|
390
394
|
}
|
|
391
395
|
/** 端点分发表(导出供测试直调:可精确注入 rebuild/ruminate 等可选控制器,验证 deps 接线)。 */
|
|
392
396
|
export async function handleEndpoint(endpoint, payload, deps) {
|
|
393
|
-
const { cfg, stores, status, live, modes, dataDir, rebuild, ruminate, embedManager, sessionInfo, longTask: longTaskDeps } = deps;
|
|
397
|
+
const { cfg, stores, status, live, modes, dataDir, rebuild, ruminate, embedManager, sessionInfo, longTask: longTaskDeps, roomRegistry } = deps;
|
|
394
398
|
switch (endpoint) {
|
|
395
399
|
case 'dsh-memory/stats':
|
|
396
400
|
return buildStats(cfg, stores, status);
|
|
@@ -647,10 +651,88 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
647
651
|
// ── Room 分类计数(标签自生长分类;展开 metadata.tags 聚合,零 schema) ──
|
|
648
652
|
case 'dsh-memory/rooms-get': {
|
|
649
653
|
const rooms = stores.l1.listRooms();
|
|
650
|
-
|
|
654
|
+
// 端点层合并注册表(Agent A 设计):注册表 active 条目 count=0 补齐并带
|
|
655
|
+
// source/label;自生长条目标 source='grown'。不动 listRooms() 的缓存语义。
|
|
656
|
+
const merged = rooms.map((r) => ({ ...r, source: 'grown' }));
|
|
657
|
+
const registry = roomRegistry?.listActive() ?? [];
|
|
658
|
+
for (const e of registry) {
|
|
659
|
+
const idx = merged.findIndex((r) => r.room === e.slug);
|
|
660
|
+
if (idx === -1) {
|
|
661
|
+
merged.push({ room: e.slug, count: 0, source: 'pre-registered', label: e.label });
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
merged[idx] = { ...merged[idx], source: 'pre-registered', label: e.label ?? merged[idx].label };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const v = {
|
|
668
|
+
rooms: merged,
|
|
669
|
+
total: merged.length,
|
|
670
|
+
orphanCount: stores.l1.untaggedL1Count(),
|
|
671
|
+
registry: roomRegistry?.list().map((e) => ({
|
|
672
|
+
slug: e.slug,
|
|
673
|
+
label: e.label,
|
|
674
|
+
description: e.description,
|
|
675
|
+
source: e.source,
|
|
676
|
+
aliases: e.aliases,
|
|
677
|
+
status: e.status,
|
|
678
|
+
})),
|
|
679
|
+
};
|
|
680
|
+
return v;
|
|
681
|
+
}
|
|
682
|
+
case 'dsh-memory/rooms-export': {
|
|
683
|
+
const p = (payload ?? {});
|
|
684
|
+
const kind = p.kind === 'orphans' ? 'orphans' : p.kind === 'rooms' ? 'rooms' : '';
|
|
685
|
+
if (!kind)
|
|
686
|
+
throw new Error(`頖kind: ${String(p.kind)}(允rooms/orphans)` + ')');
|
|
687
|
+
const limit = Math.min(Math.max(Number(p.limit) || 2000, 1), 10_000);
|
|
688
|
+
const esc = (v) => {
|
|
689
|
+
const t = String(v ?? '');
|
|
690
|
+
return /[,\r\n"]/.test(t) ? '"' + t.replace(/"/g, '""') + '"' : t;
|
|
691
|
+
};
|
|
692
|
+
if (kind === 'rooms') {
|
|
693
|
+
const registry = roomRegistry?.list() ?? [];
|
|
694
|
+
const counts = new Map(stores.l1.listRooms().map((r) => [r.room, r.count]));
|
|
695
|
+
const lines = ['slug,count,source,label,description,status,aliases'];
|
|
696
|
+
let n = 0;
|
|
697
|
+
const seen = new Set();
|
|
698
|
+
for (const e of registry) {
|
|
699
|
+
lines.push([e.slug, String(counts.get(e.slug) ?? 0), e.source, e.label ?? '', e.description ?? '', e.status, (e.aliases ?? []).join('|')].map(esc).join(','));
|
|
700
|
+
seen.add(e.slug);
|
|
701
|
+
n++;
|
|
702
|
+
}
|
|
703
|
+
for (const r of stores.l1.listRooms()) {
|
|
704
|
+
if (seen.has(r.room))
|
|
705
|
+
continue;
|
|
706
|
+
lines.push([r.room, String(r.count), 'grown', '', '', 'active', ''].map(esc).join(','));
|
|
707
|
+
n++;
|
|
708
|
+
}
|
|
709
|
+
const v = { kind, csv: lines.join('\r\n'), total: n, truncated: false };
|
|
710
|
+
return v;
|
|
711
|
+
}
|
|
712
|
+
// orphans:无 Room 绑定的活跃记录清单(id/type/scene/updated/content/候选/状态)
|
|
713
|
+
const header = 'id,type,scene_name,updated_at,content,room_candidates,room_review';
|
|
714
|
+
const lines = [header];
|
|
715
|
+
let n = 0;
|
|
716
|
+
let truncated = false;
|
|
717
|
+
for (let offset = 0; offset < limit; offset += 200) {
|
|
718
|
+
const page = stores.l1.list({ untagged: true, retired: false, limit: 200, offset });
|
|
719
|
+
for (const r of page.items) {
|
|
720
|
+
if (n >= limit) {
|
|
721
|
+
truncated = true;
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
const meta = (r.metadata ?? {});
|
|
725
|
+
lines.push([r.id, r.type, r.scene_name, new Date(r.updatedAt).toISOString(), r.content,
|
|
726
|
+
Array.isArray(meta.roomCandidates) ? meta.roomCandidates.join('|') : '',
|
|
727
|
+
String(meta.roomReview ?? '')].map(esc).join(','));
|
|
728
|
+
n++;
|
|
729
|
+
}
|
|
730
|
+
if (truncated || page.items.length < 200)
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
const v = { kind, csv: lines.join('\r\n'), total: n, truncated };
|
|
651
734
|
return v;
|
|
652
735
|
}
|
|
653
|
-
// ── 一键回填(task_15):后台任务,单飞;端点立即返回,进度看 wing-overview ──
|
|
654
736
|
case 'dsh-memory/wing-backfill': {
|
|
655
737
|
const r = startWingBackfill({
|
|
656
738
|
ctx: deps.ctx,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { MemoryLogger } from '../types.js';
|
|
2
|
+
/** 版本常量(file-versions 同款集中纪律;registry 为独立 sidecar 文件)。 */
|
|
3
|
+
export declare const ROOMS_REGISTRY_FILE_VERSION: 1;
|
|
4
|
+
export interface RoomRegistryEntry {
|
|
5
|
+
/** Room slug(小写字母数字连字符;与自生长 tags 同域,isTag 校验)。 */
|
|
6
|
+
slug: string;
|
|
7
|
+
/** 人类可读名(面板/标注器提示;可中文)。 */
|
|
8
|
+
label?: string;
|
|
9
|
+
/** 归类说明(喂标注器,对 LLM 归类价值最高)。 */
|
|
10
|
+
description?: string;
|
|
11
|
+
/** 来源:pre-registered(预注册)| grown(从自生长 slug 收编)。 */
|
|
12
|
+
source: 'pre-registered' | 'grown';
|
|
13
|
+
/** 合并/改名时的旧 slug(标注器与导出消歧;不参与 list({tag}) 查询)。 */
|
|
14
|
+
aliases?: string[];
|
|
15
|
+
status: 'active' | 'retired';
|
|
16
|
+
createdAt: string;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
}
|
|
19
|
+
export type RoomRegistryInput = Pick<RoomRegistryEntry, 'slug'> & Partial<Pick<RoomRegistryEntry, 'label' | 'description' | 'source' | 'aliases'>>;
|
|
20
|
+
export declare class RoomRegistryStore {
|
|
21
|
+
private readonly logger?;
|
|
22
|
+
private readonly file;
|
|
23
|
+
private entries;
|
|
24
|
+
private rev;
|
|
25
|
+
private loaded;
|
|
26
|
+
private degraded;
|
|
27
|
+
private degradedLogged;
|
|
28
|
+
private writeChain;
|
|
29
|
+
constructor(dataDir: string, logger?: MemoryLogger | undefined);
|
|
30
|
+
/** 载入(启动时 await 一次)。缺失 = 合法空表;损坏/不可读 = 只读降级(回退自生长目录)。 */
|
|
31
|
+
init(): Promise<void>;
|
|
32
|
+
private ensureLoaded;
|
|
33
|
+
/** 全量条目(含 retired;副本返回,活引用纪律)。 */
|
|
34
|
+
list(): RoomRegistryEntry[];
|
|
35
|
+
/** active 条目(rooms-get 合并与标注器词表来源)。 */
|
|
36
|
+
listActive(): RoomRegistryEntry[];
|
|
37
|
+
/** 精确 slug 查找(含 retired;含别名命中)。 */
|
|
38
|
+
bySlug(slug: string): RoomRegistryEntry | undefined;
|
|
39
|
+
/** 注册(幂等:已存在——含别名命中——时原地补全 label/description,不新建)。上限 200 防刷。 */
|
|
40
|
+
register(input: RoomRegistryInput): Promise<{
|
|
41
|
+
entry: RoomRegistryEntry;
|
|
42
|
+
created: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
/** 改状态(active↔retired);无变化返回 false。 */
|
|
45
|
+
setStatus(slug: string, status: 'active' | 'retired'): Promise<boolean>;
|
|
46
|
+
/** 改名(merge 1:1 收尾):slug 换名 + 旧 slug 进 aliases。新名与既有条目冲突时走 merge。 */
|
|
47
|
+
renameSlug(oldSlug: string, newSlug: string): Promise<boolean>;
|
|
48
|
+
/** 合并收尾:fromSlug 条目转 retired + toSlug 进 aliases(记录重写由调用方游标执行)。 */
|
|
49
|
+
markMerged(fromSlug: string, toSlug: string): Promise<boolean>;
|
|
50
|
+
/** 写穿持久化(串行链;失败降级内存态并告警一次)。 */
|
|
51
|
+
private persist;
|
|
52
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Room 注册表(预注册词表,分类管理 beta.4)。
|
|
3
|
+
*
|
|
4
|
+
* 动机:Room 此前纯自生长(metadata.tags 涌现),tags 标注器不参考词表——
|
|
5
|
+
* 「81 个 Room 大多 1 条」的碎片化即来源于此。注册表让「策划好的分类」先行:
|
|
6
|
+
* 候选标注器与 tags 标注器优先挂靠注册表 slug;零计数注册条目也进词表。
|
|
7
|
+
*
|
|
8
|
+
* 存储:slots 式 sidecar(`rooms-registry.json`),全量载入内存 + 写穿原子持久化,
|
|
9
|
+
* `rev` 乐观观察计数。**读侧分类**(occupancy 同款):文件缺失 = 合法空表;
|
|
10
|
+
* 损坏/不可读 → 告警 + 只读降级(内存态照常生效,停止回写)——降级即**回退纯
|
|
11
|
+
* 自生长目录**,绝不阻塞 rooms-get / 标注器(R2 红线)。
|
|
12
|
+
*/
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { isTag } from '../metadata-validators.js';
|
|
15
|
+
import { readJsonStrict, atomicWriteText, ensureDir } from '../util/io.js';
|
|
16
|
+
/** 版本常量(file-versions 同款集中纪律;registry 为独立 sidecar 文件)。 */
|
|
17
|
+
export const ROOMS_REGISTRY_FILE_VERSION = 1;
|
|
18
|
+
/** 注册表上限(防刷:词表膨胀会直接进标注器 prompt;真需求超限时先 retire/合并)。 */
|
|
19
|
+
const REGISTRY_CAP = 200;
|
|
20
|
+
/** aliases 上限(合并/改名历史)。 */
|
|
21
|
+
const ALIASES_CAP = 16;
|
|
22
|
+
export class RoomRegistryStore {
|
|
23
|
+
logger;
|
|
24
|
+
file;
|
|
25
|
+
entries = [];
|
|
26
|
+
rev = 0;
|
|
27
|
+
loaded = false;
|
|
28
|
+
degraded;
|
|
29
|
+
degradedLogged = false;
|
|
30
|
+
writeChain = Promise.resolve();
|
|
31
|
+
constructor(dataDir, logger) {
|
|
32
|
+
this.logger = logger;
|
|
33
|
+
this.file = path.join(dataDir, 'rooms-registry.json');
|
|
34
|
+
}
|
|
35
|
+
/** 载入(启动时 await 一次)。缺失 = 合法空表;损坏/不可读 = 只读降级(回退自生长目录)。 */
|
|
36
|
+
async init() {
|
|
37
|
+
await ensureDir(path.dirname(this.file));
|
|
38
|
+
const read = await readJsonStrict(this.file);
|
|
39
|
+
if (!read.ok) {
|
|
40
|
+
if (read.reason !== 'missing') {
|
|
41
|
+
this.degraded = `${read.reason}${'detail' in read && read.detail ? `: ${read.detail}` : ''}`;
|
|
42
|
+
this.logger?.warn(`[memory] Room 注册表只读降级(回退纯自生长目录): ${this.degraded}`);
|
|
43
|
+
}
|
|
44
|
+
this.loaded = true;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (read.value.version !== ROOMS_REGISTRY_FILE_VERSION) {
|
|
48
|
+
this.logger?.warn(`[memory] Room 注册表版本 ${String(read.value.version)} 未知,按当前形状读取`);
|
|
49
|
+
}
|
|
50
|
+
const rooms = Array.isArray(read.value.rooms) ? read.value.rooms : [];
|
|
51
|
+
for (const r of rooms) {
|
|
52
|
+
if (!r || typeof r.slug !== 'string' || !isTag(r.slug))
|
|
53
|
+
continue; // 非法 slug 丢弃(session-modes 同款纪律)
|
|
54
|
+
this.entries.push({
|
|
55
|
+
slug: r.slug,
|
|
56
|
+
label: typeof r.label === 'string' ? r.label : undefined,
|
|
57
|
+
description: typeof r.description === 'string' ? r.description : undefined,
|
|
58
|
+
source: r.source === 'grown' ? 'grown' : 'pre-registered',
|
|
59
|
+
aliases: Array.isArray(r.aliases) ? r.aliases.filter((a) => typeof a === 'string' && a !== '') : undefined,
|
|
60
|
+
status: r.status === 'retired' ? 'retired' : 'active',
|
|
61
|
+
createdAt: typeof r.createdAt === 'string' ? r.createdAt : '',
|
|
62
|
+
updatedAt: typeof r.updatedAt === 'string' ? r.updatedAt : '',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
this.rev = typeof read.value.rev === 'number' ? read.value.rev : 0;
|
|
66
|
+
this.loaded = true;
|
|
67
|
+
}
|
|
68
|
+
ensureLoaded() {
|
|
69
|
+
if (!this.loaded)
|
|
70
|
+
throw new Error('Room 注册表未初始化(先 await init())');
|
|
71
|
+
}
|
|
72
|
+
/** 全量条目(含 retired;副本返回,活引用纪律)。 */
|
|
73
|
+
list() {
|
|
74
|
+
this.ensureLoaded();
|
|
75
|
+
return this.entries.map((e) => ({ ...e }));
|
|
76
|
+
}
|
|
77
|
+
/** active 条目(rooms-get 合并与标注器词表来源)。 */
|
|
78
|
+
listActive() {
|
|
79
|
+
return this.list().filter((e) => e.status === 'active');
|
|
80
|
+
}
|
|
81
|
+
/** 精确 slug 查找(含 retired;含别名命中)。 */
|
|
82
|
+
bySlug(slug) {
|
|
83
|
+
this.ensureLoaded();
|
|
84
|
+
const found = this.entries.find((e) => e.slug === slug || (e.aliases ?? []).includes(slug));
|
|
85
|
+
return found ? { ...found } : undefined;
|
|
86
|
+
}
|
|
87
|
+
/** 注册(幂等:已存在——含别名命中——时原地补全 label/description,不新建)。上限 200 防刷。 */
|
|
88
|
+
async register(input) {
|
|
89
|
+
this.ensureLoaded();
|
|
90
|
+
const slug = String(input.slug ?? '').trim().toLowerCase();
|
|
91
|
+
if (!slug || !isTag(slug))
|
|
92
|
+
throw new Error(`非法 Room slug: ${String(input.slug)}(需小写字母数字连字符)`);
|
|
93
|
+
const found = this.entries.find((e) => e.slug === slug || (e.aliases ?? []).includes(slug));
|
|
94
|
+
if (found) {
|
|
95
|
+
let changed = false;
|
|
96
|
+
if (!found.label && typeof input.label === 'string' && input.label.trim()) {
|
|
97
|
+
found.label = input.label.trim().slice(0, 60);
|
|
98
|
+
changed = true;
|
|
99
|
+
}
|
|
100
|
+
if (!found.description && typeof input.description === 'string' && input.description.trim()) {
|
|
101
|
+
found.description = input.description.trim().slice(0, 300);
|
|
102
|
+
changed = true;
|
|
103
|
+
}
|
|
104
|
+
if (changed) {
|
|
105
|
+
found.updatedAt = new Date().toISOString();
|
|
106
|
+
this.rev++;
|
|
107
|
+
await this.persist();
|
|
108
|
+
}
|
|
109
|
+
return { entry: { ...found }, created: false };
|
|
110
|
+
}
|
|
111
|
+
if (this.entries.length >= REGISTRY_CAP) {
|
|
112
|
+
throw new Error(`Room 注册表已达上限(${REGISTRY_CAP}),请先 retire/合并部分条目`);
|
|
113
|
+
}
|
|
114
|
+
const now = new Date().toISOString();
|
|
115
|
+
const entry = {
|
|
116
|
+
slug,
|
|
117
|
+
label: typeof input.label === 'string' && input.label.trim() ? input.label.trim().slice(0, 60) : undefined,
|
|
118
|
+
description: typeof input.description === 'string' && input.description.trim()
|
|
119
|
+
? input.description.trim().slice(0, 300)
|
|
120
|
+
: undefined,
|
|
121
|
+
source: input.source === 'grown' ? 'grown' : 'pre-registered',
|
|
122
|
+
aliases: Array.isArray(input.aliases) && input.aliases.length > 0 ? input.aliases.slice(0, ALIASES_CAP) : undefined,
|
|
123
|
+
status: 'active',
|
|
124
|
+
createdAt: now,
|
|
125
|
+
updatedAt: now,
|
|
126
|
+
};
|
|
127
|
+
this.entries.push(entry);
|
|
128
|
+
this.rev++;
|
|
129
|
+
await this.persist();
|
|
130
|
+
return { entry: { ...entry }, created: true };
|
|
131
|
+
}
|
|
132
|
+
/** 改状态(active↔retired);无变化返回 false。 */
|
|
133
|
+
async setStatus(slug, status) {
|
|
134
|
+
this.ensureLoaded();
|
|
135
|
+
const found = this.entries.find((e) => e.slug === slug);
|
|
136
|
+
if (!found || found.status === status)
|
|
137
|
+
return false;
|
|
138
|
+
found.status = status;
|
|
139
|
+
found.updatedAt = new Date().toISOString();
|
|
140
|
+
this.rev++;
|
|
141
|
+
await this.persist();
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
/** 改名(merge 1:1 收尾):slug 换名 + 旧 slug 进 aliases。新名与既有条目冲突时走 merge。 */
|
|
145
|
+
async renameSlug(oldSlug, newSlug) {
|
|
146
|
+
this.ensureLoaded();
|
|
147
|
+
if (!isTag(newSlug))
|
|
148
|
+
throw new Error(`非法 Room slug: ${newSlug}`);
|
|
149
|
+
const found = this.entries.find((e) => e.slug === oldSlug);
|
|
150
|
+
if (!found)
|
|
151
|
+
return false;
|
|
152
|
+
if (this.entries.some((e) => e.slug === newSlug)) {
|
|
153
|
+
throw new Error(`目标 slug 已存在: ${newSlug}(应走 merge 而非 rename)`);
|
|
154
|
+
}
|
|
155
|
+
found.slug = newSlug;
|
|
156
|
+
found.aliases = [...new Set([...(found.aliases ?? []), oldSlug])].slice(0, ALIASES_CAP);
|
|
157
|
+
found.updatedAt = new Date().toISOString();
|
|
158
|
+
this.rev++;
|
|
159
|
+
await this.persist();
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
/** 合并收尾:fromSlug 条目转 retired + toSlug 进 aliases(记录重写由调用方游标执行)。 */
|
|
163
|
+
async markMerged(fromSlug, toSlug) {
|
|
164
|
+
this.ensureLoaded();
|
|
165
|
+
const found = this.entries.find((e) => e.slug === fromSlug);
|
|
166
|
+
if (!found)
|
|
167
|
+
return false;
|
|
168
|
+
found.status = 'retired';
|
|
169
|
+
const target = this.entries.find((e) => e.slug === toSlug);
|
|
170
|
+
if (target) {
|
|
171
|
+
target.aliases = [...new Set([...(target.aliases ?? []), fromSlug])].slice(0, ALIASES_CAP);
|
|
172
|
+
target.updatedAt = new Date().toISOString();
|
|
173
|
+
}
|
|
174
|
+
found.updatedAt = new Date().toISOString();
|
|
175
|
+
this.rev++;
|
|
176
|
+
await this.persist();
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
/** 写穿持久化(串行链;失败降级内存态并告警一次)。 */
|
|
180
|
+
async persist() {
|
|
181
|
+
if (this.degraded)
|
|
182
|
+
return;
|
|
183
|
+
this.writeChain = this.writeChain.then(async () => {
|
|
184
|
+
const data = {
|
|
185
|
+
version: ROOMS_REGISTRY_FILE_VERSION,
|
|
186
|
+
rev: this.rev + 1,
|
|
187
|
+
rooms: this.entries,
|
|
188
|
+
};
|
|
189
|
+
try {
|
|
190
|
+
await ensureDir(path.dirname(this.file));
|
|
191
|
+
await atomicWriteText(this.file, JSON.stringify(data, null, 2));
|
|
192
|
+
this.rev = data.rev;
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
this.degraded = err instanceof Error ? err.message : String(err);
|
|
196
|
+
if (!this.degradedLogged) {
|
|
197
|
+
this.degradedLogged = true;
|
|
198
|
+
this.logger?.warn(`[memory] Room 注册表写入失败,只读降级: ${this.degraded}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
await this.writeChain;
|
|
203
|
+
}
|
|
204
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { PersonaStore } from '../store/persona.js';
|
|
|
22
22
|
import type { SceneStore } from '../store/scenes.js';
|
|
23
23
|
import type { SessionModeStore } from '../store/session-modes.js';
|
|
24
24
|
import type { MemoryFamily, MemoryLogger } from '../types.js';
|
|
25
|
+
import type { RoomRegistryStore } from '../store/rooms-registry.js';
|
|
25
26
|
export declare function registerMemoryTools(ctx: Context, cfg: MemoryConfig, stores: {
|
|
26
27
|
l0: L0Store;
|
|
27
28
|
l1: L1Store;
|
|
@@ -31,4 +32,6 @@ export declare function registerMemoryTools(ctx: Context, cfg: MemoryConfig, sto
|
|
|
31
32
|
graph?: GraphStore;
|
|
32
33
|
}, logger: MemoryLogger, modes: SessionModeStore, live: LiveSettingsHandle,
|
|
33
34
|
/** 反刍控制器(可选:未装配时 ruminate 工具返回未启用提示)。 */
|
|
34
|
-
ruminate?: RuminateController
|
|
35
|
+
ruminate?: RuminateController,
|
|
36
|
+
/** Room 注册表(分类管理 beta.4;缺省 = memory_room_admin 返回未装配提示)。 */
|
|
37
|
+
roomRegistry?: RoomRegistryStore): void;
|
package/dist/tools/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { RECEIPTS_QUERY_LIMIT_MAX, dimensionOf, toReceiptView } from '../store/r
|
|
|
3
3
|
import { listConflictPairs, renderConflictResolution, renderConflicts, resolveConflictPair } from '../conflict-service.js';
|
|
4
4
|
import { WING_CATALOG, WING_FALLBACK, normPersistence, normScope, resolveRecordScope } from '../types.js';
|
|
5
5
|
import { scopeFilterOf, workspaceIdOf } from '../workspace.js';
|
|
6
|
+
import { annotateOrphanCandidates, confirmReview, nextReview, pendingReviewCount, skipReview, } from '../room-review.js';
|
|
6
7
|
import { applyGovernanceWeights } from '../store/governance.js';
|
|
7
8
|
import { resolveRepoScope } from '../repo-scope.js';
|
|
8
9
|
import { GRAPH_STATUS_LABELS } from '../prompts/graph-projection.js';
|
|
@@ -32,7 +33,9 @@ function applyManualRedaction(record, cfg, logger) {
|
|
|
32
33
|
}
|
|
33
34
|
export function registerMemoryTools(ctx, cfg, stores, logger, modes, live,
|
|
34
35
|
/** 反刍控制器(可选:未装配时 ruminate 工具返回未启用提示)。 */
|
|
35
|
-
ruminate
|
|
36
|
+
ruminate,
|
|
37
|
+
/** Room 注册表(分类管理 beta.4;缺省 = memory_room_admin 返回未装配提示)。 */
|
|
38
|
+
roomRegistry) {
|
|
36
39
|
if (!cfg.tools)
|
|
37
40
|
return;
|
|
38
41
|
/**
|
|
@@ -182,6 +185,76 @@ ruminate) {
|
|
|
182
185
|
};
|
|
183
186
|
},
|
|
184
187
|
}));
|
|
188
|
+
// ── memory_room_admin: Room 粒度目录治理(分类管理 beta.4)────────────────
|
|
189
|
+
// list/register 免预算零风险;merge/rename/retire 属破坏性面(beta.5),暂不注册。
|
|
190
|
+
ctx.tools.register(defineTool({
|
|
191
|
+
name: 'memory_room_admin',
|
|
192
|
+
description: 'Room 分类管理(目录粒度)。action="list" 列出注册表条目与自生长 Room 计数;' +
|
|
193
|
+
'action="register" 预注册一个 Room(高权限;slug 需小写字母数字连字符,可选人类可读名与归类说明——' +
|
|
194
|
+
'注册后候选标注器与 tags 标注器会优先把记忆挂到这些 Room 上,是治理碎片化的推荐入口)。',
|
|
195
|
+
parameters: {
|
|
196
|
+
action: { type: 'string', required: true, description: 'list(列目录)| register(预注册 Room,高权限)' },
|
|
197
|
+
slug: { type: 'string', description: 'register 的 Room slug(小写字母数字连字符)' },
|
|
198
|
+
label: { type: 'string', description: 'register 的人类可读名(可选,可中文)' },
|
|
199
|
+
description: { type: 'string', description: 'register 的归类说明(可选;喂给标注器帮助归类)' },
|
|
200
|
+
},
|
|
201
|
+
output: {
|
|
202
|
+
schema: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
notice: { type: 'string' },
|
|
206
|
+
rooms: { type: 'string' },
|
|
207
|
+
slug: { type: 'string' },
|
|
208
|
+
created: { type: 'boolean' },
|
|
209
|
+
},
|
|
210
|
+
additionalProperties: false,
|
|
211
|
+
},
|
|
212
|
+
render: (_args, value) => [{ type: 'text', text: value.notice ?? JSON.stringify(value) }],
|
|
213
|
+
},
|
|
214
|
+
execute: async (args) => {
|
|
215
|
+
const action = String(args.action ?? '');
|
|
216
|
+
if (action === 'list') {
|
|
217
|
+
if (!roomRegistry)
|
|
218
|
+
return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
|
|
219
|
+
const registry = roomRegistry.list();
|
|
220
|
+
const counts = stores.l1.listRooms();
|
|
221
|
+
const countOf = (slug) => counts.find((r) => r.room === slug)?.count ?? 0;
|
|
222
|
+
const regLines = registry.map((e) => `- ${e.slug}${e.label ? `(${e.label})` : ''} [${e.source}/${e.status}] 现有 ${countOf(e.slug)} 条${e.description ? ` — ${e.description}` : ''}`);
|
|
223
|
+
const grownOnly = counts.filter((r) => !registry.some((e) => e.slug === r.room));
|
|
224
|
+
return {
|
|
225
|
+
notice: `注册表 ${registry.length} 条(上限 200),自生长未注册 ${grownOnly.length} 个。\n` +
|
|
226
|
+
(regLines.join('\n') || '(注册表为空——用 action=register 预注册)') +
|
|
227
|
+
`\n自生长未注册:${grownOnly.map((r) => `${r.room}(${r.count})`).join(", ") || "无"}`,
|
|
228
|
+
rooms: `${registry.length}+${grownOnly.length}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (action === 'register') {
|
|
232
|
+
if (!roomRegistry)
|
|
233
|
+
return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
|
|
234
|
+
if (!live.get().memoryMutate)
|
|
235
|
+
return { notice: MUTATE_OFF_NOTICE };
|
|
236
|
+
const slug = String(args.slug ?? '').trim();
|
|
237
|
+
try {
|
|
238
|
+
const r = await roomRegistry.register({
|
|
239
|
+
slug,
|
|
240
|
+
label: typeof args.label === 'string' ? args.label : undefined,
|
|
241
|
+
description: typeof args.description === 'string' ? args.description : undefined,
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
notice: r.created
|
|
245
|
+
? `已预注册 Room:${r.entry.slug}${r.entry.label ? `(${r.entry.label})` : ''}。候选标注器将优先把相关记忆挂到它上面。`
|
|
246
|
+
: `Room 已存在:${r.entry.slug}(本次只补全了缺失的 label/description)。`,
|
|
247
|
+
slug: r.entry.slug,
|
|
248
|
+
created: r.created,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
return { notice: `注册失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { notice: `非法 action:${action}(允许 list/register;merge/rename/retire 将在后续版本提供)。` };
|
|
256
|
+
},
|
|
257
|
+
}));
|
|
185
258
|
// ── conversation_search: L0 原始对话 ──
|
|
186
259
|
ctx.tools.register(defineTool({
|
|
187
260
|
name: 'conversation_search',
|
|
@@ -778,6 +851,87 @@ ruminate) {
|
|
|
778
851
|
},
|
|
779
852
|
}));
|
|
780
853
|
// ── 反刍状态查询(查看当前反刍进度) ──
|
|
854
|
+
// ── memory_room_review:孤儿记忆的 Room 候选逐个复查(agent 长尾治理)──────
|
|
855
|
+
// next/annotate 只读或预算内标注(无需门);confirm/skip 改写 metadata,
|
|
856
|
+
// confirm 吃 live.memoryMutate 高权限门(与 memory_delete 同款)。
|
|
857
|
+
ctx.tools.register(defineTool({
|
|
858
|
+
name: 'memory_room_review',
|
|
859
|
+
description: '孤儿记忆(无 Room 绑定)的逐个复查:action="next" 取下一条待复查记录(带系统预标记的候选 Room 与现有词表,agent 依据记录内容判定);' +
|
|
860
|
+
'action="confirm" 按 id 把 1-3 个 Room slug 写入该记忆(高权限);action="skip" 跳过(不再进队列);' +
|
|
861
|
+
'action="annotate" 立即对一批孤儿做候选预标记(平时由反刍自动推进)。建议长会话空闲时逐个过,保持分类收敛——优先挂靠现有 Room。',
|
|
862
|
+
parameters: {
|
|
863
|
+
action: { type: 'string', required: true, description: 'next(取下一条)| confirm(确认写入)| skip(跳过)| annotate(批量预标记)' },
|
|
864
|
+
id: { type: 'string', description: 'confirm/skip 的目标记录 id(action=next 返回的 id)' },
|
|
865
|
+
rooms: { type: 'string', description: 'confirm 的 Room slug 列表(逗号分隔,1-3 个,小写字母数字连字符;可含词表外新 slug)' },
|
|
866
|
+
},
|
|
867
|
+
output: {
|
|
868
|
+
schema: {
|
|
869
|
+
type: 'object',
|
|
870
|
+
properties: {
|
|
871
|
+
notice: { type: 'string' },
|
|
872
|
+
id: { type: 'string' },
|
|
873
|
+
content: { type: 'string' },
|
|
874
|
+
candidates: { type: 'string' },
|
|
875
|
+
roomCatalog: { type: 'string' },
|
|
876
|
+
remaining: { type: 'number' },
|
|
877
|
+
confirmedTags: { type: 'string' },
|
|
878
|
+
annotated: { type: 'number' },
|
|
879
|
+
deferred: { type: 'number' },
|
|
880
|
+
},
|
|
881
|
+
additionalProperties: false,
|
|
882
|
+
},
|
|
883
|
+
render: (_args, value) => [{ type: 'text', text: value.notice ?? JSON.stringify(value) }],
|
|
884
|
+
},
|
|
885
|
+
execute: async (args) => {
|
|
886
|
+
const action = String(args.action ?? '');
|
|
887
|
+
const io = { l1: stores.l1, logger };
|
|
888
|
+
if (action === 'next') {
|
|
889
|
+
const item = nextReview(io.l1);
|
|
890
|
+
if (!item)
|
|
891
|
+
return { notice: '没有待复查的孤儿记忆(roomCandidates 队列为空)。可用 action=annotate 先做一轮候选预标记。' };
|
|
892
|
+
return {
|
|
893
|
+
id: item.id,
|
|
894
|
+
content: item.content,
|
|
895
|
+
candidates: item.candidates.join(', '),
|
|
896
|
+
roomCatalog: item.roomCatalog.map((r) => `${r.room}(${r.count})`).join(', '),
|
|
897
|
+
remaining: item.remaining,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
if (action === 'annotate') {
|
|
901
|
+
const r = await annotateOrphanCandidates(ctx, cfg, io, 40);
|
|
902
|
+
return {
|
|
903
|
+
notice: `候选预标记完成:选中 ${r.selected} 条,产候选 ${r.candidates} 条,写 pending ${r.written} 条。用 action=next 逐个复查。`,
|
|
904
|
+
annotated: r.candidates,
|
|
905
|
+
deferred: r.selected - r.written,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
if (action === 'confirm') {
|
|
909
|
+
if (!live.get().memoryMutate)
|
|
910
|
+
return { notice: MUTATE_OFF_NOTICE };
|
|
911
|
+
const id = String(args.id ?? '').trim();
|
|
912
|
+
if (!id)
|
|
913
|
+
return { notice: 'confirm 需要 id(action=next 返回的 id)。' };
|
|
914
|
+
const rooms = String(args.rooms ?? '')
|
|
915
|
+
.split(/[,,、]/)
|
|
916
|
+
.map((x) => x.trim())
|
|
917
|
+
.filter(Boolean);
|
|
918
|
+
if (rooms.length === 0)
|
|
919
|
+
return { notice: 'confirm 需要 rooms(逗号分隔的 1-3 个 Room slug)。' };
|
|
920
|
+
const r = confirmReview(io.l1, id, rooms);
|
|
921
|
+
if (!r)
|
|
922
|
+
return { notice: `确认失败:id=${id} 不存在、已被确认过、或 rooms 全部非法(需 1-3 个合法 slug)。` };
|
|
923
|
+
return { notice: `已挂到 Room:${r.tags.join(', ')}(面板下一次刷新即可见)。`, confirmedTags: r.tags.join(', '), id };
|
|
924
|
+
}
|
|
925
|
+
if (action === 'skip') {
|
|
926
|
+
const id = String(args.id ?? '').trim();
|
|
927
|
+
if (!id)
|
|
928
|
+
return { notice: 'skip 需要 id。' };
|
|
929
|
+
const ok = skipReview(io.l1, id);
|
|
930
|
+
return { notice: ok ? `已跳过 ${id}(不再进复查队列)。` : `跳过失败:id=${id} 不存在。`, id };
|
|
931
|
+
}
|
|
932
|
+
return { notice: `非法 action:${action}(允许 next/confirm/skip/annotate)。剩余待复查 ${pendingReviewCount(stores.l1)} 条。` };
|
|
933
|
+
},
|
|
934
|
+
}));
|
|
781
935
|
ctx.tools.register(defineTool({
|
|
782
936
|
name: 'memory_ruminate_status',
|
|
783
937
|
description: '查询当前记忆反刍的状态与进度(是否运行中、当前阶段、已完成/总会话数等)。\n\n注意:在 DSH Web GUI 的对话中无法直接调用此工具,因为模型可调用工具列表不包含 ruminate_status。如需查询进度,请在记忆库面板的蒸馏面板查看。',
|
package/dist/types.d.ts
CHANGED
|
@@ -41,6 +41,10 @@ export interface RoomCount {
|
|
|
41
41
|
room: string;
|
|
42
42
|
/** 该 Room 下的记录数(与 `wingL1Counts()` 同口径:含 retired 行)。 */
|
|
43
43
|
count: number;
|
|
44
|
+
/** 来源标示(rooms-get 端点层合并注册表后附带;'grown' = 自生长,可缺省)。 */
|
|
45
|
+
source?: 'pre-registered' | 'grown';
|
|
46
|
+
/** 人类可读名(注册表条目才有;面板 chip 可用 label 展示)。 */
|
|
47
|
+
label?: string;
|
|
44
48
|
}
|
|
45
49
|
/** 记录族标签推断:work_* 前缀 → work,其余(含 auto 档兜底)→ chat。 */
|
|
46
50
|
export declare function familyForType(type: string): MemoryFamily;
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-prime-memory",
|
|
3
3
|
"name": "dsh-prime-memory",
|
|
4
|
-
"version": "0.20.0-beta.
|
|
4
|
+
"version": "0.20.0-beta.4-dsh0.2.0.1",
|
|
5
5
|
"description": {
|
|
6
6
|
"en": "Layered distilled memory plugin (L0 capture → L1 atomic memory → L2 scenes → L3 persona) with automatic recall injection before each model step.",
|
|
7
7
|
"zh": "L0~L3 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
|