deeper-cli 1.2.4 → 1.3.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/cli/index.js +231 -44
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/chat-repl.ts +1 -1
- package/src/memory/xmemory.ts +254 -48
- package/src/model/DeepSeekClient.ts +5 -0
package/package.json
CHANGED
package/src/cli/chat-repl.ts
CHANGED
|
@@ -952,7 +952,7 @@ After writing or editing code files, ALWAYS verify the changes:
|
|
|
952
952
|
}
|
|
953
953
|
r.push(e);
|
|
954
954
|
}
|
|
955
|
-
return r;
|
|
955
|
+
return r.map(m => { if (m.role === 'tool' && !m.name) m.name = 'tool'; return m; });
|
|
956
956
|
}
|
|
957
957
|
|
|
958
958
|
function trimHistory(h: Message[], max: number) { while (h.length > max) h.shift(); }
|
package/src/memory/xmemory.ts
CHANGED
|
@@ -7,14 +7,14 @@ export interface MemoryEntry {
|
|
|
7
7
|
type: 'working' | 'episodic' | 'semantic' | 'procedural';
|
|
8
8
|
content: string;
|
|
9
9
|
tags: string[];
|
|
10
|
-
importance: number;
|
|
11
|
-
accuracy: number;
|
|
10
|
+
importance: number;
|
|
11
|
+
accuracy: number;
|
|
12
12
|
createdAt: number;
|
|
13
13
|
accessedAt: number;
|
|
14
14
|
accessCount: number;
|
|
15
15
|
sessionId: string;
|
|
16
|
-
source: string;
|
|
17
|
-
references: string[];
|
|
16
|
+
source: string;
|
|
17
|
+
references: string[];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
interface XMemStats {
|
|
@@ -22,13 +22,17 @@ interface XMemStats {
|
|
|
22
22
|
byType: Record<string, number>;
|
|
23
23
|
totalTokens: number;
|
|
24
24
|
lastCleanup: number;
|
|
25
|
+
lastConsolidate: number;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const MEM_DIR = join(DEEPER_HOME, 'xmemory');
|
|
28
29
|
const STATS_FILE = join(MEM_DIR, 'stats.json');
|
|
29
|
-
const MAX_WORKING_MEM =
|
|
30
|
-
const MAX_TOTAL_MEM =
|
|
31
|
-
const CLEANUP_THRESHOLD =
|
|
30
|
+
const MAX_WORKING_MEM = 100;
|
|
31
|
+
const MAX_TOTAL_MEM = 3000;
|
|
32
|
+
const CLEANUP_THRESHOLD = 2000;
|
|
33
|
+
const DEDUP_SIMILARITY_THRESHOLD = 0.85;
|
|
34
|
+
const CONSOLIDATE_THRESHOLD = 5;
|
|
35
|
+
const SAVE_DEBOUNCE_MS = 3000;
|
|
32
36
|
|
|
33
37
|
let currentSessionId = '';
|
|
34
38
|
|
|
@@ -41,30 +45,87 @@ function uid(): string {
|
|
|
41
45
|
function ensureDir(): void {
|
|
42
46
|
if (!existsSync(MEM_DIR)) mkdirSync(MEM_DIR, { recursive: true });
|
|
43
47
|
if (!existsSync(STATS_FILE)) {
|
|
44
|
-
writeFileSync(STATS_FILE, JSON.stringify({ totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: Date.now() }), 'utf-8');
|
|
48
|
+
writeFileSync(STATS_FILE, JSON.stringify({ totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: Date.now(), lastConsolidate: Date.now() }, null, 2), 'utf-8');
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
function loadStats(): XMemStats {
|
|
49
53
|
try {
|
|
50
54
|
return JSON.parse(readFileSync(STATS_FILE, 'utf-8')) as XMemStats;
|
|
51
|
-
} catch { return { totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: 0 }; }
|
|
55
|
+
} catch { return { totalEntries: 0, byType: {}, totalTokens: 0, lastCleanup: 0, lastConsolidate: 0 }; }
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
function saveStats(s: XMemStats): void {
|
|
55
59
|
writeFileSync(STATS_FILE, JSON.stringify(s, null, 2), 'utf-8');
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
function tokenize(text: string): string[] {
|
|
63
|
+
const lower = text.toLowerCase();
|
|
64
|
+
const words = lower.split(/[\s\p{P}\p{S}]+/).filter(w => w.length >= 2);
|
|
65
|
+
const bigrams: string[] = [];
|
|
66
|
+
for (let i = 0; i < words.length - 1; i++) bigrams.push(words[i] + '_' + words[i + 1]);
|
|
67
|
+
return [...new Set([...words, ...bigrams])];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function similarity(a: string, b: string): number {
|
|
71
|
+
if (a === b) return 1;
|
|
72
|
+
if (!a || !b) return 0;
|
|
73
|
+
const ta = tokenize(a);
|
|
74
|
+
const tb = tokenize(b);
|
|
75
|
+
if (ta.length === 0 || tb.length === 0) return 0;
|
|
76
|
+
const sa = new Set(ta);
|
|
77
|
+
const sb = new Set(tb);
|
|
78
|
+
let intersection = 0;
|
|
79
|
+
for (const t of sa) if (sb.has(t)) intersection++;
|
|
80
|
+
const union = Math.max(sa.size, sb.size);
|
|
81
|
+
return union > 0 ? intersection / union : 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class TFIDFIndexer {
|
|
85
|
+
private docFreq: Map<string, number> = new Map();
|
|
86
|
+
private numDocs = 0;
|
|
87
|
+
|
|
88
|
+
build(entries: Iterable<MemoryEntry>): void {
|
|
89
|
+
this.docFreq.clear();
|
|
90
|
+
this.numDocs = 0;
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
this.numDocs++;
|
|
93
|
+
const tokens = new Set(tokenize(entry.content));
|
|
94
|
+
for (const t of tokens) this.docFreq.set(t, (this.docFreq.get(t) || 0) + 1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
idf(term: string): number {
|
|
99
|
+
const df = this.docFreq.get(term) || 0;
|
|
100
|
+
if (df === 0 || df >= this.numDocs) return 0;
|
|
101
|
+
return Math.log((this.numDocs + 0.5) / (df + 0.5)) + 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
tfidfScore(content: string, queryTerms: string[]): number {
|
|
105
|
+
const tokens = tokenize(content);
|
|
106
|
+
const tfMap = new Map<string, number>();
|
|
107
|
+
for (const t of tokens) tfMap.set(t, (tfMap.get(t) || 0) + 1);
|
|
108
|
+
const maxTf = Math.max(...tfMap.values(), 1);
|
|
109
|
+
let score = 0;
|
|
110
|
+
for (const qt of queryTerms) {
|
|
111
|
+
const tf = ((tfMap.get(qt) || 0) / maxTf) * (1 + Math.log(tokens.length));
|
|
112
|
+
score += tf * this.idf(qt);
|
|
113
|
+
}
|
|
114
|
+
return score;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
58
118
|
export class XMemory {
|
|
59
119
|
private working: MemoryEntry[] = [];
|
|
60
120
|
private index: Map<string, MemoryEntry> = new Map();
|
|
61
121
|
private dirty = false;
|
|
122
|
+
private saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
123
|
+
private tfidf: TFIDFIndexer = new TFIDFIndexer();
|
|
62
124
|
|
|
63
125
|
constructor() {
|
|
64
126
|
ensureDir();
|
|
65
127
|
}
|
|
66
128
|
|
|
67
|
-
// ============ 写入 ============
|
|
68
129
|
store(
|
|
69
130
|
type: MemoryEntry['type'],
|
|
70
131
|
content: string,
|
|
@@ -74,9 +135,12 @@ export class XMemory {
|
|
|
74
135
|
source: MemoryEntry['source'] = 'agent',
|
|
75
136
|
references: string[] = [],
|
|
76
137
|
): string {
|
|
138
|
+
const deduped = this.deduplicate(type, content, importance);
|
|
139
|
+
if (deduped !== null) return deduped;
|
|
140
|
+
|
|
77
141
|
const id = uid();
|
|
78
142
|
const entry: MemoryEntry = {
|
|
79
|
-
id, type, content: content.slice(0,
|
|
143
|
+
id, type, content: content.slice(0, 4000), tags,
|
|
80
144
|
importance: Math.min(10, Math.max(0, importance)),
|
|
81
145
|
accuracy: Math.min(10, Math.max(0, accuracy)),
|
|
82
146
|
createdAt: Date.now(), accessedAt: Date.now(),
|
|
@@ -86,13 +150,12 @@ export class XMemory {
|
|
|
86
150
|
|
|
87
151
|
if (type === 'working') {
|
|
88
152
|
this.working.push(entry);
|
|
89
|
-
|
|
90
|
-
this.working.shift();
|
|
91
|
-
}
|
|
153
|
+
this.evictWorking();
|
|
92
154
|
}
|
|
93
155
|
|
|
94
156
|
this.index.set(id, entry);
|
|
95
157
|
this.dirty = true;
|
|
158
|
+
this.scheduleSave();
|
|
96
159
|
|
|
97
160
|
if (this.index.size > CLEANUP_THRESHOLD) {
|
|
98
161
|
this.autoCleanup();
|
|
@@ -117,38 +180,54 @@ export class XMemory {
|
|
|
117
180
|
return this.store('working', content, tags, 3, 5, 'agent');
|
|
118
181
|
}
|
|
119
182
|
|
|
120
|
-
// ============ 检索 ============
|
|
121
183
|
recall(query: string, limit = 5, minImportance = 0): MemoryEntry[] {
|
|
122
|
-
const keywords = query
|
|
184
|
+
const keywords = tokenize(query).filter(w => w.length >= 2);
|
|
123
185
|
if (keywords.length === 0) return [];
|
|
124
186
|
|
|
125
|
-
|
|
187
|
+
this.tfidf.build(this.index.values());
|
|
126
188
|
const now = Date.now();
|
|
189
|
+
const scored: Array<{ entry: MemoryEntry; score: number }> = [];
|
|
127
190
|
|
|
128
191
|
for (const entry of this.index.values()) {
|
|
129
192
|
if (entry.importance < minImportance) continue;
|
|
130
|
-
|
|
131
|
-
const c =
|
|
193
|
+
|
|
194
|
+
const c = entry.content.toLowerCase();
|
|
132
195
|
const t = entry.tags.join(' ').toLowerCase();
|
|
133
196
|
|
|
197
|
+
let keywordScore = 0;
|
|
198
|
+
let exactMatchBonus = 0;
|
|
134
199
|
for (const kw of keywords) {
|
|
135
|
-
if (c.includes(kw))
|
|
136
|
-
if (t.includes(kw))
|
|
137
|
-
if (entry.type === 'procedural' && c.includes(kw))
|
|
200
|
+
if (c.includes(kw)) { keywordScore += 3; exactMatchBonus += 1; }
|
|
201
|
+
if (t.includes(kw)) keywordScore += 2;
|
|
202
|
+
if (entry.type === 'procedural' && c.includes(kw)) keywordScore += 1;
|
|
203
|
+
if (entry.type === 'semantic' && c.includes(kw)) keywordScore += 1.5;
|
|
138
204
|
}
|
|
139
205
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
206
|
+
const tfidfScore = this.tfidf.tfidfScore(entry.content, keywords);
|
|
207
|
+
|
|
208
|
+
const recencyBoost = Math.max(0, 3 - (now - entry.accessedAt) / (1000 * 60 * 30));
|
|
209
|
+
const frequencyBoost = Math.min(entry.accessCount * 0.3, 4);
|
|
210
|
+
const importanceWeight = entry.importance * 0.6;
|
|
211
|
+
const ageHours = (now - entry.createdAt) / (1000 * 60 * 60);
|
|
212
|
+
const ageDecay = Math.min(ageHours / 48, 6);
|
|
144
213
|
|
|
145
|
-
|
|
214
|
+
const totalScore =
|
|
215
|
+
keywordScore * 1.0 +
|
|
216
|
+
tfidfScore * 2.0 +
|
|
217
|
+
importanceWeight +
|
|
218
|
+
recencyBoost +
|
|
219
|
+
frequencyBoost -
|
|
220
|
+
ageDecay +
|
|
221
|
+
(exactMatchBonus >= 3 ? 3 : 0);
|
|
222
|
+
|
|
223
|
+
if (totalScore > 0) scored.push({ entry, score: totalScore });
|
|
146
224
|
}
|
|
147
225
|
|
|
148
226
|
scored.sort((a, b) => b.score - a.score);
|
|
149
227
|
return scored.slice(0, limit).map(s => {
|
|
150
228
|
s.entry.accessedAt = now;
|
|
151
229
|
s.entry.accessCount++;
|
|
230
|
+
s.entry.importance = Math.min(10, s.entry.importance + 0.1);
|
|
152
231
|
return s.entry;
|
|
153
232
|
});
|
|
154
233
|
}
|
|
@@ -158,20 +237,23 @@ export class XMemory {
|
|
|
158
237
|
for (const entry of this.index.values()) {
|
|
159
238
|
if (entry.type === type) res.push(entry);
|
|
160
239
|
}
|
|
161
|
-
res.sort((a, b) => b.
|
|
240
|
+
res.sort((a, b) => (b.importance * 2 + b.accessCount) - (a.importance * 2 + a.accessCount));
|
|
162
241
|
return res.slice(0, limit);
|
|
163
242
|
}
|
|
164
243
|
|
|
165
244
|
getWorking(): MemoryEntry[] {
|
|
166
|
-
return this.working
|
|
245
|
+
return this.working.sort((a, b) =>
|
|
246
|
+
(b.importance * 2 + b.accessCount) - (a.importance * 2 + a.accessCount)
|
|
247
|
+
);
|
|
167
248
|
}
|
|
168
249
|
|
|
169
|
-
getWorkingContext(maxTokens =
|
|
250
|
+
getWorkingContext(maxTokens = 4000): string {
|
|
170
251
|
if (this.working.length === 0) return '';
|
|
171
|
-
const
|
|
252
|
+
const sorted = this.getWorking();
|
|
253
|
+
const lines = sorted.map(e => `[${e.source}] ${e.content.slice(0, 500)}`);
|
|
172
254
|
let result = '';
|
|
173
255
|
for (const line of lines) {
|
|
174
|
-
if ((result + line).length > maxTokens *
|
|
256
|
+
if ((result + line).length > maxTokens * 3) break;
|
|
175
257
|
result += line + '\n';
|
|
176
258
|
}
|
|
177
259
|
return result;
|
|
@@ -181,28 +263,51 @@ export class XMemory {
|
|
|
181
263
|
const recalled = this.recall(task, limit, 3);
|
|
182
264
|
const proc = recalled.filter(r => r.type === 'procedural' || r.type === 'semantic');
|
|
183
265
|
if (proc.length === 0) return '';
|
|
184
|
-
return '[记忆提示]\n' + proc.map(p => `- ${p.content.slice(0,
|
|
266
|
+
return '[记忆提示]\n' + proc.map(p => `- [${p.type === 'procedural' ? '技能' : '知识'}] ${p.content.slice(0, 350)}`).join('\n');
|
|
185
267
|
}
|
|
186
268
|
|
|
187
269
|
getSessionSummary(): string {
|
|
188
270
|
const entries = [...this.index.values()].filter(e => e.sessionId === currentSessionId);
|
|
189
271
|
if (entries.length === 0) return '';
|
|
190
|
-
|
|
191
|
-
|
|
272
|
+
|
|
273
|
+
const byType: Record<string, MemoryEntry[]> = { semantic: [], procedural: [], episodic: [], working: [] };
|
|
274
|
+
for (const e of entries) {
|
|
275
|
+
if (byType[e.type]) byType[e.type].push(e);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const parts: string[] = [];
|
|
279
|
+
|
|
280
|
+
const keySemantic = byType.semantic
|
|
281
|
+
.sort((a, b) => b.importance - a.importance)
|
|
282
|
+
.slice(0, 5);
|
|
283
|
+
if (keySemantic.length > 0) {
|
|
284
|
+
parts.push(`[知识记忆·${keySemantic.length}条]\n` +
|
|
285
|
+
keySemantic.map(e => `• ${e.content.slice(0, 250)}`).join('\n'));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const keyProcedural = byType.procedural
|
|
289
|
+
.sort((a, b) => b.importance - a.importance)
|
|
290
|
+
.slice(0, 5);
|
|
291
|
+
if (keyProcedural.length > 0) {
|
|
292
|
+
parts.push(`[技能记忆·${keyProcedural.length}条]\n` +
|
|
293
|
+
keyProcedural.map(e => `• ${e.content.slice(0, 250)}`).join('\n'));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const keyEpisodic = byType.episodic
|
|
192
297
|
.sort((a, b) => b.importance - a.importance)
|
|
193
|
-
.slice(0,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
298
|
+
.slice(0, 3);
|
|
299
|
+
if (keyEpisodic.length > 0) {
|
|
300
|
+
parts.push(`[经历记忆·${keyEpisodic.length}条]\n` +
|
|
301
|
+
keyEpisodic.map(e => `→ ${e.content.slice(0, 150)}`).join('\n'));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return `[XMemory·本会话 ${entries.length}条]\n${parts.join('\n')}`;
|
|
198
305
|
}
|
|
199
306
|
|
|
200
|
-
// ============ 持久化 ============
|
|
201
307
|
async save(): Promise<void> {
|
|
202
308
|
if (!this.dirty) return;
|
|
203
309
|
ensureDir();
|
|
204
310
|
|
|
205
|
-
// 分批写入:每个 type 一个文件
|
|
206
311
|
const byType: Record<string, MemoryEntry[]> = {};
|
|
207
312
|
for (const entry of this.index.values()) {
|
|
208
313
|
if (!byType[entry.type]) byType[entry.type] = [];
|
|
@@ -211,7 +316,7 @@ export class XMemory {
|
|
|
211
316
|
|
|
212
317
|
for (const [type, entries] of Object.entries(byType)) {
|
|
213
318
|
const file = join(MEM_DIR, `${type}.json`);
|
|
214
|
-
writeFileSync(file, JSON.stringify(entries.slice(-
|
|
319
|
+
writeFileSync(file, JSON.stringify(entries.slice(-800)), 'utf-8');
|
|
215
320
|
}
|
|
216
321
|
|
|
217
322
|
const stats = loadStats();
|
|
@@ -241,24 +346,125 @@ export class XMemory {
|
|
|
241
346
|
if (entry.type === 'working') this.working.push(entry);
|
|
242
347
|
total++;
|
|
243
348
|
}
|
|
244
|
-
} catch {
|
|
349
|
+
} catch {}
|
|
245
350
|
}
|
|
246
351
|
|
|
247
352
|
if (this.working.length > MAX_WORKING_MEM) {
|
|
248
353
|
this.working = this.working.slice(-MAX_WORKING_MEM);
|
|
249
354
|
}
|
|
355
|
+
|
|
356
|
+
this.consolidateIfNeeded();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private scheduleSave(): void {
|
|
360
|
+
if (this.saveTimer) return;
|
|
361
|
+
this.saveTimer = setTimeout(() => {
|
|
362
|
+
this.saveTimer = null;
|
|
363
|
+
this.save().catch(() => {});
|
|
364
|
+
}, SAVE_DEBOUNCE_MS);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private deduplicate(type: MemoryEntry['type'], content: string, importance: number): string | null {
|
|
368
|
+
const threshold = type === 'working' ? 0.95 : type === 'episodic' ? 0.85 : DEDUP_SIMILARITY_THRESHOLD;
|
|
369
|
+
const shortContent = content.slice(0, 500);
|
|
370
|
+
|
|
371
|
+
for (const entry of this.index.values()) {
|
|
372
|
+
if (entry.type !== type) continue;
|
|
373
|
+
const sim = similarity(shortContent, entry.content.slice(0, 500));
|
|
374
|
+
if (sim >= threshold) {
|
|
375
|
+
entry.accessedAt = Date.now();
|
|
376
|
+
entry.accessCount++;
|
|
377
|
+
entry.importance = Math.min(10, Math.max(entry.importance, importance));
|
|
378
|
+
if (content.length > entry.content.length && content.length <= 4000) {
|
|
379
|
+
entry.content = content;
|
|
380
|
+
this.dirty = true;
|
|
381
|
+
}
|
|
382
|
+
return entry.id;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private evictWorking(): void {
|
|
389
|
+
while (this.working.length > MAX_WORKING_MEM) {
|
|
390
|
+
let worstIdx = 0;
|
|
391
|
+
let worstScore = Infinity;
|
|
392
|
+
for (let i = 0; i < this.working.length; i++) {
|
|
393
|
+
const e = this.working[i];
|
|
394
|
+
const score = e.importance + e.accessCount * 0.3 - (Date.now() - e.accessedAt) / (1000 * 60 * 15);
|
|
395
|
+
if (score < worstScore) { worstScore = score; worstIdx = i; }
|
|
396
|
+
}
|
|
397
|
+
this.working.splice(worstIdx, 1);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private consolidateIfNeeded(): void {
|
|
402
|
+
const stats = loadStats();
|
|
403
|
+
const now = Date.now();
|
|
404
|
+
if (now - stats.lastConsolidate < 24 * 60 * 60 * 1000) return;
|
|
405
|
+
|
|
406
|
+
const candidates: Map<string, MemoryEntry[]> = new Map();
|
|
407
|
+
for (const entry of this.index.values()) {
|
|
408
|
+
if (entry.type === 'episodic' && entry.accessCount >= CONSOLIDATE_THRESHOLD) {
|
|
409
|
+
const key = entry.tags.slice(0, 3).sort().join('|') || '_default_';
|
|
410
|
+
if (!candidates.has(key)) candidates.set(key, []);
|
|
411
|
+
candidates.get(key)!.push(entry);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
let consolidated = 0;
|
|
416
|
+
for (const [, group] of candidates) {
|
|
417
|
+
if (group.length < 2) continue;
|
|
418
|
+
group.sort((a, b) => b.accessCount - a.accessCount);
|
|
419
|
+
const best = group[0];
|
|
420
|
+
|
|
421
|
+
const allContent = group.map(g => g.content).join('\n');
|
|
422
|
+
const summary = `[ consolidated from ${group.length} memories ]\n${allContent.slice(0, 2000)}`;
|
|
423
|
+
|
|
424
|
+
const newType = best.accessCount >= 10 ? 'procedural' : 'semantic';
|
|
425
|
+
const existing = this.findSimilar(newType, best.content);
|
|
426
|
+
if (existing) {
|
|
427
|
+
existing.content = summary.slice(0, 4000);
|
|
428
|
+
existing.accessCount += Math.floor(group.reduce((s, g) => s + g.accessCount, 0) / group.length);
|
|
429
|
+
existing.importance = Math.min(10, existing.importance + 1);
|
|
430
|
+
} else {
|
|
431
|
+
this.store(newType, summary, best.tags, Math.min(10, best.importance + 2), 9, 'system');
|
|
432
|
+
consolidated++;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
for (const old of group) {
|
|
436
|
+
if (old.id !== best.id) this.index.delete(old.id);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (consolidated > 0 || candidates.size > 0) {
|
|
441
|
+
stats.lastConsolidate = now;
|
|
442
|
+
saveStats(stats);
|
|
443
|
+
this.dirty = true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private findSimilar(type: MemoryEntry['type'], content: string): MemoryEntry | null {
|
|
448
|
+
const threshold = 0.75;
|
|
449
|
+
let best: MemoryEntry | null = null;
|
|
450
|
+
let bestSim = 0;
|
|
451
|
+
for (const entry of this.index.values()) {
|
|
452
|
+
if (entry.type !== type) continue;
|
|
453
|
+
const sim = similarity(content.slice(0, 300), entry.content.slice(0, 300));
|
|
454
|
+
if (sim > bestSim) { bestSim = sim; best = entry; }
|
|
455
|
+
}
|
|
456
|
+
return bestSim >= threshold ? best : null;
|
|
250
457
|
}
|
|
251
458
|
|
|
252
|
-
// ============ 维护 ============
|
|
253
459
|
private autoCleanup(): void {
|
|
254
460
|
const entries = [...this.index.values()];
|
|
255
461
|
entries.sort((a, b) => {
|
|
256
|
-
const scoreA = a.importance * 2 + a.accessCount - (Date.now() - a.accessedAt) / (1000 * 60 * 60 * 24);
|
|
257
|
-
const scoreB = b.importance * 2 + b.accessCount - (Date.now() - b.accessedAt) / (1000 * 60 * 60 * 24);
|
|
462
|
+
const scoreA = a.importance * 2 + a.accessCount + (a.type === 'semantic' ? 3 : a.type === 'procedural' ? 2 : 0) - (Date.now() - a.accessedAt) / (1000 * 60 * 60 * 24);
|
|
463
|
+
const scoreB = b.importance * 2 + b.accessCount + (b.type === 'semantic' ? 3 : b.type === 'procedural' ? 2 : 0) - (Date.now() - b.accessedAt) / (1000 * 60 * 60 * 24);
|
|
258
464
|
return scoreA - scoreB;
|
|
259
465
|
});
|
|
260
466
|
|
|
261
|
-
const toKeep = entries.slice(-
|
|
467
|
+
const toKeep = entries.slice(-800);
|
|
262
468
|
this.index.clear();
|
|
263
469
|
this.working = [];
|
|
264
470
|
for (const entry of toKeep) {
|
|
@@ -210,6 +210,11 @@ export class DeepSeekClient {
|
|
|
210
210
|
const rc = m.reasoning_content || m.thinking;
|
|
211
211
|
if (rc) msg.reasoning_content = rc;
|
|
212
212
|
return msg;
|
|
213
|
+
}).map((msg) => {
|
|
214
|
+
if (msg.role === 'tool' && !msg.name) {
|
|
215
|
+
(msg as Record<string, unknown>).name = 'tool';
|
|
216
|
+
}
|
|
217
|
+
return msg;
|
|
213
218
|
}),
|
|
214
219
|
temperature: config.temperature,
|
|
215
220
|
max_tokens: config.maxTokens,
|