dsh-prime-memory 0.18.5 → 0.18.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 +11 -28
- package/dist/contract.d.ts +1 -109
- package/dist/hooks/recall.d.ts +1 -4
- package/dist/hooks/recall.js +1 -7
- package/dist/hooks/slot-recall.js +24 -1
- package/dist/index.js +4 -14
- package/dist/metadata-validators.d.ts +0 -5
- package/dist/metadata-validators.js +1 -11
- package/dist/pipeline/ruminate.d.ts +1 -11
- package/dist/pipeline/ruminate.js +1 -18
- package/dist/projection/slots.js +9 -14
- package/dist/stats.d.ts +2 -26
- package/dist/stats.js +13 -261
- package/dist/store/l1.d.ts +0 -34
- package/dist/store/l1.js +1 -68
- package/dist/store/sqlite.d.ts +0 -4
- package/dist/store/sqlite.js +0 -20
- package/dist/tools/index.d.ts +1 -4
- package/dist/tools/index.js +2 -224
- package/dist/types.d.ts +0 -4
- package/package.json +1 -1
- package/dist/client.js +0 -6274
- package/dist/config.d.ts +0 -1407
- package/dist/embedding-worker.cjs +0 -176
- package/dist/index.d.ts +0 -999
- package/dist/room-review.d.ts +0 -196
- package/dist/room-review.js +0 -281
- package/dist/runtime-package-lock.json +0 -982
- package/dist/store/recall-usage.d.ts +0 -43
- package/dist/store/recall-usage.js +0 -149
- package/dist/store/rooms-registry.d.ts +0 -66
- package/dist/store/rooms-registry.js +0 -250
package/dist/room-review.d.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 孤儿记忆的 Room 候选预标记 + agent 逐个复查(长尾治理)。
|
|
3
|
-
*
|
|
4
|
-
* 背景:Room 由 metadata.tags 自生长,但 tags 只在反刍 relabel 段随 wing 补打
|
|
5
|
-
* 顺带产出(每轮 ≤40 条、且只覆盖本轮补 wing 的记录)——「有 wing 无 tags」与
|
|
6
|
-
* 「预算用尽被 deferred」的记录成为无 Room 绑定的孤儿,且标注器不参考现有
|
|
7
|
-
* Room 词表,产出碎片化新 slug(81 个 Room 大多 1 条的成因)。
|
|
8
|
-
*
|
|
9
|
-
* 两段式:
|
|
10
|
-
* 1. **预标记(annotateOrphanCandidates)**:抽取活跃孤儿,LLM 对照**现有 Room
|
|
11
|
-
* 词表**产出候选(优先挂靠现有 Room,≤2 个新 slug 兜底),写
|
|
12
|
-
* `metadata.roomCandidates` + `roomReview:'pending'`——不动 tags、不进
|
|
13
|
-
* Room、不进召回,只是"待确认的建议"。
|
|
14
|
-
* 2. **逐个复查(memory_room_review 工具)**:agent 取下一条待复查记录
|
|
15
|
-
* (带候选 + 现有 Room 词表),确认后 confirm 写入 tags(进 Room,高权限
|
|
16
|
-
* 门控),或 skip 跳过。确认/跳过都是显式动作,预标记本身零行为面。
|
|
17
|
-
*
|
|
18
|
-
* 存储即 metadata 保留键(与 relabel 写 tags 同款 patchMetadata 合并纪律,
|
|
19
|
-
* 写前重读):`roomCandidates` / `roomReview:'pending'|'confirmed'|'skipped'`。
|
|
20
|
-
* 两键不在 rooms 聚合(rooms 只读 $.tags)、不在快照哈希投影关注的治理列——
|
|
21
|
-
* 与既有 metadata 键同层,无 schema 变更。
|
|
22
|
-
*/
|
|
23
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
24
|
-
import type { MemoryConfig } from './config.js';
|
|
25
|
-
import type { MemoryLogger, MemoryRecord } from './types.js';
|
|
26
|
-
export declare const ROOM_CANDIDATES_KEY = "roomCandidates";
|
|
27
|
-
export declare const ROOM_REVIEW_KEY = "roomReview";
|
|
28
|
-
export type RoomReviewState = 'pending' | 'confirmed' | 'skipped';
|
|
29
|
-
/** 复查与候选操作对 L1 的最小依赖面(便于测试注入)。 */
|
|
30
|
-
export interface RoomReviewStore {
|
|
31
|
-
list(opts: {
|
|
32
|
-
untagged?: boolean;
|
|
33
|
-
retired?: boolean;
|
|
34
|
-
limit: number;
|
|
35
|
-
offset: number;
|
|
36
|
-
}): {
|
|
37
|
-
items: MemoryRecord[];
|
|
38
|
-
total: number;
|
|
39
|
-
};
|
|
40
|
-
getByIds(ids: readonly string[]): MemoryRecord[];
|
|
41
|
-
patchMetadata(id: string, metadata: Record<string, unknown>): boolean;
|
|
42
|
-
listRooms(): Array<{
|
|
43
|
-
room: string;
|
|
44
|
-
count: number;
|
|
45
|
-
}>;
|
|
46
|
-
/** 确认写 tags 后失效 Room 计数缓存(30s TTL)。 */
|
|
47
|
-
invalidateRooms(): void;
|
|
48
|
-
}
|
|
49
|
-
/** merge/rename 的 store 面(RoomReviewStore + 游标重写与备份;L1Store 已实现)。 */
|
|
50
|
-
export interface RoomMergeStore extends RoomReviewStore {
|
|
51
|
-
listByTagAll(tag: string, cap?: number): MemoryRecord[];
|
|
52
|
-
backupTagRecords(tag: string): {
|
|
53
|
-
file: string;
|
|
54
|
-
count: number;
|
|
55
|
-
};
|
|
56
|
-
rewriteTag(from: string, to: string, cap?: number): {
|
|
57
|
-
rewritten: number;
|
|
58
|
-
scanned: number;
|
|
59
|
-
families: string[];
|
|
60
|
-
scenes: string[];
|
|
61
|
-
hasMore: boolean;
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
/** Room 注册表最小面(merge 收尾;RoomRegistryStore 已满足)。 */
|
|
65
|
-
export interface RoomRegistryMerge {
|
|
66
|
-
markMerged(fromSlug: string, toSlug: string): Promise<boolean>;
|
|
67
|
-
renameSlug(oldSlug: string, newSlug: string): Promise<boolean>;
|
|
68
|
-
/** rename 收尾恢复先前状态用(active 条目改名后不掉出标注词表)。 */
|
|
69
|
-
bySlug?(slug: string): {
|
|
70
|
-
status: 'active' | 'retired';
|
|
71
|
-
} | undefined;
|
|
72
|
-
setStatus?(slug: string, status: 'active' | 'retired'): Promise<boolean>;
|
|
73
|
-
}
|
|
74
|
-
export interface RoomCandidate {
|
|
75
|
-
id: string;
|
|
76
|
-
rooms: string[];
|
|
77
|
-
}
|
|
78
|
-
/** 解析标注器输出的逐条候选(容错:id 配对失败丢弃;isTag 校验;目录内优先,≤3)。 */
|
|
79
|
-
export declare function parseRoomCandidateItems(parsed: ReadonlyArray<{
|
|
80
|
-
id: string;
|
|
81
|
-
rooms: readonly string[];
|
|
82
|
-
}>, ids: ReadonlyArray<string>, roomCatalog: ReadonlyArray<{
|
|
83
|
-
room: string;
|
|
84
|
-
count: number;
|
|
85
|
-
}>): RoomCandidate[];
|
|
86
|
-
/** 候选标注系统 prompt:喂现有 Room 词表(带计数),优先挂靠、少量新建兜底。 */
|
|
87
|
-
export declare function roomCandidateSystemPrompt(roomCatalog: ReadonlyArray<{
|
|
88
|
-
room: string;
|
|
89
|
-
count: number;
|
|
90
|
-
}>): string;
|
|
91
|
-
/** 候选标注器:一批孤儿记录 → 候选 Room(优先现有词表;失败返回空,调用方零改动)。 */
|
|
92
|
-
export declare function roomCandidateChunk(ctx: Context, cfg: MemoryConfig, logger: MemoryLogger, chunk: MemoryRecord[], roomCatalog: ReadonlyArray<{
|
|
93
|
-
room: string;
|
|
94
|
-
count: number;
|
|
95
|
-
}>): Promise<Array<{
|
|
96
|
-
id: string;
|
|
97
|
-
rooms: string[];
|
|
98
|
-
}>>;
|
|
99
|
-
export interface OrphanAnnotateIO {
|
|
100
|
-
l1: RoomReviewStore;
|
|
101
|
-
logger: MemoryLogger;
|
|
102
|
-
/** Room 注册表(可选:未装配 = 词表只用自生长聚合,无注册条目)。 */
|
|
103
|
-
registry?: {
|
|
104
|
-
listActive(): Array<{
|
|
105
|
-
slug: string;
|
|
106
|
-
label?: string;
|
|
107
|
-
description?: string;
|
|
108
|
-
}>;
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* 预标记消费器:活跃孤儿(≤limit 条)批量产候选并写 pending。
|
|
113
|
-
* 写前重读合并(与 relabel 同纪律);失败单条跳过、原记录零改动。
|
|
114
|
-
*/
|
|
115
|
-
export interface OrphanAnnotateOverrides {
|
|
116
|
-
/** 测试注入口:替换 LLM 候选标注器(默认走真实 callLLM 路径);第二参为合并词表。 */
|
|
117
|
-
chunkAnnotator?: (chunk: MemoryRecord[], catalog: ReadonlyArray<{
|
|
118
|
-
room: string;
|
|
119
|
-
count: number;
|
|
120
|
-
label?: string;
|
|
121
|
-
description?: string;
|
|
122
|
-
}>) => Promise<Array<{
|
|
123
|
-
id: string;
|
|
124
|
-
rooms: string[];
|
|
125
|
-
}>>;
|
|
126
|
-
}
|
|
127
|
-
export declare function annotateOrphanCandidates(ctx: Context, cfg: MemoryConfig, io: OrphanAnnotateIO, limit?: number, overrides?: OrphanAnnotateOverrides): Promise<{
|
|
128
|
-
selected: number;
|
|
129
|
-
candidates: number;
|
|
130
|
-
written: number;
|
|
131
|
-
}>;
|
|
132
|
-
export interface MergeRoomResult {
|
|
133
|
-
dryRun: boolean;
|
|
134
|
-
/** 影响条数(dryRun=预览数;实跑=实际重写数)。 */
|
|
135
|
-
affected: number;
|
|
136
|
-
preview: string[];
|
|
137
|
-
applied: number;
|
|
138
|
-
hasMore: boolean;
|
|
139
|
-
backupFile?: string;
|
|
140
|
-
/** 实跑时回传:受影响的族与场景(调用方入队 recluster 'room-merge')。 */
|
|
141
|
-
families?: string[];
|
|
142
|
-
scenes?: string[];
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Room merge/rename 编排(破坏性动作,分类管理 beta.5):
|
|
146
|
-
* - dryRun(缺省 true):只统计 affected + 前 6 条预览,**零写入**;
|
|
147
|
-
* - 实跑:执行前备份(matching rows → rooms-merge-backups/)→ 游标重写
|
|
148
|
-
* (写前重读-合并-写回,其余 metadata 键保全)→ 注册表 markMerged/renameSlug
|
|
149
|
-
* → 返回受影响 (family, scene) 供调用方入队 recluster('room-merge');
|
|
150
|
-
* - 单飞:并发 merge 直接抛错;与 relabel/ruminate 的时间错开由调用方保证。
|
|
151
|
-
* 抛错语义:非法参数/注册表冲突直接抛(调用方转 notice)。
|
|
152
|
-
*/
|
|
153
|
-
export declare function mergeRoom(io: {
|
|
154
|
-
l1: RoomMergeStore;
|
|
155
|
-
registry: RoomRegistryMerge;
|
|
156
|
-
logger: MemoryLogger;
|
|
157
|
-
}, from: string, to: string, opts?: {
|
|
158
|
-
dryRun?: boolean;
|
|
159
|
-
cap?: number;
|
|
160
|
-
}): Promise<MergeRoomResult>;
|
|
161
|
-
/** rename = merge 1:1 + 注册表改名(冲突时抛错提示走 merge)。
|
|
162
|
-
* 归类语义:改名**不是退役**——merge 编排的 markMerged 会把旧条目置 retired,
|
|
163
|
-
* 这里在改名后恢复其先前状态(active 条目改名后仍 active,不掉出标注词表)。 */
|
|
164
|
-
export declare function renameRoom(io: {
|
|
165
|
-
l1: RoomMergeStore;
|
|
166
|
-
registry: RoomRegistryMerge;
|
|
167
|
-
logger: MemoryLogger;
|
|
168
|
-
}, from: string, to: string, opts?: {
|
|
169
|
-
dryRun?: boolean;
|
|
170
|
-
cap?: number;
|
|
171
|
-
}): Promise<MergeRoomResult>;
|
|
172
|
-
/** 下一条待复查(供 agent 逐个过):记录摘要 + 候选 + 现有 Room 词表 + 剩余数。 */
|
|
173
|
-
export declare function nextReview(l1: RoomReviewStore): {
|
|
174
|
-
id: string;
|
|
175
|
-
content: string;
|
|
176
|
-
type: string;
|
|
177
|
-
sceneName: string;
|
|
178
|
-
candidates: string[];
|
|
179
|
-
roomCatalog: Array<{
|
|
180
|
-
room: string;
|
|
181
|
-
count: number;
|
|
182
|
-
}>;
|
|
183
|
-
remaining: number;
|
|
184
|
-
} | null;
|
|
185
|
-
/** 待复查剩余数(面板/工具兜底信息)。 */
|
|
186
|
-
export declare function pendingReviewCount(l1: RoomReviewStore): number;
|
|
187
|
-
/**
|
|
188
|
-
* 确认复查结果:候选(或 agent 改写的)写入 metadata.tags(**进 Room**),
|
|
189
|
-
* 清候选、标 confirmed。**高权限门控由调用方执行**(与 memory_delete 同款)。
|
|
190
|
-
* 返回写入的 tags;id 不存在/状态非法返回 null。
|
|
191
|
-
*/
|
|
192
|
-
export declare function confirmReview(l1: RoomReviewStore, id: string, rooms: readonly string[]): {
|
|
193
|
-
tags: string[];
|
|
194
|
-
} | null;
|
|
195
|
-
/** 跳过:标 skipped(不再进 pending 队列)。已 confirmed 的记录不受 skip 影响(挂了 Room 的不算长尾)。 */
|
|
196
|
-
export declare function skipReview(l1: RoomReviewStore, id: string): boolean;
|
package/dist/room-review.js
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
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
|
-
// 合并词表:注册表条目排前(附 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);
|
|
104
|
-
const rows = overrides.chunkAnnotator !== undefined
|
|
105
|
-
? await overrides.chunkAnnotator(selected, merged)
|
|
106
|
-
: await roomCandidateChunk(ctx, cfg, io.logger, selected, merged);
|
|
107
|
-
let written = 0;
|
|
108
|
-
for (const { id, rooms: candidates } of rows) {
|
|
109
|
-
if (candidates.length === 0)
|
|
110
|
-
continue;
|
|
111
|
-
const fresh = io.l1.getByIds([id])[0];
|
|
112
|
-
if (!fresh)
|
|
113
|
-
continue;
|
|
114
|
-
const meta = { ...(fresh.metadata ?? {}) };
|
|
115
|
-
meta[ROOM_CANDIDATES_KEY] = candidates;
|
|
116
|
-
meta[ROOM_REVIEW_KEY] = 'pending';
|
|
117
|
-
if (io.l1.patchMetadata(id, meta))
|
|
118
|
-
written++;
|
|
119
|
-
}
|
|
120
|
-
io.logger.info(`[memory] 孤儿候选预标记:选中 ${selected.length},产候选 ${rows.length},写 pending ${written} 条(待 agent 复查)`);
|
|
121
|
-
return { selected: selected.length, candidates: rows.length, written };
|
|
122
|
-
}
|
|
123
|
-
/** 待复查队列:pending 且有候选的活跃孤儿,按更新时间升序(最旧的先复查)。 */
|
|
124
|
-
function pendingQueue(l1) {
|
|
125
|
-
const out = [];
|
|
126
|
-
const seen = new Set();
|
|
127
|
-
for (let offset = 0; offset < 2000; offset += 200) {
|
|
128
|
-
const page = l1.list({ untagged: true, retired: false, limit: 200, offset });
|
|
129
|
-
for (const r of page.items) {
|
|
130
|
-
if (seen.has(r.id))
|
|
131
|
-
continue;
|
|
132
|
-
seen.add(r.id);
|
|
133
|
-
const meta = (r.metadata ?? {});
|
|
134
|
-
if (meta[ROOM_REVIEW_KEY] === 'pending' && Array.isArray(meta[ROOM_CANDIDATES_KEY]))
|
|
135
|
-
out.push(r);
|
|
136
|
-
}
|
|
137
|
-
if (page.items.length < 200)
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
return out.sort((a, b) => a.updatedAt - b.updatedAt);
|
|
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
|
-
const hasMore = false; // 循环到收敛,单次调用内 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
|
-
* 归类语义:改名**不是退役**——merge 编排的 markMerged 会把旧条目置 retired,
|
|
207
|
-
* 这里在改名后恢复其先前状态(active 条目改名后仍 active,不掉出标注词表)。 */
|
|
208
|
-
export async function renameRoom(io, from, to, opts = {}) {
|
|
209
|
-
const priorStatus = io.registry.bySlug?.(from)?.status ?? 'active';
|
|
210
|
-
const r = await mergeRoom(io, from, to, opts);
|
|
211
|
-
if (!r.dryRun && r.applied > 0) {
|
|
212
|
-
await io.registry.renameSlug(from, to);
|
|
213
|
-
if (priorStatus === 'active')
|
|
214
|
-
await io.registry.setStatus?.(to, 'active');
|
|
215
|
-
}
|
|
216
|
-
return r;
|
|
217
|
-
}
|
|
218
|
-
/** 下一条待复查(供 agent 逐个过):记录摘要 + 候选 + 现有 Room 词表 + 剩余数。 */
|
|
219
|
-
export function nextReview(l1) {
|
|
220
|
-
const queue = pendingQueue(l1);
|
|
221
|
-
if (queue.length === 0)
|
|
222
|
-
return null;
|
|
223
|
-
const r = queue[0];
|
|
224
|
-
const meta = (r.metadata ?? {});
|
|
225
|
-
return {
|
|
226
|
-
id: r.id,
|
|
227
|
-
content: r.content.slice(0, 600),
|
|
228
|
-
type: r.type,
|
|
229
|
-
sceneName: r.scene_name,
|
|
230
|
-
candidates: meta[ROOM_CANDIDATES_KEY] ?? [],
|
|
231
|
-
roomCatalog: l1.listRooms().slice(0, 40),
|
|
232
|
-
remaining: queue.length,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
/** 待复查剩余数(面板/工具兜底信息)。 */
|
|
236
|
-
export function pendingReviewCount(l1) {
|
|
237
|
-
return pendingQueue(l1).length;
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* 确认复查结果:候选(或 agent 改写的)写入 metadata.tags(**进 Room**),
|
|
241
|
-
* 清候选、标 confirmed。**高权限门控由调用方执行**(与 memory_delete 同款)。
|
|
242
|
-
* 返回写入的 tags;id 不存在/状态非法返回 null。
|
|
243
|
-
*/
|
|
244
|
-
export function confirmReview(l1, id, rooms) {
|
|
245
|
-
const found = l1.getByIds([id])[0];
|
|
246
|
-
if (!found)
|
|
247
|
-
return null;
|
|
248
|
-
const meta = { ...(found.metadata ?? {}) };
|
|
249
|
-
// 已确认/已跳过的不允许重复确认(防止 agent 循环空转)
|
|
250
|
-
if (meta[ROOM_REVIEW_KEY] === 'confirmed')
|
|
251
|
-
return null;
|
|
252
|
-
const cleaned = [];
|
|
253
|
-
for (const r of rooms) {
|
|
254
|
-
if (typeof r !== 'string')
|
|
255
|
-
continue;
|
|
256
|
-
const slug = r.trim().toLowerCase();
|
|
257
|
-
if (!slug || !isTag(slug) || cleaned.includes(slug))
|
|
258
|
-
continue;
|
|
259
|
-
cleaned.push(slug);
|
|
260
|
-
}
|
|
261
|
-
if (cleaned.length === 0)
|
|
262
|
-
return null;
|
|
263
|
-
meta.tags = cleaned;
|
|
264
|
-
delete meta[ROOM_CANDIDATES_KEY];
|
|
265
|
-
meta[ROOM_REVIEW_KEY] = 'confirmed';
|
|
266
|
-
if (!l1.patchMetadata(id, meta))
|
|
267
|
-
return null;
|
|
268
|
-
l1.invalidateRooms();
|
|
269
|
-
return { tags: cleaned };
|
|
270
|
-
}
|
|
271
|
-
/** 跳过:标 skipped(不再进 pending 队列)。已 confirmed 的记录不受 skip 影响(挂了 Room 的不算长尾)。 */
|
|
272
|
-
export function skipReview(l1, id) {
|
|
273
|
-
const found = l1.getByIds([id])[0];
|
|
274
|
-
if (!found)
|
|
275
|
-
return false;
|
|
276
|
-
const meta = { ...(found.metadata ?? {}) };
|
|
277
|
-
if (meta[ROOM_REVIEW_KEY] === 'confirmed')
|
|
278
|
-
return false;
|
|
279
|
-
meta[ROOM_REVIEW_KEY] = 'skipped';
|
|
280
|
-
return l1.patchMetadata(id, meta);
|
|
281
|
-
}
|