dsh-layered-memory 0.5.3
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/LICENSE +21 -0
- package/README.md +186 -0
- package/assets/readme/hero.svg +58 -0
- package/cordis.patch.yml +16 -0
- package/dist/client.js +1066 -0
- package/dist/config.d.ts +263 -0
- package/dist/config.js +65 -0
- package/dist/hooks/capture.d.ts +16 -0
- package/dist/hooks/capture.js +160 -0
- package/dist/hooks/recall.d.ts +31 -0
- package/dist/hooks/recall.js +201 -0
- package/dist/index.d.ts +190 -0
- package/dist/index.js +234 -0
- package/dist/llm.d.ts +32 -0
- package/dist/llm.js +128 -0
- package/dist/pipeline/l1.d.ts +19 -0
- package/dist/pipeline/l1.js +226 -0
- package/dist/pipeline/l2.d.ts +13 -0
- package/dist/pipeline/l2.js +77 -0
- package/dist/pipeline/l3.d.ts +15 -0
- package/dist/pipeline/l3.js +72 -0
- package/dist/pipeline/runner.d.ts +44 -0
- package/dist/pipeline/runner.js +132 -0
- package/dist/prompts/l1-dedup.d.ts +20 -0
- package/dist/prompts/l1-dedup.js +244 -0
- package/dist/prompts/l1-extraction.d.ts +18 -0
- package/dist/prompts/l1-extraction.js +434 -0
- package/dist/prompts/persona.d.ts +23 -0
- package/dist/prompts/persona.js +234 -0
- package/dist/prompts/scene.d.ts +33 -0
- package/dist/prompts/scene.js +408 -0
- package/dist/settings.d.ts +27 -0
- package/dist/settings.js +73 -0
- package/dist/stats.d.ts +46 -0
- package/dist/stats.js +234 -0
- package/dist/store/bm25.d.ts +19 -0
- package/dist/store/bm25.js +62 -0
- package/dist/store/embedding.d.ts +68 -0
- package/dist/store/embedding.js +133 -0
- package/dist/store/io.d.ts +13 -0
- package/dist/store/io.js +75 -0
- package/dist/store/l0.d.ts +28 -0
- package/dist/store/l0.js +148 -0
- package/dist/store/l1.d.ts +70 -0
- package/dist/store/l1.js +239 -0
- package/dist/store/persona.d.ts +15 -0
- package/dist/store/persona.js +60 -0
- package/dist/store/scenes.d.ts +23 -0
- package/dist/store/scenes.js +150 -0
- package/dist/store/search-utils.d.ts +22 -0
- package/dist/store/search-utils.js +71 -0
- package/dist/store/session-modes.d.ts +24 -0
- package/dist/store/session-modes.js +102 -0
- package/dist/store/sqlite.d.ts +121 -0
- package/dist/store/sqlite.js +922 -0
- package/dist/store/state.d.ts +35 -0
- package/dist/store/state.js +62 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.js +169 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +7 -0
- package/dist/util/filelog.d.ts +4 -0
- package/dist/util/filelog.js +43 -0
- package/dist/util/sanitize.d.ts +10 -0
- package/dist/util/sanitize.js +66 -0
- package/dist/util/text.d.ts +11 -0
- package/dist/util/text.js +43 -0
- package/package.json +80 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件配置:Schemastery schema + 类型。
|
|
3
|
+
*
|
|
4
|
+
* 默认数据目录:$DSH_HOME/memory(用官方 dshHomePath 解析,DSH_HOME 缺省 ~/.dsh)。
|
|
5
|
+
*/
|
|
6
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
7
|
+
import type { ExtractMode } from './types.js';
|
|
8
|
+
export interface MemoryConfig {
|
|
9
|
+
/** 数据目录;留空则用 $DSH_HOME/memory。 */
|
|
10
|
+
dataDir: string;
|
|
11
|
+
/** 新会话的默认记忆档位:auto(双族自动)| chat(个人)| work(工作)。 */
|
|
12
|
+
family: ExtractMode;
|
|
13
|
+
capture: {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
/** 助手消息是否剥离代码块(减少嵌入噪声)。 */
|
|
16
|
+
stripCodeBlocks: boolean;
|
|
17
|
+
/** 单条消息内容最大字符数。 */
|
|
18
|
+
maxMessageChars: number;
|
|
19
|
+
};
|
|
20
|
+
extract: {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
/** 至少攒够多少条新消息才跑一次 L1 抽取(省 token)。 */
|
|
23
|
+
minMessages: number;
|
|
24
|
+
/** L1 抽取时的背景消息条数(供上下文推断,不参与提取)。 */
|
|
25
|
+
backgroundMessages: number;
|
|
26
|
+
/** 去重候选池大小(每条新记忆的相似候选数)。 */
|
|
27
|
+
candidatePool: number;
|
|
28
|
+
};
|
|
29
|
+
l2: {
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/** 距上次 L2 整合的新记忆达到该数量才触发。 */
|
|
32
|
+
minNewMemories: number;
|
|
33
|
+
/** 场景块数量上限。 */
|
|
34
|
+
maxScenes: number;
|
|
35
|
+
/** L2 prompt 里附带的相似场景全文数量上限。 */
|
|
36
|
+
sceneContextLimit: number;
|
|
37
|
+
};
|
|
38
|
+
l3: {
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
/** 距上次 L3 蒸馏的新记忆数量阈值。 */
|
|
41
|
+
interval: number;
|
|
42
|
+
};
|
|
43
|
+
recall: {
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
/** 每步自动召回注入的 L1 条数。 */
|
|
46
|
+
maxResults: number;
|
|
47
|
+
includePersona: boolean;
|
|
48
|
+
includeSceneNav: boolean;
|
|
49
|
+
/** 检索策略:keyword(FTS5 BM25)| embedding(向量)| hybrid(双路 + RRF 融合)。 */
|
|
50
|
+
strategy: 'keyword' | 'embedding' | 'hybrid';
|
|
51
|
+
/** 召回路径分数阈值(0~1,低于该分不注入;工具路径不过滤)。 */
|
|
52
|
+
scoreThreshold: number;
|
|
53
|
+
};
|
|
54
|
+
embedding: {
|
|
55
|
+
/** 向量检索总开关;关闭时纯 FTS 运行(官方 provider="none" 同款)。 */
|
|
56
|
+
enabled: boolean;
|
|
57
|
+
/** OpenAI 兼容 /embeddings 服务地址,如 https://api.siliconflow.cn/v1。 */
|
|
58
|
+
baseUrl: string;
|
|
59
|
+
apiKey: string;
|
|
60
|
+
model: string;
|
|
61
|
+
/** 向量维度(启用时必填,须与模型输出一致;vec0 建表需要固定维度)。 */
|
|
62
|
+
dimensions: number;
|
|
63
|
+
/** 单条文本最大字符数(超长截断)。 */
|
|
64
|
+
maxInputChars: number;
|
|
65
|
+
/** 单次调用超时(ms)。 */
|
|
66
|
+
timeoutMs: number;
|
|
67
|
+
};
|
|
68
|
+
llm: {
|
|
69
|
+
/** 蒸馏用的 provider 路由;留空用当前默认选择。 */
|
|
70
|
+
provider: string;
|
|
71
|
+
/** 蒸馏用的模型;留空用当前默认选择。 */
|
|
72
|
+
model: string;
|
|
73
|
+
/** 单次蒸馏调用的输出 token 上限(推理模型的 reasoning 与正文共享该预算)。 */
|
|
74
|
+
maxTokens: number;
|
|
75
|
+
temperature: number;
|
|
76
|
+
/** 单次蒸馏调用的用户 prompt 字符预算(≈token 数,按中文 1 字≈1 token 保守估算)。 */
|
|
77
|
+
maxInputChars: number;
|
|
78
|
+
/** 单次蒸馏调用超时(ms)。 */
|
|
79
|
+
timeoutMs: number;
|
|
80
|
+
};
|
|
81
|
+
/** 是否注册模型可调用的记忆工具。 */
|
|
82
|
+
tools: boolean;
|
|
83
|
+
}
|
|
84
|
+
export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
85
|
+
dataDir: Schema<string, string>;
|
|
86
|
+
family: Schema<"chat" | "work" | "auto", "chat" | "work" | "auto">;
|
|
87
|
+
capture: Schema<Schemastery.ObjectS<{
|
|
88
|
+
enabled: Schema<boolean, boolean>;
|
|
89
|
+
stripCodeBlocks: Schema<boolean, boolean>;
|
|
90
|
+
maxMessageChars: Schema<number, number>;
|
|
91
|
+
}>, Schemastery.ObjectT<{
|
|
92
|
+
enabled: Schema<boolean, boolean>;
|
|
93
|
+
stripCodeBlocks: Schema<boolean, boolean>;
|
|
94
|
+
maxMessageChars: Schema<number, number>;
|
|
95
|
+
}>>;
|
|
96
|
+
extract: Schema<Schemastery.ObjectS<{
|
|
97
|
+
enabled: Schema<boolean, boolean>;
|
|
98
|
+
minMessages: Schema<number, number>;
|
|
99
|
+
backgroundMessages: Schema<number, number>;
|
|
100
|
+
candidatePool: Schema<number, number>;
|
|
101
|
+
}>, Schemastery.ObjectT<{
|
|
102
|
+
enabled: Schema<boolean, boolean>;
|
|
103
|
+
minMessages: Schema<number, number>;
|
|
104
|
+
backgroundMessages: Schema<number, number>;
|
|
105
|
+
candidatePool: Schema<number, number>;
|
|
106
|
+
}>>;
|
|
107
|
+
l2: Schema<Schemastery.ObjectS<{
|
|
108
|
+
enabled: Schema<boolean, boolean>;
|
|
109
|
+
minNewMemories: Schema<number, number>;
|
|
110
|
+
maxScenes: Schema<number, number>;
|
|
111
|
+
sceneContextLimit: Schema<number, number>;
|
|
112
|
+
}>, Schemastery.ObjectT<{
|
|
113
|
+
enabled: Schema<boolean, boolean>;
|
|
114
|
+
minNewMemories: Schema<number, number>;
|
|
115
|
+
maxScenes: Schema<number, number>;
|
|
116
|
+
sceneContextLimit: Schema<number, number>;
|
|
117
|
+
}>>;
|
|
118
|
+
l3: Schema<Schemastery.ObjectS<{
|
|
119
|
+
enabled: Schema<boolean, boolean>;
|
|
120
|
+
interval: Schema<number, number>;
|
|
121
|
+
}>, Schemastery.ObjectT<{
|
|
122
|
+
enabled: Schema<boolean, boolean>;
|
|
123
|
+
interval: Schema<number, number>;
|
|
124
|
+
}>>;
|
|
125
|
+
recall: Schema<Schemastery.ObjectS<{
|
|
126
|
+
enabled: Schema<boolean, boolean>;
|
|
127
|
+
maxResults: Schema<number, number>;
|
|
128
|
+
includePersona: Schema<boolean, boolean>;
|
|
129
|
+
includeSceneNav: Schema<boolean, boolean>;
|
|
130
|
+
strategy: Schema<"keyword" | "embedding" | "hybrid", "keyword" | "embedding" | "hybrid">;
|
|
131
|
+
scoreThreshold: Schema<number, number>;
|
|
132
|
+
}>, Schemastery.ObjectT<{
|
|
133
|
+
enabled: Schema<boolean, boolean>;
|
|
134
|
+
maxResults: Schema<number, number>;
|
|
135
|
+
includePersona: Schema<boolean, boolean>;
|
|
136
|
+
includeSceneNav: Schema<boolean, boolean>;
|
|
137
|
+
strategy: Schema<"keyword" | "embedding" | "hybrid", "keyword" | "embedding" | "hybrid">;
|
|
138
|
+
scoreThreshold: Schema<number, number>;
|
|
139
|
+
}>>;
|
|
140
|
+
embedding: Schema<Schemastery.ObjectS<{
|
|
141
|
+
enabled: Schema<boolean, boolean>;
|
|
142
|
+
baseUrl: Schema<string, string>;
|
|
143
|
+
apiKey: Schema<string, string>;
|
|
144
|
+
model: Schema<string, string>;
|
|
145
|
+
dimensions: Schema<number, number>;
|
|
146
|
+
maxInputChars: Schema<number, number>;
|
|
147
|
+
timeoutMs: Schema<number, number>;
|
|
148
|
+
}>, Schemastery.ObjectT<{
|
|
149
|
+
enabled: Schema<boolean, boolean>;
|
|
150
|
+
baseUrl: Schema<string, string>;
|
|
151
|
+
apiKey: Schema<string, string>;
|
|
152
|
+
model: Schema<string, string>;
|
|
153
|
+
dimensions: Schema<number, number>;
|
|
154
|
+
maxInputChars: Schema<number, number>;
|
|
155
|
+
timeoutMs: Schema<number, number>;
|
|
156
|
+
}>>;
|
|
157
|
+
llm: Schema<Schemastery.ObjectS<{
|
|
158
|
+
provider: Schema<string, string>;
|
|
159
|
+
model: Schema<string, string>;
|
|
160
|
+
maxTokens: Schema<number, number>;
|
|
161
|
+
temperature: Schema<number, number>;
|
|
162
|
+
maxInputChars: Schema<number, number>;
|
|
163
|
+
timeoutMs: Schema<number, number>;
|
|
164
|
+
}>, Schemastery.ObjectT<{
|
|
165
|
+
provider: Schema<string, string>;
|
|
166
|
+
model: Schema<string, string>;
|
|
167
|
+
maxTokens: Schema<number, number>;
|
|
168
|
+
temperature: Schema<number, number>;
|
|
169
|
+
maxInputChars: Schema<number, number>;
|
|
170
|
+
timeoutMs: Schema<number, number>;
|
|
171
|
+
}>>;
|
|
172
|
+
tools: Schema<boolean, boolean>;
|
|
173
|
+
}>, Schemastery.ObjectT<{
|
|
174
|
+
dataDir: Schema<string, string>;
|
|
175
|
+
family: Schema<"chat" | "work" | "auto", "chat" | "work" | "auto">;
|
|
176
|
+
capture: Schema<Schemastery.ObjectS<{
|
|
177
|
+
enabled: Schema<boolean, boolean>;
|
|
178
|
+
stripCodeBlocks: Schema<boolean, boolean>;
|
|
179
|
+
maxMessageChars: Schema<number, number>;
|
|
180
|
+
}>, Schemastery.ObjectT<{
|
|
181
|
+
enabled: Schema<boolean, boolean>;
|
|
182
|
+
stripCodeBlocks: Schema<boolean, boolean>;
|
|
183
|
+
maxMessageChars: Schema<number, number>;
|
|
184
|
+
}>>;
|
|
185
|
+
extract: Schema<Schemastery.ObjectS<{
|
|
186
|
+
enabled: Schema<boolean, boolean>;
|
|
187
|
+
minMessages: Schema<number, number>;
|
|
188
|
+
backgroundMessages: Schema<number, number>;
|
|
189
|
+
candidatePool: Schema<number, number>;
|
|
190
|
+
}>, Schemastery.ObjectT<{
|
|
191
|
+
enabled: Schema<boolean, boolean>;
|
|
192
|
+
minMessages: Schema<number, number>;
|
|
193
|
+
backgroundMessages: Schema<number, number>;
|
|
194
|
+
candidatePool: Schema<number, number>;
|
|
195
|
+
}>>;
|
|
196
|
+
l2: Schema<Schemastery.ObjectS<{
|
|
197
|
+
enabled: Schema<boolean, boolean>;
|
|
198
|
+
minNewMemories: Schema<number, number>;
|
|
199
|
+
maxScenes: Schema<number, number>;
|
|
200
|
+
sceneContextLimit: Schema<number, number>;
|
|
201
|
+
}>, Schemastery.ObjectT<{
|
|
202
|
+
enabled: Schema<boolean, boolean>;
|
|
203
|
+
minNewMemories: Schema<number, number>;
|
|
204
|
+
maxScenes: Schema<number, number>;
|
|
205
|
+
sceneContextLimit: Schema<number, number>;
|
|
206
|
+
}>>;
|
|
207
|
+
l3: Schema<Schemastery.ObjectS<{
|
|
208
|
+
enabled: Schema<boolean, boolean>;
|
|
209
|
+
interval: Schema<number, number>;
|
|
210
|
+
}>, Schemastery.ObjectT<{
|
|
211
|
+
enabled: Schema<boolean, boolean>;
|
|
212
|
+
interval: Schema<number, number>;
|
|
213
|
+
}>>;
|
|
214
|
+
recall: Schema<Schemastery.ObjectS<{
|
|
215
|
+
enabled: Schema<boolean, boolean>;
|
|
216
|
+
maxResults: Schema<number, number>;
|
|
217
|
+
includePersona: Schema<boolean, boolean>;
|
|
218
|
+
includeSceneNav: Schema<boolean, boolean>;
|
|
219
|
+
strategy: Schema<"keyword" | "embedding" | "hybrid", "keyword" | "embedding" | "hybrid">;
|
|
220
|
+
scoreThreshold: Schema<number, number>;
|
|
221
|
+
}>, Schemastery.ObjectT<{
|
|
222
|
+
enabled: Schema<boolean, boolean>;
|
|
223
|
+
maxResults: Schema<number, number>;
|
|
224
|
+
includePersona: Schema<boolean, boolean>;
|
|
225
|
+
includeSceneNav: Schema<boolean, boolean>;
|
|
226
|
+
strategy: Schema<"keyword" | "embedding" | "hybrid", "keyword" | "embedding" | "hybrid">;
|
|
227
|
+
scoreThreshold: Schema<number, number>;
|
|
228
|
+
}>>;
|
|
229
|
+
embedding: Schema<Schemastery.ObjectS<{
|
|
230
|
+
enabled: Schema<boolean, boolean>;
|
|
231
|
+
baseUrl: Schema<string, string>;
|
|
232
|
+
apiKey: Schema<string, string>;
|
|
233
|
+
model: Schema<string, string>;
|
|
234
|
+
dimensions: Schema<number, number>;
|
|
235
|
+
maxInputChars: Schema<number, number>;
|
|
236
|
+
timeoutMs: Schema<number, number>;
|
|
237
|
+
}>, Schemastery.ObjectT<{
|
|
238
|
+
enabled: Schema<boolean, boolean>;
|
|
239
|
+
baseUrl: Schema<string, string>;
|
|
240
|
+
apiKey: Schema<string, string>;
|
|
241
|
+
model: Schema<string, string>;
|
|
242
|
+
dimensions: Schema<number, number>;
|
|
243
|
+
maxInputChars: Schema<number, number>;
|
|
244
|
+
timeoutMs: Schema<number, number>;
|
|
245
|
+
}>>;
|
|
246
|
+
llm: Schema<Schemastery.ObjectS<{
|
|
247
|
+
provider: Schema<string, string>;
|
|
248
|
+
model: Schema<string, string>;
|
|
249
|
+
maxTokens: Schema<number, number>;
|
|
250
|
+
temperature: Schema<number, number>;
|
|
251
|
+
maxInputChars: Schema<number, number>;
|
|
252
|
+
timeoutMs: Schema<number, number>;
|
|
253
|
+
}>, Schemastery.ObjectT<{
|
|
254
|
+
provider: Schema<string, string>;
|
|
255
|
+
model: Schema<string, string>;
|
|
256
|
+
maxTokens: Schema<number, number>;
|
|
257
|
+
temperature: Schema<number, number>;
|
|
258
|
+
maxInputChars: Schema<number, number>;
|
|
259
|
+
timeoutMs: Schema<number, number>;
|
|
260
|
+
}>>;
|
|
261
|
+
tools: Schema<boolean, boolean>;
|
|
262
|
+
}>>;
|
|
263
|
+
export declare function resolveDataDir(cfg: MemoryConfig): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件配置:Schemastery schema + 类型。
|
|
3
|
+
*
|
|
4
|
+
* 默认数据目录:$DSH_HOME/memory(用官方 dshHomePath 解析,DSH_HOME 缺省 ~/.dsh)。
|
|
5
|
+
*/
|
|
6
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
7
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
8
|
+
export const memorySchema = Schema.object({
|
|
9
|
+
dataDir: Schema.string().default(''),
|
|
10
|
+
family: Schema.union(['auto', 'chat', 'work']).default('auto'),
|
|
11
|
+
capture: Schema.object({
|
|
12
|
+
enabled: Schema.boolean().default(true),
|
|
13
|
+
stripCodeBlocks: Schema.boolean().default(true),
|
|
14
|
+
maxMessageChars: Schema.number().default(4000),
|
|
15
|
+
}),
|
|
16
|
+
extract: Schema.object({
|
|
17
|
+
enabled: Schema.boolean().default(true),
|
|
18
|
+
minMessages: Schema.number().default(1),
|
|
19
|
+
backgroundMessages: Schema.number().default(10),
|
|
20
|
+
candidatePool: Schema.number().default(5),
|
|
21
|
+
}),
|
|
22
|
+
l2: Schema.object({
|
|
23
|
+
enabled: Schema.boolean().default(true),
|
|
24
|
+
minNewMemories: Schema.number().default(5),
|
|
25
|
+
maxScenes: Schema.number().default(12),
|
|
26
|
+
sceneContextLimit: Schema.number().default(3),
|
|
27
|
+
}),
|
|
28
|
+
l3: Schema.object({
|
|
29
|
+
enabled: Schema.boolean().default(true),
|
|
30
|
+
interval: Schema.number().default(20),
|
|
31
|
+
}),
|
|
32
|
+
recall: Schema.object({
|
|
33
|
+
enabled: Schema.boolean().default(true),
|
|
34
|
+
maxResults: Schema.number().default(5),
|
|
35
|
+
includePersona: Schema.boolean().default(true),
|
|
36
|
+
includeSceneNav: Schema.boolean().default(true),
|
|
37
|
+
strategy: Schema.union(['keyword', 'embedding', 'hybrid']).default('hybrid'),
|
|
38
|
+
scoreThreshold: Schema.number().default(0.3),
|
|
39
|
+
}),
|
|
40
|
+
embedding: Schema.object({
|
|
41
|
+
enabled: Schema.boolean().default(false),
|
|
42
|
+
baseUrl: Schema.string().default(''),
|
|
43
|
+
apiKey: Schema.string().default(''),
|
|
44
|
+
model: Schema.string().default(''),
|
|
45
|
+
dimensions: Schema.number().default(0),
|
|
46
|
+
maxInputChars: Schema.number().default(5000),
|
|
47
|
+
timeoutMs: Schema.number().default(10_000),
|
|
48
|
+
}),
|
|
49
|
+
llm: Schema.object({
|
|
50
|
+
provider: Schema.string().default(''),
|
|
51
|
+
model: Schema.string().default(''),
|
|
52
|
+
// 推理模型(如 v4-flash)的 reasoning 计入输出预算:预算不足会被思考吃光导致正文 0 字符
|
|
53
|
+
maxTokens: Schema.number().default(20_000),
|
|
54
|
+
temperature: Schema.number().default(0.3),
|
|
55
|
+
// 模型上下文 1M token,日常压到 ~700k 使用(中文按 1 字≈1 token 保守折算)
|
|
56
|
+
maxInputChars: Schema.number().default(700_000),
|
|
57
|
+
timeoutMs: Schema.number().default(120_000),
|
|
58
|
+
}),
|
|
59
|
+
tools: Schema.boolean().default(true),
|
|
60
|
+
});
|
|
61
|
+
export function resolveDataDir(cfg) {
|
|
62
|
+
if (cfg.dataDir)
|
|
63
|
+
return cfg.dataDir;
|
|
64
|
+
return dshHomePath('memory');
|
|
65
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
3
|
+
import type { MemoryConfig } from '../config.js';
|
|
4
|
+
import type { MemoryRunner } from '../pipeline/runner.js';
|
|
5
|
+
import type { SessionModeStore } from '../store/session-modes.js';
|
|
6
|
+
import type { L0Store } from '../store/l0.js';
|
|
7
|
+
import type { LiveSettingsHandle } from '../settings.js';
|
|
8
|
+
import type { MemoryLogger } from '../types.js';
|
|
9
|
+
export declare function isCaptureRelevant(type: string): boolean;
|
|
10
|
+
export declare function registerCapture(ctx: Context, cfg: MemoryConfig, runner: MemoryRunner, l0: L0Store, logger: MemoryLogger, live: LiveSettingsHandle, modes: SessionModeStore): void;
|
|
11
|
+
/**
|
|
12
|
+
* 缓冲上限裁剪(防御性;RELEVANT_TYPES 过滤后基本不可达)。
|
|
13
|
+
* 铁律:进行中轮次(turn/start 之后、turn/end 之前)的事件绝不裁——
|
|
14
|
+
* 只裁最早一个未闭合 turn/start 之前的已完成前缀。
|
|
15
|
+
*/
|
|
16
|
+
export declare function trimBuffer(buf: SessionEvent[]): void;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L0 捕获 Hook:订阅 session/event,按轮次缓冲 user/assistant 消息,
|
|
3
|
+
* turn/end 时清洗并交给 Runner 落盘 + 触发蒸馏。
|
|
4
|
+
*
|
|
5
|
+
* 冷启动保护:插件激活时间之前的事件不捕获(防止恢复会话时倾倒全部历史)。
|
|
6
|
+
*/
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { blocksToText } from '../util/text.js';
|
|
9
|
+
import { sanitizeText, shouldCaptureL0, stripCodeBlocks } from '../util/sanitize.js';
|
|
10
|
+
/**
|
|
11
|
+
* 需要进缓冲的事件类型。流式 chunk(text-delta/reasoning 等)一秒钟可达数百条,
|
|
12
|
+
* 缓冲它们会把 MAX_BUFFER 撑爆、把轮次头部(turn/start + user 消息)裁掉——
|
|
13
|
+
* 2026-08-16 真实事故:长回复轮次丢失 user 消息。
|
|
14
|
+
*/
|
|
15
|
+
const RELEVANT_TYPES = new Set(['user/message', 'assistant/message', 'turn/start', 'turn/end']);
|
|
16
|
+
export function isCaptureRelevant(type) {
|
|
17
|
+
return RELEVANT_TYPES.has(type);
|
|
18
|
+
}
|
|
19
|
+
const MAX_BUFFER = 500;
|
|
20
|
+
export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
21
|
+
if (!cfg.capture.enabled)
|
|
22
|
+
return;
|
|
23
|
+
const startFloor = Date.now();
|
|
24
|
+
const buffers = new Map();
|
|
25
|
+
// L0 即时落盘链:turn/end 立刻写,不被蒸馏队列(慢 LLM 调用)阻塞;
|
|
26
|
+
// 串行化保证同轮次顺序与单次写入(进程退出时排队中的 L0 不再依赖蒸馏完成)
|
|
27
|
+
let l0Queue = Promise.resolve();
|
|
28
|
+
ctx.on('session/event', (session, event) => {
|
|
29
|
+
try {
|
|
30
|
+
const s = live.get();
|
|
31
|
+
if (!s.enabled || !s.capture)
|
|
32
|
+
return;
|
|
33
|
+
const sid = String(session.id ?? session);
|
|
34
|
+
// off 档:本会话对记忆系统完全隐身(不缓冲、不写 L0、不蒸馏)
|
|
35
|
+
if (modes.get(sid) === 'off')
|
|
36
|
+
return;
|
|
37
|
+
if (!isCaptureRelevant(event.type))
|
|
38
|
+
return;
|
|
39
|
+
if (event.time < startFloor) {
|
|
40
|
+
// 冷启动保护拦截的 user 消息记 info(罕见,但正是"整轮只剩 assistant"现象的线索)
|
|
41
|
+
if (event.type === 'user/message') {
|
|
42
|
+
logger.info(`[memory] L0 跳过早于插件启动的 user 消息(冷启动保护,早 ${startFloor - event.time}ms)`);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let buf = buffers.get(sid);
|
|
47
|
+
if (!buf) {
|
|
48
|
+
buf = [];
|
|
49
|
+
buffers.set(sid, buf);
|
|
50
|
+
}
|
|
51
|
+
buf.push(event);
|
|
52
|
+
trimBuffer(buf);
|
|
53
|
+
if (event.type === 'turn/end') {
|
|
54
|
+
const turn = event.data.turn;
|
|
55
|
+
const startIdx = findTurnStart(buf, turn);
|
|
56
|
+
const turnEvents = startIdx === -1 ? buf : buf.slice(startIdx + 1);
|
|
57
|
+
buffers.set(sid, startIdx === -1 ? [] : buf.slice(0, startIdx));
|
|
58
|
+
const messages = turnEventsToMessages(turnEvents, cfg, logger);
|
|
59
|
+
if (messages.length > 0) {
|
|
60
|
+
const roles = messages.reduce((acc, m) => {
|
|
61
|
+
acc[m.role] = (acc[m.role] ?? 0) + 1;
|
|
62
|
+
return acc;
|
|
63
|
+
}, {});
|
|
64
|
+
// 轮末档位生效(中途切档:本轮按 turn/end 时的档位蒸馏)
|
|
65
|
+
const mode = modes.get(sid);
|
|
66
|
+
if (mode === 'off') {
|
|
67
|
+
logger.info(`[memory] turn=${turn} 结束时档位为关闭,本轮不落盘不蒸馏(session=${sid})`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
logger.info(`[memory] L0 捕获 turn=${turn} ${messages.length} 条(${Object.entries(roles)
|
|
71
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
72
|
+
.join('/')},session=${sid},mode=${mode})`);
|
|
73
|
+
// L0 立刻落盘(不等蒸馏)
|
|
74
|
+
const n = messages.length;
|
|
75
|
+
l0Queue = l0Queue
|
|
76
|
+
.then(() => l0.append(sid, messages))
|
|
77
|
+
.then(() => logger.info(`[memory] L0 落盘 ${n} 条`))
|
|
78
|
+
.catch((err) => logger.warn(`[memory] L0 落盘失败: ${err instanceof Error ? err.message : String(err)}`));
|
|
79
|
+
runner.enqueue(sid, messages, mode);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
logger.warn(`[memory] session/event 处理失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 缓冲上限裁剪(防御性;RELEVANT_TYPES 过滤后基本不可达)。
|
|
90
|
+
* 铁律:进行中轮次(turn/start 之后、turn/end 之前)的事件绝不裁——
|
|
91
|
+
* 只裁最早一个未闭合 turn/start 之前的已完成前缀。
|
|
92
|
+
*/
|
|
93
|
+
export function trimBuffer(buf) {
|
|
94
|
+
if (buf.length <= MAX_BUFFER)
|
|
95
|
+
return;
|
|
96
|
+
let openStart = -1;
|
|
97
|
+
let closed = true;
|
|
98
|
+
for (let i = 0; i < buf.length; i++) {
|
|
99
|
+
const t = buf[i].type;
|
|
100
|
+
if (t === 'turn/start' && closed) {
|
|
101
|
+
openStart = i;
|
|
102
|
+
closed = false;
|
|
103
|
+
}
|
|
104
|
+
else if (t === 'turn/end') {
|
|
105
|
+
closed = true;
|
|
106
|
+
openStart = -1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const floorIdx = openStart === -1 ? Math.max(0, buf.length - MAX_BUFFER) : openStart;
|
|
110
|
+
if (floorIdx > 0)
|
|
111
|
+
buf.splice(0, floorIdx);
|
|
112
|
+
}
|
|
113
|
+
/** 找到与 turn/end 同 turn 的最后一个 turn/start 的索引(含自身)。 */
|
|
114
|
+
function findTurnStart(buf, turn) {
|
|
115
|
+
for (let i = buf.length - 1; i >= 0; i--) {
|
|
116
|
+
const e = buf[i];
|
|
117
|
+
if (e.type === 'turn/start' && e.data.turn === turn)
|
|
118
|
+
return i;
|
|
119
|
+
}
|
|
120
|
+
return -1;
|
|
121
|
+
}
|
|
122
|
+
/** 把轮次事件转成 L0 消息(仅真实 user 消息 + assistant 消息,清洗过滤)。 */
|
|
123
|
+
function turnEventsToMessages(events, cfg, logger) {
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const event of events) {
|
|
126
|
+
if (event.type === 'user/message') {
|
|
127
|
+
const msg = event.data;
|
|
128
|
+
// 只捕获真实用户输入(source.kind === 'user'),跳过插件注入上下文
|
|
129
|
+
if (msg.source?.kind !== 'user') {
|
|
130
|
+
logger.info(`[memory] L0 跳过非用户来源消息(source.kind=${msg.source?.kind ?? 'none'})`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const content = sanitizeText(blocksToText(msg.content));
|
|
134
|
+
if (shouldCaptureL0(content)) {
|
|
135
|
+
out.push(makeMessage('user', content, event.time, cfg.capture.maxMessageChars));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if (event.type === 'assistant/message') {
|
|
139
|
+
const data = event.data;
|
|
140
|
+
let content = sanitizeText(blocksToText(data.message?.content));
|
|
141
|
+
if (cfg.capture.stripCodeBlocks)
|
|
142
|
+
content = stripCodeBlocks(content);
|
|
143
|
+
if (shouldCaptureL0(content)) {
|
|
144
|
+
out.push(makeMessage('assistant', content, event.time, cfg.capture.maxMessageChars));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (out.length > 0) {
|
|
149
|
+
logger.debug?.(`[memory] 轮次消息 ${events.length} 事件 → ${out.length} 条(清洗过滤后)`);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
function makeMessage(role, content, timestamp, maxChars) {
|
|
154
|
+
return {
|
|
155
|
+
id: `msg_${Date.now()}_${randomBytes(3).toString('hex')}`,
|
|
156
|
+
role,
|
|
157
|
+
content: content.slice(0, maxChars),
|
|
158
|
+
timestamp,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 召回 Hook:
|
|
3
|
+
* 1. agent/pre-step(waterfall):用进入步骤的用户消息做 L1 BM25 检索,缓存到该 agent 的槽位;
|
|
4
|
+
* 必须调用并返回 next()(只观察,不改写步骤)。
|
|
5
|
+
* 2. agent/created:在 agent 作用域注册动态上下文 provider(L1 召回 + L3 画像 + L2 场景导航 + 工具指南)。
|
|
6
|
+
*
|
|
7
|
+
* 注入内容使用 <relevant-memories> / <user-persona> / <scene-navigation> / <memory-tools-guide>
|
|
8
|
+
* 标签包裹——capture 侧 sanitize 会剥离这些标签,防止反馈循环。
|
|
9
|
+
* auto 档两族合并时在 <user-persona>/<scene-navigation> 内部用 <domain family="..."> 子块
|
|
10
|
+
* 分域(子块只出现在外层标签内部,随外层一起被 sanitize 剥离,无泄漏风险)。
|
|
11
|
+
*
|
|
12
|
+
* 注意:PromptContext.text 是**同步**函数,画像/场景导航这类异步文件读取必须走内存缓存,
|
|
13
|
+
* 由定时刷新 + 管线更新后的主动失效来更新。
|
|
14
|
+
*/
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
import type { MemoryConfig } from '../config.js';
|
|
17
|
+
import type { LiveSettingsHandle } from '../settings.js';
|
|
18
|
+
import type { L1Store } from '../store/l1.js';
|
|
19
|
+
import type { PersonaStore } from '../store/persona.js';
|
|
20
|
+
import type { SceneStore } from '../store/scenes.js';
|
|
21
|
+
import type { SessionModeStore } from '../store/session-modes.js';
|
|
22
|
+
import type { MemoryLogger } from '../types.js';
|
|
23
|
+
export interface RecallHooks {
|
|
24
|
+
/** 管线更新后调用:画像/场景缓存立即失效并异步刷新。 */
|
|
25
|
+
invalidateProfile(): void;
|
|
26
|
+
}
|
|
27
|
+
export declare function registerRecall(ctx: Context, cfg: MemoryConfig, stores: {
|
|
28
|
+
l1: L1Store;
|
|
29
|
+
scenes: Record<'chat' | 'work', SceneStore>;
|
|
30
|
+
persona: Record<'chat' | 'work', PersonaStore>;
|
|
31
|
+
}, logger: MemoryLogger, live: LiveSettingsHandle, modes: SessionModeStore): RecallHooks;
|