dsh-prime-memory 0.20.0-beta.4 → 0.20.0-beta.6
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 +19 -1
- package/dist/client.js +48 -1
- package/dist/contract.d.ts +14 -0
- package/dist/index.js +4 -3
- package/dist/pipeline/ruminate.d.ts +11 -1
- package/dist/pipeline/ruminate.js +7 -2
- package/dist/room-review.d.ts +73 -2
- package/dist/room-review.js +81 -5
- package/dist/stats.js +22 -0
- package/dist/store/l1.d.ts +28 -0
- package/dist/store/l1.js +60 -1
- package/dist/store/rooms-registry.d.ts +7 -0
- package/dist/store/rooms-registry.js +35 -6
- package/dist/tools/index.js +64 -6
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,7 +17,25 @@
|
|
|
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.6] — 2026-10-01
|
|
21
|
+
|
|
22
|
+
### 修复
|
|
23
|
+
|
|
24
|
+
- **标注器词表接入注册表(beta.5 漏项补齐)**:beta.5 的 merge/rename/retire 已落,但候选标注器(`annotateOrphanCandidates`)的词表仍是纯自生长聚合——预注册的 Room 根本进不了候选。现 OrphanAnnotateIO 带 registry:合并词表 = 注册表条目排前(count=0,附 label/description)+ 自生长目录,封顶 120;ruminate 与 memory_room_admin annotate 两条路径都传参。**预注册 Room 从此真实影响候选标注**。
|
|
25
|
+
|
|
26
|
+
## [0.20.0-beta.5] — 2026-10-01## [0.20.0-beta.5] — 2026-10-01
|
|
27
|
+
|
|
28
|
+
> Room 分类管理第二船(**破坏性面**)。dryRun 默认 true——merge/rename 先预览影响面,确认后才实跑;执行前自动备份到 `rooms-merge-backups/`。
|
|
29
|
+
|
|
30
|
+
### 新增
|
|
31
|
+
|
|
32
|
+
- **Room 注册表两级制**:slug 支持 `major` 或 `major/minor` 两级(如 `dsh-plugin`、`dsh-plugin/merge`);**大类上限 200**,小类细分不占大类额度(总量保险 2000)。isTag 词表不放宽(slash 仅注册表层允许)。
|
|
33
|
+
- **merge/rename/retire(工具 `memory_room_admin`,高权限)**:merge 游标重写全部匹配记录的 tags(写前重读-合并-写回,roomCandidates/roomReview/hall/anchors 等键保全;含已退场行,与计数口径一致);rename = merge 1:1 + 注册表改名(aliases 收旧名);retire 仅词表退役(存量不动)。merge/rename 与反刍并发会相互覆盖——请错峰执行(工具提示与 ADR-0020 均已声明)。
|
|
34
|
+
- **重写安全三件套**:dryRun 默认只返 affected+预览零写入;执行前自动备份匹配记录到 `rooms-merge-backups/<ts>-<slug>.json`;完成自动 `invalidateRooms` + 按受影响 (family, scene) 入队场景重算(`source='room-merge'`)。
|
|
35
|
+
- **面板 Room 管理子面板**(高权限开启后出现):注册表单(slug/名称/说明,走新 RPC `room-register`)、孤儿清单导出;合并/退役建议走 agent 工具(dryRun 预览更详细)。
|
|
36
|
+
- 端点面 46 → 47(`room-register`)。
|
|
37
|
+
|
|
38
|
+
## [0.20.0-beta.4] — 2026-10-01## [0.20.0-beta.4] — 2026-10-01
|
|
21
39
|
|
|
22
40
|
> Room 分类管理第一船(**零漂移面**):注册表空 = 与 beta.3 行为逐字一致。merge/rename/retire 破坏性面将随 beta.5(dryRun 默认 true 保留一版)。
|
|
23
41
|
|
package/dist/client.js
CHANGED
|
@@ -3711,7 +3711,54 @@ var __defProp = Object.defineProperty;
|
|
|
3711
3711
|
children: "▶"
|
|
3712
3712
|
}
|
|
3713
3713
|
)
|
|
3714
|
-
] }) : null
|
|
3714
|
+
] }) : null,
|
|
3715
|
+
roomsOpen && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { marginTop: 8, paddingTop: 8, borderTop: "1px solid var(--dsh-mem-border)" }, children: [
|
|
3716
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }, children: [
|
|
3717
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "管理(高权限)" }),
|
|
3718
|
+
!hiPriv && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "需先开启高权限模式(见上方工具栏)" })
|
|
3719
|
+
] }),
|
|
3720
|
+
hiPriv && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center" }, children: [
|
|
3721
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("input", { "data-adm-slug": true, placeholder: "slug(如 dsh-plugin 或 dsh-plugin/merge)", style: { ...S.input, width: 150, fontSize: 12 } }),
|
|
3722
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("input", { "data-adm-label": true, placeholder: "名称(可中文)", style: { ...S.input, width: 110, fontSize: 12 } }),
|
|
3723
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("input", { "data-adm-desc": true, placeholder: "归类说明(喂标注器)", style: { ...S.input, width: 170, fontSize: 12 } }),
|
|
3724
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
3725
|
+
"button",
|
|
3726
|
+
{
|
|
3727
|
+
type: "button",
|
|
3728
|
+
onClick: async () => {
|
|
3729
|
+
const q = (sel2) => document.querySelector(sel2)?.value ?? "";
|
|
3730
|
+
const r = await rpc("dsh-memory/room-register", { slug: q("[data-adm-slug]"), label: q("[data-adm-label]"), description: q("[data-adm-desc]") }).catch(() => null);
|
|
3731
|
+
void r;
|
|
3732
|
+
loadRooms();
|
|
3733
|
+
},
|
|
3734
|
+
style: { cursor: "pointer", fontSize: 12, padding: "3px 10px", borderRadius: 6, border: "1px solid var(--dsh-mem-accent)", background: "transparent", color: "var(--dsh-mem-accent)" },
|
|
3735
|
+
children: "注册"
|
|
3736
|
+
}
|
|
3737
|
+
),
|
|
3738
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
3739
|
+
"button",
|
|
3740
|
+
{
|
|
3741
|
+
type: "button",
|
|
3742
|
+
title: "导出无 Room 绑定的孤儿清单(含候选)",
|
|
3743
|
+
onClick: () => {
|
|
3744
|
+
rpc("dsh-memory/rooms-export", { kind: "orphans" }).then((r) => {
|
|
3745
|
+
if (!r || !r.ok || !r.value?.csv) return;
|
|
3746
|
+
const blob = new Blob([r.value.csv], { type: "text/csv;charset=utf-8" });
|
|
3747
|
+
const url = URL.createObjectURL(blob);
|
|
3748
|
+
const a = document.createElement("a");
|
|
3749
|
+
a.href = url;
|
|
3750
|
+
a.download = "dsh-memory-room-orphans.csv";
|
|
3751
|
+
a.click();
|
|
3752
|
+
URL.revokeObjectURL(url);
|
|
3753
|
+
}).catch(() => void 0);
|
|
3754
|
+
},
|
|
3755
|
+
style: { cursor: "pointer", fontSize: 12, padding: "3px 10px", borderRadius: 6, border: "1px solid var(--dsh-mem-border)", background: "transparent", color: "var(--dsh-mem-text-2)" },
|
|
3756
|
+
children: "导出孤儿"
|
|
3757
|
+
}
|
|
3758
|
+
),
|
|
3759
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "合并/改名/退役:用 agent 工具 memory_room_admin(merge/rename/retire,dryRun 默认预览)" })
|
|
3760
|
+
] })
|
|
3761
|
+
] })
|
|
3715
3762
|
] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: S.muted, children: "Room 分类:暂无(由反刍涌现的标签自动生成,无需手工建立)" })
|
|
3716
3763
|
] }),
|
|
3717
3764
|
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10 }, children: [
|
package/dist/contract.d.ts
CHANGED
|
@@ -574,6 +574,18 @@ export interface RoomsGetResponse {
|
|
|
574
574
|
status: 'active' | 'retired';
|
|
575
575
|
}>;
|
|
576
576
|
}
|
|
577
|
+
/** dsh-memory/room-register(预注册 Room;高权限:面板 memoryMutate 开启才生效)。 */
|
|
578
|
+
export interface RoomRegisterRequest {
|
|
579
|
+
slug: string;
|
|
580
|
+
label?: string;
|
|
581
|
+
description?: string;
|
|
582
|
+
}
|
|
583
|
+
export interface RoomRegisterResponse {
|
|
584
|
+
slug: string;
|
|
585
|
+
created: boolean;
|
|
586
|
+
/** 幂等补全时的提示(如「已存在,只补全缺失字段」)。 */
|
|
587
|
+
notice?: string;
|
|
588
|
+
}
|
|
577
589
|
/** dsh-memory/rooms-export(分类数据导出,CSV 直出)。 */
|
|
578
590
|
export interface RoomsExportRequest {
|
|
579
591
|
/** rooms = 词表+计数;orphans = 无 Room 绑定的记录清单。 */
|
|
@@ -1288,6 +1300,7 @@ export interface DshMemoryRequestMap {
|
|
|
1288
1300
|
'dsh-memory/wing-overview': Record<string, never>;
|
|
1289
1301
|
'dsh-memory/rooms-get': Record<string, never>;
|
|
1290
1302
|
'dsh-memory/rooms-export': RoomsExportRequest;
|
|
1303
|
+
'dsh-memory/room-register': RoomRegisterRequest;
|
|
1291
1304
|
'dsh-memory/wing-backfill': Record<string, never>;
|
|
1292
1305
|
'dsh-memory/session-stats': SessionStatsRequest;
|
|
1293
1306
|
'dsh-memory/settings-get': Record<string, never>;
|
|
@@ -1336,6 +1349,7 @@ export interface DshMemoryResponseMap {
|
|
|
1336
1349
|
'dsh-memory/wing-overview': WingOverviewResponse;
|
|
1337
1350
|
'dsh-memory/rooms-get': RoomsGetResponse;
|
|
1338
1351
|
'dsh-memory/rooms-export': RoomsExportResponse;
|
|
1352
|
+
'dsh-memory/room-register': RoomRegisterResponse;
|
|
1339
1353
|
'dsh-memory/wing-backfill': WingBackfillResponse;
|
|
1340
1354
|
'dsh-memory/session-stats': SessionStatsResponse;
|
|
1341
1355
|
'dsh-memory/settings-get': SettingsGetResponse;
|
package/dist/index.js
CHANGED
|
@@ -354,8 +354,11 @@ export async function apply(ctx, config) {
|
|
|
354
354
|
backend = await createMemoryBackend({ dbPath: path.join(dataDir, 'memory.db'), dimensions: initial.dims, logger }, backend);
|
|
355
355
|
}
|
|
356
356
|
stores.backend = backend;
|
|
357
|
+
// Room 注册表(分类管理 beta.4;损坏降级为纯自生长目录)
|
|
358
|
+
const roomRegistry = new RoomRegistryStore(dataDir, logger);
|
|
359
|
+
await roomRegistry.init();
|
|
357
360
|
const ruminate = storageOk && !db.isDegraded() && ruminateFile
|
|
358
|
-
? new RuminateController(ctx, config, runner, stores, logger, live, ruminateFile)
|
|
361
|
+
? new RuminateController(ctx, config, runner, stores, logger, live, ruminateFile, roomRegistry)
|
|
359
362
|
: undefined;
|
|
360
363
|
let flushL0;
|
|
361
364
|
if (storageOk) {
|
|
@@ -366,8 +369,6 @@ export async function apply(ctx, config) {
|
|
|
366
369
|
const contextUsage = new ContextUsageStore(logger);
|
|
367
370
|
const todoRef = new TodoRefStore();
|
|
368
371
|
// Room 注册表(分类管理 beta.4;损坏降级为纯自生长目录)
|
|
369
|
-
const roomRegistry = new RoomRegistryStore(dataDir, logger);
|
|
370
|
-
await roomRegistry.init();
|
|
371
372
|
if (storageOk) {
|
|
372
373
|
registerLongTaskMonitor(ctx, {
|
|
373
374
|
cfg: config,
|
|
@@ -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
|
/**
|
|
@@ -46,11 +46,15 @@ export class RuminateController {
|
|
|
46
46
|
sessions = [];
|
|
47
47
|
totalL1 = 0;
|
|
48
48
|
pendingFile;
|
|
49
|
+
/** Room 注册表(可选;孤儿候选预标记的合并词表来源)。 */
|
|
50
|
+
roomRegistry;
|
|
49
51
|
/** 记忆后端:未注入时包 l1(进程内,行为与改造前等价)。 */
|
|
50
52
|
backend() {
|
|
51
53
|
return this.stores.backend ?? new InProcMemoryBackend(this.stores.l1);
|
|
52
54
|
}
|
|
53
|
-
constructor(ctx, cfg, runner, stores, logger, live, pendingFile
|
|
55
|
+
constructor(ctx, cfg, runner, stores, logger, live, pendingFile,
|
|
56
|
+
/** Room 注册表(可选;孤儿候选预标记的合并词表来源)。 */
|
|
57
|
+
roomRegistry) {
|
|
54
58
|
this.ctx = ctx;
|
|
55
59
|
this.cfg = cfg;
|
|
56
60
|
this.runner = runner;
|
|
@@ -58,6 +62,7 @@ export class RuminateController {
|
|
|
58
62
|
this.logger = logger;
|
|
59
63
|
this.live = live;
|
|
60
64
|
this.pendingFile = pendingFile;
|
|
65
|
+
this.roomRegistry = roomRegistry;
|
|
61
66
|
}
|
|
62
67
|
/** 状态快照 */
|
|
63
68
|
getStatus() {
|
|
@@ -238,7 +243,7 @@ export class RuminateController {
|
|
|
238
243
|
// 候选(roomReview='pending'),由 agent 用 memory_room_review 逐个复查;
|
|
239
244
|
// 每轮至多 40 条,吞错不拖垮反刍。
|
|
240
245
|
try {
|
|
241
|
-
const r = await annotateOrphanCandidates(this.ctx, this.cfg, { l1: this.stores.l1, logger: this.logger }, 40);
|
|
246
|
+
const r = await annotateOrphanCandidates(this.ctx, this.cfg, { l1: this.stores.l1, logger: this.logger, registry: this.roomRegistry }, 40);
|
|
242
247
|
if (r.written > 0)
|
|
243
248
|
this.status.detail = `孤儿候选预标记 ${r.written} 条(待 agent 复查,工具 memory_room_review)`;
|
|
244
249
|
}
|
package/dist/room-review.d.ts
CHANGED
|
@@ -46,6 +46,26 @@ export interface RoomReviewStore {
|
|
|
46
46
|
/** 确认写 tags 后失效 Room 计数缓存(30s TTL)。 */
|
|
47
47
|
invalidateRooms(): void;
|
|
48
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
|
+
}
|
|
49
69
|
export interface RoomCandidate {
|
|
50
70
|
id: string;
|
|
51
71
|
rooms: string[];
|
|
@@ -74,14 +94,27 @@ export declare function roomCandidateChunk(ctx: Context, cfg: MemoryConfig, logg
|
|
|
74
94
|
export interface OrphanAnnotateIO {
|
|
75
95
|
l1: RoomReviewStore;
|
|
76
96
|
logger: MemoryLogger;
|
|
97
|
+
/** Room 注册表(可选:未装配 = 词表只用自生长聚合,无注册条目)。 */
|
|
98
|
+
registry?: {
|
|
99
|
+
listActive(): Array<{
|
|
100
|
+
slug: string;
|
|
101
|
+
label?: string;
|
|
102
|
+
description?: string;
|
|
103
|
+
}>;
|
|
104
|
+
};
|
|
77
105
|
}
|
|
78
106
|
/**
|
|
79
107
|
* 预标记消费器:活跃孤儿(≤limit 条)批量产候选并写 pending。
|
|
80
108
|
* 写前重读合并(与 relabel 同纪律);失败单条跳过、原记录零改动。
|
|
81
109
|
*/
|
|
82
110
|
export interface OrphanAnnotateOverrides {
|
|
83
|
-
/** 测试注入口:替换 LLM 候选标注器(默认走真实 callLLM 路径)
|
|
84
|
-
chunkAnnotator?: (chunk: MemoryRecord[]
|
|
111
|
+
/** 测试注入口:替换 LLM 候选标注器(默认走真实 callLLM 路径);第二参为合并词表。 */
|
|
112
|
+
chunkAnnotator?: (chunk: MemoryRecord[], catalog: ReadonlyArray<{
|
|
113
|
+
room: string;
|
|
114
|
+
count: number;
|
|
115
|
+
label?: string;
|
|
116
|
+
description?: string;
|
|
117
|
+
}>) => Promise<Array<{
|
|
85
118
|
id: string;
|
|
86
119
|
rooms: string[];
|
|
87
120
|
}>>;
|
|
@@ -91,6 +124,44 @@ export declare function annotateOrphanCandidates(ctx: Context, cfg: MemoryConfig
|
|
|
91
124
|
candidates: number;
|
|
92
125
|
written: number;
|
|
93
126
|
}>;
|
|
127
|
+
export interface MergeRoomResult {
|
|
128
|
+
dryRun: boolean;
|
|
129
|
+
/** 影响条数(dryRun=预览数;实跑=实际重写数)。 */
|
|
130
|
+
affected: number;
|
|
131
|
+
preview: string[];
|
|
132
|
+
applied: number;
|
|
133
|
+
hasMore: boolean;
|
|
134
|
+
backupFile?: string;
|
|
135
|
+
/** 实跑时回传:受影响的族与场景(调用方入队 recluster 'room-merge')。 */
|
|
136
|
+
families?: string[];
|
|
137
|
+
scenes?: string[];
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Room merge/rename 编排(破坏性动作,分类管理 beta.5):
|
|
141
|
+
* - dryRun(缺省 true):只统计 affected + 前 6 条预览,**零写入**;
|
|
142
|
+
* - 实跑:执行前备份(matching rows → rooms-merge-backups/)→ 游标重写
|
|
143
|
+
* (写前重读-合并-写回,其余 metadata 键保全)→ 注册表 markMerged/renameSlug
|
|
144
|
+
* → 返回受影响 (family, scene) 供调用方入队 recluster('room-merge');
|
|
145
|
+
* - 单飞:并发 merge 直接抛错;与 relabel/ruminate 的时间错开由调用方保证。
|
|
146
|
+
* 抛错语义:非法参数/注册表冲突直接抛(调用方转 notice)。
|
|
147
|
+
*/
|
|
148
|
+
export declare function mergeRoom(io: {
|
|
149
|
+
l1: RoomMergeStore;
|
|
150
|
+
registry: RoomRegistryMerge;
|
|
151
|
+
logger: MemoryLogger;
|
|
152
|
+
}, from: string, to: string, opts?: {
|
|
153
|
+
dryRun?: boolean;
|
|
154
|
+
cap?: number;
|
|
155
|
+
}): Promise<MergeRoomResult>;
|
|
156
|
+
/** rename = merge 1:1 + 注册表改名(冲突时抛错提示走 merge)。 */
|
|
157
|
+
export declare function renameRoom(io: {
|
|
158
|
+
l1: RoomMergeStore;
|
|
159
|
+
registry: RoomRegistryMerge;
|
|
160
|
+
logger: MemoryLogger;
|
|
161
|
+
}, from: string, to: string, opts?: {
|
|
162
|
+
dryRun?: boolean;
|
|
163
|
+
cap?: number;
|
|
164
|
+
}): Promise<MergeRoomResult>;
|
|
94
165
|
/** 下一条待复查(供 agent 逐个过):记录摘要 + 候选 + 现有 Room 词表 + 剩余数。 */
|
|
95
166
|
export declare function nextReview(l1: RoomReviewStore): {
|
|
96
167
|
id: string;
|
package/dist/room-review.js
CHANGED
|
@@ -92,12 +92,18 @@ export async function annotateOrphanCandidates(ctx, cfg, io, limit = 40, overrid
|
|
|
92
92
|
const selected = selectPendingOrphans(io.l1, limit);
|
|
93
93
|
if (selected.length === 0)
|
|
94
94
|
return { selected: 0, candidates: 0, written: 0 };
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
const
|
|
95
|
+
// 合并词表:注册表条目排前(附 description,对 LLM 归类价值最高),封顶 120;
|
|
96
|
+
// 无注册表 = 纯自生长聚合(计数降序)。整批共享一张词表(一次 LLM 调用)。
|
|
97
|
+
const grown = io.l1.listRooms();
|
|
98
|
+
const regActive = io.registry?.listActive() ?? [];
|
|
99
|
+
const regSet = new Set(regActive.map((e) => e.slug));
|
|
100
|
+
const merged = [
|
|
101
|
+
...regActive.map((e) => ({ room: e.slug, count: 0, label: e.label, description: e.description })),
|
|
102
|
+
...grown.filter((r) => !regSet.has(r.room)).map((r) => ({ room: r.room, count: r.count })),
|
|
103
|
+
].slice(0, 120);
|
|
98
104
|
const rows = overrides.chunkAnnotator !== undefined
|
|
99
|
-
? await overrides.chunkAnnotator(selected)
|
|
100
|
-
: await roomCandidateChunk(ctx, cfg, io.logger, selected,
|
|
105
|
+
? await overrides.chunkAnnotator(selected, merged)
|
|
106
|
+
: await roomCandidateChunk(ctx, cfg, io.logger, selected, merged);
|
|
101
107
|
let written = 0;
|
|
102
108
|
for (const { id, rooms: candidates } of rows) {
|
|
103
109
|
if (candidates.length === 0)
|
|
@@ -133,6 +139,76 @@ function pendingQueue(l1) {
|
|
|
133
139
|
}
|
|
134
140
|
return out.sort((a, b) => a.updatedAt - b.updatedAt);
|
|
135
141
|
}
|
|
142
|
+
/** merge 单飞哨兵(模块级;与 ruminate/wing-backfill 的时间错开由调用方保证)。 */
|
|
143
|
+
let roomMergeRunning = false;
|
|
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 async function mergeRoom(io, from, to, opts = {}) {
|
|
154
|
+
const dryRun = opts.dryRun !== false;
|
|
155
|
+
const cap = Math.min(Math.max(opts.cap ?? 500, 1), 2000);
|
|
156
|
+
if (from === to)
|
|
157
|
+
throw new Error('from 与 to 相同');
|
|
158
|
+
if (!from.trim())
|
|
159
|
+
throw new Error('from 不能为空');
|
|
160
|
+
if (roomMergeRunning)
|
|
161
|
+
throw new Error('Room merge 已在进行中(单飞)');
|
|
162
|
+
const previewRows = io.l1.listByTagAll(from, 7);
|
|
163
|
+
const affected = io.l1.listByTagAll(from, cap + 1).length;
|
|
164
|
+
const preview = previewRows.slice(0, 6).map((r) => r.id);
|
|
165
|
+
if (dryRun)
|
|
166
|
+
return { dryRun: true, affected, preview, applied: 0, hasMore: affected > cap };
|
|
167
|
+
if (roomMergeRunning)
|
|
168
|
+
throw new Error('Room merge 已在进行中(单飞)');
|
|
169
|
+
roomMergeRunning = true;
|
|
170
|
+
try {
|
|
171
|
+
const backup = io.l1.backupTagRecords(from);
|
|
172
|
+
io.logger.info(`[memory] Room merge 备份:${from} → ${to},${backup.count} 条 → ${backup.file}`);
|
|
173
|
+
let applied = 0;
|
|
174
|
+
let hasMore = false;
|
|
175
|
+
const families = new Set();
|
|
176
|
+
const scenes = new Set();
|
|
177
|
+
// 游标重写:上限 cap/次,hasMore 时继续(总量受 cap×轮次约束,单飞内收敛)
|
|
178
|
+
for (;;) {
|
|
179
|
+
const r = io.l1.rewriteTag(from, to, cap);
|
|
180
|
+
applied += r.rewritten;
|
|
181
|
+
for (const f of r.families)
|
|
182
|
+
families.add(f);
|
|
183
|
+
for (const sc of r.scenes)
|
|
184
|
+
scenes.add(sc);
|
|
185
|
+
if (!r.hasMore || r.rewritten === 0)
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
await io.registry.markMerged(from, to);
|
|
189
|
+
io.l1.invalidateRooms();
|
|
190
|
+
io.logger.info(`[memory] Room merge 完成:${from} → ${to},重写 ${applied} 条;受影响场景 ${scenes.size} 个`);
|
|
191
|
+
return {
|
|
192
|
+
dryRun: false,
|
|
193
|
+
affected,
|
|
194
|
+
preview,
|
|
195
|
+
applied,
|
|
196
|
+
hasMore,
|
|
197
|
+
backupFile: backup.file,
|
|
198
|
+
...(families.size > 0 || scenes.size > 0 ? { families: [...families], scenes: [...scenes] } : {}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
roomMergeRunning = false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** rename = merge 1:1 + 注册表改名(冲突时抛错提示走 merge)。 */
|
|
206
|
+
export async function renameRoom(io, from, to, opts = {}) {
|
|
207
|
+
const r = await mergeRoom(io, from, to, opts);
|
|
208
|
+
if (!r.dryRun && r.applied > 0)
|
|
209
|
+
await io.registry.renameSlug(from, to);
|
|
210
|
+
return r;
|
|
211
|
+
}
|
|
136
212
|
/** 下一条待复查(供 agent 逐个过):记录摘要 + 候选 + 现有 Room 词表 + 剩余数。 */
|
|
137
213
|
export function nextReview(l1) {
|
|
138
214
|
const queue = pendingQueue(l1);
|
package/dist/stats.js
CHANGED
|
@@ -56,6 +56,7 @@ export const MEMORY_ENDPOINTS = [
|
|
|
56
56
|
'dsh-memory/wing-overview',
|
|
57
57
|
'dsh-memory/rooms-get',
|
|
58
58
|
'dsh-memory/rooms-export',
|
|
59
|
+
'dsh-memory/room-register',
|
|
59
60
|
'dsh-memory/wing-backfill',
|
|
60
61
|
'dsh-memory/session-stats',
|
|
61
62
|
'dsh-memory/settings-get',
|
|
@@ -679,6 +680,27 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
679
680
|
};
|
|
680
681
|
return v;
|
|
681
682
|
}
|
|
683
|
+
case 'dsh-memory/room-register': {
|
|
684
|
+
if (!roomRegistry)
|
|
685
|
+
throw new Error('Room 注册表未装配(需要插件重载以初始化)。');
|
|
686
|
+
if (!live?.get().memoryMutate)
|
|
687
|
+
throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后,模型才能预注册 Room。');
|
|
688
|
+
const p = (payload ?? {});
|
|
689
|
+
const slug = String(p.slug ?? '').trim();
|
|
690
|
+
if (!slug)
|
|
691
|
+
throw new Error('slug 不能为空');
|
|
692
|
+
const r = await roomRegistry.register({
|
|
693
|
+
slug,
|
|
694
|
+
label: typeof p.label === 'string' ? p.label : undefined,
|
|
695
|
+
description: typeof p.description === 'string' ? p.description : undefined,
|
|
696
|
+
});
|
|
697
|
+
const v = {
|
|
698
|
+
slug: r.entry.slug,
|
|
699
|
+
created: r.created,
|
|
700
|
+
notice: r.created ? '已预注册。' : '已存在(只补全了缺失的 label/description)。',
|
|
701
|
+
};
|
|
702
|
+
return v;
|
|
703
|
+
}
|
|
682
704
|
case 'dsh-memory/rooms-export': {
|
|
683
705
|
const p = (payload ?? {});
|
|
684
706
|
const kind = p.kind === 'orphans' ? 'orphans' : p.kind === 'rooms' ? 'rooms' : '';
|
package/dist/store/l1.d.ts
CHANGED
|
@@ -110,6 +110,34 @@ export declare class L1Store {
|
|
|
110
110
|
source: string;
|
|
111
111
|
} | null;
|
|
112
112
|
finishSceneRecluster(jobId: string, ok: boolean): void;
|
|
113
|
+
/**
|
|
114
|
+
* 收集某 Room 的全部匹配记录(含已退场行,口径与 Room 计数一致)。
|
|
115
|
+
* 内部分页拉取,上限 cap 防跑飞;返回记录数组供备份与干跑预览。
|
|
116
|
+
*/
|
|
117
|
+
listByTagAll(tag: string, cap?: number): MemoryRecord[];
|
|
118
|
+
/**
|
|
119
|
+
* 备份某 Room 的匹配记录到 `<dataDir>/rooms-merge-backups/<ts>-<tag>.json`
|
|
120
|
+
* (执行前快照;破坏性重写前的唯一回滚物料)。返回备份文件路径与条数。
|
|
121
|
+
*/
|
|
122
|
+
backupTagRecords(tag: string): {
|
|
123
|
+
file: string;
|
|
124
|
+
count: number;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* 游标重写:把 metadata.tags 里的 `from` 全部替换为 `to`(**含已退场行**,
|
|
128
|
+
* 口径与 Room 计数一致)。写前重读-合并-写回(relabel 同款):只动 tags,
|
|
129
|
+
* roomCandidates/roomReview/hall/cogHall/sourceAnchors 等其余键原样保全。
|
|
130
|
+
*
|
|
131
|
+
* 单次上限 cap(500)可续跑:再次调用同一 from 即继续处理剩余行。
|
|
132
|
+
* 返回重写条数 + 受影响的 (family, scene) 集(供 recluster 'room-merge' 入队)。
|
|
133
|
+
*/
|
|
134
|
+
rewriteTag(from: string, to: string, cap?: number): {
|
|
135
|
+
rewritten: number;
|
|
136
|
+
scanned: number;
|
|
137
|
+
families: string[];
|
|
138
|
+
scenes: string[];
|
|
139
|
+
hasMore: boolean;
|
|
140
|
+
};
|
|
113
141
|
/** 按 id 精确取记录(去重决策的版本号查询用,避免全表扫描)。 */
|
|
114
142
|
getByIds(ids: string[]): MemoryRecord[];
|
|
115
143
|
/**
|
package/dist/store/l1.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 去重/合并的更新记录走 upsert(新 record id + 版本递增),不再全量重写文件。
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync, promises as fs } from 'node:fs';
|
|
9
|
+
import { existsSync, mkdirSync, promises as fs, writeFileSync } from 'node:fs';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import { familyForType, isScopeVisible } from '../types.js';
|
|
12
12
|
import { graphHitRecordIds } from '../graph/search.js';
|
|
@@ -162,6 +162,65 @@ export class L1Store {
|
|
|
162
162
|
finishSceneRecluster(jobId, ok) {
|
|
163
163
|
this.db.finishSceneRecluster(jobId, ok);
|
|
164
164
|
}
|
|
165
|
+
// ── Room merge/rename 的游标重写(分类管理 beta.5;破坏性面)──────────────
|
|
166
|
+
/**
|
|
167
|
+
* 收集某 Room 的全部匹配记录(含已退场行,口径与 Room 计数一致)。
|
|
168
|
+
* 内部分页拉取,上限 cap 防跑飞;返回记录数组供备份与干跑预览。
|
|
169
|
+
*/
|
|
170
|
+
listByTagAll(tag, cap = 500) {
|
|
171
|
+
const out = [];
|
|
172
|
+
for (let offset = 0; offset < cap; offset += 200) {
|
|
173
|
+
const page = this.list({ tag, retired: undefined, limit: Math.min(200, cap - out.length), offset });
|
|
174
|
+
out.push(...page.items);
|
|
175
|
+
if (page.items.length < 200 || out.length >= cap)
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 备份某 Room 的匹配记录到 `<dataDir>/rooms-merge-backups/<ts>-<tag>.json`
|
|
182
|
+
* (执行前快照;破坏性重写前的唯一回滚物料)。返回备份文件路径与条数。
|
|
183
|
+
*/
|
|
184
|
+
backupTagRecords(tag) {
|
|
185
|
+
const records = this.listByTagAll(tag, 2000);
|
|
186
|
+
const backupDir = path.join(this.dataDir, 'rooms-merge-backups');
|
|
187
|
+
if (!existsSync(backupDir))
|
|
188
|
+
mkdirSync(backupDir, { recursive: true });
|
|
189
|
+
const file = path.join(backupDir, `${Date.now()}-${tag.replace(/[^a-z0-9-]/gi, '_')}.json`);
|
|
190
|
+
writeFileSync(file, JSON.stringify({ tag, exportedAt: new Date().toISOString(), records }, null, 2), 'utf8');
|
|
191
|
+
return { file, count: records.length };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 游标重写:把 metadata.tags 里的 `from` 全部替换为 `to`(**含已退场行**,
|
|
195
|
+
* 口径与 Room 计数一致)。写前重读-合并-写回(relabel 同款):只动 tags,
|
|
196
|
+
* roomCandidates/roomReview/hall/cogHall/sourceAnchors 等其余键原样保全。
|
|
197
|
+
*
|
|
198
|
+
* 单次上限 cap(500)可续跑:再次调用同一 from 即继续处理剩余行。
|
|
199
|
+
* 返回重写条数 + 受影响的 (family, scene) 集(供 recluster 'room-merge' 入队)。
|
|
200
|
+
*/
|
|
201
|
+
rewriteTag(from, to, cap = 500) {
|
|
202
|
+
const rows = this.listByTagAll(from, cap + 1);
|
|
203
|
+
const hasMore = rows.length > cap;
|
|
204
|
+
const batch = rows.slice(0, cap);
|
|
205
|
+
let rewritten = 0;
|
|
206
|
+
const families = new Set();
|
|
207
|
+
const scenes = new Set();
|
|
208
|
+
for (const r of batch) {
|
|
209
|
+
const meta = { ...(r.metadata ?? {}) };
|
|
210
|
+
const tags = Array.isArray(meta.tags) ? meta.tags.map(String) : [];
|
|
211
|
+
if (!tags.includes(from))
|
|
212
|
+
continue; // 命中是 json_each 判等,数组缺失即无可改
|
|
213
|
+
const next = tags.map((t) => (t === from ? to : t));
|
|
214
|
+
meta.tags = [...new Set(next)];
|
|
215
|
+
if (this.patchMetadata(r.id, meta)) {
|
|
216
|
+
rewritten++;
|
|
217
|
+
families.add(r.family ?? 'chat');
|
|
218
|
+
if (r.scene_name)
|
|
219
|
+
scenes.add(r.scene_name);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { rewritten, scanned: batch.length, families: [...families], scenes: [...scenes], hasMore };
|
|
223
|
+
}
|
|
165
224
|
/** 按 id 精确取记录(去重决策的版本号查询用,避免全表扫描)。 */
|
|
166
225
|
getByIds(ids) {
|
|
167
226
|
return this.db.getL1ByIds(ids);
|
|
@@ -17,6 +17,13 @@ export interface RoomRegistryEntry {
|
|
|
17
17
|
updatedAt: string;
|
|
18
18
|
}
|
|
19
19
|
export type RoomRegistryInput = Pick<RoomRegistryEntry, 'slug'> & Partial<Pick<RoomRegistryEntry, 'label' | 'description' | 'source' | 'aliases'>>;
|
|
20
|
+
/**
|
|
21
|
+
* 两级 Room slug:`major` 或 `major/minor`(如 `dsh-plugin`、`dsh-plugin/merge`)。
|
|
22
|
+
* 两段各自满足 isTag 词表;小类归属其大类。
|
|
23
|
+
*/
|
|
24
|
+
export declare function isRoomSlug(v: unknown): boolean;
|
|
25
|
+
/** 大类归属:`major/minor` 取 major;平级 slug 自身即大类。 */
|
|
26
|
+
export declare function majorOf(slug: string): string;
|
|
20
27
|
export declare class RoomRegistryStore {
|
|
21
28
|
private readonly logger?;
|
|
22
29
|
private readonly file;
|
|
@@ -15,8 +15,27 @@ import { isTag } from '../metadata-validators.js';
|
|
|
15
15
|
import { readJsonStrict, atomicWriteText, ensureDir } from '../util/io.js';
|
|
16
16
|
/** 版本常量(file-versions 同款集中纪律;registry 为独立 sidecar 文件)。 */
|
|
17
17
|
export const ROOMS_REGISTRY_FILE_VERSION = 1;
|
|
18
|
-
/**
|
|
19
|
-
const
|
|
18
|
+
/** 大类(顶层)注册上限(防刷)。小类(`major/minor`)不占大类额度,另有总量保险。 */
|
|
19
|
+
const REGISTRY_MAJOR_CAP = 200;
|
|
20
|
+
/** 全量保险上限(大类+小类总和;防极端滥用)。 */
|
|
21
|
+
const REGISTRY_TOTAL_CAP = 2000;
|
|
22
|
+
/**
|
|
23
|
+
* 两级 Room slug:`major` 或 `major/minor`(如 `dsh-plugin`、`dsh-plugin/merge`)。
|
|
24
|
+
* 两段各自满足 isTag 词表;小类归属其大类。
|
|
25
|
+
*/
|
|
26
|
+
export function isRoomSlug(v) {
|
|
27
|
+
if (typeof v !== 'string')
|
|
28
|
+
return false;
|
|
29
|
+
const parts = v.split('/');
|
|
30
|
+
if (parts.length > 2)
|
|
31
|
+
return false;
|
|
32
|
+
return parts.every((p) => isTag(p));
|
|
33
|
+
}
|
|
34
|
+
/** 大类归属:`major/minor` 取 major;平级 slug 自身即大类。 */
|
|
35
|
+
export function majorOf(slug) {
|
|
36
|
+
const i = slug.indexOf('/');
|
|
37
|
+
return i === -1 ? slug : slug.slice(0, i);
|
|
38
|
+
}
|
|
20
39
|
/** aliases 上限(合并/改名历史)。 */
|
|
21
40
|
const ALIASES_CAP = 16;
|
|
22
41
|
export class RoomRegistryStore {
|
|
@@ -88,8 +107,9 @@ export class RoomRegistryStore {
|
|
|
88
107
|
async register(input) {
|
|
89
108
|
this.ensureLoaded();
|
|
90
109
|
const slug = String(input.slug ?? '').trim().toLowerCase();
|
|
91
|
-
if (!slug || !
|
|
92
|
-
throw new Error(`非法 Room slug: ${String(input.slug)}(
|
|
110
|
+
if (!slug || !isRoomSlug(slug)) {
|
|
111
|
+
throw new Error(`非法 Room slug: ${String(input.slug)}(两级制:major 或 major/minor,各段小写字母数字连字符)`);
|
|
112
|
+
}
|
|
93
113
|
const found = this.entries.find((e) => e.slug === slug || (e.aliases ?? []).includes(slug));
|
|
94
114
|
if (found) {
|
|
95
115
|
let changed = false;
|
|
@@ -108,8 +128,17 @@ export class RoomRegistryStore {
|
|
|
108
128
|
}
|
|
109
129
|
return { entry: { ...found }, created: false };
|
|
110
130
|
}
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
// 额度两级制(用户裁定:200 上限只管大类;小类细分不占大类额度):
|
|
132
|
+
// 大类 = slug 无 '/' 的条目 ∪ 已用 major 前缀;小类只受总量保险约束。
|
|
133
|
+
const isMinor = slug.includes('/');
|
|
134
|
+
if (isMinor && this.entries.length >= REGISTRY_TOTAL_CAP) {
|
|
135
|
+
throw new Error(`Room 注册表总量已达上限(${REGISTRY_TOTAL_CAP}),请先合并/退役部分条目`);
|
|
136
|
+
}
|
|
137
|
+
if (!isMinor) {
|
|
138
|
+
const majors = new Set(this.entries.map((e) => majorOf(e.slug)));
|
|
139
|
+
if (!majors.has(slug) && majors.size >= REGISTRY_MAJOR_CAP) {
|
|
140
|
+
throw new Error(`大类已达上限(${REGISTRY_MAJOR_CAP}):请用「大类/小类」细分(如 ${slug}/子项),或合并/退役部分大类`);
|
|
141
|
+
}
|
|
113
142
|
}
|
|
114
143
|
const now = new Date().toISOString();
|
|
115
144
|
const entry = {
|
package/dist/tools/index.js
CHANGED
|
@@ -3,7 +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
|
+
import { annotateOrphanCandidates, confirmReview, mergeRoom, nextReview, pendingReviewCount, renameRoom, skipReview, } from '../room-review.js';
|
|
7
7
|
import { applyGovernanceWeights } from '../store/governance.js';
|
|
8
8
|
import { resolveRepoScope } from '../repo-scope.js';
|
|
9
9
|
import { GRAPH_STATUS_LABELS } from '../prompts/graph-projection.js';
|
|
@@ -190,13 +190,18 @@ roomRegistry) {
|
|
|
190
190
|
ctx.tools.register(defineTool({
|
|
191
191
|
name: 'memory_room_admin',
|
|
192
192
|
description: 'Room 分类管理(目录粒度)。action="list" 列出注册表条目与自生长 Room 计数;' +
|
|
193
|
-
'action="register" 预注册一个 Room(
|
|
194
|
-
'
|
|
193
|
+
'action="register" 预注册一个 Room(高权限;两级制——大类上限 200,小类用「大类/小类」细分不占额度,如 dsh-plugin/merge;' +
|
|
194
|
+
'action="merge" 合并两个 Room(高权限;dryRun 默认 true 只返预览,实跑改写全部匹配记录的 tags 并入队场景重算,执行前自动备份);' +
|
|
195
|
+
'action="rename" 改名(= merge 1:1 + 注册表改名);action="retire" 退役一个注册 Room(高权限;存量记录不动)。' +
|
|
196
|
+
'merge/rename 与反刍并发会相互覆盖,请在反刍空闲时执行。',
|
|
195
197
|
parameters: {
|
|
196
|
-
action: { type: 'string', required: true, description: 'list(
|
|
198
|
+
action: { type: 'string', required: true, description: 'list | register(高权限) | merge(高权限) | rename(高权限) | retire(高权限)' },
|
|
197
199
|
slug: { type: 'string', description: 'register 的 Room slug(小写字母数字连字符)' },
|
|
198
200
|
label: { type: 'string', description: 'register 的人类可读名(可选,可中文)' },
|
|
199
201
|
description: { type: 'string', description: 'register 的归类说明(可选;喂给标注器帮助归类)' },
|
|
202
|
+
from: { type: 'string', description: 'merge/rename 的源 Room slug' },
|
|
203
|
+
to: { type: 'string', description: 'merge/rename 的目标 Room slug' },
|
|
204
|
+
dryRun: { type: 'string', description: 'merge/rename:默认 true 只返预览;"false" 才实际执行' },
|
|
200
205
|
},
|
|
201
206
|
output: {
|
|
202
207
|
schema: {
|
|
@@ -252,7 +257,60 @@ roomRegistry) {
|
|
|
252
257
|
return { notice: `注册失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
253
258
|
}
|
|
254
259
|
}
|
|
255
|
-
|
|
260
|
+
if (action === 'merge' || action === 'rename') {
|
|
261
|
+
if (!roomRegistry)
|
|
262
|
+
return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
|
|
263
|
+
if (!live.get().memoryMutate)
|
|
264
|
+
return { notice: MUTATE_OFF_NOTICE };
|
|
265
|
+
const from = String(args.from ?? '').trim();
|
|
266
|
+
const to = String(args.to ?? '').trim();
|
|
267
|
+
if (!from || !to)
|
|
268
|
+
return { notice: 'merge/rename 需要 from 与 to 两个 slug。' };
|
|
269
|
+
const dryRun = String(args.dryRun ?? 'true').toLowerCase() !== 'false';
|
|
270
|
+
try {
|
|
271
|
+
const fn = action === 'merge' ? mergeRoom : renameRoom;
|
|
272
|
+
const r = await fn({ l1: stores.l1, registry: roomRegistry, logger }, from, to, { dryRun });
|
|
273
|
+
if (r.dryRun) {
|
|
274
|
+
return {
|
|
275
|
+
notice: `[dryRun 预览] ${from} → ${to}:影响 ${r.affected} 条${r.hasMore ? '(超过单次上限,需续跑)' : ''}。` +
|
|
276
|
+
`样例:${r.preview.join(', ') || '无'}。确认无误后以 dryRun="false" 执行(执行前自动备份,重写含已退场行,完成后自动入队场景重算)。`,
|
|
277
|
+
affected: r.affected,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// 实跑:受影响场景入队重算(source='room-merge')
|
|
281
|
+
let requeued = 0;
|
|
282
|
+
for (const family of r.families ?? []) {
|
|
283
|
+
for (const scene of r.scenes ?? []) {
|
|
284
|
+
stores.l1.enqueueSceneRecluster(family, [scene], `room-merge:${Date.now()}`, 'room-merge');
|
|
285
|
+
requeued++;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
notice: `${action === 'merge' ? '合并' : '改名'}完成:${from} → ${to},重写 ${r.applied} 条` +
|
|
290
|
+
`(备份:${r.backupFile ?? '无'})${requeued > 0 ? `;已入队 ${requeued} 个场景重算` : ''}。`,
|
|
291
|
+
applied: r.applied,
|
|
292
|
+
id: to,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
return { notice: `${action} 失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (action === 'retire') {
|
|
300
|
+
if (!roomRegistry)
|
|
301
|
+
return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
|
|
302
|
+
if (!live.get().memoryMutate)
|
|
303
|
+
return { notice: MUTATE_OFF_NOTICE };
|
|
304
|
+
const slug = String(args.slug ?? '').trim();
|
|
305
|
+
try {
|
|
306
|
+
const ok = await roomRegistry.setStatus(slug, 'retired');
|
|
307
|
+
return { notice: ok ? `已退役 Room:${slug}(存量记录的 tags 不动,仅词表不再推荐)。` : `未找到注册条目:${slug}(自生长 Room 无需退役,会随 tags 自然消失)。`, slug };
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
return { notice: `退役失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { notice: `非法 action:${action}(允许 list/register/merge/rename/retire)。` };
|
|
256
314
|
},
|
|
257
315
|
}));
|
|
258
316
|
// ── conversation_search: L0 原始对话 ──
|
|
@@ -898,7 +956,7 @@ roomRegistry) {
|
|
|
898
956
|
};
|
|
899
957
|
}
|
|
900
958
|
if (action === 'annotate') {
|
|
901
|
-
const r = await annotateOrphanCandidates(ctx, cfg, io, 40);
|
|
959
|
+
const r = await annotateOrphanCandidates(ctx, cfg, { ...io, registry: roomRegistry }, 40);
|
|
902
960
|
return {
|
|
903
961
|
notice: `候选预标记完成:选中 ${r.selected} 条,产候选 ${r.candidates} 条,写 pending ${r.written} 条。用 action=next 逐个复查。`,
|
|
904
962
|
annotated: r.candidates,
|
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.6-dsh0.2.0.1",
|
|
5
5
|
"description": {
|
|
6
6
|
"en": "Layered distilled memory plugin (L0 capture → L1 atomic memory → L2 scenes → L3 persona) with automatic recall injection before each model step.",
|
|
7
7
|
"zh": "L0~L3 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
|