dsh-prime-memory 0.26.4 → 0.26.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/dist/client.js +36785 -134
- package/dist/clients/drsc.d.ts +93 -0
- package/dist/clients/drsc.js +139 -0
- package/dist/contract.d.ts +74 -0
- package/dist/drs-anchor-backfill.d.ts +88 -0
- package/dist/drs-anchor-backfill.js +150 -0
- package/dist/graph/constraints.d.ts +3 -1
- package/dist/graph/constraints.js +4 -2
- package/dist/graph/preview.d.ts +92 -0
- package/dist/graph/preview.js +186 -0
- package/dist/graph/search.d.ts +22 -5
- package/dist/graph/search.js +69 -7
- package/dist/graph/types.d.ts +26 -1
- package/dist/graph/types.js +26 -1
- package/dist/hooks/capture.d.ts +4 -4
- package/dist/hooks/capture.js +34 -27
- package/dist/hooks/recall.js +8 -1
- package/dist/index.js +58 -20
- package/dist/pipeline/graph.d.ts +9 -0
- package/dist/pipeline/graph.js +29 -2
- package/dist/pipeline/runner.d.ts +17 -4
- package/dist/pipeline/runner.js +33 -6
- package/dist/stats.js +30 -1
- package/dist/store/embedding-source.d.ts +1 -3
- package/dist/store/embedding-source.js +6 -13
- package/dist/store/graph-store.d.ts +40 -3
- package/dist/store/graph-store.js +104 -8
- package/dist/store/l0-watermark.d.ts +46 -0
- package/dist/store/l0-watermark.js +93 -0
- package/dist/store/l0.d.ts +12 -42
- package/dist/store/l0.js +2 -208
- package/dist/store/l1.d.ts +12 -9
- package/dist/store/l1.js +37 -16
- package/dist/store/session-recalled.d.ts +9 -3
- package/dist/store/session-recalled.js +24 -5
- package/dist/store/sqlite.d.ts +3 -0
- package/dist/store/sqlite.js +7 -0
- package/dist/tools/index.d.ts +3 -2
- package/dist/tools/index.js +27 -10
- package/dist/types.d.ts +9 -0
- package/dsh.plugin.json +1 -1
- package/package.json +8 -3
package/dist/hooks/recall.js
CHANGED
|
@@ -531,7 +531,14 @@ usage) {
|
|
|
531
531
|
usage?.recordAttempt(scoped.map((h) => h.id), fresh.slice(0, lines.length).map((h) => h.id));
|
|
532
532
|
// G-live:按 **sessionKey**(=agent.session?.id ?? agent.id,与 pill 的 sessionId / 投影 header.id 同源,
|
|
533
533
|
// 非 ownerId)记入本会话“活召回集”,供侧栏卡读取;压缩时同键 reset。
|
|
534
|
-
|
|
534
|
+
// 路归因随注入透传(hybrid 融合才挂 lanes;单路策略下表空=不记,徽标无从渲染)。
|
|
535
|
+
const injectedHits = fresh.slice(0, lines.length);
|
|
536
|
+
const laneById = new Map();
|
|
537
|
+
for (const h of injectedHits) {
|
|
538
|
+
if (h.lanes !== undefined && h.lanes.length > 0)
|
|
539
|
+
laneById.set(h.id, h.lanes);
|
|
540
|
+
}
|
|
541
|
+
stores.recalled?.add(sessionKey, injectedHits.map((h) => h.id), injection.id, laneById);
|
|
535
542
|
// 入账在成功构造注入消息之后、返回 enter 之前——任何前置抛错路径账目零扰动
|
|
536
543
|
const led = ledgerFor(payload.agent.id);
|
|
537
544
|
recordRecallInjection(led, text.length);
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,7 @@ import { makeFileSyncHook } from './store/memory-md-sync.js';
|
|
|
50
50
|
import { ObsoleteNoticeStore } from './store/obsolete-notices.js';
|
|
51
51
|
import { registerSlotsProjection } from './projection/slots.js';
|
|
52
52
|
import { registerRecallProjection } from './projection/recall.js';
|
|
53
|
+
import { DrscClient } from './clients/drsc.js';
|
|
53
54
|
import { errDetail, withFileLog } from './util/filelog.js';
|
|
54
55
|
import { buildRouteChain, resolveModelRoute, invalidateEffortCache } from './llm.js';
|
|
55
56
|
import { effectiveCfg } from './pipeline/runner.js';
|
|
@@ -57,6 +58,30 @@ import { pendingPathFor } from './store/pending.js';
|
|
|
57
58
|
import { initTokenCost, resetTokenCost } from './token-cost.js';
|
|
58
59
|
import { initTraceStore } from './store/trace.js';
|
|
59
60
|
export const name = 'dsh-memory-plugin';
|
|
61
|
+
/**
|
|
62
|
+
* webStartup(0.2.1-a.2 起更名自 webRuntime)的 best-effort 读取:trustedHosts
|
|
63
|
+
* 供 DrscClient 同源基址解析。宿主服务缺席/未声明 inject 时属性读会抛
|
|
64
|
+
* (cordis Proxy 面),双名回退链 + 两层 try,安全返回 undefined(→ 缺省 8080 口)。
|
|
65
|
+
*/
|
|
66
|
+
function readStartupTrustedHosts(ctx) {
|
|
67
|
+
for (const name of ['webStartup', 'webRuntime']) {
|
|
68
|
+
try {
|
|
69
|
+
const direct = ctx[name];
|
|
70
|
+
const svc = (direct !== undefined
|
|
71
|
+
? direct
|
|
72
|
+
: typeof ctx.get === 'function'
|
|
73
|
+
? ctx.get(name)
|
|
74
|
+
: undefined);
|
|
75
|
+
if (svc !== null && typeof svc === 'object' && Array.isArray(svc.trustedHosts)) {
|
|
76
|
+
return svc.trustedHosts;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* 未声明服务的属性读抛:落下一名/缺省 */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
60
85
|
/**
|
|
61
86
|
* 硬依赖:蒸馏要用 llm,工具注册要用 tools,召回注入要用 systemPrompt。
|
|
62
87
|
* 0.1.5 起 connection.rpc.handle() 把 /rpc 路由登记到 webServer 时按**调用方
|
|
@@ -170,12 +195,25 @@ export async function apply(ctx, config) {
|
|
|
170
195
|
logger.error(`[memory] 检索库打开失败,记忆功能停用: ${err instanceof Error ? err.message : String(err)}`);
|
|
171
196
|
}
|
|
172
197
|
}
|
|
198
|
+
// §D 第 3 路装配(回链 + 路内向量种子 + 1-hop,见 graph-store.searchLaneRecords):
|
|
199
|
+
// embed 查询惰性接 stores.l1.embedText(嵌入源运行时活切换不被钉死在启动 service 上,
|
|
200
|
+
// 且复用其超时/失败吞并语义——未就绪/失败 = undefined = 纯词法路,与旧 searchNodes
|
|
201
|
+
// 表达式逐位一致)。图谱不可用时 searchLaneRecords 自带空返回,不影响双路。
|
|
202
|
+
// eslint-disable-next-line prefer-const -- 声明早于创建,后续单点赋值(与 embedManagerRef 同款惰性缝;TS 不允许 const 无初值)
|
|
203
|
+
let laneEmbedQuery;
|
|
204
|
+
const graphLaneProvider = async (query, limit, family) => {
|
|
205
|
+
const vec = laneEmbedQuery ? await laneEmbedQuery(query).catch(() => undefined) : undefined;
|
|
206
|
+
return db.graphStore.searchLaneRecords(query, vec, limit, family ? [family] : undefined);
|
|
207
|
+
};
|
|
173
208
|
const stores = {
|
|
174
209
|
usage: undefined, // 装配于下方(依赖 dataDir 就绪)
|
|
175
|
-
|
|
210
|
+
// DRSC 客户端(T8.4 读切):蒸馏背景窗 + conversation_search 工具面的历史供给。
|
|
211
|
+
// 硬依赖无本地回退(l0 表退役);trustedHosts 惰性读取(webStartup 可能晚到)。
|
|
212
|
+
drsc: new DrscClient({ trustedHosts: () => readStartupTrustedHosts(ctx) }),
|
|
213
|
+
l0: new L0Store(db),
|
|
176
214
|
l1: new L1Store(dataDir, db, embed, config.recall.strategy, logger, config.recall.decayHalfLifeDays,
|
|
177
|
-
// §D 第 3
|
|
178
|
-
|
|
215
|
+
// §D 第 3 路:图谱回链(声明于 stores 之前,embed 查询在下方接线)。
|
|
216
|
+
graphLaneProvider,
|
|
179
217
|
// 投射开着才给没名字的新记录起名(“关时零动作”这条承诺不改)。
|
|
180
218
|
// 传函数而不是布尔:读的是当前配置对象,不把启动那一刻的值钉进 store。
|
|
181
219
|
() => config.memExport?.enabled === true),
|
|
@@ -207,10 +245,12 @@ export async function apply(ctx, config) {
|
|
|
207
245
|
// 后台记忆后端:初始化完成后填充(worker 隔离,失败则进程内)
|
|
208
246
|
backend: undefined,
|
|
209
247
|
};
|
|
248
|
+
// 惰性缝接线:provider 在检索时才解引用(l1 已就绪);声明序上的"先声明后赋值"
|
|
249
|
+
// 只影响装配期,而装配期不会有检索在飞(启动未完就没有 agent 回合)。
|
|
250
|
+
laneEmbedQuery = (q) => stores.l1.embedText(q);
|
|
210
251
|
if (storageOk) {
|
|
211
252
|
try {
|
|
212
253
|
await Promise.all([
|
|
213
|
-
stores.l0.init(),
|
|
214
254
|
stores.l1.init(),
|
|
215
255
|
stores.scenes.chat.init(),
|
|
216
256
|
stores.scenes.work.init(),
|
|
@@ -255,18 +295,18 @@ export async function apply(ctx, config) {
|
|
|
255
295
|
// 增量:配置变化路径向量表已被 drop,全部记录均"缺失",等效全量;
|
|
256
296
|
// 活切换发起时让路(swap 会 drop 表,并发跑只是白烧嵌入调用)
|
|
257
297
|
const shouldCancel = () => disposed || !!embedManagerRef?.isBusy();
|
|
298
|
+
// T8.4:l0 向量腿随写停退役(表清空后无缺失,无需重建)
|
|
258
299
|
const r1 = await stores.l1.reindex({ shouldCancel });
|
|
259
|
-
const r0 = await stores.l0.reindex({ shouldCancel });
|
|
260
300
|
// missing 复查:嵌入服务未就绪时 reindex 短路 0/0/0——那是"没跑"不是"成功",
|
|
261
301
|
// 不得标记 meta(否则启动链不再补,补齐被无限推迟到 backfill)
|
|
262
|
-
const missingAfter = db.countL1VecMissing(db.getVecSkipSet('l1'))
|
|
263
|
-
if (r1.failed === 0 &&
|
|
302
|
+
const missingAfter = db.countL1VecMissing(db.getVecSkipSet('l1'));
|
|
303
|
+
if (r1.failed === 0 && missingAfter === 0) {
|
|
264
304
|
db.markEmbeddingSynced(currentProviderInfo() ?? info);
|
|
265
|
-
const skipNote = r1.skipped
|
|
266
|
-
logger.info(`[memory] 向量重建完成:L1 ${r1.written}
|
|
305
|
+
const skipNote = r1.skipped > 0 ? `,跳过不可嵌入 ${r1.skipped} 条` : '';
|
|
306
|
+
logger.info(`[memory] 向量重建完成:L1 ${r1.written} 条${skipNote}(原因:${dbInit.reason ?? '配置变化'})`);
|
|
267
307
|
}
|
|
268
308
|
else {
|
|
269
|
-
logger.warn(`[memory] 向量重建未完成(L1 失败 ${r1.failed}
|
|
309
|
+
logger.warn(`[memory] 向量重建未完成(L1 失败 ${r1.failed},剩缺 ${Math.max(missingAfter, 0)} 条),下次启动/补齐周期重试`);
|
|
270
310
|
}
|
|
271
311
|
}
|
|
272
312
|
catch (err) {
|
|
@@ -302,19 +342,18 @@ export async function apply(ctx, config) {
|
|
|
302
342
|
if (embedManagerRef?.isBusy())
|
|
303
343
|
return;
|
|
304
344
|
const l1Missing = db.countL1VecMissing(db.getVecSkipSet('l1')) > 0;
|
|
305
|
-
|
|
306
|
-
if (!l1Missing && !l0Missing)
|
|
345
|
+
if (!l1Missing)
|
|
307
346
|
return;
|
|
308
347
|
const shouldCancel = () => disposed || !!embedManagerRef?.isBusy();
|
|
348
|
+
// T8.4:l0 向量腿随写停退役
|
|
309
349
|
const r1 = await stores.l1.reindex({ shouldCancel });
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
if (r1.failed === 0 && r0.failed === 0 && missingAfter === 0) {
|
|
350
|
+
const missingAfter = db.countL1VecMissing(db.getVecSkipSet('l1'));
|
|
351
|
+
if (r1.failed === 0 && missingAfter === 0) {
|
|
313
352
|
db.markEmbeddingSynced(infoNow);
|
|
314
|
-
logger.info(`[memory] 向量补齐完成:L1 ${r1.written}
|
|
353
|
+
logger.info(`[memory] 向量补齐完成:L1 ${r1.written} 条(跳过不可嵌入 ${r1.skipped} 条)`);
|
|
315
354
|
}
|
|
316
|
-
else if (r1.failed > 0
|
|
317
|
-
logger.warn(`[memory] 向量补齐未完成(L1 失败 ${r1.failed}
|
|
355
|
+
else if (r1.failed > 0) {
|
|
356
|
+
logger.warn(`[memory] 向量补齐未完成(L1 失败 ${r1.failed}),下个周期重试`);
|
|
318
357
|
}
|
|
319
358
|
}
|
|
320
359
|
catch (err) {
|
|
@@ -342,7 +381,6 @@ export async function apply(ctx, config) {
|
|
|
342
381
|
dataDir,
|
|
343
382
|
cfg: config,
|
|
344
383
|
db,
|
|
345
|
-
l0: stores.l0,
|
|
346
384
|
l1: stores.l1,
|
|
347
385
|
sourceStore,
|
|
348
386
|
installer,
|
|
@@ -388,7 +426,7 @@ export async function apply(ctx, config) {
|
|
|
388
426
|
: undefined;
|
|
389
427
|
let flushL0;
|
|
390
428
|
if (storageOk) {
|
|
391
|
-
flushL0 = registerCapture(ctx, config, runner,
|
|
429
|
+
flushL0 = registerCapture(ctx, config, runner, db.l0Watermark, logger, live, modes);
|
|
392
430
|
}
|
|
393
431
|
// 召回使用统计(进 topN 才算 used;热路径只碰内存,去抖落盘)
|
|
394
432
|
const recallUsage = new RecallUsageStore(dataDir, logger);
|
package/dist/pipeline/graph.d.ts
CHANGED
|
@@ -25,6 +25,15 @@ export declare const GRAPH_CONTEXT_EDGE_LIMIT = 120;
|
|
|
25
25
|
export declare function selectContextNodes(records: readonly MemoryRecord[], nodes: readonly GraphNode[], limit?: number): GraphNode[];
|
|
26
26
|
/** 选择进入上下文的边:两端都入选节点的 active 边,按更新时间新→旧,≤120。 */
|
|
27
27
|
export declare function selectContextEdges(nodes: readonly GraphNode[], edges: readonly GraphEdge[], limit?: number): GraphEdge[];
|
|
28
|
+
/**
|
|
29
|
+
* 瞬态失败判据(与永久错误分流,由 GraphStore.fail 选不同重试封顶)。
|
|
30
|
+
*
|
|
31
|
+
* 真机归因(2026-10-20 审计):14 个 dead 全是 `llm aborted: Request was aborted` ×13 +
|
|
32
|
+
* `llm error: terminated` ×1——零校验类失败,全部是网关负载/超时把在飞请求掉断。
|
|
33
|
+
* 这类错误与 prompt/数据无关,退避后重试即可恢复,不该和“模型真的产坏了”同封顶。
|
|
34
|
+
* 匹配串源自 callLLM/callRoute/callDirect 的实际抛出形态(小写子串包含)。
|
|
35
|
+
*/
|
|
36
|
+
export declare function isTransientGraphFailure(msg: string): boolean;
|
|
28
37
|
/**
|
|
29
38
|
* 执行一次图谱投影(claim 一个 job → LLM → 校验落库;无 job 时静默返回)。
|
|
30
39
|
* 返回是否实际认领了任务(泵据此决定是否续排)。失败转 fail(退避/封顶由
|
package/dist/pipeline/graph.js
CHANGED
|
@@ -2,6 +2,7 @@ import { callLLM, parseJsonLogged, resolveLayerTokens } from '../llm.js';
|
|
|
2
2
|
import { buildGraphProjectionPrompt, getGraphProjectionSystemPrompt, } from '../prompts/graph-projection.js';
|
|
3
3
|
import { normalizeEntityName } from '../graph/apply.js';
|
|
4
4
|
import { tokenize } from '../util/text.js';
|
|
5
|
+
import { GRAPH_PROJECTION_CALL_TIMEOUT_MS } from '../graph/types.js';
|
|
5
6
|
import { errDetail } from '../util/filelog.js';
|
|
6
7
|
/** claim 上下文装配的节点上限(词法相关 + 最近更新,总量封顶)。 */
|
|
7
8
|
export const GRAPH_CONTEXT_NODE_LIMIT = 80;
|
|
@@ -64,6 +65,27 @@ export function selectContextEdges(nodes, edges, limit = GRAPH_CONTEXT_EDGE_LIMI
|
|
|
64
65
|
.sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0))
|
|
65
66
|
.slice(0, limit);
|
|
66
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* 瞬态失败判据(与永久错误分流,由 GraphStore.fail 选不同重试封顶)。
|
|
70
|
+
*
|
|
71
|
+
* 真机归因(2026-10-20 审计):14 个 dead 全是 `llm aborted: Request was aborted` ×13 +
|
|
72
|
+
* `llm error: terminated` ×1——零校验类失败,全部是网关负载/超时把在飞请求掉断。
|
|
73
|
+
* 这类错误与 prompt/数据无关,退避后重试即可恢复,不该和“模型真的产坏了”同封顶。
|
|
74
|
+
* 匹配串源自 callLLM/callRoute/callDirect 的实际抛出形态(小写子串包含)。
|
|
75
|
+
*/
|
|
76
|
+
export function isTransientGraphFailure(msg) {
|
|
77
|
+
const low = msg.toLowerCase();
|
|
78
|
+
return (low.includes('aborted') ||
|
|
79
|
+
low.includes('timeout') ||
|
|
80
|
+
low.includes('timed out') ||
|
|
81
|
+
low.includes('terminated') ||
|
|
82
|
+
low.includes('empty output') ||
|
|
83
|
+
low.includes('econn') ||
|
|
84
|
+
low.includes('etimedout') ||
|
|
85
|
+
low.includes('fetch failed') ||
|
|
86
|
+
low.includes('socket hang') ||
|
|
87
|
+
low.includes('network'));
|
|
88
|
+
}
|
|
67
89
|
/**
|
|
68
90
|
* 执行一次图谱投影(claim 一个 job → LLM → 校验落库;无 job 时静默返回)。
|
|
69
91
|
* 返回是否实际认领了任务(泵据此决定是否续排)。失败转 fail(退避/封顶由
|
|
@@ -84,12 +106,17 @@ export async function runGraphProjection(ctx, cfg, graphs, logger) {
|
|
|
84
106
|
maxTokens: resolveLayerTokens(cfg, 'graph'),
|
|
85
107
|
layer: 'graph',
|
|
86
108
|
logger,
|
|
109
|
+
// 自带宽超时信号:不传则沿用 cfg.llm.timeoutMs(120s)——图谱批 prompt 在本地网关上
|
|
110
|
+
// 实测 ~2min/job,正好撞线(14 dead 根因);照 backfill callTimeoutMs 先例给 240s。
|
|
111
|
+
signal: AbortSignal.timeout(GRAPH_PROJECTION_CALL_TIMEOUT_MS),
|
|
87
112
|
});
|
|
88
113
|
graphs.complete(job.id, parseProjection(raw, logger), { now: new Date().toISOString() });
|
|
89
114
|
}
|
|
90
115
|
catch (err) {
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
const detail = errDetail(err);
|
|
117
|
+
logger.warn(`[memory] 图谱投影失败(job=${job.id},attempts=${job.attempts}): ${detail}`);
|
|
118
|
+
// 瞬态(网关掉断/超时/空输出)与永久(校验/解析)分档——前者多给 3 次退避重试机会。
|
|
119
|
+
graphs.fail(job.id, detail, { transient: isTransientGraphFailure(detail) });
|
|
93
120
|
}
|
|
94
121
|
return true;
|
|
95
122
|
}
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
15
15
|
import { type MemoryConfig } from '../config.js';
|
|
16
16
|
import type { LiveSettingsHandle } from '../settings.js';
|
|
17
|
+
import type { DrscClient } from '../clients/drsc.js';
|
|
17
18
|
import type { GraphStore } from '../store/graph-store.js';
|
|
18
|
-
import type { L0Store } from '../store/l0.js';
|
|
19
19
|
import type { L1Store } from '../store/l1.js';
|
|
20
20
|
import type { PersonaStore } from '../store/persona.js';
|
|
21
21
|
import type { SceneStore } from '../store/scenes.js';
|
|
@@ -23,7 +23,11 @@ import type { StateStore } from '../store/state.js';
|
|
|
23
23
|
import type { ConversationMessage, ExtractMode, MemoryFamily, MemoryLogger } from '../types.js';
|
|
24
24
|
import type { FamilyStates } from './l1.js';
|
|
25
25
|
export interface MemoryStores {
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* DRSC 客户端(T8.4 读切):蒸馏背景窗的历史供给面(op=session&tail&redact=1)。
|
|
28
|
+
* 缺席(测试缝/未装配)时背景窗为空数组;拉取失败空窗降级,不抛穿管线。
|
|
29
|
+
*/
|
|
30
|
+
drsc?: DrscClient;
|
|
27
31
|
l1: L1Store;
|
|
28
32
|
scenes: Record<MemoryFamily, SceneStore>;
|
|
29
33
|
persona: Record<MemoryFamily, PersonaStore>;
|
|
@@ -171,10 +175,19 @@ export declare class MemoryRunner {
|
|
|
171
175
|
* 蒸馏尝试时反复整量序列化落盘(多 MB 级 IO);重建轮豁免维持。 */
|
|
172
176
|
private persistPending;
|
|
173
177
|
private runTurn;
|
|
178
|
+
/**
|
|
179
|
+
* 蒸馏背景窗(T8.4 读切,task_19):DRSC 会话尾窗(redact 臂)→ ConversationMessage。
|
|
180
|
+
* - 形状映射:segment 行 → 消息(drscRowToConversationMessage;非 user/assistant 段跳过);
|
|
181
|
+
* - 坐标剔除:切片自身轮次整轮剔除(尾窗在 DRS 30s interval 同步后会含刚捕获
|
|
182
|
+
* 的 turn——旧 l0 读面按消息 id 剔除,跨系统 id 不同构,按坐标等价替身);
|
|
183
|
+
* - 报错口径:collector 缺席/超时 → warn 后空窗降级,**不抛穿蒸馏管线**
|
|
184
|
+
* (工具面 conversation_search 的显式报错口径在 tools 层分清,spec R7)。
|
|
185
|
+
*/
|
|
186
|
+
private drscBackground;
|
|
174
187
|
/**
|
|
175
188
|
* 抽取并消费一个会话切片:成功才把切片移出桶并推进爬坡阈值;失败保留切片待重试。
|
|
176
|
-
* 调用方已保证切片达到生效阈值(或 force)。背景参考按会话从
|
|
177
|
-
*
|
|
189
|
+
* 调用方已保证切片达到生效阈值(或 force)。背景参考按会话从 DRSC 尾窗现查并
|
|
190
|
+
* 剔除切片自身轮次(ADR-0003:会话间互不污染、重启不丢背景)。
|
|
178
191
|
*/
|
|
179
192
|
private extractSessionSlice;
|
|
180
193
|
}
|
package/dist/pipeline/runner.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { resolveDataDir } from '../config.js';
|
|
2
2
|
import { GRAPH_PRIORITY_NEW } from '../graph/types.js';
|
|
3
|
+
import { drscRowToConversationMessage } from '../clients/drsc.js';
|
|
3
4
|
import { trace } from '../store/trace.js';
|
|
4
5
|
import { emptyPending, freshWarmup, groupPendingBySession, loadPending, PENDING_MODES, pendingPathFor, savePending, } from '../store/pending.js';
|
|
5
6
|
import { errDetail } from '../util/filelog.js';
|
|
6
7
|
import { sessionWorkspaceIdOf } from '../workspace.js';
|
|
7
|
-
import { advanceWarmupThreshold, effectiveExtractThreshold, extractionBackoffMs, idleSessionsToFlush, modeSwitchAction,
|
|
8
|
+
import { advanceWarmupThreshold, effectiveExtractThreshold, extractionBackoffMs, idleSessionsToFlush, modeSwitchAction, } from './trigger.js';
|
|
8
9
|
import { runExtraction } from './l1.js';
|
|
9
10
|
import { buildAnchorMap } from './anchors.js';
|
|
10
11
|
import { runGraphProjection } from './graph.js';
|
|
@@ -666,10 +667,34 @@ export class MemoryRunner {
|
|
|
666
667
|
this.afterRun?.();
|
|
667
668
|
return newRecords.length;
|
|
668
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* 蒸馏背景窗(T8.4 读切,task_19):DRSC 会话尾窗(redact 臂)→ ConversationMessage。
|
|
672
|
+
* - 形状映射:segment 行 → 消息(drscRowToConversationMessage;非 user/assistant 段跳过);
|
|
673
|
+
* - 坐标剔除:切片自身轮次整轮剔除(尾窗在 DRS 30s interval 同步后会含刚捕获
|
|
674
|
+
* 的 turn——旧 l0 读面按消息 id 剔除,跨系统 id 不同构,按坐标等价替身);
|
|
675
|
+
* - 报错口径:collector 缺席/超时 → warn 后空窗降级,**不抛穿蒸馏管线**
|
|
676
|
+
* (工具面 conversation_search 的显式报错口径在 tools 层分清,spec R7)。
|
|
677
|
+
*/
|
|
678
|
+
async drscBackground(sessionId, n, sliceTurns) {
|
|
679
|
+
if (n <= 0)
|
|
680
|
+
return [];
|
|
681
|
+
try {
|
|
682
|
+
const rows = await this.stores.drsc.sessionTail(sessionId, n);
|
|
683
|
+
return rows
|
|
684
|
+
.map((r) => drscRowToConversationMessage(r, sessionId))
|
|
685
|
+
.filter((m) => m !== undefined)
|
|
686
|
+
.filter((m) => m.anchor === undefined || !sliceTurns.has(m.anchor.turn))
|
|
687
|
+
.slice(-n);
|
|
688
|
+
}
|
|
689
|
+
catch (err) {
|
|
690
|
+
this.logger.warn(`[memory] 蒸馏背景窗拉取失败(DRSC),本轮空窗降级: ${errDetail(err)}`);
|
|
691
|
+
return [];
|
|
692
|
+
}
|
|
693
|
+
}
|
|
669
694
|
/**
|
|
670
695
|
* 抽取并消费一个会话切片:成功才把切片移出桶并推进爬坡阈值;失败保留切片待重试。
|
|
671
|
-
* 调用方已保证切片达到生效阈值(或 force)。背景参考按会话从
|
|
672
|
-
*
|
|
696
|
+
* 调用方已保证切片达到生效阈值(或 force)。背景参考按会话从 DRSC 尾窗现查并
|
|
697
|
+
* 剔除切片自身轮次(ADR-0003:会话间互不污染、重启不丢背景)。
|
|
673
698
|
*/
|
|
674
699
|
async extractSessionSlice(sessionId, mode, cfg, effectiveThreshold, opts) {
|
|
675
700
|
const bucket = this.pending[mode];
|
|
@@ -678,9 +703,11 @@ export class MemoryRunner {
|
|
|
678
703
|
return [];
|
|
679
704
|
const rest = bucket.filter((m) => m.sessionId !== sessionId);
|
|
680
705
|
try {
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
706
|
+
// 背景参考(T8.4 读切):DRSC 会话尾窗(redact 臂)按会话现查(ADR-0003 等价),
|
|
707
|
+
// 剔除切片自身轮次(切片消息可能已进 DRS 尾窗——30s interval 同步后)。
|
|
708
|
+
// 多取切片条数补偿被剔除者,与旧 l0 读面同口径。
|
|
709
|
+
const background = this.stores.drsc
|
|
710
|
+
? await this.drscBackground(sessionId, cfg.extract.backgroundMessages + slice.length, new Set(slice.map((m) => m.anchor?.turn).filter((t) => typeof t === 'number')))
|
|
684
711
|
: [];
|
|
685
712
|
const t = Date.now();
|
|
686
713
|
// R7:锚点映射同时收切片与背景——模型偶尔会引用背景消息的 id,收了它
|
package/dist/stats.js
CHANGED
|
@@ -107,6 +107,8 @@ export const MEMORY_ENDPOINTS = [
|
|
|
107
107
|
'dsh-memory/conflicts-rejected',
|
|
108
108
|
'dsh-memory/graph-search',
|
|
109
109
|
'dsh-memory/graph-node-get',
|
|
110
|
+
'dsh-memory/graph-preview',
|
|
111
|
+
'dsh-memory/graph-cleanup-dead',
|
|
110
112
|
'dsh-memory/scenes',
|
|
111
113
|
'dsh-memory/persona',
|
|
112
114
|
'dsh-memory/log-tail',
|
|
@@ -474,6 +476,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
474
476
|
return { sessionId, records: [], typeCatalog: typeCatalogEntries() };
|
|
475
477
|
// 压缩水位之上的那批:正文已被摘要取代。与 retired 正交(真实实现见 session-recalled.ts)。
|
|
476
478
|
const superseded = stores.recalled?.supersededOf(sessionId);
|
|
479
|
+
// 路归因表(record id → 管道串):不在表里 = 无从归因,不挂 lanes 字段。
|
|
480
|
+
const laneTab = stores.recalled?.lanesOf(sessionId);
|
|
477
481
|
const halfLife = effectiveCfg(cfg, live).recall?.decayHalfLifeDays ?? 0;
|
|
478
482
|
const byId = new Map(stores.l1.getByIds(ids).map((r) => [r.id, r]));
|
|
479
483
|
const records = ids
|
|
@@ -486,7 +490,11 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
486
490
|
userRelevance: stores.feedback?.meanOf(r.id) ?? null,
|
|
487
491
|
userMark: stores.feedback?.lastOf(r.id) ?? null,
|
|
488
492
|
});
|
|
489
|
-
|
|
493
|
+
let out = superseded?.has(r.id) === true ? { ...ui, superseded: true } : ui;
|
|
494
|
+
const laneStr = laneTab?.get(r.id);
|
|
495
|
+
if (laneStr !== undefined && laneStr.length > 0)
|
|
496
|
+
out = { ...out, lanes: laneStr.split('|') };
|
|
497
|
+
return out;
|
|
490
498
|
});
|
|
491
499
|
return { sessionId, records, typeCatalog: typeCatalogEntries() };
|
|
492
500
|
}
|
|
@@ -2019,6 +2027,27 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
2019
2027
|
return { node: null, edges: [] };
|
|
2020
2028
|
return { node, edges: graph.edgesOf(id) };
|
|
2021
2029
|
}
|
|
2030
|
+
case 'dsh-memory/graph-preview': {
|
|
2031
|
+
// 面板图谱预览:缺省种子推荐榜,seedId 给定展开该邻域(悬挂/不可见→空 seeds 不抛)。
|
|
2032
|
+
// mode==='full' 附全图轻量块(大图星图);非 'full' 一律按缺省——无 full 键零漂移。
|
|
2033
|
+
const p = (payload ?? {});
|
|
2034
|
+
const seedId = typeof p.seedId === 'string' && p.seedId.length > 0 ? p.seedId.slice(0, 200) : undefined;
|
|
2035
|
+
const mode = p.mode === 'full' ? 'full' : undefined;
|
|
2036
|
+
const graph = stores.graph;
|
|
2037
|
+
if (!graph) {
|
|
2038
|
+
const zero = { seeds: [], stats: { nodes: 0, edges: 0, causalEdges: 0, pendingJobs: 0, jobsTotal: 0 } };
|
|
2039
|
+
return mode === 'full' ? { ...zero, full: { nodes: [], edges: [], truncated: false } } : zero;
|
|
2040
|
+
}
|
|
2041
|
+
return graph.buildPreview(seedId, mode);
|
|
2042
|
+
}
|
|
2043
|
+
case 'dsh-memory/graph-cleanup-dead': {
|
|
2044
|
+
const graph = stores.graph;
|
|
2045
|
+
if (!graph)
|
|
2046
|
+
return { removed: 0 };
|
|
2047
|
+
const p = (payload ?? {});
|
|
2048
|
+
const ms = typeof p.olderThanMs === 'number' && p.olderThanMs > 0 ? p.olderThanMs : undefined;
|
|
2049
|
+
return { removed: graph.cleanupDead(ms) };
|
|
2050
|
+
}
|
|
2022
2051
|
case 'dsh-memory/scenes': {
|
|
2023
2052
|
// 两族拼接展示(浏览器保持混合视图;路径冲突时后写入的族覆盖显示名,读取仍各自独立)
|
|
2024
2053
|
const items = [];
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { MemoryConfig } from '../config.js';
|
|
2
2
|
import type { MemoryLogger } from '../types.js';
|
|
3
3
|
import type { EmbeddingProviderInfo, EmbeddingService } from './embedding.js';
|
|
4
|
-
import type { L0Store } from './l0.js';
|
|
5
4
|
import type { L1Store } from './l1.js';
|
|
6
5
|
import { LocalEmbeddingService } from './local-embedding.js';
|
|
7
6
|
import { ModelDownloadQueue } from './download-queue.js';
|
|
@@ -74,7 +73,6 @@ export interface EmbeddingManagerDeps {
|
|
|
74
73
|
* 否则设置页里的编辑即时生效不到活切换。 */
|
|
75
74
|
getEff?: () => MemoryConfig;
|
|
76
75
|
db: MemoryDb;
|
|
77
|
-
l0: L0Store;
|
|
78
76
|
l1: L1Store;
|
|
79
77
|
sourceStore: EmbeddingSourceStore;
|
|
80
78
|
installer: RuntimeInstaller;
|
|
@@ -155,7 +153,7 @@ export declare class EmbeddingManager {
|
|
|
155
153
|
* 门槛全部前置,且**宁可拒绝也不谎报**:以下四种情况直接抛错而非"成功受理":
|
|
156
154
|
*
|
|
157
155
|
* - 插件已卸载 / 重嵌已在跑 / 嵌入源切换占着锁:并发语义,重复触发无意义;
|
|
158
|
-
* - 嵌入服务未就绪:`L1Store.reindex`
|
|
156
|
+
* - 嵌入服务未就绪:`L1Store.reindex` 会**静默短路**成
|
|
159
157
|
* `0/0/0`,受理了就等于告诉用户"重建成功、零条待补"——而真相是它根本没开始。
|
|
160
158
|
*
|
|
161
159
|
* 用抛错而不是返回 `{accepted:false, error}`:与 `embedding-model-delete` 等既有
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* embedding.allowLocalModels 上限约束;
|
|
9
9
|
* - 活切换链(后台执行,RPC 立即返回 accepted,进度靠轮询):
|
|
10
10
|
* 安装运行时(首次本地)→ 预热加载 → 换服务 + swapProvider(维度变化 drop 向量表)
|
|
11
|
-
* → 后台全量重嵌(L1
|
|
11
|
+
* → 后台全量重嵌(L1 计数进度,可取消;l0 计数进度字段随契约保留恒 0)→ 持久化状态。
|
|
12
12
|
* 失败语义:异常(安装失败/模型加载失败/db 拒绝)→ 状态不持久化,重启回到旧源;
|
|
13
13
|
* 重嵌取消/部分失败 → 已切换(物理表即新维度,meta 已同步),缺失向量由周期
|
|
14
14
|
* backfill 补齐——不回滚(回滚需要再 drop 一次表,得不偿失)。
|
|
@@ -330,7 +330,7 @@ export class EmbeddingManager {
|
|
|
330
330
|
* 门槛全部前置,且**宁可拒绝也不谎报**:以下四种情况直接抛错而非"成功受理":
|
|
331
331
|
*
|
|
332
332
|
* - 插件已卸载 / 重嵌已在跑 / 嵌入源切换占着锁:并发语义,重复触发无意义;
|
|
333
|
-
* - 嵌入服务未就绪:`L1Store.reindex`
|
|
333
|
+
* - 嵌入服务未就绪:`L1Store.reindex` 会**静默短路**成
|
|
334
334
|
* `0/0/0`,受理了就等于告诉用户"重建成功、零条待补"——而真相是它根本没开始。
|
|
335
335
|
*
|
|
336
336
|
* 用抛错而不是返回 `{accepted:false, error}`:与 `embedding-model-delete` 等既有
|
|
@@ -345,7 +345,7 @@ export class EmbeddingManager {
|
|
|
345
345
|
throw new Error('嵌入源切换进行中,请稍后再试');
|
|
346
346
|
if (!this.currentInfo)
|
|
347
347
|
throw new Error('嵌入源已关闭,重建无意义(请先启用嵌入)');
|
|
348
|
-
if (!this.deps.l1.vectorsReady()
|
|
348
|
+
if (!this.deps.l1.vectorsReady()) {
|
|
349
349
|
throw new Error('嵌入服务未就绪(模型加载中或推理运行时未安装),请稍后再试');
|
|
350
350
|
}
|
|
351
351
|
void this.reindexNow()
|
|
@@ -435,7 +435,6 @@ export class EmbeddingManager {
|
|
|
435
435
|
const oldLocal = this.localSvc;
|
|
436
436
|
this.localSvc = next.source === 'local' ? svc : null;
|
|
437
437
|
this.current = svc;
|
|
438
|
-
this.deps.l0.setEmbeddingService(svc);
|
|
439
438
|
this.deps.l1.setEmbeddingService(svc);
|
|
440
439
|
oldLocal?.close();
|
|
441
440
|
let pendingNote = '';
|
|
@@ -510,15 +509,9 @@ export class EmbeddingManager {
|
|
|
510
509
|
shouldCancel: () => this.reindexCancel,
|
|
511
510
|
});
|
|
512
511
|
failedTotal += r1.failed;
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
this.reindex.l0Total = total;
|
|
517
|
-
},
|
|
518
|
-
shouldCancel: () => this.reindexCancel,
|
|
519
|
-
});
|
|
520
|
-
failedTotal += r0.failed;
|
|
521
|
-
const cancelled = !!(r1.cancelled || r0.cancelled);
|
|
512
|
+
// T8.4:l0 向量腿随写停退役(表清空后无缺失,无需重建);契约的
|
|
513
|
+
// l0Done/l0Total 字段保留恒 0,client 进度面零改动。
|
|
514
|
+
const cancelled = !!r1.cancelled;
|
|
522
515
|
this.reindex.cancelled = cancelled;
|
|
523
516
|
this.reindex.running = false;
|
|
524
517
|
this.invalidateVecCache(); // 收尾:计数必须立刻反映重建结果(TTL 可能还压着 1s 旧值)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { buildFullView } from '../graph/preview.js';
|
|
3
|
+
import type { GraphPreviewResult } from '../graph/preview.js';
|
|
2
4
|
import type { GraphEdge, GraphNode, GraphNodeSearchResult, GraphProjectionJob, GraphProjectionResult } from '../graph/types.js';
|
|
3
5
|
import type { MemoryFamily, MemoryLogger, MemoryRecord } from '../types.js';
|
|
4
6
|
/**
|
|
@@ -99,6 +101,37 @@ export declare class GraphStore {
|
|
|
99
101
|
* 宁可漏不可串)。降级/异常返回空数组。
|
|
100
102
|
*/
|
|
101
103
|
searchNodes(query: string, limit: number, families?: readonly MemoryFamily[]): GraphNodeSearchResult[];
|
|
104
|
+
/**
|
|
105
|
+
* §D 第 3 路存储缝(回链记录 id 序;graphLane 直接消费):词法命中 ∪ 路内向量种子
|
|
106
|
+
* → 1-hop 边扩展 → 保序去重的 L1 记录 id 序列。
|
|
107
|
+
*
|
|
108
|
+
* - **向量种子是路内并集、不是新路**:embedding 由调用方传入(装配层惰性查
|
|
109
|
+
* l1.embedText;未就绪/失败传 undefined = 纯词法路,与旧 searchNodes+直连回链
|
|
110
|
+
* 逐位一致);并集不占 RRF 路数名额,"未接线 = 恰为 2 路"不变量不动;
|
|
111
|
+
* - **种子序**:词法命中在前、向量种子追加在后(RRF 用 rank,顺序即权重),
|
|
112
|
+
* 同节点去重;向量种子同样过 visible 族集与 active|disputed 状态闸(宁可漏不可串);
|
|
113
|
+
* - **1-hop 邻居不按族预筛**:来源记录的跨族由 graphLane 尾部记录级 family/scope
|
|
114
|
+
* 双闸兜底(与本路既有纪律同源);
|
|
115
|
+
* - 降级(db 未就绪/空图)返回空数组,不抛。
|
|
116
|
+
*
|
|
117
|
+
* 返回记录 id 而非 hits 是刻意的:回链/扩展属于读路径细节,路侧只需排名序列。
|
|
118
|
+
*/
|
|
119
|
+
searchLaneRecords(query: string, embedding: Float32Array | undefined, limit: number, families?: readonly MemoryFamily[]): readonly string[];
|
|
120
|
+
/**
|
|
121
|
+
* 面板图谱预览存储缝(契约 `dsh-memory/graph-preview` 的数据源):
|
|
122
|
+
* loadGraph 一次 + job 计数(pending/running/failed 未定项与全量分母)传入纯函数。
|
|
123
|
+
* 降级(db 未就绪)返**全零结构**不抛——预览是展示面,永不拖垮端点。
|
|
124
|
+
* mode==='full' 时响应附 `full` 轻量块;**缺省时该键缺席**(可选键零漂移纪律,
|
|
125
|
+
* 不是 undefined 占位——消费方 `'full' in res` 与 JSON 形状双判据都干净)。
|
|
126
|
+
*/
|
|
127
|
+
buildPreview(seedId?: string, mode?: 'full'): GraphPreviewResult & {
|
|
128
|
+
full?: ReturnType<typeof buildFullView>;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* 清理历史 dead 作业行(纯审计痕迹删除,不影响已重入队的记录)。
|
|
132
|
+
* olderThanMs: 只删早于 now-olderThanMs 的;缺省删全部 dead。
|
|
133
|
+
*/
|
|
134
|
+
cleanupDead(olderThanMs?: number): number;
|
|
102
135
|
private newJobId;
|
|
103
136
|
/**
|
|
104
137
|
* 投影入队(按 GRAPH_JOB_BATCH 分片成多个 job;去重下推 SQL):
|
|
@@ -134,10 +167,14 @@ export declare class GraphStore {
|
|
|
134
167
|
*/
|
|
135
168
|
complete(jobId: string, result: GraphProjectionResult, opts?: CompleteOptions): void;
|
|
136
169
|
/**
|
|
137
|
-
* 投影失败收尾:attempts 已在 claim 时 +1
|
|
138
|
-
*
|
|
170
|
+
* 投影失败收尾:attempts 已在 claim 时 +1——根据错误分类选不同封顶:
|
|
171
|
+
* - 永久错误(校验/解析):attempts >= GRAPH_JOB_MAX_ATTEMPTS(3) → dead
|
|
172
|
+
* - 瞬态错误(aborted/timeout/网络):attempts >= GRAPH_JOB_TRANSIENT_MAX_ATTEMPTS(6) → dead
|
|
173
|
+
* dead 放掉 mapping(允许下次 backfill 重新入队);未封顶转 failed + 指数退避。
|
|
139
174
|
*/
|
|
140
|
-
fail(jobId: string, error: string
|
|
175
|
+
fail(jobId: string, error: string, opts?: {
|
|
176
|
+
transient?: boolean;
|
|
177
|
+
}): void;
|
|
141
178
|
/** 启动回收:上次进程退出时卡在 running 的 job 放回 pending(dispose 缝不永久卡批)。 */
|
|
142
179
|
recoverRunning(): number;
|
|
143
180
|
/** 最近 job 列表(诊断/面板用;未就绪返回空)。 */
|