@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.
Files changed (56) hide show
  1. package/dist/consolidator.d.ts +13 -0
  2. package/dist/consolidator.d.ts.map +1 -0
  3. package/dist/consolidator.js +184 -0
  4. package/dist/consolidator.js.map +1 -0
  5. package/dist/event-grouper.d.ts +13 -0
  6. package/dist/event-grouper.d.ts.map +1 -0
  7. package/dist/event-grouper.js +178 -0
  8. package/dist/event-grouper.js.map +1 -0
  9. package/dist/graph-store.d.ts.map +1 -1
  10. package/dist/graph-store.js +2 -4
  11. package/dist/graph-store.js.map +1 -1
  12. package/dist/hybrid-search.d.ts +7 -4
  13. package/dist/hybrid-search.d.ts.map +1 -1
  14. package/dist/hybrid-search.js +69 -8
  15. package/dist/hybrid-search.js.map +1 -1
  16. package/dist/index.d.ts +14 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +7 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/l2-evaluator.d.ts +31 -0
  21. package/dist/l2-evaluator.d.ts.map +1 -0
  22. package/dist/l2-evaluator.js +103 -0
  23. package/dist/l2-evaluator.js.map +1 -0
  24. package/dist/llm-provider.d.ts +9 -0
  25. package/dist/llm-provider.d.ts.map +1 -0
  26. package/dist/llm-provider.js +37 -0
  27. package/dist/llm-provider.js.map +1 -0
  28. package/dist/memory-engine.d.ts +27 -4
  29. package/dist/memory-engine.d.ts.map +1 -1
  30. package/dist/memory-engine.js +275 -182
  31. package/dist/memory-engine.js.map +1 -1
  32. package/dist/memory-gate.d.ts +116 -0
  33. package/dist/memory-gate.d.ts.map +1 -0
  34. package/dist/memory-gate.js +385 -0
  35. package/dist/memory-gate.js.map +1 -0
  36. package/dist/memory-store.d.ts +234 -32
  37. package/dist/memory-store.d.ts.map +1 -1
  38. package/dist/memory-store.js +729 -142
  39. package/dist/memory-store.js.map +1 -1
  40. package/dist/project-validator.d.ts +10 -0
  41. package/dist/project-validator.d.ts.map +1 -0
  42. package/dist/project-validator.js +36 -0
  43. package/dist/project-validator.js.map +1 -0
  44. package/dist/session-summarizer.d.ts +10 -0
  45. package/dist/session-summarizer.d.ts.map +1 -0
  46. package/dist/session-summarizer.js +197 -0
  47. package/dist/session-summarizer.js.map +1 -0
  48. package/dist/signal-buffer.d.ts +42 -0
  49. package/dist/signal-buffer.d.ts.map +1 -0
  50. package/dist/signal-buffer.js +104 -0
  51. package/dist/signal-buffer.js.map +1 -0
  52. package/dist/signal-detector.d.ts +39 -0
  53. package/dist/signal-detector.d.ts.map +1 -0
  54. package/dist/signal-detector.js +239 -0
  55. package/dist/signal-detector.js.map +1 -0
  56. package/package.json +5 -5
@@ -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, isExtractionAvailable } from './extractor.js';
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 memories = this.store.listByCategory('semantic');
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.category),
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
- return `session:${Date.now()}:${task.slice(0, 20)}`;
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
- void sessionId;
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.category,
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.category, content: m.content, confidence: m.confidence, scope: m.scope },
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.category, snippet: r.entry.content.slice(0, 150), score: r.score })), { topK: limit });
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(filter);
196
- return rows.map(r => ({
197
- id: r.id,
198
- category: r.category,
199
- content: r.content,
200
- originalText: r.original_text ?? undefined,
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 (all statuses)
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 (all statuses)
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
- const newEmb = await this.embedder.embed(entry.content);
248
- const all = this.store.list({ category: entry.category });
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.update(bestMatch.id, { content: entry.content });
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
- return {
275
- id: bestMatch.id,
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 — continue without it */ }
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
- this.embedder.embed(entry.content).then(emb => {
305
- this.store.insertEmbedding(row.id, emb);
306
- }).catch(() => { });
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.detectContradictions(row.id).catch(() => { });
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.category,
401
+ category: row.type,
312
402
  content: row.content,
313
403
  originalText: row.original_text ?? undefined,
314
404
  source: row.source,
315
- status: row.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.approved_at ?? undefined,
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.exitCode !== 0 && e.exitCode != null).length;
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
- async detectContradictions(memoryId) {
396
- const row = this.store.get(memoryId);
397
- if (!row)
398
- return;
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
- if (!NEGATION_WORDS.test(row.content))
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 !== newEmb.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 < newEmb.length; i++) {
421
- dot += newEmb[i] * existEmb[i];
422
- normA += newEmb[i] * newEmb[i];
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 > 0.85) {
427
- this.store.addEdge(memoryId, existing.id, 'contradicts', 0.9);
428
- this.store.addEdge(existing.id, memoryId, 'contradicts', 0.9);
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(event);
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.05;
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 confidence >= 0.85 ? 'approved' : 'pending';
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
- let ruleCount = 0;
482
- if (unprocessed.length > 0) {
483
- const HIGH_VALUE_TOOLS = new Set([
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
- // 4. Run cleanup
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: ruleCount,
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
- function compressEvents(events) {
564
- const compressed = [];
565
- for (let i = 0; i < events.length; i++) {
566
- const e = events[i];
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