dsh-layered-memory 0.5.4 → 0.6.1
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/README.en.md +211 -0
- package/README.md +26 -9
- package/dist/client.js +466 -70
- package/dist/config.d.ts +6 -0
- package/dist/config.js +4 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -1
- package/dist/llm.js +5 -1
- package/dist/pipeline/rebuild.d.ts +78 -0
- package/dist/pipeline/rebuild.js +300 -0
- package/dist/pipeline/runner.d.ts +36 -4
- package/dist/pipeline/runner.js +119 -24
- package/dist/settings.d.ts +4 -0
- package/dist/settings.js +10 -3
- package/dist/stats.d.ts +2 -1
- package/dist/stats.js +46 -6
- package/dist/store/pending.d.ts +15 -0
- package/dist/store/pending.js +56 -0
- package/dist/store/sqlite.d.ts +15 -0
- package/dist/store/sqlite.js +76 -0
- package/dist/store/state.d.ts +6 -0
- package/dist/store/state.js +11 -0
- package/package.json +1 -1
package/dist/pipeline/runner.js
CHANGED
|
@@ -1,16 +1,41 @@
|
|
|
1
|
+
import { resolveDataDir } from '../config.js';
|
|
2
|
+
import { emptyPending, loadPending, PENDING_MODES, pendingPathFor, savePending } from '../store/pending.js';
|
|
3
|
+
import { errDetail } from '../util/filelog.js';
|
|
1
4
|
import { runExtraction } from './l1.js';
|
|
2
5
|
import { runSceneConsolidation } from './l2.js';
|
|
3
6
|
import { runPersona } from './l3.js';
|
|
4
|
-
|
|
7
|
+
/** 选取下一个要执行的任务下标:最早的 live 优先,否则队首(rebuild 分块让位)。 */
|
|
8
|
+
export function pickNextTaskIndex(tasks) {
|
|
9
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
10
|
+
if (tasks[i].kind === 'live')
|
|
11
|
+
return i;
|
|
12
|
+
}
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 运行时调参视图:UI 选择器可临时覆盖蒸馏思考档位(空串回退静态 config 默认)。
|
|
17
|
+
* 浅拷贝只覆盖 llm 一层,其余键与原 cfg 共享只读引用;pipeline 全链继续收 cfg,无需感知。
|
|
18
|
+
*/
|
|
19
|
+
export function effectiveCfg(cfg, live) {
|
|
20
|
+
const eff = live.get().reasoningEffort;
|
|
21
|
+
return eff ? { ...cfg, llm: { ...cfg.llm, reasoningEffort: eff } } : cfg;
|
|
22
|
+
}
|
|
23
|
+
/** 单桶堆积上限(防无限堆积;重建分块不受限——历史会话需全量入桶蒸馏)。 */
|
|
24
|
+
const PENDING_BUCKET_CAP = 200;
|
|
25
|
+
/** 启动补跑延迟:避开宿主启动期忙乱。 */
|
|
26
|
+
const STARTUP_RETRY_DELAY_MS = 20_000;
|
|
5
27
|
export class MemoryRunner {
|
|
6
28
|
ctx;
|
|
7
29
|
cfg;
|
|
8
30
|
stores;
|
|
9
31
|
logger;
|
|
10
32
|
live;
|
|
11
|
-
|
|
12
|
-
|
|
33
|
+
tasks = [];
|
|
34
|
+
draining = false;
|
|
35
|
+
pending = emptyPending();
|
|
36
|
+
pendingFile;
|
|
13
37
|
background = [];
|
|
38
|
+
/** 分族 checkpoint(init 后可用;重建收尾也从这里读活引用)。 */
|
|
14
39
|
states;
|
|
15
40
|
afterRun;
|
|
16
41
|
constructor(ctx, cfg, stores, logger, live) {
|
|
@@ -19,6 +44,7 @@ export class MemoryRunner {
|
|
|
19
44
|
this.stores = stores;
|
|
20
45
|
this.logger = logger;
|
|
21
46
|
this.live = live;
|
|
47
|
+
this.pendingFile = pendingPathFor(resolveDataDir(cfg));
|
|
22
48
|
}
|
|
23
49
|
async init() {
|
|
24
50
|
await this.stores.state.load();
|
|
@@ -30,6 +56,33 @@ export class MemoryRunner {
|
|
|
30
56
|
this.logger.info('[memory] state.json 已迁移为 v2 分族格式(旧数据归 chat 桶)');
|
|
31
57
|
await this.stores.state.save();
|
|
32
58
|
}
|
|
59
|
+
// 恢复未蒸馏缓冲(上次进程退出前未蒸馏的消息,含失败待重试与攒阈值中途的)
|
|
60
|
+
try {
|
|
61
|
+
const loaded = await loadPending(this.pendingFile, this.logger);
|
|
62
|
+
for (const key of PENDING_MODES) {
|
|
63
|
+
if (loaded[key].length > PENDING_BUCKET_CAP)
|
|
64
|
+
loaded[key] = loaded[key].slice(-PENDING_BUCKET_CAP);
|
|
65
|
+
}
|
|
66
|
+
this.pending = loaded;
|
|
67
|
+
if (this.pendingCount > 0) {
|
|
68
|
+
this.logger.info(`[memory] 未蒸馏缓冲已恢复 ${this.pendingCount} 条(auto=${this.pending.auto.length}/chat=${this.pending.chat.length}/work=${this.pending.work.length}),${STARTUP_RETRY_DELAY_MS / 1000}s 后自动补跑`);
|
|
69
|
+
this.scheduleStartupRetry();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
this.logger.warn(`[memory] 未蒸馏缓冲恢复失败(空桶起步): ${errDetail(err)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** 启动补跑:对每个非空桶入队一次蒸馏尝试(受 live 开关与阈值约束,失败不无限重试)。 */
|
|
77
|
+
scheduleStartupRetry() {
|
|
78
|
+
const modes = PENDING_MODES.filter((m) => this.pending[m].length > 0);
|
|
79
|
+
this.ctx.effect(() => {
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
for (const mode of modes)
|
|
82
|
+
this.enqueue('startup-retry', [], mode);
|
|
83
|
+
}, STARTUP_RETRY_DELAY_MS);
|
|
84
|
+
return () => clearTimeout(timer);
|
|
85
|
+
});
|
|
33
86
|
}
|
|
34
87
|
/** L1 抽取待重试的消息条数(状态面板用)。 */
|
|
35
88
|
get pendingCount() {
|
|
@@ -39,41 +92,82 @@ export class MemoryRunner {
|
|
|
39
92
|
setAfterRun(fn) {
|
|
40
93
|
this.afterRun = fn;
|
|
41
94
|
}
|
|
42
|
-
/** 一轮对话结束后入队(L0
|
|
95
|
+
/** 一轮对话结束后入队(L0 落盘由 capture 在 turn/end 即时完成,不排蒸馏队列)。 */
|
|
43
96
|
enqueue(sessionId, messages, mode) {
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
97
|
+
this.pushTask({ kind: 'live', run: () => this.runTurn(sessionId, messages, mode) });
|
|
98
|
+
}
|
|
99
|
+
/** 重建任务入队(低优先级:让位于正常轮次;由 RebuildController 分块驱动)。 */
|
|
100
|
+
enqueueRebuildTask(run) {
|
|
101
|
+
this.pushTask({ kind: 'rebuild', run });
|
|
102
|
+
}
|
|
103
|
+
/** 重建蒸馏轮:统一 auto 档,不受缓冲 200 上限(历史会话全量入桶,由 char 预算分块)。 */
|
|
104
|
+
runRebuildTurn(sessionId, messages) {
|
|
105
|
+
return this.runTurn(sessionId, messages, 'auto', { noBufferCap: true });
|
|
49
106
|
}
|
|
50
|
-
|
|
107
|
+
pushTask(task) {
|
|
108
|
+
this.tasks.push(task);
|
|
109
|
+
void this.drain();
|
|
110
|
+
}
|
|
111
|
+
async drain() {
|
|
112
|
+
if (this.draining)
|
|
113
|
+
return;
|
|
114
|
+
this.draining = true;
|
|
115
|
+
try {
|
|
116
|
+
while (this.tasks.length > 0) {
|
|
117
|
+
const [task] = this.tasks.splice(pickNextTaskIndex(this.tasks), 1);
|
|
118
|
+
try {
|
|
119
|
+
await task.run();
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
this.logger.warn(`[memory] 管线失败(已兜底): ${errDetail(err)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
this.draining = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** 缓冲落盘(每次蒸馏尝试后调用;失败只告警不阻断管线)。 */
|
|
131
|
+
async persistPending() {
|
|
132
|
+
try {
|
|
133
|
+
await savePending(this.pendingFile, this.pending);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
this.logger.warn(`[memory] 未蒸馏缓冲落盘失败: ${errDetail(err)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async runTurn(sessionId, messages, mode, opts) {
|
|
51
140
|
const turnStart = Date.now();
|
|
52
141
|
this.logger.info(`[memory] 蒸馏管线开始(session=${sessionId},mode=${mode},本轮 ${messages.length} 条消息,待重试 ${this.pendingCount} 条)`);
|
|
53
142
|
// ── L0:原始对话已由 capture 在 turn/end 即时落盘(不排蒸馏队列,防慢 LLM 阻塞/退出丢消息) ──
|
|
143
|
+
const cfg = effectiveCfg(this.cfg, this.live);
|
|
54
144
|
// ── L1:抽取 + 去重(按档分桶,失败按桶保留待重试) ──
|
|
55
145
|
let newRecords = [];
|
|
56
|
-
const
|
|
57
|
-
|
|
146
|
+
const liveNow = this.live.get();
|
|
147
|
+
const distillOn = liveNow.enabled && liveNow.distill;
|
|
148
|
+
if (cfg.extract.enabled && distillOn) {
|
|
58
149
|
const bucket = this.pending[mode];
|
|
59
150
|
bucket.push(...messages);
|
|
60
|
-
if (bucket.length >
|
|
61
|
-
bucket.splice(0, bucket.length -
|
|
151
|
+
if (!opts?.noBufferCap && bucket.length > PENDING_BUCKET_CAP) {
|
|
152
|
+
bucket.splice(0, bucket.length - PENDING_BUCKET_CAP);
|
|
153
|
+
}
|
|
62
154
|
try {
|
|
63
155
|
const t = Date.now();
|
|
64
|
-
const result = await runExtraction(this.ctx,
|
|
156
|
+
const result = await runExtraction(this.ctx, cfg, this.stores.l1, this.states, bucket, this.background, this.logger, mode);
|
|
65
157
|
if (!result.skipped)
|
|
66
158
|
this.pending[mode] = [];
|
|
67
|
-
this.background = [...this.background.slice(-
|
|
159
|
+
this.background = [...this.background.slice(-cfg.extract.backgroundMessages), ...messages];
|
|
68
160
|
newRecords = result.newRecords;
|
|
69
161
|
this.logger.info(`[memory] L1 阶段完成(${Date.now() - t}ms)`);
|
|
70
162
|
}
|
|
71
163
|
catch (err) {
|
|
72
|
-
// 保留 pending
|
|
73
|
-
if (this.pending[mode].length >
|
|
164
|
+
// 保留 pending,下次重试;但防止无限堆积(重建轮不限,量被会话规模天然约束)
|
|
165
|
+
if (!opts?.noBufferCap && this.pending[mode].length > PENDING_BUCKET_CAP)
|
|
74
166
|
this.pending[mode] = [];
|
|
75
167
|
this.logger.warn(`[memory] L1 抽取失败(mode=${mode},pending=${this.pending[mode].length}): ${errDetail(err)}`);
|
|
76
168
|
}
|
|
169
|
+
// 缓冲每次尝试后立即落盘:进程中途退出不丢待重试/攒阈值状态
|
|
170
|
+
await this.persistPending();
|
|
77
171
|
// L1 计数推进后立即落盘:L2/L3 失败或进程中途退出不得回滚阈值进度
|
|
78
172
|
// (记录已入库但计数丢失会让该族 L2 永远差一截,state 与 DB 脱节)
|
|
79
173
|
try {
|
|
@@ -84,16 +178,16 @@ export class MemoryRunner {
|
|
|
84
178
|
}
|
|
85
179
|
}
|
|
86
180
|
// ── L2/L3:按记录族各自判定与执行 ──
|
|
87
|
-
if (
|
|
181
|
+
if (cfg.l2.enabled && distillOn) {
|
|
88
182
|
for (const family of ['chat', 'work']) {
|
|
89
183
|
const familyRecords = newRecords.filter((r) => (r.family ?? 'chat') === family);
|
|
90
184
|
if (familyRecords.length === 0)
|
|
91
185
|
continue;
|
|
92
186
|
const fstate = this.states[family];
|
|
93
|
-
if (fstate.newMemoriesSinceL2 >=
|
|
187
|
+
if (fstate.newMemoriesSinceL2 >= cfg.l2.minNewMemories) {
|
|
94
188
|
try {
|
|
95
189
|
const t = Date.now();
|
|
96
|
-
const result = await runSceneConsolidation(this.ctx,
|
|
190
|
+
const result = await runSceneConsolidation(this.ctx, cfg, this.stores.scenes[family], familyRecords, this.logger, family);
|
|
97
191
|
fstate.lastL2At = Date.now();
|
|
98
192
|
fstate.newMemoriesSinceL2 = 0;
|
|
99
193
|
if (result.personaRequestedReason)
|
|
@@ -105,14 +199,14 @@ export class MemoryRunner {
|
|
|
105
199
|
}
|
|
106
200
|
}
|
|
107
201
|
else {
|
|
108
|
-
this.logger.debug?.(`[memory] L2 跳过(family=${family},本族新增 ${familyRecords.length} 条,累计未整合 ${fstate.newMemoriesSinceL2}/${
|
|
202
|
+
this.logger.debug?.(`[memory] L2 跳过(family=${family},本族新增 ${familyRecords.length} 条,累计未整合 ${fstate.newMemoriesSinceL2}/${cfg.l2.minNewMemories})`);
|
|
109
203
|
}
|
|
110
204
|
}
|
|
111
205
|
}
|
|
112
|
-
if (
|
|
206
|
+
if (cfg.l3.enabled && distillOn) {
|
|
113
207
|
for (const family of ['chat', 'work']) {
|
|
114
208
|
try {
|
|
115
|
-
await runPersona(this.ctx,
|
|
209
|
+
await runPersona(this.ctx, cfg, this.stores.scenes[family], this.stores.persona[family], this.states[family], this.logger, family);
|
|
116
210
|
}
|
|
117
211
|
catch (err) {
|
|
118
212
|
this.logger.warn(`[memory] L3 画像蒸馏失败(family=${family}): ${errDetail(err)}`);
|
|
@@ -128,5 +222,6 @@ export class MemoryRunner {
|
|
|
128
222
|
}
|
|
129
223
|
this.logger.info(`[memory] 蒸馏管线结束(本轮新增 ${newRecords.length} 条,总耗时 ${Date.now() - turnStart}ms)`);
|
|
130
224
|
this.afterRun?.();
|
|
225
|
+
return newRecords.length;
|
|
131
226
|
}
|
|
132
227
|
}
|
package/dist/settings.d.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import type { Context } from '@deepseek-ai/cordis';
|
|
7
7
|
import Schema from '@deepseek-ai/schemastery';
|
|
8
8
|
import type { MemoryLogger } from './types.js';
|
|
9
|
+
/** 蒸馏思考档位可选项:'' = 跟随静态 config(部署默认)。 */
|
|
10
|
+
export type EffortChoice = '' | 'off' | 'high' | 'max';
|
|
9
11
|
export interface MemoryLiveSettings {
|
|
10
12
|
/** 总开关:关 = 捕获/蒸馏/召回注入全停(数据保留) */
|
|
11
13
|
enabled: boolean;
|
|
@@ -15,6 +17,8 @@ export interface MemoryLiveSettings {
|
|
|
15
17
|
distill: boolean;
|
|
16
18
|
/** 召回注入(画像/记忆上下文) */
|
|
17
19
|
recall: boolean;
|
|
20
|
+
/** 蒸馏思考档位运行时覆盖:'' = 跟随静态 config(llm.reasoningEffort) */
|
|
21
|
+
reasoningEffort: EffortChoice;
|
|
18
22
|
}
|
|
19
23
|
export interface LiveSettingsHandle {
|
|
20
24
|
/** settings 服务是否可用(不可用时 UI 侧隐藏开关面板) */
|
package/dist/settings.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import Schema from '@deepseek-ai/schemastery';
|
|
2
2
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
3
3
|
const NS = settingsNamespace('dsh-memory');
|
|
4
|
-
const ALWAYS_ON = { enabled: true, capture: true, distill: true, recall: true };
|
|
4
|
+
const ALWAYS_ON = { enabled: true, capture: true, distill: true, recall: true, reasoningEffort: '' };
|
|
5
5
|
export function liveSettingsSchema() {
|
|
6
6
|
return Schema.object({
|
|
7
7
|
enabled: Schema.boolean().default(true),
|
|
8
8
|
capture: Schema.boolean().default(true),
|
|
9
9
|
distill: Schema.boolean().default(true),
|
|
10
10
|
recall: Schema.boolean().default(true),
|
|
11
|
+
reasoningEffort: Schema.union(['', 'off', 'high', 'max']).default(''),
|
|
11
12
|
});
|
|
12
13
|
}
|
|
13
14
|
export function registerLiveSettings(ctx, logger) {
|
|
@@ -27,7 +28,8 @@ export function registerLiveSettings(ctx, logger) {
|
|
|
27
28
|
scope.watch((next) => {
|
|
28
29
|
const prev = current;
|
|
29
30
|
current = resolveSettings(next);
|
|
30
|
-
logger.info(`[memory] 记忆模式开关更新:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall}
|
|
31
|
+
logger.info(`[memory] 记忆模式开关更新:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall}` +
|
|
32
|
+
`,蒸馏思考=${current.reasoningEffort || '跟随配置'}(此前 总=${prev.enabled})`);
|
|
31
33
|
});
|
|
32
34
|
inner = {
|
|
33
35
|
supported: true,
|
|
@@ -36,7 +38,8 @@ export function registerLiveSettings(ctx, logger) {
|
|
|
36
38
|
await scope.update(patch);
|
|
37
39
|
},
|
|
38
40
|
};
|
|
39
|
-
logger.info(`[memory] 记忆模式开关就绪(settings 命名空间 dsh-memory,当前:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall}
|
|
41
|
+
logger.info(`[memory] 记忆模式开关就绪(settings 命名空间 dsh-memory,当前:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall}` +
|
|
42
|
+
`,蒸馏思考=${current.reasoningEffort || '跟随配置'})`);
|
|
40
43
|
return true;
|
|
41
44
|
}
|
|
42
45
|
catch (err) {
|
|
@@ -64,10 +67,14 @@ function resolveSettings(value) {
|
|
|
64
67
|
if (!value || typeof value !== 'object')
|
|
65
68
|
return { ...ALWAYS_ON };
|
|
66
69
|
const v = value;
|
|
70
|
+
const efforts = ['', 'off', 'high', 'max'];
|
|
67
71
|
return {
|
|
68
72
|
enabled: v.enabled !== false,
|
|
69
73
|
capture: v.capture !== false,
|
|
70
74
|
distill: v.distill !== false,
|
|
71
75
|
recall: v.recall !== false,
|
|
76
|
+
reasoningEffort: typeof v.reasoningEffort === 'string' && efforts.includes(v.reasoningEffort)
|
|
77
|
+
? v.reasoningEffort
|
|
78
|
+
: '',
|
|
72
79
|
};
|
|
73
80
|
}
|
package/dist/stats.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import { type MemoryConfig } from './config.js';
|
|
3
|
+
import type { RebuildController } from './pipeline/rebuild.js';
|
|
3
4
|
import type { LiveSettingsHandle } from './settings.js';
|
|
4
5
|
import type { L0Store } from './store/l0.js';
|
|
5
6
|
import type { L1Store } from './store/l1.js';
|
|
@@ -43,4 +44,4 @@ export declare function registerMemoryRpc(ctx: Context, cfg: MemoryConfig, store
|
|
|
43
44
|
scenes: Record<MemoryFamily, SceneStore>;
|
|
44
45
|
persona: Record<MemoryFamily, PersonaStore>;
|
|
45
46
|
state: StateStore;
|
|
46
|
-
}, logger: MemoryLogger, status?: MemoryStatusSource, live?: LiveSettingsHandle, modes?: SessionModeStore, dataDir?: string): void;
|
|
47
|
+
}, logger: MemoryLogger, status?: MemoryStatusSource, live?: LiveSettingsHandle, modes?: SessionModeStore, dataDir?: string, rebuild?: RebuildController): void;
|
package/dist/stats.js
CHANGED
|
@@ -12,7 +12,7 @@ import { resolveDataDir } from './config.js';
|
|
|
12
12
|
const require = createRequire(import.meta.url);
|
|
13
13
|
export const PLUGIN_VERSION = require('../package.json').version;
|
|
14
14
|
/** 注册状态 RPC(web 侧 connection 服务可选,缺失时跳过,不影响插件主体)。 */
|
|
15
|
-
export function registerMemoryRpc(ctx, cfg, stores, logger, status, live, modes, dataDir) {
|
|
15
|
+
export function registerMemoryRpc(ctx, cfg, stores, logger, status, live, modes, dataDir, rebuild) {
|
|
16
16
|
/** 当前是否持有一段有效注册(dispose 完成后清空,允许服务重上线时重注册)。 */
|
|
17
17
|
let holding = false;
|
|
18
18
|
const tryRegister = () => {
|
|
@@ -34,6 +34,7 @@ export function registerMemoryRpc(ctx, cfg, stores, logger, status, live, modes,
|
|
|
34
34
|
modes,
|
|
35
35
|
dataDir: dataDir ?? resolveDataDir(cfg),
|
|
36
36
|
logger,
|
|
37
|
+
rebuild,
|
|
37
38
|
});
|
|
38
39
|
return { ok: true, value };
|
|
39
40
|
}
|
|
@@ -104,7 +105,7 @@ async function buildStats(cfg, stores, status) {
|
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
async function handleEndpoint(endpoint, payload, deps) {
|
|
107
|
-
const { cfg, stores, status, live, modes, dataDir } = deps;
|
|
108
|
+
const { cfg, stores, status, live, modes, dataDir, rebuild } = deps;
|
|
108
109
|
switch (endpoint) {
|
|
109
110
|
case 'dsh-memory/stats':
|
|
110
111
|
return buildStats(cfg, stores, status);
|
|
@@ -130,26 +131,41 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
130
131
|
deps.logger.info(`[memory] 会话档位设置 session=${p.sessionId} mode=${p.mode}`);
|
|
131
132
|
return { sessionId: p.sessionId, mode: p.mode };
|
|
132
133
|
}
|
|
133
|
-
case 'dsh-memory/settings-get':
|
|
134
|
+
case 'dsh-memory/settings-get': {
|
|
135
|
+
const s = live?.get();
|
|
134
136
|
return {
|
|
135
137
|
supported: live?.supported ?? false,
|
|
136
|
-
settings:
|
|
138
|
+
settings: s ?? { enabled: true, capture: true, distill: true, recall: true, reasoningEffort: '' },
|
|
137
139
|
// 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
|
|
138
140
|
ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
|
|
141
|
+
// 蒸馏思考档位:current 是运行时覆盖('' = 跟随配置),effective 是实际生效值
|
|
142
|
+
effort: {
|
|
143
|
+
current: s?.reasoningEffort ?? '',
|
|
144
|
+
effective: s?.reasoningEffort || cfg.llm.reasoningEffort,
|
|
145
|
+
fallback: cfg.llm.reasoningEffort,
|
|
146
|
+
},
|
|
139
147
|
};
|
|
148
|
+
}
|
|
140
149
|
case 'dsh-memory/settings-set': {
|
|
141
150
|
if (!live)
|
|
142
151
|
throw new Error('开关通道未初始化');
|
|
143
152
|
const patch = (payload ?? {});
|
|
144
|
-
const allowed = ['enabled', 'capture', 'distill', 'recall'];
|
|
145
153
|
const clean = {};
|
|
146
|
-
for (const key of
|
|
154
|
+
for (const key of ['enabled', 'capture', 'distill', 'recall']) {
|
|
147
155
|
if (typeof patch[key] === 'boolean')
|
|
148
156
|
clean[key] = patch[key];
|
|
149
157
|
}
|
|
158
|
+
if (patch.reasoningEffort !== undefined) {
|
|
159
|
+
const v = String(patch.reasoningEffort);
|
|
160
|
+
if (!['', 'off', 'high', 'max'].includes(v)) {
|
|
161
|
+
throw new Error(`非法思考档位: ${v}(允许 ''/off/high/max)`);
|
|
162
|
+
}
|
|
163
|
+
clean.reasoningEffort = v;
|
|
164
|
+
}
|
|
150
165
|
if (Object.keys(clean).length === 0)
|
|
151
166
|
throw new Error('开关更新载荷为空');
|
|
152
167
|
await live.update(clean);
|
|
168
|
+
deps.logger.info(`[memory] 设置更新:${JSON.stringify(clean)}`);
|
|
153
169
|
return { ok: true, settings: live.get() };
|
|
154
170
|
}
|
|
155
171
|
case 'dsh-memory/list-records': {
|
|
@@ -200,6 +216,30 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
200
216
|
const p = (payload ?? {});
|
|
201
217
|
return { lines: readLogTail(join(dataDir, 'memory.log'), Math.min(Math.max(Number(p.lines) || 200, 1), 1000)) };
|
|
202
218
|
}
|
|
219
|
+
case 'dsh-memory/rebuild-status': {
|
|
220
|
+
if (!rebuild)
|
|
221
|
+
return { supported: false, running: false, phase: 'idle' };
|
|
222
|
+
return rebuild.getStatus();
|
|
223
|
+
}
|
|
224
|
+
case 'dsh-memory/rebuild-start': {
|
|
225
|
+
if (!rebuild)
|
|
226
|
+
throw new Error('重建控制器未初始化(存储不可用)');
|
|
227
|
+
if (status?.degraded())
|
|
228
|
+
throw new Error('存储处于降级状态,无法重建');
|
|
229
|
+
const s = live?.get();
|
|
230
|
+
if (s && (!s.enabled || !s.distill))
|
|
231
|
+
throw new Error('蒸馏开关已关闭,请先开启蒸馏再重建');
|
|
232
|
+
if (!cfg.extract.enabled)
|
|
233
|
+
throw new Error('部署配置已停用蒸馏(extract.enabled=false),无法重建');
|
|
234
|
+
const result = rebuild.start();
|
|
235
|
+
deps.logger.info('[memory] 收到重建指令(设置页按钮)');
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
case 'dsh-memory/rebuild-cancel': {
|
|
239
|
+
if (!rebuild)
|
|
240
|
+
throw new Error('重建控制器未初始化');
|
|
241
|
+
return rebuild.requestCancel();
|
|
242
|
+
}
|
|
203
243
|
default:
|
|
204
244
|
throw new Error(`unknown endpoint: ${endpoint}`);
|
|
205
245
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ConversationMessage, ExtractMode, MemoryLogger } from '../types.js';
|
|
2
|
+
/** 三档蒸馏缓冲桶(off 在捕获侧已被拦截,永远不到这里)。 */
|
|
3
|
+
export interface PendingBuckets {
|
|
4
|
+
auto: ConversationMessage[];
|
|
5
|
+
chat: ConversationMessage[];
|
|
6
|
+
work: ConversationMessage[];
|
|
7
|
+
}
|
|
8
|
+
export declare function emptyPending(): PendingBuckets;
|
|
9
|
+
/** 读取缓冲文件:文件缺失/损坏 → 空桶(不抛出——丢了缓冲 L0 事实源仍在)。 */
|
|
10
|
+
export declare function loadPending(file: string, logger?: MemoryLogger): Promise<PendingBuckets>;
|
|
11
|
+
/** 全量原子落盘(每次蒸馏尝试后调用;桶有上限,量级为百条级)。 */
|
|
12
|
+
export declare function savePending(file: string, buckets: PendingBuckets): Promise<void>;
|
|
13
|
+
export declare function pendingPathFor(dataDir: string): string;
|
|
14
|
+
/** 三档 key(调度/遍历用)。 */
|
|
15
|
+
export declare const PENDING_MODES: readonly ExtractMode[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 未蒸馏缓冲持久化(pending.json):
|
|
3
|
+
* 按档分桶的"已捕获但尚未成功蒸馏"消息——既包括抽取失败的待重试消息,
|
|
4
|
+
* 也包括攒够触发阈值之前的消息。跨重启不丢(CONTEXT.md「未蒸馏缓冲」)。
|
|
5
|
+
*
|
|
6
|
+
* 与 state.json / session-modes.json 同款原子写;读取宽容(坏行丢弃、坏文件空桶起步)。
|
|
7
|
+
*/
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { atomicWriteJson, readJsonIfExists } from './io.js';
|
|
10
|
+
export function emptyPending() {
|
|
11
|
+
return { auto: [], chat: [], work: [] };
|
|
12
|
+
}
|
|
13
|
+
function isMessage(m) {
|
|
14
|
+
if (!m || typeof m !== 'object')
|
|
15
|
+
return false;
|
|
16
|
+
const r = m;
|
|
17
|
+
return typeof r.id === 'string' && typeof r.content === 'string' && (r.role === 'user' || r.role === 'assistant');
|
|
18
|
+
}
|
|
19
|
+
/** 读取缓冲文件:文件缺失/损坏 → 空桶(不抛出——丢了缓冲 L0 事实源仍在)。 */
|
|
20
|
+
export async function loadPending(file, logger) {
|
|
21
|
+
const out = emptyPending();
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = await readJsonIfExists(file);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
raw = undefined;
|
|
28
|
+
}
|
|
29
|
+
if (!raw || typeof raw !== 'object' || !raw.buckets || typeof raw.buckets !== 'object')
|
|
30
|
+
return out;
|
|
31
|
+
let dropped = 0;
|
|
32
|
+
for (const key of ['auto', 'chat', 'work']) {
|
|
33
|
+
const arr = raw.buckets[key];
|
|
34
|
+
if (!Array.isArray(arr))
|
|
35
|
+
continue;
|
|
36
|
+
for (const m of arr) {
|
|
37
|
+
if (isMessage(m))
|
|
38
|
+
out[key].push(m);
|
|
39
|
+
else
|
|
40
|
+
dropped++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (dropped > 0)
|
|
44
|
+
logger?.warn(`[memory] 未蒸馏缓冲文件含 ${dropped} 条坏记录,已丢弃`);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** 全量原子落盘(每次蒸馏尝试后调用;桶有上限,量级为百条级)。 */
|
|
48
|
+
export async function savePending(file, buckets) {
|
|
49
|
+
const payload = { version: 1, buckets };
|
|
50
|
+
await atomicWriteJson(file, payload);
|
|
51
|
+
}
|
|
52
|
+
export function pendingPathFor(dataDir) {
|
|
53
|
+
return path.join(dataDir, 'pending.json');
|
|
54
|
+
}
|
|
55
|
+
/** 三档 key(调度/遍历用)。 */
|
|
56
|
+
export const PENDING_MODES = ['auto', 'chat', 'work'];
|
package/dist/store/sqlite.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ export declare class MemoryDb {
|
|
|
75
75
|
upsertL1(record: MemoryRecord, embedding?: Float32Array): boolean;
|
|
76
76
|
/** 批量删除 L1(元数据 + 向量 + FTS),返回删除条数。 */
|
|
77
77
|
deleteL1Batch(ids: string[]): number;
|
|
78
|
+
/**
|
|
79
|
+
* 清空 L1 检索库全部数据(重建用)。records/FTS 直接 DELETE;
|
|
80
|
+
* 向量表走 DROP + 重建(vec0 的全表 DELETE 语义不可靠,dropVectorTables
|
|
81
|
+
* 会连 l0_vec 一起删——L0 向量必须保留——故此处单独处理 l1_vec)。
|
|
82
|
+
* L0 表与 embedding_meta 不动:backfill 的行数比对天然重新一致。
|
|
83
|
+
*/
|
|
84
|
+
clearL1(): boolean;
|
|
78
85
|
countL1(): number;
|
|
79
86
|
/** 全量读取(调试/迁移/重嵌入用;检索请走 FTS/向量)。 */
|
|
80
87
|
getAllL1(): MemoryRecord[];
|
|
@@ -101,6 +108,14 @@ export declare class MemoryDb {
|
|
|
101
108
|
countL0(): number;
|
|
102
109
|
/** 统计 recorded_at >= iso 的消息数(状态面板"今日捕获"用)。 */
|
|
103
110
|
countL0Since(iso: string): number;
|
|
111
|
+
/** L0 全量列举(重建快照用;按时间升序,事务一致性避开 JSONL 追加竞态)。 */
|
|
112
|
+
listL0All(): L0MessageRecord[];
|
|
113
|
+
/** 重建成本预估(一次全表聚合:会话数 / 消息数 / 字符量)。 */
|
|
114
|
+
l0RebuildEstimate(): {
|
|
115
|
+
sessions: number;
|
|
116
|
+
messages: number;
|
|
117
|
+
chars: number;
|
|
118
|
+
};
|
|
104
119
|
/** 向量表行数(backfill 判据:与元数据行数的差值即缺失向量数;不可用时返回 -1)。 */
|
|
105
120
|
countL1Vec(): number;
|
|
106
121
|
countL0Vec(): number;
|
package/dist/store/sqlite.js
CHANGED
|
@@ -518,6 +518,46 @@ export class MemoryDb {
|
|
|
518
518
|
return 0;
|
|
519
519
|
}
|
|
520
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* 清空 L1 检索库全部数据(重建用)。records/FTS 直接 DELETE;
|
|
523
|
+
* 向量表走 DROP + 重建(vec0 的全表 DELETE 语义不可靠,dropVectorTables
|
|
524
|
+
* 会连 l0_vec 一起删——L0 向量必须保留——故此处单独处理 l1_vec)。
|
|
525
|
+
* L0 表与 embedding_meta 不动:backfill 的行数比对天然重新一致。
|
|
526
|
+
*/
|
|
527
|
+
clearL1() {
|
|
528
|
+
if (this.degraded)
|
|
529
|
+
return false;
|
|
530
|
+
try {
|
|
531
|
+
this.db.exec('BEGIN');
|
|
532
|
+
try {
|
|
533
|
+
this.db.exec('DELETE FROM l1_records');
|
|
534
|
+
if (this.ftsAvailable)
|
|
535
|
+
this.db.exec('DELETE FROM l1_fts');
|
|
536
|
+
this.db.exec('COMMIT');
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
try {
|
|
540
|
+
this.db.exec('ROLLBACK');
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
/* ignore */
|
|
544
|
+
}
|
|
545
|
+
throw err;
|
|
546
|
+
}
|
|
547
|
+
// vec0 全表 DELETE 语义不可靠 → DROP + 重建空表;放事务外(vtab DDL 事务性弱)。
|
|
548
|
+
// 中间态:向量行短暂孤儿——检索侧对缺 meta 的向量本就跳过(searchL1Vector 回查过滤)。
|
|
549
|
+
if (this.stmtDeleteL1Vec) {
|
|
550
|
+
this.db.exec('DROP TABLE IF EXISTS l1_vec');
|
|
551
|
+
this.prepareL1VecStatements();
|
|
552
|
+
}
|
|
553
|
+
this.logger?.info(`${TAG} L1 检索库已清空(重建)`);
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
this.logger?.warn(`${TAG} L1 清空失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
521
561
|
countL1() {
|
|
522
562
|
if (this.degraded)
|
|
523
563
|
return 0;
|
|
@@ -724,6 +764,42 @@ export class MemoryDb {
|
|
|
724
764
|
return 0;
|
|
725
765
|
}
|
|
726
766
|
}
|
|
767
|
+
/** L0 全量列举(重建快照用;按时间升序,事务一致性避开 JSONL 追加竞态)。 */
|
|
768
|
+
listL0All() {
|
|
769
|
+
if (this.degraded)
|
|
770
|
+
return [];
|
|
771
|
+
try {
|
|
772
|
+
const rows = this.db
|
|
773
|
+
.prepare('SELECT record_id, session_id, role, message_text, recorded_at, timestamp FROM l0_conversations ORDER BY timestamp ASC')
|
|
774
|
+
.all();
|
|
775
|
+
return rows.map((r) => ({
|
|
776
|
+
sessionId: r.session_id,
|
|
777
|
+
recordedAt: r.recorded_at,
|
|
778
|
+
id: r.record_id,
|
|
779
|
+
role: r.role,
|
|
780
|
+
content: r.message_text,
|
|
781
|
+
timestamp: r.timestamp ?? 0,
|
|
782
|
+
}));
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
this.logger?.warn(`${TAG} L0 全量列举失败(返回空): ${err instanceof Error ? err.message : String(err)}`);
|
|
786
|
+
return [];
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
/** 重建成本预估(一次全表聚合:会话数 / 消息数 / 字符量)。 */
|
|
790
|
+
l0RebuildEstimate() {
|
|
791
|
+
if (this.degraded)
|
|
792
|
+
return { sessions: 0, messages: 0, chars: 0 };
|
|
793
|
+
try {
|
|
794
|
+
const row = this.db
|
|
795
|
+
.prepare('SELECT COUNT(DISTINCT session_id) AS s, COUNT(*) AS n, COALESCE(SUM(LENGTH(message_text)), 0) AS c FROM l0_conversations')
|
|
796
|
+
.get();
|
|
797
|
+
return { sessions: row?.s ?? 0, messages: row?.n ?? 0, chars: row?.c ?? 0 };
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
return { sessions: 0, messages: 0, chars: 0 };
|
|
801
|
+
}
|
|
802
|
+
}
|
|
727
803
|
/** 向量表行数(backfill 判据:与元数据行数的差值即缺失向量数;不可用时返回 -1)。 */
|
|
728
804
|
countL1Vec() {
|
|
729
805
|
if (this.degraded || !this.stmtSearchL1Vec)
|
package/dist/store/state.d.ts
CHANGED
|
@@ -30,6 +30,12 @@ export declare class StateStore {
|
|
|
30
30
|
get didMigrate(): boolean;
|
|
31
31
|
/** 取某族的 checkpoint(活引用——改字段后 save 生效)。 */
|
|
32
32
|
forFamily(family: MemoryFamily): MemoryState;
|
|
33
|
+
/**
|
|
34
|
+
* 重建用:两族 checkpoint 重置为默认值。
|
|
35
|
+
* 必须原地突变(Object.assign)——runner.states 等处持有桶对象的活引用,
|
|
36
|
+
* 换新对象会让引用指向已废弃的桶,后续计数写到内存孤儿上。
|
|
37
|
+
*/
|
|
38
|
+
reset(): void;
|
|
33
39
|
save(): Promise<void>;
|
|
34
40
|
static pathFor(dataDir: string): string;
|
|
35
41
|
}
|
package/dist/store/state.js
CHANGED
|
@@ -52,6 +52,17 @@ export class StateStore {
|
|
|
52
52
|
forFamily(family) {
|
|
53
53
|
return this.buckets[family];
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* 重建用:两族 checkpoint 重置为默认值。
|
|
57
|
+
* 必须原地突变(Object.assign)——runner.states 等处持有桶对象的活引用,
|
|
58
|
+
* 换新对象会让引用指向已废弃的桶,后续计数写到内存孤儿上。
|
|
59
|
+
*/
|
|
60
|
+
reset() {
|
|
61
|
+
for (const family of ['chat', 'work']) {
|
|
62
|
+
Object.assign(this.buckets[family], defaultState());
|
|
63
|
+
this.buckets[family].personaRequestedReason = undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
55
66
|
async save() {
|
|
56
67
|
const file = { version: 2, families: this.buckets };
|
|
57
68
|
await atomicWriteJson(this.file, file);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-layered-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "L0~L3 分层蒸馏记忆插件 for DeepSeek Harness:自动捕获对话(L0)、抽取原子记忆(L1)、整合场景块(L2)、蒸馏核心画像/团队方法论(L3),并在模型步骤前自动召回注入。移植自 MemoryCore (TencentDB Agent Memory) 的管线设计。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|