dsh-prime-memory 0.18.1 → 0.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +22 -0
- package/CHANGELOG.ja.md +22 -0
- package/CHANGELOG.ko.md +22 -0
- package/CHANGELOG.md +22 -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 +36 -9
- package/dist/config.d.ts +37 -0
- package/dist/config.js +7 -0
- package/dist/contract.d.ts +46 -0
- package/dist/hooks/capture-recovery.d.ts +59 -0
- package/dist/hooks/capture-recovery.js +88 -0
- package/dist/hooks/capture.js +110 -1
- package/dist/hooks/recall-ack.d.ts +48 -0
- package/dist/hooks/recall-ack.js +79 -0
- package/dist/hooks/recall.d.ts +29 -0
- package/dist/hooks/recall.js +131 -11
- package/dist/hooks/slot-recall.js +3 -2
- package/dist/index.d.ts +22 -0
- package/dist/index.js +4 -0
- 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/relabel.js +1 -1
- package/dist/pipeline/runner.js +62 -0
- package/dist/projection/slots.js +4 -2
- 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/stats.js +9 -0
- package/dist/store/evidence-source.js +15 -1
- package/dist/store/l0.d.ts +4 -0
- package/dist/store/l0.js +8 -0
- package/dist/store/memory-worker.js +1 -1
- package/dist/store/sqlite.d.ts +12 -0
- package/dist/store/sqlite.js +37 -0
- package/dist/store/trace.d.ts +39 -0
- package/dist/store/trace.js +149 -0
- package/dist/tools/index.js +25 -1
- package/dist/types.d.ts +17 -0
- package/dist/util/redact.d.ts +27 -0
- package/dist/util/redact.js +228 -0
- package/package.json +1 -1
package/dist/hooks/capture.js
CHANGED
|
@@ -6,7 +6,32 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { randomBytes } from 'node:crypto';
|
|
8
8
|
import { blocksToText } from '../util/text.js';
|
|
9
|
+
import { redactSecrets } from '../util/redact.js';
|
|
9
10
|
import { sanitizeText, shouldCaptureL0, stripCodeBlocks } from '../util/sanitize.js';
|
|
11
|
+
import { foldRecoverableTurns, readPersistedEventsViaServices } from './capture-recovery.js';
|
|
12
|
+
import { confirmInjectionByMessageId } from './recall-ack.js';
|
|
13
|
+
/** §C 一次性告警标记:脱敏自身异常时放行原文,只提示一次(不阻断捕获)。 */
|
|
14
|
+
let redactWarnedOnce = false;
|
|
15
|
+
/**
|
|
16
|
+
* §C 捕获单点脱敏(memorax-absorb):`capture.redactSecrets !== false` 时把
|
|
17
|
+
* 8 类密钥替换为 `[REDACTED:<KIND>]`。位置在 sanitize 之后、L0/蒸馏分发之前——
|
|
18
|
+
* 同一份 messages 覆盖 L0 JSONL/L0 SQLite/蒸馏输入三条下游,无分裂态。
|
|
19
|
+
* 红线:脱敏失败放行原文 + 一次性 warn(不阻断捕获)。
|
|
20
|
+
*/
|
|
21
|
+
function redactContent(content, cfg, logger) {
|
|
22
|
+
if (cfg.capture?.redactSecrets === false)
|
|
23
|
+
return content;
|
|
24
|
+
try {
|
|
25
|
+
return redactSecrets(content).text;
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (!redactWarnedOnce) {
|
|
29
|
+
redactWarnedOnce = true;
|
|
30
|
+
logger.warn(`[memory] 脱敏失败,本轮放行原文(一次性提示): ${err instanceof Error ? err.message : String(err)}`);
|
|
31
|
+
}
|
|
32
|
+
return content;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
10
35
|
/**
|
|
11
36
|
* 需要进缓冲的事件类型。流式 chunk(text-delta/reasoning 等)一秒钟可达数百条,
|
|
12
37
|
* 缓冲它们会把 MAX_BUFFER 撑爆、把轮次头部(turn/start + user 消息)裁掉——
|
|
@@ -70,6 +95,8 @@ export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
|
70
95
|
return;
|
|
71
96
|
const startFloor = Date.now();
|
|
72
97
|
const buffers = new CaptureBuffers();
|
|
98
|
+
// task_8 一次性提示标记:宿主读日志服务不可得时只提示一次(防每次 resume 刷屏)
|
|
99
|
+
let warnedRecoveryUnavailable = false;
|
|
73
100
|
// L0 即时落盘链:turn/end 立刻写,不被蒸馏队列(慢 LLM 调用)阻塞;
|
|
74
101
|
// 串行化保证同轮次顺序与单次写入(进程退出时排队中的 L0 不再依赖蒸馏完成)
|
|
75
102
|
let l0Queue = Promise.resolve();
|
|
@@ -79,6 +106,15 @@ export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
|
79
106
|
if (!s.enabled || !s.capture)
|
|
80
107
|
return;
|
|
81
108
|
const sid = String(session.id ?? session);
|
|
109
|
+
// §D 日志回执(memorax-absorb):注入消息以原样 user/message 入日志时在此确认,
|
|
110
|
+
// 才真正 dedupe.mark。**必须置于 source.kind 过滤之前**——过滤在
|
|
111
|
+
// turnEventsToMessages 内且只放行 kind='user',注入消息(kind='plugin:memory')
|
|
112
|
+
// 走不到那里;也置于 off 档返回之前(确认与档位无关,且代价是一次 Map 查找)。
|
|
113
|
+
if (event.type === 'user/message') {
|
|
114
|
+
const mid = event.data.id;
|
|
115
|
+
if (typeof mid === 'string' && mid)
|
|
116
|
+
confirmInjectionByMessageId(mid);
|
|
117
|
+
}
|
|
82
118
|
// off 档:本会话对记忆系统完全隐身(不缓冲、不写 L0、不蒸馏)
|
|
83
119
|
if (modes.get(sid) === 'off')
|
|
84
120
|
return;
|
|
@@ -124,6 +160,78 @@ export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
|
124
160
|
logger.warn(`[memory] session/event 处理失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
161
|
}
|
|
126
162
|
});
|
|
163
|
+
// ── §A 崩溃恢复(memorax-absorb Wave 1 / task_7):resume 时对账宿主持久化日志,
|
|
164
|
+
// 补收 (L0 水位线, 末尾] 内 bracket 完整的尾轮(≤2 轮)——崩溃轮在缓冲丢失后
|
|
165
|
+
// 仍可从持久化层找回(persistence 会给孤儿轮补 interrupted 闭合,探针② 实证 98 处)。
|
|
166
|
+
// fail-open 纪律:任何异常只 warn,不阻塞会话启动;幂等 = 水位线 + 逐 turn 存在性检查。
|
|
167
|
+
ctx.on('agent/session-start', (payload) => {
|
|
168
|
+
try {
|
|
169
|
+
if (payload.source !== 'resume')
|
|
170
|
+
return;
|
|
171
|
+
const s = live.get();
|
|
172
|
+
if (!s.enabled || !s.capture)
|
|
173
|
+
return;
|
|
174
|
+
const session = payload.agent?.session;
|
|
175
|
+
const sid = session?.id !== undefined && session.id !== null ? String(session.id) : '';
|
|
176
|
+
if (!sid)
|
|
177
|
+
return;
|
|
178
|
+
const mode = modes.get(sid);
|
|
179
|
+
if (mode === 'off')
|
|
180
|
+
return; // off 档对记忆系统完全隐身,对齐现行为
|
|
181
|
+
let events = session?.events;
|
|
182
|
+
if (!events || events.length === 0) {
|
|
183
|
+
// task_8 降级链:events 不可得(宿主版本偏差)时依次试
|
|
184
|
+
// ctx.sessionQuery.readSession / ctx.sessionPersistence.readFrom(守卫探针式)
|
|
185
|
+
const get = ctx.get;
|
|
186
|
+
const svcs = typeof get === 'function'
|
|
187
|
+
? {
|
|
188
|
+
sessionQuery: get.call(ctx, 'sessionQuery'),
|
|
189
|
+
sessionPersistence: get.call(ctx, 'sessionPersistence'),
|
|
190
|
+
}
|
|
191
|
+
: {};
|
|
192
|
+
const degraded = readPersistedEventsViaServices(svcs, sid);
|
|
193
|
+
if (degraded === undefined) {
|
|
194
|
+
if (!warnedRecoveryUnavailable) {
|
|
195
|
+
warnedRecoveryUnavailable = true;
|
|
196
|
+
logger.info('[memory] resume 恢复:宿主读日志服务不可得,跳过对账(一次性提示)');
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
events = degraded;
|
|
201
|
+
}
|
|
202
|
+
const watermark = l0.maxCapturedTurn(sid);
|
|
203
|
+
const brackets = foldRecoverableTurns(events, watermark);
|
|
204
|
+
if (brackets.length === 0)
|
|
205
|
+
return;
|
|
206
|
+
let recovered = 0;
|
|
207
|
+
let skipped = 0;
|
|
208
|
+
let total = 0;
|
|
209
|
+
for (const bracket of brackets) {
|
|
210
|
+
if (l0.hasAnyL0Message(sid, bracket.turn)) {
|
|
211
|
+
// 部分写残行:水位线只判"整轮无",存在性检查兜底——整轮跳过,宁少收不重复
|
|
212
|
+
skipped += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const messages = turnEventsToMessages(bracket.events, cfg, logger, sid, bracket.turn);
|
|
216
|
+
if (messages.length === 0)
|
|
217
|
+
continue;
|
|
218
|
+
recovered += 1;
|
|
219
|
+
total += messages.length;
|
|
220
|
+
const n = messages.length;
|
|
221
|
+
l0Queue = l0Queue
|
|
222
|
+
.then(() => l0.append(sid, messages))
|
|
223
|
+
.then(() => logger.info(`[memory] L0 恢复落盘 ${n} 条`))
|
|
224
|
+
.catch((err) => logger.warn(`[memory] L0 恢复落盘失败: ${err instanceof Error ? err.message : String(err)}`));
|
|
225
|
+
runner.enqueue(sid, messages, mode);
|
|
226
|
+
}
|
|
227
|
+
if (recovered > 0 || skipped > 0) {
|
|
228
|
+
logger.info(`[memory] resume 对账:水位线 ${watermark ?? '无'} → 恢复 ${recovered} 轮 ${total} 条,幂等跳过 ${skipped} 轮(session=${sid})`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
logger.warn(`[memory] resume 恢复失败(不阻塞会话启动): ${err instanceof Error ? err.message : String(err)}`);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
127
235
|
// 冲刷 = 等待串行链排空(链上每环自带 catch,永不 reject)。
|
|
128
236
|
// 注:极小概率在 await 期间又入队的新消息会落到链尾——其 JSONL 事实源照写、
|
|
129
237
|
// DB 写入由 upsert 内部兜底(关库后 warn),下次重建自愈。
|
|
@@ -190,7 +298,7 @@ function turnEventsToMessages(events, cfg, logger, sessionId, turn) {
|
|
|
190
298
|
logger.info(`[memory] L0 跳过非用户来源消息(source.kind=${msg.source?.kind ?? 'none'})`);
|
|
191
299
|
continue;
|
|
192
300
|
}
|
|
193
|
-
const content = sanitizeText(blocksToText(msg.content));
|
|
301
|
+
const content = redactContent(sanitizeText(blocksToText(msg.content)), cfg, logger);
|
|
194
302
|
if (shouldCaptureL0(content)) {
|
|
195
303
|
const anchor = { sessionId, turn };
|
|
196
304
|
if (currentStep !== undefined)
|
|
@@ -203,6 +311,7 @@ function turnEventsToMessages(events, cfg, logger, sessionId, turn) {
|
|
|
203
311
|
let content = sanitizeText(blocksToText(data.message?.content));
|
|
204
312
|
if (cfg.capture.stripCodeBlocks)
|
|
205
313
|
content = stripCodeBlocks(content);
|
|
314
|
+
content = redactContent(content, cfg, logger);
|
|
206
315
|
if (shouldCaptureL0(content)) {
|
|
207
316
|
// 事件自带 turn/step 优先(fold 只服务于不带该字段的事件类型)
|
|
208
317
|
const evTurn = typeof data.turn === 'number' && Number.isFinite(data.turn) ? data.turn : turn;
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
export interface PendingInjection {
|
|
22
|
+
agentId: string;
|
|
23
|
+
messageId: string;
|
|
24
|
+
recordIds: string[];
|
|
25
|
+
at: number;
|
|
26
|
+
}
|
|
27
|
+
/** 确认后执行的实际标记动作(recall.ts 注入 = dedupe.mark 的绑定)。 */
|
|
28
|
+
type InjectionMarker = (agentId: string, recordIds: string[]) => void;
|
|
29
|
+
export declare const PENDING_TIMEOUT_MS: number;
|
|
30
|
+
/** recall.ts 注册实际标记动作(dedupe.mark 绑定);重复注册以最后一次为准。 */
|
|
31
|
+
export declare function setInjectionMarker(fn: InjectionMarker): void;
|
|
32
|
+
/** ① 登记待确认注入(超容量淘汰最旧——积压到 64 条说明确认链路已异常)。 */
|
|
33
|
+
export declare function registerPendingInjection(agentId: string, messageId: string, recordIds: string[], now?: number): void;
|
|
34
|
+
/**
|
|
35
|
+
* ② 日志回执确认:该注入消息已真实进入会话历史。执行标记并移除 pending。
|
|
36
|
+
* @returns 是否命中一条 pending(未命中 = 与本插件注入无关的事件,静默)。
|
|
37
|
+
*/
|
|
38
|
+
export declare function confirmInjectionByMessageId(messageId: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* 超时清扫:超时的 pending 照旧标记(降级保连续,防 dedupe 失效回归),并移除。
|
|
41
|
+
* 未超时的不动。@returns 本次降级标记的条数(recall 侧据此前瞻性提示一次)。
|
|
42
|
+
*/
|
|
43
|
+
export declare function expirePendingInjections(now?: number, timeoutMs?: number): number;
|
|
44
|
+
/** 诊断/测试:当前 pending 条数。 */
|
|
45
|
+
export declare function pendingCount(): number;
|
|
46
|
+
/** 测试隔离:清空 pending 与 marker(模块级状态,用例间互扰)。 */
|
|
47
|
+
export declare function resetAckForTests(): void;
|
|
48
|
+
export {};
|
|
@@ -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
|
@@ -28,9 +28,38 @@ import { type OccupancyLedger } from '../util/context-occupancy.js';
|
|
|
28
28
|
* 从会话消息构建召回查询(纯函数):末尾 N 条 + 总长截断,空输入返回空串。
|
|
29
29
|
* 全史拼接会让 MATCH 表达式随会话长度线性膨胀(整会话累计二次方成本)。
|
|
30
30
|
*/
|
|
31
|
+
/**
|
|
32
|
+
* 本插件 recall 注入的署名判据(v4 `plugin:memory` 与 v3 旧行双形状;宿主迁移负责
|
|
33
|
+
* 升格旧行)。导出供双形状单测钉住——估算函数(estimateRecallFromStorage /
|
|
34
|
+
* estimateRecallTokens)对旧形状漏判会让"记忆召回份额"静默归零。
|
|
35
|
+
*/
|
|
36
|
+
export declare function isOwnRecallSource(src: {
|
|
37
|
+
kind?: unknown;
|
|
38
|
+
plugin?: unknown;
|
|
39
|
+
form?: unknown;
|
|
40
|
+
} | undefined | null): boolean;
|
|
31
41
|
export declare function buildRecallQuery(messages: Array<{
|
|
32
42
|
content: unknown;
|
|
33
43
|
}>, tailMessages?: number, maxChars?: number): string;
|
|
44
|
+
/**
|
|
45
|
+
* §E 压缩感知标记器(memorax-absorb / task_24-26):记录刚完成压缩的会话,
|
|
46
|
+
* pre-step 消费后做一次性增强召回。纯内存状态,CaptureBuffers 同款可单测导出。
|
|
47
|
+
*
|
|
48
|
+
* - `onCompactionEnd`:只认无 error 的 compaction/end(失败的压缩没有把内容逐出上下文);
|
|
49
|
+
* - `consume`:消费即清(一次性增强);容量上限淘汰最旧(积压即异常,回退路径兜底)。
|
|
50
|
+
*/
|
|
51
|
+
export declare class PostCompactionTracker {
|
|
52
|
+
private readonly cap;
|
|
53
|
+
private readonly map;
|
|
54
|
+
constructor(cap?: number);
|
|
55
|
+
/** @returns 是否真的置了标记(false = 非 compaction/end 或带 error)。 */
|
|
56
|
+
onCompactionEnd(sessionId: string, data?: {
|
|
57
|
+
error?: unknown;
|
|
58
|
+
}): boolean;
|
|
59
|
+
/** 消费该会话的增强标记(存在即清,返回是否存在)。 */
|
|
60
|
+
consume(sessionId: string): boolean;
|
|
61
|
+
get size(): number;
|
|
62
|
+
}
|
|
34
63
|
/** 单会话召回统计(悬浮卡信息区数据源;每轮 O(1) 记账,agent/disposed 清理)。
|
|
35
64
|
* 口径声明:这是"注入统计"而非 bench 的离线 recall@k——运行时没有 ground truth,
|
|
36
65
|
* 命中率 = hitTurns / injectedTurns。去重语义:全量压制轮计入 hitTurns
|
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';
|
|
@@ -27,6 +30,14 @@ const RECALL_QUERY_MAX_CHARS = 2_000;
|
|
|
27
30
|
* 从会话消息构建召回查询(纯函数):末尾 N 条 + 总长截断,空输入返回空串。
|
|
28
31
|
* 全史拼接会让 MATCH 表达式随会话长度线性膨胀(整会话累计二次方成本)。
|
|
29
32
|
*/
|
|
33
|
+
/**
|
|
34
|
+
* 本插件 recall 注入的署名判据(v4 `plugin:memory` 与 v3 旧行双形状;宿主迁移负责
|
|
35
|
+
* 升格旧行)。导出供双形状单测钉住——估算函数(estimateRecallFromStorage /
|
|
36
|
+
* estimateRecallTokens)对旧形状漏判会让"记忆召回份额"静默归零。
|
|
37
|
+
*/
|
|
38
|
+
export function isOwnRecallSource(src) {
|
|
39
|
+
return !!src && src.form === 'recall' && (src.kind === 'plugin:memory' || (src.kind === 'plugin' && src.plugin === 'memory'));
|
|
40
|
+
}
|
|
30
41
|
export function buildRecallQuery(messages, tailMessages = RECALL_QUERY_TAIL_MESSAGES, maxChars = RECALL_QUERY_MAX_CHARS) {
|
|
31
42
|
const tail = messages.slice(-tailMessages);
|
|
32
43
|
let text = tail.map((m) => blocksToText(m.content)).join(' ').trim();
|
|
@@ -34,6 +45,43 @@ export function buildRecallQuery(messages, tailMessages = RECALL_QUERY_TAIL_MESS
|
|
|
34
45
|
text = text.slice(-maxChars);
|
|
35
46
|
return text;
|
|
36
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
|
+
}
|
|
37
85
|
const MEMORY_TOOLS_GUIDE = `<memory-tools-guide>
|
|
38
86
|
## 记忆工具调用指南
|
|
39
87
|
|
|
@@ -152,6 +200,21 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
152
200
|
recallStats.delete(payload.agent.id);
|
|
153
201
|
occupancyByAgent.delete(payload.agent.id);
|
|
154
202
|
});
|
|
203
|
+
// §D:绑定实际标记动作 + 超时清扫挂到 pre-step 入口(见注入点注释)。
|
|
204
|
+
setInjectionMarker((agentId, recordIds) => dedupe.mark(agentId, recordIds));
|
|
205
|
+
let ackDegradeWarned = false;
|
|
206
|
+
// §E 压缩感知(memorax-absorb):宿主 `compaction/end`(无 error)→ 该会话下一轮
|
|
207
|
+
// 做一次"定向增强召回"——跳过去重压制、画像立即重注入、占用账本归零;消费即清。
|
|
208
|
+
// 会话级回退路径保留(下方 session-start source='compact' 全量重置),两者幂等。
|
|
209
|
+
// 宿主无压缩计数器(compactionId 为 per-transaction UUID),故不做 generation 对账。
|
|
210
|
+
const postCompaction = new PostCompactionTracker();
|
|
211
|
+
ctx.on('session/event', (session, event) => {
|
|
212
|
+
if (event.type !== 'compaction/end')
|
|
213
|
+
return;
|
|
214
|
+
if (postCompaction.onCompactionEnd(String(session.id ?? session))) {
|
|
215
|
+
logger.info(`[memory] compaction/end:下一轮将做一次增强召回(session=${String(session.id ?? session)})`);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
155
218
|
// 上下文压缩/清空 → 已注入内容从模型上下文丢失,重置该会话的去重压制
|
|
156
219
|
// (resume/startup 不重置:历史仍在,已注入的记忆模型还持有)。
|
|
157
220
|
// 占用账本同步全量归零(宁低勿高;轮级粒度近似)。
|
|
@@ -197,6 +260,13 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
197
260
|
// prepend 注册 + 先 next() 再改写:不劫持其他监听器(dsh-time-context 官方范式)。
|
|
198
261
|
if (cfg.recall.enabled) {
|
|
199
262
|
ctx.on('agent/pre-step', async (payload, next) => {
|
|
263
|
+
// §D 超时清扫:超时未确认的注入照旧标记(降级保连续,防 dedupe 失效回归);
|
|
264
|
+
// 正常确认在秒级完成,这里只是兜底。一次性提示。
|
|
265
|
+
const expiredAcks = expirePendingInjections();
|
|
266
|
+
if (expiredAcks > 0 && !ackDegradeWarned) {
|
|
267
|
+
ackDegradeWarned = true;
|
|
268
|
+
logger.warn(`[memory] 注入确认:${expiredAcks} 条 pending 超时未回执,已降级为照常标记(一次性提示)`);
|
|
269
|
+
}
|
|
200
270
|
const decision = await next();
|
|
201
271
|
if (decision.kind === 'reject' || payload.signal.aborted)
|
|
202
272
|
return decision;
|
|
@@ -212,6 +282,17 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
212
282
|
const hasNewUserMessage = decision.messages.some((m) => m.source?.kind === 'user');
|
|
213
283
|
if (!hasNewUserMessage)
|
|
214
284
|
return decision;
|
|
285
|
+
// §E 消费增强标记:压缩后下一轮跳过去重压制(预算/档位/门控照常),
|
|
286
|
+
// 画像 TTL 失效立即重注入,占用账本归零。消费即清(一次性)。
|
|
287
|
+
const sessionKey = String(payload.agent.session?.id ?? payload.agent.id);
|
|
288
|
+
const boosted = postCompaction.consume(sessionKey);
|
|
289
|
+
if (boosted) {
|
|
290
|
+
invalidateProfile();
|
|
291
|
+
const led = ledgerFor(payload.agent.id);
|
|
292
|
+
resetForCompaction(led);
|
|
293
|
+
occupancyStore.save(payload.agent.id, led);
|
|
294
|
+
logger.info(`[memory] 压缩后增强召回:本轮跳过去重压制 + 画像重注入(session=${sessionKey})`);
|
|
295
|
+
}
|
|
215
296
|
const query = buildRecallQuery(payload.messages);
|
|
216
297
|
// 空查询是退化轮(无用户文本),重置命中信号但不计入统计
|
|
217
298
|
if (!query) {
|
|
@@ -219,6 +300,10 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
219
300
|
degenerate.lastHits = 0;
|
|
220
301
|
return decision;
|
|
221
302
|
}
|
|
303
|
+
// §F 追踪字段:metadata-only 默认(chars+sha256 前 16),captureContent 才存原文
|
|
304
|
+
const queryChars = query.length;
|
|
305
|
+
const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
|
|
306
|
+
const queryText = cfg.trace?.captureContent === true ? query.slice(0, 200) : undefined;
|
|
222
307
|
const st = statFor(payload.agent.id);
|
|
223
308
|
st.injectedTurns++;
|
|
224
309
|
st.lastHits = 0;
|
|
@@ -237,6 +322,11 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
237
322
|
if (hits === undefined) {
|
|
238
323
|
st.timeouts++;
|
|
239
324
|
logger.warn('[memory] 召回超时,跳过本轮注入(不阻塞对话)');
|
|
325
|
+
trace({
|
|
326
|
+
kind: 'recall_turn', ts: Date.now(), sessionId: sessionKey, queryChars, querySha, queryText,
|
|
327
|
+
hitIds: [], hitScores: [], injectedIds: [], suppressedCount: 0,
|
|
328
|
+
durationMs: Date.now() - searchStart, outcome: 'timeout',
|
|
329
|
+
});
|
|
240
330
|
return decision;
|
|
241
331
|
}
|
|
242
332
|
st.lastDurationMs = Date.now() - searchStart;
|
|
@@ -267,7 +357,8 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
267
357
|
}
|
|
268
358
|
// 召回去重:同会话已注入过的记录不再重复注入(模型上下文已持有,省 token)。
|
|
269
359
|
// 纯过滤——剩几条注几条,全量压制(0 条新鲜命中)是正确状态而非未命中。
|
|
270
|
-
|
|
360
|
+
// §E:压缩后增强轮跳过压制(压缩把已注入内容逐出了模型上下文)。
|
|
361
|
+
const seen = boosted ? new Set() : dedupe.seen(payload.agent.id);
|
|
271
362
|
const fresh = scoped.filter((h) => !seen.has(h.id));
|
|
272
363
|
const suppressed = scoped.length - fresh.length;
|
|
273
364
|
st.suppressedRecalls += suppressed;
|
|
@@ -279,13 +370,27 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
279
370
|
st.hitTurns++;
|
|
280
371
|
st.totalHits += fresh.length;
|
|
281
372
|
}
|
|
282
|
-
if (fresh.length === 0)
|
|
373
|
+
if (fresh.length === 0) {
|
|
374
|
+
// §F 追踪:全量压制轮——零注入是正确状态,如实记录
|
|
375
|
+
trace({
|
|
376
|
+
kind: 'recall_turn', ts: Date.now(), sessionId: sessionKey, queryChars, querySha, queryText,
|
|
377
|
+
hitIds: scoped.map((h) => h.id), hitScores: scoped.map((h) => Number(h.score.toFixed(4))),
|
|
378
|
+
injectedIds: [], suppressedCount: scoped.length - fresh.length,
|
|
379
|
+
durationMs: Date.now() - searchStart, outcome: 'suppressed',
|
|
380
|
+
});
|
|
283
381
|
return decision;
|
|
382
|
+
}
|
|
284
383
|
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 });
|
|
285
|
-
if (lines.length === 0)
|
|
384
|
+
if (lines.length === 0) {
|
|
385
|
+
// §F 追踪:预算清零轮(截断后无完整行)
|
|
386
|
+
trace({
|
|
387
|
+
kind: 'recall_turn', ts: Date.now(), sessionId: sessionKey, queryChars, querySha, queryText,
|
|
388
|
+
hitIds: fresh.map((h) => h.id), hitScores: fresh.map((h) => Number(h.score.toFixed(4))),
|
|
389
|
+
injectedIds: [], suppressedCount: scoped.length - fresh.length,
|
|
390
|
+
durationMs: Date.now() - searchStart, outcome: 'suppressed',
|
|
391
|
+
});
|
|
286
392
|
return decision;
|
|
287
|
-
|
|
288
|
-
dedupe.mark(payload.agent.id, fresh.slice(0, lines.length).map((h) => h.id));
|
|
393
|
+
}
|
|
289
394
|
st.lastHits = lines.length;
|
|
290
395
|
const text = [
|
|
291
396
|
'<relevant-memories>',
|
|
@@ -298,10 +403,23 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
298
403
|
logger.info(`[memory] 召回注入 ${lines.length} 条 L1(mode=${mode},query="${query.slice(0, 30).replace(/\n/g, ' ')}…",agent=${payload.agent.id},消息侧)`);
|
|
299
404
|
const injection = createUserMessage({
|
|
300
405
|
content: [{ type: 'text', text }],
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
|
|
406
|
+
// v4(宿主 ≥0.1.7-rc.1)署名为 producer-owned kind:plugin:<子系统名>。
|
|
407
|
+
// 用展示友好的子系统名 memory,不用 cordis id(dsh-memory);宿主 UI 标题
|
|
408
|
+
// 规则随之变化(不再有 catch-all 'plugin' 的通用"上下文注入"标题)。
|
|
409
|
+
// v3 旧行(kind:'plugin'+plugin 字段)由宿主 v3→v4 迁移升格,读侧双形状兼容。
|
|
410
|
+
source: { kind: 'plugin:memory', form: 'recall' },
|
|
411
|
+
});
|
|
412
|
+
// §D 注入确认(memorax-absorb):宿主接受 enter 后,注入消息以原样
|
|
413
|
+
// `user/message` 进入会话日志(message.id 稳定保留)——capture 侧观察到
|
|
414
|
+
// 该 id 才真正 dedupe.mark(recall-ack 两段式)。这里只登记 pending,
|
|
415
|
+
// 未确认不标记:被覆盖/取消的注入不该压制这些记忆(下轮可重注)。
|
|
416
|
+
registerPendingInjection(payload.agent.id, injection.id, fresh.slice(0, lines.length).map((h) => h.id));
|
|
417
|
+
// §F 追踪:注入轮
|
|
418
|
+
trace({
|
|
419
|
+
kind: 'recall_turn', ts: Date.now(), sessionId: sessionKey, queryChars, querySha, queryText,
|
|
420
|
+
hitIds: scoped.map((h) => h.id), hitScores: scoped.map((h) => Number(h.score.toFixed(4))),
|
|
421
|
+
injectedIds: fresh.slice(0, lines.length).map((h) => h.id), suppressedCount: scoped.length - fresh.length,
|
|
422
|
+
durationMs: Date.now() - searchStart, outcome: 'injected',
|
|
305
423
|
});
|
|
306
424
|
// 入账在成功构造注入消息之后、返回 enter 之前——任何前置抛错路径账目零扰动
|
|
307
425
|
const led = ledgerFor(payload.agent.id);
|
|
@@ -342,6 +460,8 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
342
460
|
return '';
|
|
343
461
|
return body ? `${body}\n\n${MEMORY_TOOLS_GUIDE}` : MEMORY_TOOLS_GUIDE;
|
|
344
462
|
};
|
|
463
|
+
/** 本插件 recall 注入的署名判据(v4 `plugin:memory` 与 v3 旧行双形状;宿主迁移负责升格旧行)。 */
|
|
464
|
+
const isOwnRecall = isOwnRecallSource;
|
|
345
465
|
async function estimateRecallFromStorage(sessionId) {
|
|
346
466
|
if (storedEstimateCache.has(sessionId))
|
|
347
467
|
return storedEstimateCache.get(sessionId) ?? null;
|
|
@@ -358,7 +478,7 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
358
478
|
if (ev.type !== 'user/message')
|
|
359
479
|
continue;
|
|
360
480
|
const src = ev.data?.source;
|
|
361
|
-
if (!src
|
|
481
|
+
if (!isOwnRecall(src))
|
|
362
482
|
continue;
|
|
363
483
|
let chars = 0;
|
|
364
484
|
for (const b of ev.data?.content ?? []) {
|
|
@@ -395,7 +515,7 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
|
|
|
395
515
|
continue;
|
|
396
516
|
const msg = ev.data;
|
|
397
517
|
const src = msg?.source;
|
|
398
|
-
if (!src ||
|
|
518
|
+
if (!isOwnRecall(src) || !msg)
|
|
399
519
|
continue;
|
|
400
520
|
let chars = 0;
|
|
401
521
|
for (const b of msg.content ?? []) {
|
|
@@ -30,8 +30,9 @@ export function registerSlotRecall(ctx, cfg, slots, logger, _live) {
|
|
|
30
30
|
return decision;
|
|
31
31
|
const injection = createUserMessage({
|
|
32
32
|
content: [{ type: 'text', text }],
|
|
33
|
-
//
|
|
34
|
-
|
|
33
|
+
// v4 producer-owned kind(宿主 ≥0.1.7-rc.1);form 沿用 'recall' 分组
|
|
34
|
+
// (宿主 ContextFormed 的 recall 变体无伴随字段)
|
|
35
|
+
source: { kind: 'plugin:memory', form: 'recall' },
|
|
35
36
|
});
|
|
36
37
|
return { kind: 'enter', messages: [injection, ...decision.messages] };
|
|
37
38
|
}
|
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
|
@@ -42,6 +42,7 @@ import { buildRouteChain, resolveModelRoute, invalidateEffortCache } from './llm
|
|
|
42
42
|
import { effectiveCfg } from './pipeline/runner.js';
|
|
43
43
|
import { pendingPathFor } from './store/pending.js';
|
|
44
44
|
import { initTokenCost, resetTokenCost } from './token-cost.js';
|
|
45
|
+
import { initTraceStore } from './store/trace.js';
|
|
45
46
|
export const name = 'dsh-memory-plugin';
|
|
46
47
|
/**
|
|
47
48
|
* 硬依赖:蒸馏要用 llm,工具注册要用 tools,召回注入要用 systemPrompt。
|
|
@@ -137,6 +138,9 @@ export async function apply(ctx, config) {
|
|
|
137
138
|
const embed = initial.svc;
|
|
138
139
|
const db = new MemoryDb(path.join(dataDir, 'memory.db'), initial.dims, logger);
|
|
139
140
|
initTokenCost(db, config.tokenCost.retentionDays);
|
|
141
|
+
// §F 追踪(trace.enabled=false 时保持未初始化,埋点零副作用)
|
|
142
|
+
if (config.trace?.enabled)
|
|
143
|
+
initTraceStore(dataDir, { retentionDays: config.trace.retentionDays });
|
|
140
144
|
// 插件卸载时关闭连接(WAL 落盘),注册一次即可
|
|
141
145
|
ctx.effect(() => () => db.close());
|
|
142
146
|
let dbInit = { needsReindex: false };
|
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>;
|