dsh-prime-memory 0.26.3 → 0.26.5-beta.0
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/clients/drsc.d.ts +93 -0
- package/dist/clients/drsc.js +139 -0
- package/dist/drs-anchor-backfill.d.ts +88 -0
- package/dist/drs-anchor-backfill.js +150 -0
- package/dist/hooks/capture.d.ts +4 -4
- package/dist/hooks/capture.js +34 -27
- package/dist/hooks/recall.d.ts +11 -0
- package/dist/hooks/recall.js +25 -2
- package/dist/index.js +43 -18
- package/dist/pipeline/runner.d.ts +17 -4
- package/dist/pipeline/runner.js +33 -6
- package/dist/store/embedding-source.d.ts +1 -3
- package/dist/store/embedding-source.js +6 -13
- 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/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/dsh.plugin.json +1 -1
- package/package.json +8 -4
package/dist/hooks/recall.js
CHANGED
|
@@ -64,6 +64,20 @@ export function ownInjectionFromEvent(ev) {
|
|
|
64
64
|
chars,
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* 会话事件统一读形状(issue #8):旧宿主 `session.events` 是数组属性;
|
|
69
|
+
* 0.2.x 宿主已删该属性,官方访问器是 `snapshotEvents()`(全量日志,seq 与日志
|
|
70
|
+
* 位置对齐,含 fork 前缀——与旧属性同口径)。两者都拿不到时返回 undefined,
|
|
71
|
+
* 由调用点的既有退路处置(粗标记 / 存储回填);不假设形状,不抛。
|
|
72
|
+
* 导出供双形状单测钉住。
|
|
73
|
+
*/
|
|
74
|
+
export function readSessionEvents(session) {
|
|
75
|
+
if (Array.isArray(session.events))
|
|
76
|
+
return session.events;
|
|
77
|
+
if (typeof session.snapshotEvents === 'function')
|
|
78
|
+
return session.snapshotEvents();
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
67
81
|
export function buildRecallQuery(messages, tailMessages = RECALL_QUERY_TAIL_MESSAGES, maxChars = RECALL_QUERY_MAX_CHARS) {
|
|
68
82
|
const tail = messages.slice(-tailMessages);
|
|
69
83
|
let text = tail.map((m) => blocksToText(m.content)).join(' ').trim();
|
|
@@ -572,14 +586,18 @@ usage) {
|
|
|
572
586
|
return false;
|
|
573
587
|
try {
|
|
574
588
|
// 可选服务一律走 ctx.get() 的宽容路径(属性访问对未 inject 的服务会抛)
|
|
589
|
+
// events 形状双面:旧宿主数组属性 / 0.2.x snapshotEvents() 方法(issue #8)
|
|
575
590
|
const sessions = ctx.get?.('sessions');
|
|
576
591
|
const session = typeof sessions?.get === 'function' ? sessions.get(sessionId) : undefined;
|
|
577
592
|
if (session === undefined)
|
|
578
593
|
return false;
|
|
594
|
+
const events = readSessionEvents(session);
|
|
595
|
+
if (events === undefined)
|
|
596
|
+
return false; // 事件面不可得:调用方回退粗标记
|
|
579
597
|
const visibleSeqs = new Set(session.surface.nodes);
|
|
580
598
|
const visibleIds = new Set();
|
|
581
599
|
const knownIds = new Set();
|
|
582
|
-
for (const ev of
|
|
600
|
+
for (const ev of events) {
|
|
583
601
|
const own = ownInjectionFromEvent(ev);
|
|
584
602
|
if (own === undefined || own.id.length === 0)
|
|
585
603
|
continue;
|
|
@@ -633,12 +651,17 @@ usage) {
|
|
|
633
651
|
try {
|
|
634
652
|
// cordis 属性访问(ctx.sessions)对未 inject 的服务抛 "without inject";
|
|
635
653
|
// 可选服务一律走 ctx.get() 的宽容路径
|
|
654
|
+
// events 形状双面:旧宿主数组属性 / 0.2.x snapshotEvents() 方法(issue #8)
|
|
636
655
|
const sessions = ctx.get?.('sessions');
|
|
637
656
|
const session = typeof sessions?.get === 'function' ? sessions.get(sessionId) : undefined;
|
|
638
657
|
if (session) {
|
|
658
|
+
const events = readSessionEvents(session);
|
|
659
|
+
// 事件形状不可得:与「不在 live store」同一退路,走存储回填,不再静默归 null
|
|
660
|
+
if (events === undefined)
|
|
661
|
+
return estimateRecallFromStorage(sessionId);
|
|
639
662
|
const visible = new Set(session.surface.nodes);
|
|
640
663
|
let total = 0;
|
|
641
|
-
for (const ev of
|
|
664
|
+
for (const ev of events) {
|
|
642
665
|
const own = ownInjectionFromEvent(ev);
|
|
643
666
|
// 无 seq = 无法证明它可见,就不能算进份额(与原行为一致:原来它必须 visible.has(seq))。
|
|
644
667
|
if (own === undefined || own.chars === 0 || own.seq === undefined || !visible.has(own.seq))
|
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 时按**调用方
|
|
@@ -172,7 +197,10 @@ export async function apply(ctx, config) {
|
|
|
172
197
|
}
|
|
173
198
|
const stores = {
|
|
174
199
|
usage: undefined, // 装配于下方(依赖 dataDir 就绪)
|
|
175
|
-
|
|
200
|
+
// DRSC 客户端(T8.4 读切):蒸馏背景窗 + conversation_search 工具面的历史供给。
|
|
201
|
+
// 硬依赖无本地回退(l0 表退役);trustedHosts 惰性读取(webStartup 可能晚到)。
|
|
202
|
+
drsc: new DrscClient({ trustedHosts: () => readStartupTrustedHosts(ctx) }),
|
|
203
|
+
l0: new L0Store(db),
|
|
176
204
|
l1: new L1Store(dataDir, db, embed, config.recall.strategy, logger, config.recall.decayHalfLifeDays,
|
|
177
205
|
// §D 第 3 路:图谱回链。图谱不可用时 searchNodes 自带 no-op,不影响双路。
|
|
178
206
|
(query, limit, family) => db.graphStore.searchNodes(query, limit, family ? [family] : undefined),
|
|
@@ -210,7 +238,6 @@ export async function apply(ctx, config) {
|
|
|
210
238
|
if (storageOk) {
|
|
211
239
|
try {
|
|
212
240
|
await Promise.all([
|
|
213
|
-
stores.l0.init(),
|
|
214
241
|
stores.l1.init(),
|
|
215
242
|
stores.scenes.chat.init(),
|
|
216
243
|
stores.scenes.work.init(),
|
|
@@ -255,18 +282,18 @@ export async function apply(ctx, config) {
|
|
|
255
282
|
// 增量:配置变化路径向量表已被 drop,全部记录均"缺失",等效全量;
|
|
256
283
|
// 活切换发起时让路(swap 会 drop 表,并发跑只是白烧嵌入调用)
|
|
257
284
|
const shouldCancel = () => disposed || !!embedManagerRef?.isBusy();
|
|
285
|
+
// T8.4:l0 向量腿随写停退役(表清空后无缺失,无需重建)
|
|
258
286
|
const r1 = await stores.l1.reindex({ shouldCancel });
|
|
259
|
-
const r0 = await stores.l0.reindex({ shouldCancel });
|
|
260
287
|
// missing 复查:嵌入服务未就绪时 reindex 短路 0/0/0——那是"没跑"不是"成功",
|
|
261
288
|
// 不得标记 meta(否则启动链不再补,补齐被无限推迟到 backfill)
|
|
262
|
-
const missingAfter = db.countL1VecMissing(db.getVecSkipSet('l1'))
|
|
263
|
-
if (r1.failed === 0 &&
|
|
289
|
+
const missingAfter = db.countL1VecMissing(db.getVecSkipSet('l1'));
|
|
290
|
+
if (r1.failed === 0 && missingAfter === 0) {
|
|
264
291
|
db.markEmbeddingSynced(currentProviderInfo() ?? info);
|
|
265
|
-
const skipNote = r1.skipped
|
|
266
|
-
logger.info(`[memory] 向量重建完成:L1 ${r1.written}
|
|
292
|
+
const skipNote = r1.skipped > 0 ? `,跳过不可嵌入 ${r1.skipped} 条` : '';
|
|
293
|
+
logger.info(`[memory] 向量重建完成:L1 ${r1.written} 条${skipNote}(原因:${dbInit.reason ?? '配置变化'})`);
|
|
267
294
|
}
|
|
268
295
|
else {
|
|
269
|
-
logger.warn(`[memory] 向量重建未完成(L1 失败 ${r1.failed}
|
|
296
|
+
logger.warn(`[memory] 向量重建未完成(L1 失败 ${r1.failed},剩缺 ${Math.max(missingAfter, 0)} 条),下次启动/补齐周期重试`);
|
|
270
297
|
}
|
|
271
298
|
}
|
|
272
299
|
catch (err) {
|
|
@@ -302,19 +329,18 @@ export async function apply(ctx, config) {
|
|
|
302
329
|
if (embedManagerRef?.isBusy())
|
|
303
330
|
return;
|
|
304
331
|
const l1Missing = db.countL1VecMissing(db.getVecSkipSet('l1')) > 0;
|
|
305
|
-
|
|
306
|
-
if (!l1Missing && !l0Missing)
|
|
332
|
+
if (!l1Missing)
|
|
307
333
|
return;
|
|
308
334
|
const shouldCancel = () => disposed || !!embedManagerRef?.isBusy();
|
|
335
|
+
// T8.4:l0 向量腿随写停退役
|
|
309
336
|
const r1 = await stores.l1.reindex({ shouldCancel });
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
if (r1.failed === 0 && r0.failed === 0 && missingAfter === 0) {
|
|
337
|
+
const missingAfter = db.countL1VecMissing(db.getVecSkipSet('l1'));
|
|
338
|
+
if (r1.failed === 0 && missingAfter === 0) {
|
|
313
339
|
db.markEmbeddingSynced(infoNow);
|
|
314
|
-
logger.info(`[memory] 向量补齐完成:L1 ${r1.written}
|
|
340
|
+
logger.info(`[memory] 向量补齐完成:L1 ${r1.written} 条(跳过不可嵌入 ${r1.skipped} 条)`);
|
|
315
341
|
}
|
|
316
|
-
else if (r1.failed > 0
|
|
317
|
-
logger.warn(`[memory] 向量补齐未完成(L1 失败 ${r1.failed}
|
|
342
|
+
else if (r1.failed > 0) {
|
|
343
|
+
logger.warn(`[memory] 向量补齐未完成(L1 失败 ${r1.failed}),下个周期重试`);
|
|
318
344
|
}
|
|
319
345
|
}
|
|
320
346
|
catch (err) {
|
|
@@ -342,7 +368,6 @@ export async function apply(ctx, config) {
|
|
|
342
368
|
dataDir,
|
|
343
369
|
cfg: config,
|
|
344
370
|
db,
|
|
345
|
-
l0: stores.l0,
|
|
346
371
|
l1: stores.l1,
|
|
347
372
|
sourceStore,
|
|
348
373
|
installer,
|
|
@@ -388,7 +413,7 @@ export async function apply(ctx, config) {
|
|
|
388
413
|
: undefined;
|
|
389
414
|
let flushL0;
|
|
390
415
|
if (storageOk) {
|
|
391
|
-
flushL0 = registerCapture(ctx, config, runner,
|
|
416
|
+
flushL0 = registerCapture(ctx, config, runner, db.l0Watermark, logger, live, modes);
|
|
392
417
|
}
|
|
393
418
|
// 召回使用统计(进 topN 才算 used;热路径只碰内存,去抖落盘)
|
|
394
419
|
const recallUsage = new RecallUsageStore(dataDir, logger);
|
|
@@ -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,收了它
|
|
@@ -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 旧值)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L0 水位表(T8.4 l0 写停,drsc-redact-memory-wiring task_16)。
|
|
3
|
+
*
|
|
4
|
+
* capture 停写 l0_conversations/l0_fts/l0_vec 三表后,§A 崩溃恢复的幂等判据
|
|
5
|
+
* (maxCapturedTurn / hasAnyL0Message)失去数据源——本小表接住:每会话一行,
|
|
6
|
+
* 只存 (max_turn, last_step),几十 KB 量级。正文改由 DRSC 投影供读(历史面),
|
|
7
|
+
* 水位只服务「重启不重放旧 turn」的自检,不再承载任何文本。
|
|
8
|
+
*
|
|
9
|
+
* 语义对齐被退役的 SQL 口径:
|
|
10
|
+
* - `maxTurn(sid)` ≡ 退役前 `MAX(turn)`;只前进不回退(重放/乱序登记无害);
|
|
11
|
+
* - `hasAny(sid, t)` ≡ 退役前 `t <= MAX(turn)` 的存在性判定——恢复窗口
|
|
12
|
+
* (foldRecoverableTurns 只产出 turn > 水位 的括号)内恒 false,行为与
|
|
13
|
+
* MAX 口径等价;写停后正文无「部分写残行」,逐 turn 点查的原始动机消失。
|
|
14
|
+
*
|
|
15
|
+
* 基线物化:写停生效前已存在的 l0_conversations 行由 {@link ensureBaseline}
|
|
16
|
+
* 一次性灌入(INSERT OR IGNORE,存量水位行不覆盖)——没有它,升级后的首次
|
|
17
|
+
* resume 会把水位当空、重放旧 turn 重复蒸馏。每次 init 幂等重跑(空表扫描
|
|
18
|
+
* 零成本;task_22 存量清退删表后同样安静)。
|
|
19
|
+
*
|
|
20
|
+
* 与 CostLedger/GraphStore 同款纪律:独立职责类,唯一耦合是共享 node:sqlite
|
|
21
|
+
* 连接;未 init / 降级时全部方法安全 no-op。
|
|
22
|
+
*/
|
|
23
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
24
|
+
import type { MemoryLogger } from '../types.js';
|
|
25
|
+
export declare const L0_WATERMARK_DDL = "\nCREATE TABLE IF NOT EXISTS l0_watermark (\n session_id TEXT PRIMARY KEY,\n max_turn INTEGER NOT NULL DEFAULT 0,\n last_step INTEGER,\n updated_at TEXT DEFAULT (datetime('now'))\n)\n";
|
|
26
|
+
export declare class L0WatermarkStore {
|
|
27
|
+
private db;
|
|
28
|
+
private logger;
|
|
29
|
+
init(db: DatabaseSync, logger?: MemoryLogger): void;
|
|
30
|
+
close(): void;
|
|
31
|
+
/**
|
|
32
|
+
* 基线物化:从现存 l0_conversations 把每会话最大 turn 灌进水位表。
|
|
33
|
+
* INSERT OR IGNORE = 已有水位行(活捕获已推进)一律不覆盖;旧行文与
|
|
34
|
+
* 水位并存期两侧取大者由 record 的单调语义兜底。
|
|
35
|
+
*/
|
|
36
|
+
ensureBaseline(): void;
|
|
37
|
+
/**
|
|
38
|
+
* 登记一个已捕获轮次(写停后 capture 的唯一落盘点)。单调:turn 更小/相等的
|
|
39
|
+
* 登记不回退水位(恢复对账与乱序事件都安全);step 取该轮已知最大 step。
|
|
40
|
+
*/
|
|
41
|
+
record(sessionId: string, turn: number, step?: number): void;
|
|
42
|
+
/** §A 水位线:该会话已捕获的最大 turn(空会话/未 init 返回 undefined)。 */
|
|
43
|
+
maxTurn(sessionId: string): number | undefined;
|
|
44
|
+
/** §A 幂等检查:该 (session, turn) 是否已捕获(≡ 退役前 hasAnyL0Message)。 */
|
|
45
|
+
hasAny(sessionId: string, turn: number): boolean;
|
|
46
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const L0_WATERMARK_DDL = `
|
|
2
|
+
CREATE TABLE IF NOT EXISTS l0_watermark (
|
|
3
|
+
session_id TEXT PRIMARY KEY,
|
|
4
|
+
max_turn INTEGER NOT NULL DEFAULT 0,
|
|
5
|
+
last_step INTEGER,
|
|
6
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
7
|
+
)
|
|
8
|
+
`;
|
|
9
|
+
export class L0WatermarkStore {
|
|
10
|
+
db = null;
|
|
11
|
+
logger;
|
|
12
|
+
init(db, logger) {
|
|
13
|
+
this.db = db;
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
db.exec(L0_WATERMARK_DDL);
|
|
16
|
+
this.ensureBaseline();
|
|
17
|
+
}
|
|
18
|
+
close() {
|
|
19
|
+
this.db = null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 基线物化:从现存 l0_conversations 把每会话最大 turn 灌进水位表。
|
|
23
|
+
* INSERT OR IGNORE = 已有水位行(活捕获已推进)一律不覆盖;旧行文与
|
|
24
|
+
* 水位并存期两侧取大者由 record 的单调语义兜底。
|
|
25
|
+
*/
|
|
26
|
+
ensureBaseline() {
|
|
27
|
+
if (!this.db)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
this.db.exec(`
|
|
31
|
+
INSERT OR IGNORE INTO l0_watermark (session_id, max_turn, last_step)
|
|
32
|
+
SELECT session_id, MAX(turn), NULL
|
|
33
|
+
FROM l0_conversations
|
|
34
|
+
WHERE turn IS NOT NULL
|
|
35
|
+
GROUP BY session_id
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
// l0_conversations 可能已被存量清退(task_22)删除:表缺 = 无基线可灌,安静跳过
|
|
40
|
+
this.logger?.debug?.(`[memory] L0 水位基线物化跳过: ${err instanceof Error ? err.message : String(err)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 登记一个已捕获轮次(写停后 capture 的唯一落盘点)。单调:turn 更小/相等的
|
|
45
|
+
* 登记不回退水位(恢复对账与乱序事件都安全);step 取该轮已知最大 step。
|
|
46
|
+
*/
|
|
47
|
+
record(sessionId, turn, step) {
|
|
48
|
+
if (!this.db || !sessionId || !Number.isFinite(turn))
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
this.db
|
|
52
|
+
.prepare(`INSERT INTO l0_watermark (session_id, max_turn, last_step, updated_at)
|
|
53
|
+
VALUES (?, ?, ?, datetime('now'))
|
|
54
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
55
|
+
max_turn = MAX(l0_watermark.max_turn, excluded.max_turn),
|
|
56
|
+
last_step = CASE WHEN excluded.max_turn > l0_watermark.max_turn
|
|
57
|
+
THEN excluded.last_step ELSE l0_watermark.last_step END,
|
|
58
|
+
updated_at = excluded.updated_at`)
|
|
59
|
+
.run(sessionId, turn, typeof step === 'number' && Number.isFinite(step) ? step : null);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
this.logger?.warn(`[memory] L0 水位登记失败(幂等自检可能重放): ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** §A 水位线:该会话已捕获的最大 turn(空会话/未 init 返回 undefined)。 */
|
|
66
|
+
maxTurn(sessionId) {
|
|
67
|
+
if (!this.db || !sessionId)
|
|
68
|
+
return undefined;
|
|
69
|
+
try {
|
|
70
|
+
const row = this.db
|
|
71
|
+
.prepare('SELECT max_turn FROM l0_watermark WHERE session_id = ?')
|
|
72
|
+
.get(sessionId);
|
|
73
|
+
return typeof row?.max_turn === 'number' ? row.max_turn : undefined;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** §A 幂等检查:该 (session, turn) 是否已捕获(≡ 退役前 hasAnyL0Message)。 */
|
|
80
|
+
hasAny(sessionId, turn) {
|
|
81
|
+
if (!this.db || !sessionId || !Number.isFinite(turn))
|
|
82
|
+
return false;
|
|
83
|
+
try {
|
|
84
|
+
const row = this.db
|
|
85
|
+
.prepare('SELECT 1 FROM l0_watermark WHERE session_id = ? AND max_turn >= ? LIMIT 1')
|
|
86
|
+
.get(sessionId, turn);
|
|
87
|
+
return row !== undefined;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
package/dist/store/l0.d.ts
CHANGED
|
@@ -1,48 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* L0 存量计数门面(T8.4 l0 写路径退役后)。
|
|
3
|
+
*
|
|
4
|
+
* capture 已停写 l0 正文三表与 JSONL 事实源(见 hooks/capture.ts 与
|
|
5
|
+
* store/l0-watermark.ts),历史读面(recentBySession / search)改经
|
|
6
|
+
* `clients/drsc.ts` 的 DrscClient(硬依赖,无本地回退)——本类只剩状态
|
|
7
|
+
* 面板的两个只读计数(今日捕获 / 按会话累计)。task_22 存量清退后表为
|
|
8
|
+
* 空壳,计数自然归零,读路径无需判空。
|
|
9
|
+
*/
|
|
10
|
+
import type { MemoryDb } from './sqlite.js';
|
|
4
11
|
export declare class L0Store {
|
|
5
12
|
private readonly db;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
private readonly helper;
|
|
9
|
-
private embedSvc;
|
|
10
|
-
private readonly logger?;
|
|
11
|
-
constructor(dataDir: string, db: MemoryDb, embed?: EmbeddingService, logger?: MemoryLogger);
|
|
12
|
-
init(): Promise<void>;
|
|
13
|
-
/** 旧版 l0/*.jsonl 一次性导入检索库,成功后目录改名 l0.imported/。 */
|
|
14
|
-
private importLegacy;
|
|
15
|
-
append(sessionId: string, messages: ConversationMessage[]): Promise<void>;
|
|
16
|
-
/** 今日已捕获消息数(SQL 计数,不再读整文件)。 */
|
|
13
|
+
constructor(db: MemoryDb);
|
|
14
|
+
/** 今日已捕获消息数(SQL 计数,不再读整文件;写停后归零,面板口径保留)。 */
|
|
17
15
|
countToday(): Promise<number>;
|
|
18
16
|
/** 该会话累计已捕获消息数(session-stats 数据源;索引 COUNT)。 */
|
|
19
17
|
countBySession(sessionId: string): Promise<number>;
|
|
20
|
-
/** 该会话最近 n 条消息(时间升序;蒸馏背景参考用,按会话现查——ADR-0003)。 */
|
|
21
|
-
recentBySession(sessionId: string, limit: number): Promise<ConversationMessage[]>;
|
|
22
|
-
/** §A 水位线:该会话 L0 已落盘的最大 turn(空会话 undefined;委托 MemoryDb)。 */
|
|
23
|
-
maxCapturedTurn(sessionId: string): number | undefined;
|
|
24
|
-
/** §A 幂等检查:该 (session, turn) 是否已有任意 L0 行(委托 MemoryDb)。 */
|
|
25
|
-
hasAnyL0Message(sessionId: string, turn: number): boolean;
|
|
26
|
-
/** 检索:FTS + 向量 hybrid(RRF 融合),返回按相关性排序的消息。 */
|
|
27
|
-
search(query: string, limit: number): Promise<L0MessageRecord[]>;
|
|
28
|
-
/** 活切换嵌入源:同步换底层服务(嵌入源三态切换用)。 */
|
|
29
|
-
setEmbeddingService(svc: EmbeddingService): void;
|
|
30
|
-
/** 向量写入能力是否就绪。`reindex` 在未就绪时**静默短路**成 0/0/0
|
|
31
|
-
* (见本文件 `reindex` 首行),调用方必须自己问这里——否则"根本没跑"
|
|
32
|
-
* 会长得和"跑完了、零条待补"一模一样。 */
|
|
33
|
-
vectorsReady(): boolean;
|
|
34
|
-
/**
|
|
35
|
-
* 增量重嵌入(同 L1Store.reindex:只补缺失向量,零向量记 skipped 并入 skip 集,
|
|
36
|
-
* 不算失败、不阻塞同步标记——保证补齐判据收敛)。onProgress/shouldCancel
|
|
37
|
-
* 供活切换的进度展示与取消。
|
|
38
|
-
*/
|
|
39
|
-
reindex(opts?: {
|
|
40
|
-
onProgress?: (done: number, total: number) => void;
|
|
41
|
-
shouldCancel?: () => boolean;
|
|
42
|
-
}): Promise<{
|
|
43
|
-
written: number;
|
|
44
|
-
failed: number;
|
|
45
|
-
skipped: number;
|
|
46
|
-
cancelled?: boolean;
|
|
47
|
-
}>;
|
|
48
18
|
}
|