@devflow-tools/memory-engine 0.13.3 → 0.14.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/dist/consolidator.d.ts +13 -0
- package/dist/consolidator.d.ts.map +1 -0
- package/dist/consolidator.js +184 -0
- package/dist/consolidator.js.map +1 -0
- package/dist/event-grouper.d.ts +13 -0
- package/dist/event-grouper.d.ts.map +1 -0
- package/dist/event-grouper.js +178 -0
- package/dist/event-grouper.js.map +1 -0
- package/dist/graph-store.d.ts.map +1 -1
- package/dist/graph-store.js +2 -4
- package/dist/graph-store.js.map +1 -1
- package/dist/hybrid-search.d.ts +7 -4
- package/dist/hybrid-search.d.ts.map +1 -1
- package/dist/hybrid-search.js +69 -8
- package/dist/hybrid-search.js.map +1 -1
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/l2-evaluator.d.ts +31 -0
- package/dist/l2-evaluator.d.ts.map +1 -0
- package/dist/l2-evaluator.js +103 -0
- package/dist/l2-evaluator.js.map +1 -0
- package/dist/llm-provider.d.ts +9 -0
- package/dist/llm-provider.d.ts.map +1 -0
- package/dist/llm-provider.js +37 -0
- package/dist/llm-provider.js.map +1 -0
- package/dist/memory-engine.d.ts +27 -4
- package/dist/memory-engine.d.ts.map +1 -1
- package/dist/memory-engine.js +275 -182
- package/dist/memory-engine.js.map +1 -1
- package/dist/memory-gate.d.ts +116 -0
- package/dist/memory-gate.d.ts.map +1 -0
- package/dist/memory-gate.js +385 -0
- package/dist/memory-gate.js.map +1 -0
- package/dist/memory-store.d.ts +234 -32
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +729 -142
- package/dist/memory-store.js.map +1 -1
- package/dist/project-validator.d.ts +10 -0
- package/dist/project-validator.d.ts.map +1 -0
- package/dist/project-validator.js +36 -0
- package/dist/project-validator.js.map +1 -0
- package/dist/session-summarizer.d.ts +10 -0
- package/dist/session-summarizer.d.ts.map +1 -0
- package/dist/session-summarizer.js +197 -0
- package/dist/session-summarizer.js.map +1 -0
- package/dist/signal-buffer.d.ts +42 -0
- package/dist/signal-buffer.d.ts.map +1 -0
- package/dist/signal-buffer.js +104 -0
- package/dist/signal-buffer.js.map +1 -0
- package/dist/signal-detector.d.ts +39 -0
- package/dist/signal-detector.d.ts.map +1 -0
- package/dist/signal-detector.js +239 -0
- package/dist/signal-detector.js.map +1 -0
- package/package.json +5 -5
package/dist/memory-engine.js
CHANGED
|
@@ -5,20 +5,27 @@ import { MemoryStore } from './memory-store.js';
|
|
|
5
5
|
import { EmbeddingProvider } from './embedding-provider.js';
|
|
6
6
|
import { HybridSearch } from './hybrid-search.js';
|
|
7
7
|
import { GraphStore } from './graph-store.js';
|
|
8
|
-
import { extractMemories, extractFromEvents
|
|
8
|
+
import { extractMemories, extractFromEvents } from './extractor.js';
|
|
9
9
|
import { checkOllama } from './ollama-checker.js';
|
|
10
|
+
import { setLLMClient } from './llm-provider.js';
|
|
11
|
+
import { setL2LLMClient } from './l2-evaluator.js';
|
|
10
12
|
export class MemoryEngine {
|
|
11
13
|
store;
|
|
12
14
|
embedder;
|
|
13
15
|
searcher;
|
|
14
16
|
graph;
|
|
15
17
|
rootPath;
|
|
16
|
-
constructor(rootPath, embedder) {
|
|
18
|
+
constructor(rootPath, embedder, llmClient) {
|
|
17
19
|
this.rootPath = rootPath;
|
|
18
20
|
this.store = new MemoryStore(rootPath);
|
|
19
21
|
this.embedder = embedder ?? new EmbeddingProvider();
|
|
20
22
|
this.searcher = new HybridSearch(this.store, this.embedder, this);
|
|
21
23
|
this.graph = new GraphStore(this.store);
|
|
24
|
+
if (llmClient) {
|
|
25
|
+
setLLMClient(llmClient);
|
|
26
|
+
setL2LLMClient(llmClient);
|
|
27
|
+
console.log('[MemoryEngine] LLM client connected', { provider: llmClient.name });
|
|
28
|
+
}
|
|
22
29
|
}
|
|
23
30
|
_initPromise = null;
|
|
24
31
|
_embedderAvailable = true;
|
|
@@ -46,11 +53,12 @@ export class MemoryEngine {
|
|
|
46
53
|
// User preferences kept minimal — not stored in memory system
|
|
47
54
|
}
|
|
48
55
|
async getProjectContext() {
|
|
49
|
-
const
|
|
56
|
+
const projectTypes = ['project_knowledge', 'semantic', 'pattern', 'constraint', 'preference'];
|
|
57
|
+
const memories = this.store.listByTypes(projectTypes);
|
|
50
58
|
return {
|
|
51
59
|
stack: this.detectTechStack(),
|
|
52
60
|
conventions: memories.map(m => ({
|
|
53
|
-
key: m.scope || this.deriveConventionKey(m.content, m.
|
|
61
|
+
key: m.scope || this.deriveConventionKey(m.content, m.type),
|
|
54
62
|
value: m.content,
|
|
55
63
|
source: m.source,
|
|
56
64
|
confidence: m.confidence,
|
|
@@ -125,24 +133,54 @@ export class MemoryEngine {
|
|
|
125
133
|
return ctx.conventions;
|
|
126
134
|
}
|
|
127
135
|
async createSession(task) {
|
|
128
|
-
|
|
136
|
+
const id = `session:${Date.now().toString(36)}:${task.slice(0, 20)}`;
|
|
137
|
+
this.store.createSession(id, this.rootPath, task);
|
|
138
|
+
return id;
|
|
129
139
|
}
|
|
130
140
|
async appendToSession(_sessionId, _entry) {
|
|
131
141
|
// Session tracking delegated to existing IDataProvider
|
|
132
142
|
}
|
|
133
143
|
async getSession(sessionId) {
|
|
144
|
+
const row = this.store.getSession(sessionId);
|
|
145
|
+
if (row) {
|
|
146
|
+
return { sessionId, task: row.task, entries: [], createdAt: row.created_at };
|
|
147
|
+
}
|
|
134
148
|
return { sessionId, task: '', entries: [], createdAt: Date.now() };
|
|
135
149
|
}
|
|
136
150
|
async closeSession(sessionId) {
|
|
137
|
-
|
|
151
|
+
// Close in DB
|
|
152
|
+
this.store.closeSession(sessionId);
|
|
153
|
+
// Update session stats from events
|
|
154
|
+
const events = this.store.getSessionEvents(sessionId);
|
|
155
|
+
this.store.updateSessionStats(sessionId, {
|
|
156
|
+
tool_count: events.length,
|
|
157
|
+
error_count: events.filter(e => e.exit_code !== 0 && e.exit_code != null).length,
|
|
158
|
+
});
|
|
159
|
+
// Run summarizer to produce observations
|
|
160
|
+
await this.summarizeSessionEvents(sessionId, this.rootPath);
|
|
161
|
+
// Run consolidator for cross-session aggregation
|
|
162
|
+
try {
|
|
163
|
+
const { consolidate } = await import('./consolidator.js');
|
|
164
|
+
await consolidate(this.store, this.rootPath);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
console.warn('[MemoryEngine] Consolidator failed', { error: err.message });
|
|
168
|
+
}
|
|
169
|
+
// Run retention sweep (incremental — sweeps all candidates)
|
|
170
|
+
try {
|
|
171
|
+
this.store.runRetentionSweep(this.rootPath);
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
console.warn('[MemoryEngine] Retention sweep failed', { error: err.message });
|
|
175
|
+
}
|
|
138
176
|
}
|
|
139
177
|
async searchGlobal(query) {
|
|
140
178
|
await this.ensureInit();
|
|
141
179
|
const results = await this.searcher.search(query, { limit: 10 });
|
|
142
180
|
return results.map(r => ({
|
|
143
181
|
id: r.entry.id,
|
|
144
|
-
source: r.entry.
|
|
145
|
-
title: r.entry.content.slice(0, 80),
|
|
182
|
+
source: r.entry.type,
|
|
183
|
+
title: r.entry.title || r.entry.content.slice(0, 80),
|
|
146
184
|
content: r.entry.content,
|
|
147
185
|
embedding: [],
|
|
148
186
|
}));
|
|
@@ -163,7 +201,7 @@ export class MemoryEngine {
|
|
|
163
201
|
return pending.map(m => ({
|
|
164
202
|
id: m.id,
|
|
165
203
|
type: 'project',
|
|
166
|
-
content: { category: m.
|
|
204
|
+
content: { category: m.type, content: m.content, confidence: m.confidence, scope: m.scope },
|
|
167
205
|
confidence: m.confidence,
|
|
168
206
|
source: m.source,
|
|
169
207
|
}));
|
|
@@ -183,7 +221,7 @@ export class MemoryEngine {
|
|
|
183
221
|
if (results.length > limit) {
|
|
184
222
|
try {
|
|
185
223
|
const { claudeRerank } = await import('@devflow-tools/context-engine/claude-rerank.js');
|
|
186
|
-
const reranked = await claudeRerank(query, results.map(r => ({ id: r.entry.id, name: r.entry.
|
|
224
|
+
const reranked = await claudeRerank(query, results.map(r => ({ id: r.entry.id, name: r.entry.type, snippet: r.entry.content.slice(0, 150), score: r.score })), { topK: limit });
|
|
187
225
|
const rerankIds = new Set(reranked.map(r => r.id));
|
|
188
226
|
return results.filter(r => rerankIds.has(r.entry.id)).slice(0, limit);
|
|
189
227
|
}
|
|
@@ -192,24 +230,16 @@ export class MemoryEngine {
|
|
|
192
230
|
return results;
|
|
193
231
|
}
|
|
194
232
|
async listMemories(filter) {
|
|
195
|
-
const rows = this.store.list(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
source: r.source,
|
|
202
|
-
status: r.status,
|
|
203
|
-
confidence: r.confidence,
|
|
204
|
-
scope: r.scope,
|
|
205
|
-
sessionId: r.session_id ?? undefined,
|
|
206
|
-
createdAt: r.created_at,
|
|
207
|
-
approvedAt: r.approved_at ?? undefined,
|
|
208
|
-
}));
|
|
233
|
+
const rows = this.store.list({
|
|
234
|
+
type: filter?.category,
|
|
235
|
+
status: filter?.status,
|
|
236
|
+
scope: filter?.scope,
|
|
237
|
+
});
|
|
238
|
+
return rows.map(r => this._rowToRecord(r));
|
|
209
239
|
}
|
|
210
240
|
async addMemory(entry) {
|
|
211
241
|
await this.ensureInit();
|
|
212
|
-
// 1. SHA-256 content hash exact dedup
|
|
242
|
+
// 1. SHA-256 content hash exact dedup
|
|
213
243
|
const normalizedContent = entry.content
|
|
214
244
|
.replace(/\/Users\/[^/\s]+(\/\S+)?/g, '{PROJECT_ROOT}$1')
|
|
215
245
|
.replace(/\/tmp\/[^/\s]+/g, '{TMP}')
|
|
@@ -226,26 +256,15 @@ export class MemoryEngine {
|
|
|
226
256
|
});
|
|
227
257
|
if (dupe) {
|
|
228
258
|
this.store.incrementAccess(dupe.id);
|
|
229
|
-
return
|
|
230
|
-
id: dupe.id,
|
|
231
|
-
category: dupe.category,
|
|
232
|
-
content: dupe.content,
|
|
233
|
-
originalText: dupe.original_text ?? undefined,
|
|
234
|
-
source: dupe.source,
|
|
235
|
-
status: dupe.status,
|
|
236
|
-
confidence: dupe.confidence,
|
|
237
|
-
scope: dupe.scope,
|
|
238
|
-
sessionId: dupe.session_id ?? undefined,
|
|
239
|
-
createdAt: dupe.created_at,
|
|
240
|
-
approvedAt: dupe.approved_at ?? undefined,
|
|
241
|
-
};
|
|
259
|
+
return this._rowToRecord(dupe);
|
|
242
260
|
}
|
|
243
|
-
// 2. Semantic dedup by embedding similarity
|
|
261
|
+
// 2. Semantic dedup by embedding similarity + contradiction detection
|
|
244
262
|
let insertStatus = 'approved';
|
|
263
|
+
let newEmb = null;
|
|
245
264
|
if (this._embedderAvailable) {
|
|
246
265
|
try {
|
|
247
|
-
|
|
248
|
-
const all = this.store.list({
|
|
266
|
+
newEmb = await this.embedder.embed(entry.content);
|
|
267
|
+
const all = this.store.list({ type: entry.category });
|
|
249
268
|
let bestSim = 0;
|
|
250
269
|
let bestMatch = null;
|
|
251
270
|
for (const m of all.slice(0, 200)) {
|
|
@@ -269,27 +288,78 @@ export class MemoryEngine {
|
|
|
269
288
|
}
|
|
270
289
|
if (bestSim > 0.85 && bestMatch) {
|
|
271
290
|
if (entry.content.length > bestMatch.content.length) {
|
|
272
|
-
this.store.
|
|
291
|
+
const newRow = this.store.add({
|
|
292
|
+
type: bestMatch.type,
|
|
293
|
+
category: entry.category,
|
|
294
|
+
content: entry.content,
|
|
295
|
+
originalText: entry.originalText,
|
|
296
|
+
source: entry.source,
|
|
297
|
+
status: bestMatch.status,
|
|
298
|
+
confidence: entry.confidence ?? bestMatch.confidence,
|
|
299
|
+
scope: entry.scope ?? bestMatch.scope,
|
|
300
|
+
sessionId: entry.sessionId,
|
|
301
|
+
});
|
|
302
|
+
this.store.update(newRow.id, {
|
|
303
|
+
version: (bestMatch.version || 1) + 1,
|
|
304
|
+
supersedes: bestMatch.id,
|
|
305
|
+
});
|
|
306
|
+
this.store.update(bestMatch.id, {
|
|
307
|
+
status: 'superseded',
|
|
308
|
+
superseded_by: newRow.id,
|
|
309
|
+
});
|
|
310
|
+
this.store.recordHistory({
|
|
311
|
+
memoryId: bestMatch.id,
|
|
312
|
+
event: 'SUPERSEDE',
|
|
313
|
+
oldContent: bestMatch.content,
|
|
314
|
+
newContent: entry.content,
|
|
315
|
+
oldStrength: bestMatch.strength,
|
|
316
|
+
reason: `superseded_by:${newRow.id}`,
|
|
317
|
+
});
|
|
318
|
+
console.log('[MemoryEngine] Memory versioned', {
|
|
319
|
+
oldId: bestMatch.id,
|
|
320
|
+
newId: newRow.id,
|
|
321
|
+
version: (bestMatch.version || 1) + 1,
|
|
322
|
+
});
|
|
323
|
+
return this._rowToRecord(newRow);
|
|
273
324
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
category: bestMatch.category,
|
|
277
|
-
content: bestMatch.content,
|
|
278
|
-
originalText: bestMatch.original_text ?? undefined,
|
|
279
|
-
source: bestMatch.source,
|
|
280
|
-
status: bestMatch.status,
|
|
281
|
-
confidence: bestMatch.confidence,
|
|
282
|
-
scope: bestMatch.scope,
|
|
283
|
-
sessionId: bestMatch.session_id ?? undefined,
|
|
284
|
-
createdAt: bestMatch.created_at,
|
|
285
|
-
approvedAt: bestMatch.approved_at ?? undefined,
|
|
286
|
-
};
|
|
325
|
+
this.store.incrementAccess(bestMatch.id);
|
|
326
|
+
return this._rowToRecord(bestMatch);
|
|
287
327
|
}
|
|
288
328
|
if (bestSim > 0.7) {
|
|
289
329
|
insertStatus = 'pending';
|
|
290
330
|
}
|
|
291
331
|
}
|
|
292
|
-
catch { /* Semantic dedup failed
|
|
332
|
+
catch { /* Semantic dedup failed */ }
|
|
333
|
+
}
|
|
334
|
+
// 3. Contradiction detection (blocking, before write)
|
|
335
|
+
if (newEmb) {
|
|
336
|
+
try {
|
|
337
|
+
const arbitration = await this.detectAndArbitrateContradiction(entry.content, newEmb);
|
|
338
|
+
if (arbitration.decision === 'skip') {
|
|
339
|
+
console.log('[MemoryEngine] Memory skipped by contradiction arbitration', { reason: arbitration.reason });
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
if (arbitration.decision === 'merge' && arbitration.conflictingId) {
|
|
343
|
+
const mergedContent = arbitration.mergedContent || `${entry.content}`;
|
|
344
|
+
this.store.update(arbitration.conflictingId, { content: mergedContent });
|
|
345
|
+
this.store.incrementAccess(arbitration.conflictingId);
|
|
346
|
+
this.store.recordHistory({
|
|
347
|
+
memoryId: arbitration.conflictingId,
|
|
348
|
+
event: 'UPDATE',
|
|
349
|
+
newContent: mergedContent,
|
|
350
|
+
reason: `contradiction_merged: ${arbitration.reason ?? 'unknown'}`,
|
|
351
|
+
});
|
|
352
|
+
console.log('[MemoryEngine] Memory merged via contradiction arbitration', {
|
|
353
|
+
conflictingId: arbitration.conflictingId,
|
|
354
|
+
reason: arbitration.reason,
|
|
355
|
+
});
|
|
356
|
+
const mergedRow = this.store.get(arbitration.conflictingId);
|
|
357
|
+
return mergedRow ? this._rowToRecord(mergedRow) : null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
console.warn('[MemoryEngine] Contradiction detection failed, proceeding with write', { error: err.message });
|
|
362
|
+
}
|
|
293
363
|
}
|
|
294
364
|
const row = this.store.add({
|
|
295
365
|
category: entry.category,
|
|
@@ -301,29 +371,70 @@ export class MemoryEngine {
|
|
|
301
371
|
scope: entry.scope,
|
|
302
372
|
sessionId: entry.sessionId,
|
|
303
373
|
});
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
374
|
+
// 4. Audit: record ADD event
|
|
375
|
+
this.store.recordHistory({
|
|
376
|
+
memoryId: row.id,
|
|
377
|
+
event: 'ADD',
|
|
378
|
+
newContent: entry.content,
|
|
379
|
+
newStrength: entry.confidence ?? 0.5,
|
|
380
|
+
reason: entry.source === 'manual' ? 'manual' : entry.source === 'consolidation' ? 'consolidation' : 'auto',
|
|
381
|
+
});
|
|
382
|
+
// 5. Post-processing (async, non-blocking)
|
|
383
|
+
if (newEmb) {
|
|
384
|
+
this.store.insertEmbedding(row.id, newEmb);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
this.embedder.embed(entry.content).then(emb => {
|
|
388
|
+
this.store.insertEmbedding(row.id, emb);
|
|
389
|
+
}).catch(() => { });
|
|
390
|
+
}
|
|
307
391
|
this.graph.buildEdges(row.id).catch(() => { });
|
|
308
|
-
this.
|
|
392
|
+
return this._rowToRecord(row);
|
|
393
|
+
}
|
|
394
|
+
/** Map internal MemoryRow → SDK MemoryEntryRecord */
|
|
395
|
+
_rowToRecord(row) {
|
|
396
|
+
const sdkStatus = row.status === 'active' ? 'approved' :
|
|
397
|
+
row.status === 'superseded' ? 'expired' :
|
|
398
|
+
row.status;
|
|
309
399
|
return {
|
|
310
400
|
id: row.id,
|
|
311
|
-
category: row.
|
|
401
|
+
category: row.type,
|
|
312
402
|
content: row.content,
|
|
313
403
|
originalText: row.original_text ?? undefined,
|
|
314
404
|
source: row.source,
|
|
315
|
-
status:
|
|
405
|
+
status: sdkStatus,
|
|
316
406
|
confidence: row.confidence,
|
|
317
407
|
scope: row.scope,
|
|
318
408
|
sessionId: row.session_id ?? undefined,
|
|
319
409
|
createdAt: row.created_at,
|
|
320
|
-
approvedAt: row.
|
|
410
|
+
approvedAt: (row.status === 'active' || row.status === 'approved') ? row.updated_at : undefined,
|
|
321
411
|
};
|
|
322
412
|
}
|
|
323
413
|
async updateMemory(id, updates) {
|
|
414
|
+
const old = this.store.get(id);
|
|
324
415
|
this.store.update(id, updates);
|
|
416
|
+
if (old) {
|
|
417
|
+
this.store.recordHistory({
|
|
418
|
+
memoryId: id,
|
|
419
|
+
event: 'UPDATE',
|
|
420
|
+
oldContent: updates.content ? old.content : undefined,
|
|
421
|
+
newContent: updates.content,
|
|
422
|
+
oldStrength: old.confidence,
|
|
423
|
+
newStrength: updates.status ? old.confidence : undefined,
|
|
424
|
+
reason: 'manual_update',
|
|
425
|
+
});
|
|
426
|
+
}
|
|
325
427
|
}
|
|
326
428
|
async deleteMemory(id) {
|
|
429
|
+
const old = this.store.get(id);
|
|
430
|
+
if (old) {
|
|
431
|
+
this.store.recordHistory({
|
|
432
|
+
memoryId: id,
|
|
433
|
+
event: 'DELETE',
|
|
434
|
+
oldContent: old.content,
|
|
435
|
+
reason: 'manual_delete',
|
|
436
|
+
});
|
|
437
|
+
}
|
|
327
438
|
this.store.delete(id);
|
|
328
439
|
}
|
|
329
440
|
async getPendingMemories() {
|
|
@@ -331,6 +442,11 @@ export class MemoryEngine {
|
|
|
331
442
|
}
|
|
332
443
|
async approveMemory(id) {
|
|
333
444
|
this.store.update(id, { status: 'approved' });
|
|
445
|
+
this.store.recordHistory({
|
|
446
|
+
memoryId: id,
|
|
447
|
+
event: 'REINFORCE',
|
|
448
|
+
reason: 'user_approved',
|
|
449
|
+
});
|
|
334
450
|
}
|
|
335
451
|
async rejectMemory(id) {
|
|
336
452
|
this.store.delete(id);
|
|
@@ -375,7 +491,7 @@ export class MemoryEngine {
|
|
|
375
491
|
.map(e => e.command)
|
|
376
492
|
.filter(Boolean)
|
|
377
493
|
.slice(0, 20);
|
|
378
|
-
const errorCount = events.filter(e => e.
|
|
494
|
+
const errorCount = events.filter(e => e.exit_code !== 0 && e.exit_code != null).length;
|
|
379
495
|
const summary = [
|
|
380
496
|
`Session involved ${events.length} tool calls across ${toolSet.size} distinct tools`,
|
|
381
497
|
touchFiles.length > 0 ? `Touched files: ${touchFiles.join(', ')}` : null,
|
|
@@ -392,54 +508,101 @@ export class MemoryEngine {
|
|
|
392
508
|
this.store.setExpires(entry.id, expiresAt);
|
|
393
509
|
return entry;
|
|
394
510
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
511
|
+
/**
|
|
512
|
+
* Pre-write contradiction check with LLM arbitration.
|
|
513
|
+
* Returns the decision: 'keep' (write new), 'merge' (update existing), 'skip' (don't write).
|
|
514
|
+
*/
|
|
515
|
+
async detectAndArbitrateContradiction(content, embedding) {
|
|
399
516
|
const NEGATION_WORDS = /\b(not|never|don't|doesn't|no longer|instead|rather than|avoid)\b/i;
|
|
400
|
-
|
|
401
|
-
return;
|
|
402
|
-
let newEmb = null;
|
|
403
|
-
try {
|
|
404
|
-
newEmb = await this.embedder.embed(row.content);
|
|
405
|
-
}
|
|
406
|
-
catch {
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
517
|
+
const hasNegation = NEGATION_WORDS.test(content);
|
|
409
518
|
const allApproved = this.store.list({ status: 'approved' });
|
|
410
519
|
for (const existing of allApproved.slice(0, 100)) {
|
|
411
|
-
if (existing.id === memoryId)
|
|
412
|
-
continue;
|
|
413
520
|
const embBuf = this.store.getEmbedding(existing.id);
|
|
414
521
|
if (!embBuf)
|
|
415
522
|
continue;
|
|
416
523
|
const existEmb = new Float32Array(embBuf.buffer, embBuf.byteOffset, embBuf.byteLength / 4);
|
|
417
|
-
if (existEmb.length !==
|
|
524
|
+
if (existEmb.length !== embedding.length)
|
|
418
525
|
continue;
|
|
419
526
|
let dot = 0, normA = 0, normB = 0;
|
|
420
|
-
for (let i = 0; i <
|
|
421
|
-
dot +=
|
|
422
|
-
normA +=
|
|
527
|
+
for (let i = 0; i < embedding.length; i++) {
|
|
528
|
+
dot += embedding[i] * existEmb[i];
|
|
529
|
+
normA += embedding[i] * embedding[i];
|
|
423
530
|
normB += existEmb[i] * existEmb[i];
|
|
424
531
|
}
|
|
425
532
|
const sim = dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
426
|
-
if (sim
|
|
427
|
-
|
|
428
|
-
|
|
533
|
+
if (sim < 0.85)
|
|
534
|
+
continue;
|
|
535
|
+
console.log('[MemoryEngine] Contradiction detected', {
|
|
536
|
+
memoryId: existing.id,
|
|
537
|
+
conflictingContent: content.slice(0, 100),
|
|
538
|
+
similarity: sim,
|
|
539
|
+
});
|
|
540
|
+
// Try LLM arbitration
|
|
541
|
+
try {
|
|
542
|
+
const { callLLM } = await import('./llm-provider.js');
|
|
543
|
+
const ARBITRATION_SYSTEM = `You are a knowledge arbitrator. Two memory entries appear to contradict each other.
|
|
544
|
+
Decide what to do:
|
|
545
|
+
- "keep": the new entry is correct, keep it as-is
|
|
546
|
+
- "merge": merge the new information into the existing entry (provide the merged text)
|
|
547
|
+
- "skip": the new entry is wrong or redundant, discard it
|
|
548
|
+
|
|
549
|
+
Output ONLY a JSON object: {"decision":"keep|merge|skip","mergedContent":"...","confidence":0.0-1.0,"reason":"..."}`;
|
|
550
|
+
const arbitrationPrompt = `Existing memory: "${existing.content}"
|
|
551
|
+
New entry: "${content}"
|
|
552
|
+
Similarity: ${sim.toFixed(3)}
|
|
553
|
+
Has negation words: ${hasNegation}
|
|
554
|
+
|
|
555
|
+
Decision:`;
|
|
556
|
+
const response = await callLLM(ARBITRATION_SYSTEM, arbitrationPrompt);
|
|
557
|
+
if (response.provider !== 'rules') {
|
|
558
|
+
const result = JSON.parse(response.content);
|
|
559
|
+
console.log('[MemoryEngine] Contradiction LLM arbitration', {
|
|
560
|
+
decision: result.decision,
|
|
561
|
+
confidence: result.confidence,
|
|
562
|
+
});
|
|
563
|
+
return {
|
|
564
|
+
decision: result.decision,
|
|
565
|
+
conflictingId: existing.id,
|
|
566
|
+
mergedContent: result.mergedContent,
|
|
567
|
+
confidence: result.confidence,
|
|
568
|
+
reason: result.reason,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
console.warn('[MemoryEngine] LLM arbitration failed, using rule fallback', { error: err.message });
|
|
574
|
+
}
|
|
575
|
+
// Rule fallback
|
|
576
|
+
if (hasNegation) {
|
|
577
|
+
return {
|
|
578
|
+
decision: 'merge',
|
|
579
|
+
conflictingId: existing.id,
|
|
580
|
+
mergedContent: `${existing.content}\n\nUpdated: ${content}`,
|
|
581
|
+
confidence: 0.5,
|
|
582
|
+
reason: 'negation_rule_fallback',
|
|
583
|
+
};
|
|
429
584
|
}
|
|
585
|
+
return { decision: 'keep', confidence: 0.5, reason: 'rule_fallback_no_negation' };
|
|
430
586
|
}
|
|
587
|
+
return { decision: 'keep' };
|
|
431
588
|
}
|
|
432
589
|
close() {
|
|
433
590
|
this.store.close();
|
|
434
591
|
}
|
|
435
592
|
// --- Event Recording & Extraction Pipeline ---
|
|
436
593
|
async recordEvent(event) {
|
|
437
|
-
this.store.recordEvent(
|
|
594
|
+
this.store.recordEvent({
|
|
595
|
+
...event,
|
|
596
|
+
project: event.sessionId ? undefined : undefined,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
recordSignals(signals) {
|
|
600
|
+
this.store.recordSignals(signals);
|
|
438
601
|
}
|
|
439
602
|
getEffectiveConfidence(entry) {
|
|
440
603
|
const now = Date.now();
|
|
441
604
|
const ageDays = (now - entry.createdAt) / (1000 * 60 * 60 * 24);
|
|
442
|
-
const lambdaBase = 0.
|
|
605
|
+
const lambdaBase = 0.006;
|
|
443
606
|
const accessCount = entry.accessCount ?? 0;
|
|
444
607
|
const lambdaActual = lambdaBase / (1 + 0.1 * accessCount);
|
|
445
608
|
return entry.confidence * Math.exp(-lambdaActual * ageDays);
|
|
@@ -447,7 +610,9 @@ export class MemoryEngine {
|
|
|
447
610
|
applyDecisionMatrix(category, confidence) {
|
|
448
611
|
switch (category) {
|
|
449
612
|
case 'procedural': return confidence >= 0.85 ? 'approved' : 'pending';
|
|
450
|
-
case 'failure_lesson': return
|
|
613
|
+
case 'failure_lesson': return 'pending';
|
|
614
|
+
case 'mcp_correction': return confidence >= 0.8 ? 'approved' : 'pending';
|
|
615
|
+
case 'project_knowledge': return 'approved';
|
|
451
616
|
case 'semantic': return confidence >= 0.65 ? 'approved' : 'pending';
|
|
452
617
|
case 'episodic': return 'pending';
|
|
453
618
|
default: return 'pending';
|
|
@@ -478,106 +643,34 @@ export class MemoryEngine {
|
|
|
478
643
|
async processSessionEvents() {
|
|
479
644
|
await this.ensureInit();
|
|
480
645
|
const unprocessed = this.store.getUnprocessedEvents();
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
'Write', 'Edit', 'Read',
|
|
485
|
-
'mcp__devflow__get_project_context', 'mcp__devflow__get_memory',
|
|
486
|
-
'mcp__devflow__get_knowledge', 'mcp__devflow__search_symbol',
|
|
487
|
-
'mcp__devflow__get_dependency_graph', 'mcp__devflow__run_workflow',
|
|
488
|
-
]);
|
|
489
|
-
const LOW_VALUE_FIRST_WORDS = new Set([
|
|
490
|
-
'cat', 'ls', 'grep', 'find', 'echo', 'cd', 'pwd', 'which',
|
|
491
|
-
'head', 'tail', 'wc', 'date', 'tsc', 'npm', 'npx', 'mkdir',
|
|
492
|
-
]);
|
|
493
|
-
const filtered = unprocessed.filter(e => {
|
|
494
|
-
if (!HIGH_VALUE_TOOLS.has(e.tool))
|
|
495
|
-
return false;
|
|
496
|
-
if (e.tool === 'Bash' && e.command) {
|
|
497
|
-
const firstWord = e.command.split(' ')[0] ?? '';
|
|
498
|
-
if (LOW_VALUE_FIRST_WORDS.has(firstWord))
|
|
499
|
-
return false;
|
|
500
|
-
}
|
|
501
|
-
return true;
|
|
502
|
-
});
|
|
503
|
-
// 1. Rule engine fallback on filtered events
|
|
504
|
-
const eventsToProcess = filtered.length > 0 ? filtered : unprocessed;
|
|
505
|
-
const ruleResults = await this.extractFromEventsRealtime(eventsToProcess.map(e => ({ tool: e.tool, command: e.command ?? undefined, exitCode: e.exitCode ?? undefined, stderr: e.stderr ?? undefined })));
|
|
506
|
-
ruleCount = ruleResults.length;
|
|
507
|
-
// 2. AI extraction via Ollama
|
|
508
|
-
const ollamaStatus = await checkOllama();
|
|
509
|
-
if (ollamaStatus.installed && ollamaStatus.running && ollamaStatus.modelAvailable) {
|
|
510
|
-
const compressed = compressEvents(eventsToProcess);
|
|
511
|
-
try {
|
|
512
|
-
const aiCandidates = await extractMemories(compressed, ollamaStatus);
|
|
513
|
-
for (const c of aiCandidates) {
|
|
514
|
-
await this.addMemory({
|
|
515
|
-
category: c.category,
|
|
516
|
-
content: c.content,
|
|
517
|
-
originalText: c.originalText,
|
|
518
|
-
source: 'ai_extract',
|
|
519
|
-
confidence: c.confidence,
|
|
520
|
-
scope: '',
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
catch {
|
|
525
|
-
// AI extraction failed — rule engine results are already saved
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
else if (isExtractionAvailable()) {
|
|
529
|
-
// 2.5. Claude API fallback when Ollama unavailable
|
|
530
|
-
try {
|
|
531
|
-
const { extractViaClaudeAPI } = await import('./extractor.js');
|
|
532
|
-
const eventsForExtraction = filtered.length > 0 ? filtered : unprocessed;
|
|
533
|
-
const compressed = compressEvents(eventsForExtraction);
|
|
534
|
-
const aiCandidates = await extractViaClaudeAPI(compressed, `session:${Date.now().toString(36)}`);
|
|
535
|
-
for (const c of aiCandidates) {
|
|
536
|
-
await this.addMemory({
|
|
537
|
-
category: c.category,
|
|
538
|
-
content: c.content,
|
|
539
|
-
source: 'claude_api',
|
|
540
|
-
confidence: c.confidence,
|
|
541
|
-
scope: '',
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
catch (err) {
|
|
546
|
-
console.error('[devflow] Claude API extraction failed:', err.message);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
// 3. Mark events processed
|
|
550
|
-
this.store.markEventsProcessed(unprocessed.map(e => e.id));
|
|
646
|
+
if (unprocessed.length === 0) {
|
|
647
|
+
this.store.cleanupExpired();
|
|
648
|
+
return { extracted: 0, approved: 0, pending: 0 };
|
|
551
649
|
}
|
|
552
|
-
//
|
|
650
|
+
// Use Session Summarizer pipeline (event grouping → triage → LLM → observations)
|
|
651
|
+
const { summarizeSession } = await import('./session-summarizer.js');
|
|
652
|
+
const result = await summarizeSession(this.store, `session:${Date.now().toString(36)}`, this.rootPath);
|
|
653
|
+
// Mark events as processed
|
|
654
|
+
this.store.markEventsProcessed(unprocessed.map(e => e.id));
|
|
655
|
+
// Run cleanup
|
|
553
656
|
this.store.cleanupExpired();
|
|
554
|
-
this.store.cleanupLowQuality();
|
|
555
657
|
const allMemories = this.store.list();
|
|
658
|
+
console.log('[MemoryEngine] Session events processed', {
|
|
659
|
+
observations: result.observationCount,
|
|
660
|
+
provider: result.provider,
|
|
661
|
+
latencyMs: result.latencyMs,
|
|
662
|
+
});
|
|
556
663
|
return {
|
|
557
|
-
extracted:
|
|
558
|
-
approved: allMemories.filter(m => m.status === 'approved').length,
|
|
664
|
+
extracted: result.observationCount,
|
|
665
|
+
approved: allMemories.filter(m => m.status === 'active' || m.status === 'approved').length,
|
|
559
666
|
pending: allMemories.filter(m => m.status === 'pending').length,
|
|
560
667
|
};
|
|
561
668
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (!e)
|
|
568
|
-
continue;
|
|
569
|
-
const prev = compressed[compressed.length - 1];
|
|
570
|
-
if (prev && prev.tool === e.tool && prev.command === e.command && prev.exitCode === e.exitCode) {
|
|
571
|
-
continue;
|
|
572
|
-
}
|
|
573
|
-
compressed.push({
|
|
574
|
-
toolName: e.tool,
|
|
575
|
-
command: e.command,
|
|
576
|
-
error: e.exitCode !== 0 ? (e.stderr ?? `exit code ${e.exitCode}`) : undefined,
|
|
577
|
-
errorMessage: e.stderr,
|
|
578
|
-
exitCode: e.exitCode,
|
|
579
|
-
});
|
|
669
|
+
/** New: summarize a specific session's events into observations. */
|
|
670
|
+
async summarizeSessionEvents(sessionId, projectRoot) {
|
|
671
|
+
await this.ensureInit();
|
|
672
|
+
const { summarizeSession } = await import('./session-summarizer.js');
|
|
673
|
+
return summarizeSession(this.store, sessionId, projectRoot);
|
|
580
674
|
}
|
|
581
|
-
return compressed;
|
|
582
675
|
}
|
|
583
676
|
//# sourceMappingURL=memory-engine.js.map
|