dsh-prime-memory 0.18.2 → 0.18.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +14 -0
- package/CHANGELOG.ja.md +14 -0
- package/CHANGELOG.ko.md +14 -0
- package/CHANGELOG.md +42 -0
- package/README.en.md +4 -0
- package/README.ja.md +4 -0
- package/README.ko.md +4 -0
- package/README.md +4 -0
- package/cordis.patch.yml +6 -0
- package/dist/client.js +1333 -168
- package/dist/config.d.ts +37 -0
- package/dist/config.js +7 -0
- package/dist/contract.d.ts +155 -1
- package/dist/hooks/capture-recovery.d.ts +59 -0
- package/dist/hooks/capture-recovery.js +88 -0
- package/dist/hooks/capture.js +114 -1
- package/dist/hooks/recall-ack.d.ts +48 -0
- package/dist/hooks/recall-ack.js +79 -0
- package/dist/hooks/recall.d.ts +23 -1
- package/dist/hooks/recall.js +124 -8
- package/dist/index.d.ts +22 -0
- package/dist/index.js +18 -4
- package/dist/metadata-validators.d.ts +5 -0
- package/dist/metadata-validators.js +11 -1
- package/dist/pipeline/l1.d.ts +4 -0
- package/dist/pipeline/l1.js +9 -2
- package/dist/pipeline/reconcile.js +4 -0
- package/dist/pipeline/ruminate.d.ts +11 -1
- package/dist/pipeline/ruminate.js +18 -1
- package/dist/pipeline/runner.js +62 -0
- package/dist/prompts/boundary.d.ts +17 -0
- package/dist/prompts/boundary.js +20 -0
- package/dist/prompts/graph-projection.js +4 -1
- package/dist/prompts/l1-dedup.d.ts +3 -3
- package/dist/prompts/l1-dedup.js +9 -1
- package/dist/prompts/l1-extraction.d.ts +3 -3
- package/dist/prompts/l1-extraction.js +12 -0
- package/dist/prompts/persona.js +6 -0
- package/dist/prompts/scene.js +7 -1
- package/dist/room-review.d.ts +196 -0
- package/dist/room-review.js +281 -0
- package/dist/stats.d.ts +26 -2
- package/dist/stats.js +270 -13
- package/dist/store/l0.d.ts +4 -0
- package/dist/store/l0.js +8 -0
- package/dist/store/l1.d.ts +34 -0
- package/dist/store/l1.js +68 -1
- package/dist/store/recall-usage.d.ts +43 -0
- package/dist/store/recall-usage.js +149 -0
- package/dist/store/rooms-registry.d.ts +66 -0
- package/dist/store/rooms-registry.js +250 -0
- package/dist/store/sqlite.d.ts +16 -0
- package/dist/store/sqlite.js +57 -0
- package/dist/store/trace.d.ts +39 -0
- package/dist/store/trace.js +149 -0
- package/dist/tools/index.d.ts +4 -1
- package/dist/tools/index.js +249 -2
- package/dist/types.d.ts +4 -0
- package/dist/util/redact.d.ts +27 -0
- package/dist/util/redact.js +228 -0
- package/package.json +13 -12
- package/dist/hall-backfill.d.ts +0 -33
- package/dist/hall-backfill.js +0 -95
- package/dist/hall-migrate.d.ts +0 -32
- package/dist/hall-migrate.js +0 -88
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §D 注入确认——日志回执协议(memorax-absorb Wave 3 / task_20-22)。
|
|
3
|
+
*
|
|
4
|
+
* 问题:`dedupe.mark` 原在 pre-step 返回注入的瞬间执行,但宿主是否真的把该
|
|
5
|
+
* 注入消息收进会话历史,此时并未确认(pre-step 被其他插件覆盖/步骤取消时,
|
|
6
|
+
* 模型根本没看到)——误标记会把这些记忆压制整个会话。
|
|
7
|
+
*
|
|
8
|
+
* 协议(两段式):
|
|
9
|
+
* ① recall.ts 构造注入消息后只登记 pending(messageId → 记录 id 集),不标记;
|
|
10
|
+
* ② capture.ts 在 `session/event` 观察到该 `user/message` 真正入日志时确认,
|
|
11
|
+
* 此时才 `dedupe.mark`(与 §D 红线一致:确认匹配在 source.kind 过滤之前——
|
|
12
|
+
* 注入消息的 source.kind 是 `plugin:memory`,放过滤后永远到不了匹配)。
|
|
13
|
+
*
|
|
14
|
+
* 超时降级(task_22):pending 超 5 分钟未确认 → 照旧标记 + 一次性提示。
|
|
15
|
+
* 防确认链路自身故障导致 dedupe 永不生效、记忆每轮重注的回归;正常确认时延
|
|
16
|
+
* <2s(同进程事件循环)。未确认且未超时的 pending 过期即弃(不标记)——
|
|
17
|
+
* 模型没看到的东西不应被抑制,允许下轮重注。
|
|
18
|
+
*
|
|
19
|
+
* 进程内 volatile,容量 64 条 LRU(注入速率低,正常情况秒级确认,积压即异常)。
|
|
20
|
+
*/
|
|
21
|
+
const PENDING_CAP = 64;
|
|
22
|
+
export const PENDING_TIMEOUT_MS = 5 * 60_000;
|
|
23
|
+
const pending = new Map();
|
|
24
|
+
let marker;
|
|
25
|
+
/** recall.ts 注册实际标记动作(dedupe.mark 绑定);重复注册以最后一次为准。 */
|
|
26
|
+
export function setInjectionMarker(fn) {
|
|
27
|
+
marker = fn;
|
|
28
|
+
}
|
|
29
|
+
/** ① 登记待确认注入(超容量淘汰最旧——积压到 64 条说明确认链路已异常)。 */
|
|
30
|
+
export function registerPendingInjection(agentId, messageId, recordIds, now = Date.now()) {
|
|
31
|
+
if (!messageId || recordIds.length === 0)
|
|
32
|
+
return;
|
|
33
|
+
pending.delete(messageId); // 同 id 重登记 = 刷新(置尾)
|
|
34
|
+
pending.set(messageId, { agentId, messageId, recordIds, at: now });
|
|
35
|
+
while (pending.size > PENDING_CAP) {
|
|
36
|
+
const oldest = pending.keys().next().value;
|
|
37
|
+
if (oldest === undefined)
|
|
38
|
+
break;
|
|
39
|
+
pending.delete(oldest);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* ② 日志回执确认:该注入消息已真实进入会话历史。执行标记并移除 pending。
|
|
44
|
+
* @returns 是否命中一条 pending(未命中 = 与本插件注入无关的事件,静默)。
|
|
45
|
+
*/
|
|
46
|
+
export function confirmInjectionByMessageId(messageId) {
|
|
47
|
+
const entry = pending.get(messageId);
|
|
48
|
+
if (!entry)
|
|
49
|
+
return false;
|
|
50
|
+
pending.delete(messageId);
|
|
51
|
+
marker?.(entry.agentId, entry.recordIds);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 超时清扫:超时的 pending 照旧标记(降级保连续,防 dedupe 失效回归),并移除。
|
|
56
|
+
* 未超时的不动。@returns 本次降级标记的条数(recall 侧据此前瞻性提示一次)。
|
|
57
|
+
*/
|
|
58
|
+
export function expirePendingInjections(now = Date.now(), timeoutMs = PENDING_TIMEOUT_MS) {
|
|
59
|
+
if (pending.size === 0)
|
|
60
|
+
return 0;
|
|
61
|
+
let expired = 0;
|
|
62
|
+
for (const [key, entry] of pending) {
|
|
63
|
+
if (now - entry.at <= timeoutMs)
|
|
64
|
+
continue;
|
|
65
|
+
pending.delete(key);
|
|
66
|
+
marker?.(entry.agentId, entry.recordIds);
|
|
67
|
+
expired += 1;
|
|
68
|
+
}
|
|
69
|
+
return expired;
|
|
70
|
+
}
|
|
71
|
+
/** 诊断/测试:当前 pending 条数。 */
|
|
72
|
+
export function pendingCount() {
|
|
73
|
+
return pending.size;
|
|
74
|
+
}
|
|
75
|
+
/** 测试隔离:清空 pending 与 marker(模块级状态,用例间互扰)。 */
|
|
76
|
+
export function resetAckForTests() {
|
|
77
|
+
pending.clear();
|
|
78
|
+
marker = undefined;
|
|
79
|
+
}
|
package/dist/hooks/recall.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { L1Store } from '../store/l1.js';
|
|
|
22
22
|
import type { PersonaStore } from '../store/persona.js';
|
|
23
23
|
import type { SceneStore } from '../store/scenes.js';
|
|
24
24
|
import type { SessionModeStore } from '../store/session-modes.js';
|
|
25
|
+
import type { RecallUsageStore } from '../store/recall-usage.js';
|
|
25
26
|
import type { MemoryLogger } from '../types.js';
|
|
26
27
|
import { type OccupancyLedger } from '../util/context-occupancy.js';
|
|
27
28
|
/**
|
|
@@ -41,6 +42,25 @@ export declare function isOwnRecallSource(src: {
|
|
|
41
42
|
export declare function buildRecallQuery(messages: Array<{
|
|
42
43
|
content: unknown;
|
|
43
44
|
}>, tailMessages?: number, maxChars?: number): string;
|
|
45
|
+
/**
|
|
46
|
+
* §E 压缩感知标记器(memorax-absorb / task_24-26):记录刚完成压缩的会话,
|
|
47
|
+
* pre-step 消费后做一次性增强召回。纯内存状态,CaptureBuffers 同款可单测导出。
|
|
48
|
+
*
|
|
49
|
+
* - `onCompactionEnd`:只认无 error 的 compaction/end(失败的压缩没有把内容逐出上下文);
|
|
50
|
+
* - `consume`:消费即清(一次性增强);容量上限淘汰最旧(积压即异常,回退路径兜底)。
|
|
51
|
+
*/
|
|
52
|
+
export declare class PostCompactionTracker {
|
|
53
|
+
private readonly cap;
|
|
54
|
+
private readonly map;
|
|
55
|
+
constructor(cap?: number);
|
|
56
|
+
/** @returns 是否真的置了标记(false = 非 compaction/end 或带 error)。 */
|
|
57
|
+
onCompactionEnd(sessionId: string, data?: {
|
|
58
|
+
error?: unknown;
|
|
59
|
+
}): boolean;
|
|
60
|
+
/** 消费该会话的增强标记(存在即清,返回是否存在)。 */
|
|
61
|
+
consume(sessionId: string): boolean;
|
|
62
|
+
get size(): number;
|
|
63
|
+
}
|
|
44
64
|
/** 单会话召回统计(悬浮卡信息区数据源;每轮 O(1) 记账,agent/disposed 清理)。
|
|
45
65
|
* 口径声明:这是"注入统计"而非 bench 的离线 recall@k——运行时没有 ground truth,
|
|
46
66
|
* 命中率 = hitTurns / injectedTurns。去重语义:全量压制轮计入 hitTurns
|
|
@@ -70,4 +90,6 @@ export declare function registerRecall(ctx: Context, cfg: MemoryConfig, stores:
|
|
|
70
90
|
l1: L1Store;
|
|
71
91
|
scenes: Record<'chat' | 'work', SceneStore>;
|
|
72
92
|
persona: Record<'chat' | 'work', PersonaStore>;
|
|
73
|
-
}, logger: MemoryLogger, live: LiveSettingsHandle, modes: SessionModeStore, dataDir: string
|
|
93
|
+
}, logger: MemoryLogger, live: LiveSettingsHandle, modes: SessionModeStore, dataDir: string,
|
|
94
|
+
/** 召回使用统计(可选;未装配 = 零成本旁路):进 topN(注入)才算 used。 */
|
|
95
|
+
usage?: RecallUsageStore): RecallHooks;
|
package/dist/hooks/recall.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { trace } from '../store/trace.js';
|
|
2
4
|
import { RecallDedupeStore } from '../store/recall-dedupe.js';
|
|
3
5
|
import { OccupancyStore } from '../store/occupancy.js';
|
|
6
|
+
import { expirePendingInjections, registerPendingInjection, setInjectionMarker, } from './recall-ack.js';
|
|
4
7
|
import { scopeFilterOf } from '../workspace.js';
|
|
5
8
|
import { applyRecallBudget, raceRecallTimeout, RECALL_EMBED_CAP_MS } from '../util/recall-budget.js';
|
|
6
9
|
import { clearProfileShare, emptyOccupancyLedger, estimateInjectedMessageTokens, estimateStableSectionTokens, recordProfileShare, recordRecallInjection, resetForCompaction, } from '../util/context-occupancy.js';
|
|
@@ -42,6 +45,43 @@ export function buildRecallQuery(messages, tailMessages = RECALL_QUERY_TAIL_MESS
|
|
|
42
45
|
text = text.slice(-maxChars);
|
|
43
46
|
return text;
|
|
44
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* §E 压缩感知标记器(memorax-absorb / task_24-26):记录刚完成压缩的会话,
|
|
50
|
+
* pre-step 消费后做一次性增强召回。纯内存状态,CaptureBuffers 同款可单测导出。
|
|
51
|
+
*
|
|
52
|
+
* - `onCompactionEnd`:只认无 error 的 compaction/end(失败的压缩没有把内容逐出上下文);
|
|
53
|
+
* - `consume`:消费即清(一次性增强);容量上限淘汰最旧(积压即异常,回退路径兜底)。
|
|
54
|
+
*/
|
|
55
|
+
export class PostCompactionTracker {
|
|
56
|
+
cap;
|
|
57
|
+
map = new Map();
|
|
58
|
+
constructor(cap = 64) {
|
|
59
|
+
this.cap = cap;
|
|
60
|
+
}
|
|
61
|
+
/** @returns 是否真的置了标记(false = 非 compaction/end 或带 error)。 */
|
|
62
|
+
onCompactionEnd(sessionId, data) {
|
|
63
|
+
if (!sessionId || data?.error)
|
|
64
|
+
return false;
|
|
65
|
+
this.map.delete(sessionId); // 置尾刷新
|
|
66
|
+
this.map.set(sessionId, true);
|
|
67
|
+
while (this.map.size > this.cap) {
|
|
68
|
+
const oldest = this.map.keys().next().value;
|
|
69
|
+
if (oldest === undefined)
|
|
70
|
+
break;
|
|
71
|
+
this.map.delete(oldest);
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
/** 消费该会话的增强标记(存在即清,返回是否存在)。 */
|
|
76
|
+
consume(sessionId) {
|
|
77
|
+
if (!this.map.delete(sessionId))
|
|
78
|
+
return false;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
get size() {
|
|
82
|
+
return this.map.size;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
45
85
|
const MEMORY_TOOLS_GUIDE = `<memory-tools-guide>
|
|
46
86
|
## 记忆工具调用指南
|
|
47
87
|
|
|
@@ -72,7 +112,9 @@ export function emptyRecallStats(now = Date.now()) {
|
|
|
72
112
|
updatedAt: now,
|
|
73
113
|
};
|
|
74
114
|
}
|
|
75
|
-
export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir
|
|
115
|
+
export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir,
|
|
116
|
+
/** 召回使用统计(可选;未装配 = 零成本旁路):进 topN(注入)才算 used。 */
|
|
117
|
+
usage) {
|
|
76
118
|
/** 召回去重存储(同会话已注入的记忆不再重复注入;写穿持久化,重启不丢)。 */
|
|
77
119
|
const dedupe = new RecallDedupeStore(dataDir, logger);
|
|
78
120
|
/** 记忆占用流水(账本迁移写穿;重启后历史会话账目由此复生)。 */
|
|
@@ -160,10 +202,26 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
160
202
|
recallStats.delete(payload.agent.id);
|
|
161
203
|
occupancyByAgent.delete(payload.agent.id);
|
|
162
204
|
});
|
|
205
|
+
// §D:绑定实际标记动作 + 超时清扫挂到 pre-step 入口(见注入点注释)。
|
|
206
|
+
setInjectionMarker((agentId, recordIds) => dedupe.mark(agentId, recordIds));
|
|
207
|
+
let ackDegradeWarned = false;
|
|
208
|
+
// §E 压缩感知(memorax-absorb):宿主 `compaction/end`(无 error)→ 该会话下一轮
|
|
209
|
+
// 做一次"定向增强召回"——跳过去重压制、画像立即重注入、占用账本归零;消费即清。
|
|
210
|
+
// 会话级回退路径保留(下方 session-start source='compact' 全量重置),两者幂等。
|
|
211
|
+
// 宿主无压缩计数器(compactionId 为 per-transaction UUID),故不做 generation 对账。
|
|
212
|
+
const postCompaction = new PostCompactionTracker();
|
|
213
|
+
ctx.on('session/event', (session, event) => {
|
|
214
|
+
if (event.type !== 'compaction/end')
|
|
215
|
+
return;
|
|
216
|
+
if (postCompaction.onCompactionEnd(String(session.id ?? session))) {
|
|
217
|
+
logger.info(`[memory] compaction/end:下一轮将做一次增强召回(session=${String(session.id ?? session)})`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
163
220
|
// 上下文压缩/清空 → 已注入内容从模型上下文丢失,重置该会话的去重压制
|
|
164
221
|
// (resume/startup 不重置:历史仍在,已注入的记忆模型还持有)。
|
|
165
222
|
// 占用账本同步全量归零(宁低勿高;轮级粒度近似)。
|
|
166
|
-
|
|
223
|
+
// 0.1.7-rc.2 起并入 agent/created(serial:监听器需 async)。
|
|
224
|
+
ctx.on('agent/created', async (payload) => {
|
|
167
225
|
if (payload.source === 'compact' || payload.source === 'clear') {
|
|
168
226
|
dedupe.reset(payload.agent.id);
|
|
169
227
|
const led = ledgerFor(payload.agent.id);
|
|
@@ -205,6 +263,13 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
205
263
|
// prepend 注册 + 先 next() 再改写:不劫持其他监听器(dsh-time-context 官方范式)。
|
|
206
264
|
if (cfg.recall.enabled) {
|
|
207
265
|
ctx.on('agent/pre-step', async (payload, next) => {
|
|
266
|
+
// §D 超时清扫:超时未确认的注入照旧标记(降级保连续,防 dedupe 失效回归);
|
|
267
|
+
// 正常确认在秒级完成,这里只是兜底。一次性提示。
|
|
268
|
+
const expiredAcks = expirePendingInjections();
|
|
269
|
+
if (expiredAcks > 0 && !ackDegradeWarned) {
|
|
270
|
+
ackDegradeWarned = true;
|
|
271
|
+
logger.warn(`[memory] 注入确认:${expiredAcks} 条 pending 超时未回执,已降级为照常标记(一次性提示)`);
|
|
272
|
+
}
|
|
208
273
|
const decision = await next();
|
|
209
274
|
if (decision.kind === 'reject' || payload.signal.aborted)
|
|
210
275
|
return decision;
|
|
@@ -220,6 +285,17 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
220
285
|
const hasNewUserMessage = decision.messages.some((m) => m.source?.kind === 'user');
|
|
221
286
|
if (!hasNewUserMessage)
|
|
222
287
|
return decision;
|
|
288
|
+
// §E 消费增强标记:压缩后下一轮跳过去重压制(预算/档位/门控照常),
|
|
289
|
+
// 画像 TTL 失效立即重注入,占用账本归零。消费即清(一次性)。
|
|
290
|
+
const sessionKey = String(payload.agent.session?.id ?? payload.agent.id);
|
|
291
|
+
const boosted = postCompaction.consume(sessionKey);
|
|
292
|
+
if (boosted) {
|
|
293
|
+
invalidateProfile();
|
|
294
|
+
const led = ledgerFor(payload.agent.id);
|
|
295
|
+
resetForCompaction(led);
|
|
296
|
+
occupancyStore.save(payload.agent.id, led);
|
|
297
|
+
logger.info(`[memory] 压缩后增强召回:本轮跳过去重压制 + 画像重注入(session=${sessionKey})`);
|
|
298
|
+
}
|
|
223
299
|
const query = buildRecallQuery(payload.messages);
|
|
224
300
|
// 空查询是退化轮(无用户文本),重置命中信号但不计入统计
|
|
225
301
|
if (!query) {
|
|
@@ -227,6 +303,10 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
227
303
|
degenerate.lastHits = 0;
|
|
228
304
|
return decision;
|
|
229
305
|
}
|
|
306
|
+
// §F 追踪字段:metadata-only 默认(chars+sha256 前 16),captureContent 才存原文
|
|
307
|
+
const queryChars = query.length;
|
|
308
|
+
const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
|
|
309
|
+
const queryText = cfg.trace?.captureContent === true ? query.slice(0, 200) : undefined;
|
|
230
310
|
const st = statFor(payload.agent.id);
|
|
231
311
|
st.injectedTurns++;
|
|
232
312
|
st.lastHits = 0;
|
|
@@ -245,6 +325,11 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
245
325
|
if (hits === undefined) {
|
|
246
326
|
st.timeouts++;
|
|
247
327
|
logger.warn('[memory] 召回超时,跳过本轮注入(不阻塞对话)');
|
|
328
|
+
trace({
|
|
329
|
+
kind: 'recall_turn', ts: Date.now(), sessionId: sessionKey, queryChars, querySha, queryText,
|
|
330
|
+
hitIds: [], hitScores: [], injectedIds: [], suppressedCount: 0,
|
|
331
|
+
durationMs: Date.now() - searchStart, outcome: 'timeout',
|
|
332
|
+
});
|
|
248
333
|
return decision;
|
|
249
334
|
}
|
|
250
335
|
st.lastDurationMs = Date.now() - searchStart;
|
|
@@ -275,7 +360,8 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
275
360
|
}
|
|
276
361
|
// 召回去重:同会话已注入过的记录不再重复注入(模型上下文已持有,省 token)。
|
|
277
362
|
// 纯过滤——剩几条注几条,全量压制(0 条新鲜命中)是正确状态而非未命中。
|
|
278
|
-
|
|
363
|
+
// §E:压缩后增强轮跳过压制(压缩把已注入内容逐出了模型上下文)。
|
|
364
|
+
const seen = boosted ? new Set() : dedupe.seen(payload.agent.id);
|
|
279
365
|
const fresh = scoped.filter((h) => !seen.has(h.id));
|
|
280
366
|
const suppressed = scoped.length - fresh.length;
|
|
281
367
|
st.suppressedRecalls += suppressed;
|
|
@@ -287,13 +373,29 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
287
373
|
st.hitTurns++;
|
|
288
374
|
st.totalHits += fresh.length;
|
|
289
375
|
}
|
|
290
|
-
if (fresh.length === 0)
|
|
376
|
+
if (fresh.length === 0) {
|
|
377
|
+
// §F 追踪:全量压制轮——零注入是正确状态,如实记录
|
|
378
|
+
trace({
|
|
379
|
+
kind: 'recall_turn', ts: Date.now(), sessionId: sessionKey, queryChars, querySha, queryText,
|
|
380
|
+
hitIds: scoped.map((h) => h.id), hitScores: scoped.map((h) => Number(h.score.toFixed(4))),
|
|
381
|
+
injectedIds: [], suppressedCount: scoped.length - fresh.length,
|
|
382
|
+
durationMs: Date.now() - searchStart, outcome: 'suppressed',
|
|
383
|
+
});
|
|
384
|
+
usage?.recordAttempt(scoped.map((h) => h.id), []);
|
|
291
385
|
return decision;
|
|
386
|
+
}
|
|
292
387
|
const lines = applyRecallBudget(fresh.map((h) => `- [${h.scene_name ? `${h.type}|${h.scene_name}` : h.type}] ${h.content}`), { maxCharsPerMemory: cfg.recall.maxCharsPerMemory, maxTotalRecallChars: cfg.recall.maxTotalRecallChars });
|
|
293
|
-
if (lines.length === 0)
|
|
388
|
+
if (lines.length === 0) {
|
|
389
|
+
// §F 追踪:预算清零轮(截断后无完整行)
|
|
390
|
+
trace({
|
|
391
|
+
kind: 'recall_turn', ts: Date.now(), sessionId: sessionKey, queryChars, querySha, queryText,
|
|
392
|
+
hitIds: fresh.map((h) => h.id), hitScores: fresh.map((h) => Number(h.score.toFixed(4))),
|
|
393
|
+
injectedIds: [], suppressedCount: scoped.length - fresh.length,
|
|
394
|
+
durationMs: Date.now() - searchStart, outcome: 'suppressed',
|
|
395
|
+
});
|
|
396
|
+
usage?.recordAttempt(fresh.map((h) => h.id), []);
|
|
294
397
|
return decision;
|
|
295
|
-
|
|
296
|
-
dedupe.mark(payload.agent.id, fresh.slice(0, lines.length).map((h) => h.id));
|
|
398
|
+
}
|
|
297
399
|
st.lastHits = lines.length;
|
|
298
400
|
const text = [
|
|
299
401
|
'<relevant-memories>',
|
|
@@ -312,6 +414,20 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
312
414
|
// v3 旧行(kind:'plugin'+plugin 字段)由宿主 v3→v4 迁移升格,读侧双形状兼容。
|
|
313
415
|
source: { kind: 'plugin:memory', form: 'recall' },
|
|
314
416
|
});
|
|
417
|
+
// §D 注入确认(memorax-absorb):宿主接受 enter 后,注入消息以原样
|
|
418
|
+
// `user/message` 进入会话日志(message.id 稳定保留)——capture 侧观察到
|
|
419
|
+
// 该 id 才真正 dedupe.mark(recall-ack 两段式)。这里只登记 pending,
|
|
420
|
+
// 未确认不标记:被覆盖/取消的注入不该压制这些记忆(下轮可重注)。
|
|
421
|
+
registerPendingInjection(payload.agent.id, injection.id, fresh.slice(0, lines.length).map((h) => h.id));
|
|
422
|
+
// §F 追踪:注入轮
|
|
423
|
+
trace({
|
|
424
|
+
kind: 'recall_turn', ts: Date.now(), sessionId: sessionKey, queryChars, querySha, queryText,
|
|
425
|
+
hitIds: scoped.map((h) => h.id), hitScores: scoped.map((h) => Number(h.score.toFixed(4))),
|
|
426
|
+
injectedIds: fresh.slice(0, lines.length).map((h) => h.id), suppressedCount: scoped.length - fresh.length,
|
|
427
|
+
durationMs: Date.now() - searchStart, outcome: 'injected',
|
|
428
|
+
});
|
|
429
|
+
// 使用统计:候选=scoped(尝试),进 topN(注入)=used
|
|
430
|
+
usage?.recordAttempt(scoped.map((h) => h.id), fresh.slice(0, lines.length).map((h) => h.id));
|
|
315
431
|
// 入账在成功构造注入消息之后、返回 enter 之前——任何前置抛错路径账目零扰动
|
|
316
432
|
const led = ledgerFor(payload.agent.id);
|
|
317
433
|
recordRecallInjection(led, text.length);
|
|
@@ -459,7 +575,7 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
459
575
|
for (const agent of agents.list())
|
|
460
576
|
registerForAgent(agent);
|
|
461
577
|
}
|
|
462
|
-
ctx.on('agent/created', (payload) => {
|
|
578
|
+
ctx.on('agent/created', async (payload) => {
|
|
463
579
|
registerForAgent(payload.agent);
|
|
464
580
|
});
|
|
465
581
|
ctx.effect(() => () => {
|
package/dist/index.d.ts
CHANGED
|
@@ -23,10 +23,21 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
23
23
|
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
24
24
|
stripCodeBlocks: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
25
25
|
maxMessageChars: import("@deepseek-ai/schemastery").default<number, number, "defined">;
|
|
26
|
+
redactSecrets: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
26
27
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
27
28
|
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
28
29
|
stripCodeBlocks: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
29
30
|
maxMessageChars: import("@deepseek-ai/schemastery").default<number, number, "defined">;
|
|
31
|
+
redactSecrets: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
32
|
+
}>>, "plain">;
|
|
33
|
+
trace: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
34
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
35
|
+
retentionDays: import("@deepseek-ai/schemastery").default<number, number, "defined">;
|
|
36
|
+
captureContent: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
37
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
38
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
39
|
+
retentionDays: import("@deepseek-ai/schemastery").default<number, number, "defined">;
|
|
40
|
+
captureContent: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
30
41
|
}>>, "plain">;
|
|
31
42
|
extract: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
32
43
|
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
@@ -502,10 +513,21 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
502
513
|
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
503
514
|
stripCodeBlocks: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
504
515
|
maxMessageChars: import("@deepseek-ai/schemastery").default<number, number, "defined">;
|
|
516
|
+
redactSecrets: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
505
517
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
506
518
|
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
507
519
|
stripCodeBlocks: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
508
520
|
maxMessageChars: import("@deepseek-ai/schemastery").default<number, number, "defined">;
|
|
521
|
+
redactSecrets: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
522
|
+
}>>, "plain">;
|
|
523
|
+
trace: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
524
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
525
|
+
retentionDays: import("@deepseek-ai/schemastery").default<number, number, "defined">;
|
|
526
|
+
captureContent: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
527
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
528
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
529
|
+
retentionDays: import("@deepseek-ai/schemastery").default<number, number, "defined">;
|
|
530
|
+
captureContent: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
|
509
531
|
}>>, "plain">;
|
|
510
532
|
extract: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
|
|
511
533
|
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
|
package/dist/index.js
CHANGED
|
@@ -36,12 +36,15 @@ import { SlotStore } from './store/slots.js';
|
|
|
36
36
|
import { registerMemoryTools } from './tools/index.js';
|
|
37
37
|
import { registerSlotTools } from './tools/slots.js';
|
|
38
38
|
import { registerSlotRecall } from './hooks/slot-recall.js';
|
|
39
|
+
import { RoomRegistryStore } from './store/rooms-registry.js';
|
|
40
|
+
import { RecallUsageStore } from './store/recall-usage.js';
|
|
39
41
|
import { registerSlotsProjection } from './projection/slots.js';
|
|
40
42
|
import { errDetail, withFileLog } from './util/filelog.js';
|
|
41
43
|
import { buildRouteChain, resolveModelRoute, invalidateEffortCache } from './llm.js';
|
|
42
44
|
import { effectiveCfg } from './pipeline/runner.js';
|
|
43
45
|
import { pendingPathFor } from './store/pending.js';
|
|
44
46
|
import { initTokenCost, resetTokenCost } from './token-cost.js';
|
|
47
|
+
import { initTraceStore } from './store/trace.js';
|
|
45
48
|
export const name = 'dsh-memory-plugin';
|
|
46
49
|
/**
|
|
47
50
|
* 硬依赖:蒸馏要用 llm,工具注册要用 tools,召回注入要用 systemPrompt。
|
|
@@ -137,6 +140,9 @@ export async function apply(ctx, config) {
|
|
|
137
140
|
const embed = initial.svc;
|
|
138
141
|
const db = new MemoryDb(path.join(dataDir, 'memory.db'), initial.dims, logger);
|
|
139
142
|
initTokenCost(db, config.tokenCost.retentionDays);
|
|
143
|
+
// §F 追踪(trace.enabled=false 时保持未初始化,埋点零副作用)
|
|
144
|
+
if (config.trace?.enabled)
|
|
145
|
+
initTraceStore(dataDir, { retentionDays: config.trace.retentionDays });
|
|
140
146
|
// 插件卸载时关闭连接(WAL 落盘),注册一次即可
|
|
141
147
|
ctx.effect(() => () => db.close());
|
|
142
148
|
let dbInit = { needsReindex: false };
|
|
@@ -154,6 +160,7 @@ export async function apply(ctx, config) {
|
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
const stores = {
|
|
163
|
+
usage: undefined, // 装配于下方(依赖 dataDir 就绪)
|
|
157
164
|
l0: new L0Store(dataDir, db, embed, logger),
|
|
158
165
|
l1: new L1Store(dataDir, db, embed, config.recall.strategy, logger, config.recall.decayHalfLifeDays,
|
|
159
166
|
// §D 第 3 路:图谱回链。图谱不可用时 searchNodes 自带 no-op,不影响双路。
|
|
@@ -346,16 +353,23 @@ export async function apply(ctx, config) {
|
|
|
346
353
|
backend = await createMemoryBackend({ dbPath: path.join(dataDir, 'memory.db'), dimensions: initial.dims, logger }, backend);
|
|
347
354
|
}
|
|
348
355
|
stores.backend = backend;
|
|
356
|
+
// Room 注册表(分类管理 beta.4;损坏降级为纯自生长目录)
|
|
357
|
+
const roomRegistry = new RoomRegistryStore(dataDir, logger);
|
|
358
|
+
await roomRegistry.init();
|
|
349
359
|
const ruminate = storageOk && !db.isDegraded() && ruminateFile
|
|
350
|
-
? new RuminateController(ctx, config, runner, stores, logger, live, ruminateFile)
|
|
360
|
+
? new RuminateController(ctx, config, runner, stores, logger, live, ruminateFile, roomRegistry)
|
|
351
361
|
: undefined;
|
|
352
362
|
let flushL0;
|
|
353
363
|
if (storageOk) {
|
|
354
364
|
flushL0 = registerCapture(ctx, config, runner, stores.l0, logger, live, modes);
|
|
355
365
|
}
|
|
356
|
-
|
|
366
|
+
// 召回使用统计(进 topN 才算 used;热路径只碰内存,去抖落盘)
|
|
367
|
+
const recallUsage = new RecallUsageStore(dataDir, logger);
|
|
368
|
+
await recallUsage.init();
|
|
369
|
+
stores.usage = recallUsage;
|
|
370
|
+
const recall = registerRecall(ctx, config, stores, logger, live, modes, dataDir, recallUsage);
|
|
357
371
|
runner.setAfterRun(recall.invalidateProfile);
|
|
358
|
-
registerMemoryTools(ctx, config, stores, logger, modes, live, ruminate);
|
|
372
|
+
registerMemoryTools(ctx, config, stores, logger, modes, live, ruminate, storageOk ? roomRegistry : undefined);
|
|
359
373
|
// 激活槽位(active slot):工具面 + 常驻注入 + 服务端投影(均走 ctx.effect,可撤销)
|
|
360
374
|
registerSlotTools(ctx, config, stores.slots, logger, modes, live, stores.l1);
|
|
361
375
|
const slotRecall = registerSlotRecall(ctx, config, stores.slots, logger, live);
|
|
@@ -374,7 +388,7 @@ export async function apply(ctx, config) {
|
|
|
374
388
|
runnerView: (sid, mode) => runner.sessionView(sid, mode),
|
|
375
389
|
l0Count: (sid) => stores.l0.countBySession(sid),
|
|
376
390
|
capabilities: () => db.getCapabilities(),
|
|
377
|
-
}, ruminate);
|
|
391
|
+
}, ruminate, storageOk ? roomRegistry : undefined);
|
|
378
392
|
// bench 控制服务(config.benchControl 门控,默认关):仅基准/调试部署注册,
|
|
379
393
|
// 供同进程的 bench-runner lifecycle 赛道触发 rebuild / 设置会话档位
|
|
380
394
|
// (宿主侧 RPC 无 call(),见 bench-control.ts)
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* 已是 string 的变量收窄成 `never`(该分支内无法再 `.slice`,已在 wing-backfill 踩过)。
|
|
11
11
|
*/
|
|
12
12
|
export declare function isWingId(v: unknown): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* 两级 Room slug(`major` 或 `major/minor`,各段满足 isTag;至多一个 slash)。
|
|
15
|
+
* Room 注册表/归类写回的 tags 域;与 rooms-registry.ts 同一事实源(就近定义避免依赖环)。
|
|
16
|
+
*/
|
|
17
|
+
export declare function isRoomSlug(v: unknown): boolean;
|
|
13
18
|
/** 同为布尔谓词(理由见 isWingId 注释)。 */
|
|
14
19
|
export declare function isTag(v: unknown): boolean;
|
|
15
20
|
/**
|
|
@@ -31,6 +31,16 @@ export function isWingId(v) {
|
|
|
31
31
|
}
|
|
32
32
|
/** 标签(Room 的构成单元)校验:小写字母数字连字符,1-32 字符。 */
|
|
33
33
|
const TAG_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
34
|
+
/**
|
|
35
|
+
* 两级 Room slug(`major` 或 `major/minor`,各段满足 isTag;至多一个 slash)。
|
|
36
|
+
* Room 注册表/归类写回的 tags 域;与 rooms-registry.ts 同一事实源(就近定义避免依赖环)。
|
|
37
|
+
*/
|
|
38
|
+
export function isRoomSlug(v) {
|
|
39
|
+
if (typeof v !== 'string')
|
|
40
|
+
return false;
|
|
41
|
+
const parts = v.split('/');
|
|
42
|
+
return parts.length <= 2 && parts.every((p) => TAG_RE.test(p));
|
|
43
|
+
}
|
|
34
44
|
/** 同为布尔谓词(理由见 isWingId 注释)。 */
|
|
35
45
|
export function isTag(v) {
|
|
36
46
|
return typeof v === 'string' && TAG_RE.test(v);
|
|
@@ -47,7 +57,7 @@ export function normTags(v, max = 3) {
|
|
|
47
57
|
if (typeof raw !== 'string')
|
|
48
58
|
continue;
|
|
49
59
|
const t = raw.trim().toLowerCase();
|
|
50
|
-
if (!isTag(t) || out.includes(t))
|
|
60
|
+
if (!(isTag(t) || isRoomSlug(t)) || out.includes(t))
|
|
51
61
|
continue;
|
|
52
62
|
out.push(t);
|
|
53
63
|
if (out.length >= max)
|
package/dist/pipeline/l1.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ export interface ExtractionResult {
|
|
|
8
8
|
skipped: boolean;
|
|
9
9
|
sceneName: string;
|
|
10
10
|
newRecords: MemoryRecord[];
|
|
11
|
+
/** §F 追踪捎带:与本轮 l1_receipts 同源的 runId(可交叉审计);早退分支缺省。 */
|
|
12
|
+
runId?: string;
|
|
13
|
+
/** §F 追踪捎带:六值决策词表聚合;早退分支缺省。 */
|
|
14
|
+
byKind?: Record<string, number>;
|
|
11
15
|
}
|
|
12
16
|
/** 分族 checkpoint 桶(活引用,改动由调用方 save 落盘)。 */
|
|
13
17
|
export type FamilyStates = Record<MemoryFamily, MemoryState>;
|
package/dist/pipeline/l1.js
CHANGED
|
@@ -262,13 +262,20 @@ anchorMap) {
|
|
|
262
262
|
// runId 提到块外:§C 冻结对要沿用同一批次 id,使「这一轮判了什么」与
|
|
263
263
|
// 「这一轮冻结了哪一对」在记忆库侧可交叉审计(findings.md §3 / §9)。
|
|
264
264
|
const runId = newRunId();
|
|
265
|
+
let byKind;
|
|
265
266
|
{
|
|
266
267
|
const items = extracted.map((m, i) => ({
|
|
267
268
|
recordId: m.record_id,
|
|
268
269
|
candidateIds: (matches[i]?.candidates ?? []).map((c) => c.id),
|
|
269
270
|
action: byRecord.get(m.record_id)?.action,
|
|
270
271
|
}));
|
|
271
|
-
|
|
272
|
+
const rows = buildReceipts(runId, new Date().toISOString(), items);
|
|
273
|
+
persistReceiptsSafely((rows) => store.recordReceipts(rows), rows, logger);
|
|
274
|
+
// §F 追踪捎带:六值决策词表聚合(与凭证同源,runner 侧并入 distill_run 事件)
|
|
275
|
+
byKind = rows.reduce((acc, r) => {
|
|
276
|
+
acc[r.kind] = (acc[r.kind] ?? 0) + 1;
|
|
277
|
+
return acc;
|
|
278
|
+
}, {});
|
|
272
279
|
}
|
|
273
280
|
// ── Step 3: 应用决策(官方语义:新记录追加进事实源,被替换目标只从检索库删除) ──
|
|
274
281
|
// 只按需取决策涉及的记录(候选 + 目标 id 并集),避免每轮全表扫描
|
|
@@ -521,7 +528,7 @@ anchorMap) {
|
|
|
521
528
|
}
|
|
522
529
|
markExtracted(states, mode, lastScene);
|
|
523
530
|
logger.info(`[memory] L1 抽取完成(mode=${mode}):消息 ${pending.length} 条,抽取 ${extracted.length} 条,去重后新增 ${added.length} 条(取代退场 ${supersededBy.size} 条,chat=${addedByFamily.chat}/work=${addedByFamily.work}),累计 chat=${states.chat.totalExtracted}/work=${states.work.totalExtracted}`);
|
|
524
|
-
return { stored: added.length, skipped: false, sceneName: lastScene, newRecords: added };
|
|
531
|
+
return { stored: added.length, skipped: false, sceneName: lastScene, newRecords: added, runId, byKind };
|
|
525
532
|
}
|
|
526
533
|
/** auto 档取最近活跃的族 checkpoint(情境链/计数锚点)。 */
|
|
527
534
|
function activeState(states) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { callLLM, parseJson } from '../llm.js';
|
|
2
|
+
import { CONTENT_IS_DATA_CLAUSE, DATA_DELIMITER_NOTE } from '../prompts/boundary.js';
|
|
2
3
|
/** 三态判定。顺序即优先级(报告与面板按此排序)。 */
|
|
3
4
|
export const RECONCILE_STATES = ['contradicted', 'supported', 'unverifiable'];
|
|
4
5
|
const DEFAULT_MAX_RECORDS = 50;
|
|
@@ -46,6 +47,8 @@ export function buildReconcilePrompt(memory, events, maxEvidenceChars) {
|
|
|
46
47
|
const system = [
|
|
47
48
|
'你是一个记忆核对员。给你一条「记忆」和它在会话里的「原文证据」,判断这条记忆是否与原文相符。',
|
|
48
49
|
'',
|
|
50
|
+
CONTENT_IS_DATA_CLAUSE,
|
|
51
|
+
'',
|
|
49
52
|
'只输出一个 JSON 对象,不要解释、不要 Markdown 代码块:',
|
|
50
53
|
'{"state": "supported|contradicted|unverifiable", "reason": "一句话理由", "quote": "支撑你判断的原文片段"}',
|
|
51
54
|
'',
|
|
@@ -57,6 +60,7 @@ export function buildReconcilePrompt(memory, events, maxEvidenceChars) {
|
|
|
57
60
|
'- 拿不准就选 unverifiable。**不要猜。**',
|
|
58
61
|
].join('\n');
|
|
59
62
|
const user = [
|
|
63
|
+
DATA_DELIMITER_NOTE,
|
|
60
64
|
`记忆:${memory.text}`,
|
|
61
65
|
'',
|
|
62
66
|
'原文证据:',
|
|
@@ -66,6 +66,8 @@ export declare class RuminateController {
|
|
|
66
66
|
private sessions;
|
|
67
67
|
private totalL1;
|
|
68
68
|
private pendingFile;
|
|
69
|
+
/** Room 注册表(可选;孤儿候选预标记的合并词表来源)。 */
|
|
70
|
+
private readonly roomRegistry;
|
|
69
71
|
/** 记忆后端:未注入时包 l1(进程内,行为与改造前等价)。 */
|
|
70
72
|
private backend;
|
|
71
73
|
constructor(ctx: Context, cfg: MemoryConfig, runner: Pick<MemoryRunner, 'enqueue' | 'states'>, stores: {
|
|
@@ -75,7 +77,15 @@ export declare class RuminateController {
|
|
|
75
77
|
scenes: Record<MemoryFamily, SceneStore>;
|
|
76
78
|
persona: Record<MemoryFamily, PersonaStore>;
|
|
77
79
|
state: StateStore;
|
|
78
|
-
}, logger: MemoryLogger, live: LiveSettingsHandle, pendingFile: string
|
|
80
|
+
}, logger: MemoryLogger, live: LiveSettingsHandle, pendingFile: string,
|
|
81
|
+
/** Room 注册表(可选;孤儿候选预标记的合并词表来源)。 */
|
|
82
|
+
roomRegistry?: {
|
|
83
|
+
listActive(): Array<{
|
|
84
|
+
slug: string;
|
|
85
|
+
label?: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
}>;
|
|
88
|
+
});
|
|
79
89
|
/** 状态快照 */
|
|
80
90
|
getStatus(): RuminateStatus;
|
|
81
91
|
/**
|
|
@@ -3,6 +3,7 @@ import { groupPendingBySession, loadPending, } from '../store/pending.js';
|
|
|
3
3
|
import { errDetail } from '../util/filelog.js';
|
|
4
4
|
import { runSceneConsolidation } from './l2.js';
|
|
5
5
|
import { relabelPass } from './relabel.js';
|
|
6
|
+
import { annotateOrphanCandidates } from '../room-review.js';
|
|
6
7
|
import { runPersona } from './l3.js';
|
|
7
8
|
const IDLE_STATUS = {
|
|
8
9
|
running: false,
|
|
@@ -44,11 +45,15 @@ export class RuminateController {
|
|
|
44
45
|
sessions = [];
|
|
45
46
|
totalL1 = 0;
|
|
46
47
|
pendingFile;
|
|
48
|
+
/** Room 注册表(可选;孤儿候选预标记的合并词表来源)。 */
|
|
49
|
+
roomRegistry;
|
|
47
50
|
/** 记忆后端:未注入时包 l1(进程内,行为与改造前等价)。 */
|
|
48
51
|
backend() {
|
|
49
52
|
return this.stores.backend ?? new InProcMemoryBackend(this.stores.l1);
|
|
50
53
|
}
|
|
51
|
-
constructor(ctx, cfg, runner, stores, logger, live, pendingFile
|
|
54
|
+
constructor(ctx, cfg, runner, stores, logger, live, pendingFile,
|
|
55
|
+
/** Room 注册表(可选;孤儿候选预标记的合并词表来源)。 */
|
|
56
|
+
roomRegistry) {
|
|
52
57
|
this.ctx = ctx;
|
|
53
58
|
this.cfg = cfg;
|
|
54
59
|
this.runner = runner;
|
|
@@ -56,6 +61,7 @@ export class RuminateController {
|
|
|
56
61
|
this.logger = logger;
|
|
57
62
|
this.live = live;
|
|
58
63
|
this.pendingFile = pendingFile;
|
|
64
|
+
this.roomRegistry = roomRegistry;
|
|
59
65
|
}
|
|
60
66
|
/** 状态快照 */
|
|
61
67
|
getStatus() {
|
|
@@ -222,6 +228,17 @@ export class RuminateController {
|
|
|
222
228
|
// 重标定失败不拖垮反刍整体(蒸馏/L2/L3 产物保留)
|
|
223
229
|
this.logger.warn(`[memory] 反刍重标定失败(不影响本次产物): ${errDetail(err)}`);
|
|
224
230
|
}
|
|
231
|
+
// 孤儿候选预标记(长尾治理):无 Room 的活跃孤儿对照现有 Room 词表产出
|
|
232
|
+
// 候选(roomReview='pending'),由 agent 用 memory_room_review 逐个复查;
|
|
233
|
+
// 每轮至多 40 条,吞错不拖垮反刍。
|
|
234
|
+
try {
|
|
235
|
+
const r = await annotateOrphanCandidates(this.ctx, this.cfg, { l1: this.stores.l1, logger: this.logger, registry: this.roomRegistry }, 40);
|
|
236
|
+
if (r.written > 0)
|
|
237
|
+
this.status.detail = `孤儿候选预标记 ${r.written} 条(待 agent 复查,工具 memory_room_review)`;
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
this.logger.warn(`[memory] 孤儿候选预标记失败(忽略): ${errDetail(err)}`);
|
|
241
|
+
}
|
|
225
242
|
this.status.phase = 'done';
|
|
226
243
|
this.finish(null);
|
|
227
244
|
}
|