@devflow-tools/memory-engine 0.17.0 → 0.17.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 (42) hide show
  1. package/dist/consolidator.d.ts.map +1 -1
  2. package/dist/consolidator.js +11 -12
  3. package/dist/consolidator.js.map +1 -1
  4. package/dist/event-grouper.d.ts +1 -0
  5. package/dist/event-grouper.d.ts.map +1 -1
  6. package/dist/event-grouper.js +18 -13
  7. package/dist/event-grouper.js.map +1 -1
  8. package/dist/hybrid-search.d.ts +1 -0
  9. package/dist/hybrid-search.d.ts.map +1 -1
  10. package/dist/hybrid-search.js +73 -6
  11. package/dist/hybrid-search.js.map +1 -1
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/memory-capsule.d.ts +3 -0
  17. package/dist/memory-capsule.d.ts.map +1 -1
  18. package/dist/memory-capsule.js +27 -0
  19. package/dist/memory-capsule.js.map +1 -1
  20. package/dist/memory-engine.d.ts.map +1 -1
  21. package/dist/memory-engine.js +75 -34
  22. package/dist/memory-engine.js.map +1 -1
  23. package/dist/memory-gate.d.ts +5 -0
  24. package/dist/memory-gate.d.ts.map +1 -1
  25. package/dist/memory-gate.js +276 -30
  26. package/dist/memory-gate.js.map +1 -1
  27. package/dist/memory-relevance.d.ts +3 -0
  28. package/dist/memory-relevance.d.ts.map +1 -1
  29. package/dist/memory-relevance.js +8 -2
  30. package/dist/memory-relevance.js.map +1 -1
  31. package/dist/memory-store.d.ts +1 -0
  32. package/dist/memory-store.d.ts.map +1 -1
  33. package/dist/memory-store.js +57 -18
  34. package/dist/memory-store.js.map +1 -1
  35. package/dist/semantic-memory-compiler.d.ts +29 -0
  36. package/dist/semantic-memory-compiler.d.ts.map +1 -0
  37. package/dist/semantic-memory-compiler.js +148 -0
  38. package/dist/semantic-memory-compiler.js.map +1 -0
  39. package/dist/session-summarizer.d.ts +2 -0
  40. package/dist/session-summarizer.d.ts.map +1 -1
  41. package/dist/session-summarizer.js.map +1 -1
  42. package/package.json +4 -3
@@ -12,7 +12,8 @@ import { join } from 'node:path';
12
12
  import { isObservationType, observationDedupKey, } from './observation-types.js';
13
13
  import { evaluateObservationQuality } from './observation-quality.js';
14
14
  import { mergeSemanticEntities, normalizeEntityKey, normalizeSemanticMemory, } from './semantic-memory.js';
15
- import { validateMemoryCapsules } from './memory-capsule.js';
15
+ import { validateEvidenceSpans, validateMemoryCapsules } from './memory-capsule.js';
16
+ import { emitSemanticControlLog } from '@devflow-tools/telemetry';
16
17
  const BATCH_CONCEPT_STOP_WORDS = new Set([
17
18
  'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
18
19
  'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'should',
@@ -180,10 +181,12 @@ export class MemoryGate {
180
181
  if (this.engine && signals.length > 0) {
181
182
  try {
182
183
  this.engine.recordSignals(signals);
183
- console.log('[MemoryGate] Signals detected', {
184
- count: signals.length,
185
- types: signals.map(s => s.type),
186
- sessionId: event.sessionId,
184
+ emitSemanticControlLog({
185
+ event: 'memory.write.completed',
186
+ identity: { projectRoot: this.projectRoot, sessionId: event.sessionId },
187
+ level: 'debug',
188
+ timestamp: Date.now(),
189
+ data: { stage: 'signals', count: signals.length, types: signals.map(signal => signal.type) },
187
190
  });
188
191
  }
189
192
  catch { /* non-critical */ }
@@ -263,9 +266,9 @@ export class MemoryGate {
263
266
  .filter(event => sessionId === undefined || event.session_id === sessionId);
264
267
  if (events.length === 0)
265
268
  return null;
266
- const groups = groupEventsBySession(events);
269
+ const groups = groupEventsBySession(events, { projectRoot: this.projectRoot });
267
270
  const preserveSessionIds = this.getActiveSessionIds(groups);
268
- const { selected, discarded } = triageGroupsWithDisposition(groups, 8, { preserveSessionIds });
271
+ const { selected, discarded } = triageGroupsWithDisposition(groups, 8, { preserveSessionIds, projectRoot: this.projectRoot });
269
272
  const discardedEventIds = discarded.flatMap(group => group.events).map(event => event.id);
270
273
  if (discardedEventIds.length > 0) {
271
274
  const discardedSessionIds = [...new Set(discarded.flatMap(group => group.events).map(event => event.session_id).filter(Boolean))];
@@ -452,10 +455,12 @@ export class MemoryGate {
452
455
  leases.commitLease(batchId);
453
456
  });
454
457
  transaction();
455
- console.log('[MemoryGate] Distilled observations saved by batch', {
456
- batchId,
457
- observationCount: validObs.length,
458
- eventsMarked: lease.eventIds.length,
458
+ emitSemanticControlLog({
459
+ event: 'memory.write.completed',
460
+ identity: { projectRoot: this.projectRoot, sessionId: lease.sessionIds[0] },
461
+ level: 'info',
462
+ timestamp: Date.now(),
463
+ data: { stage: 'distill_batch', batchId, observationCount: validObs.length, eventsMarked: lease.eventIds.length },
459
464
  });
460
465
  return validObs.length;
461
466
  }
@@ -503,6 +508,7 @@ export class MemoryGate {
503
508
  observations: validObs,
504
509
  sourceEvents: [event],
505
510
  defaultSourceEventIds: [turnEventId],
511
+ evidenceText: readCanonicalPrompt(event.payload, event.user_message) || undefined,
506
512
  });
507
513
  if (capsules.rejected.length > 0 || capsules.accepted.length !== validObs.length) {
508
514
  throw new Error(`memory_commit_turn failed semantic capsule validation: ${JSON.stringify(capsules.rejected)}`);
@@ -579,6 +585,139 @@ export class MemoryGate {
579
585
  })();
580
586
  return saved;
581
587
  }
588
+ async refineExplicitTurnObservations(turnEventId, sessionId, observations) {
589
+ if (!this.engine)
590
+ await this.forceWarmUp();
591
+ const engine = this.requireEngine();
592
+ const event = engine.store.getEvent(turnEventId);
593
+ if (!event || event.session_id !== sessionId) {
594
+ throw new Error(`Memory turn event ${turnEventId} was not found for session ${sessionId}`);
595
+ }
596
+ const sourceText = readCanonicalPrompt(event.payload, event.user_message);
597
+ if (!sourceText)
598
+ throw new Error(`Memory turn event ${turnEventId} has no canonical user prompt`);
599
+ for (const observation of observations) {
600
+ validateEvidenceSpans(sourceText, observation.evidenceSpans ?? []);
601
+ }
602
+ const durableCandidates = observations.filter(observation => observation.disposition !== 'turn_constraint');
603
+ const turnConstraints = observations.filter(observation => observation.disposition === 'turn_constraint');
604
+ const validObs = durableCandidates.length > 0 ? validateObservationBatch(durableCandidates) : [];
605
+ if (!validObs)
606
+ throw new Error('Explicit memory refinement contains invalid durable candidates');
607
+ const capsules = validateMemoryCapsules({
608
+ projectRoot: this.projectRoot,
609
+ observations: validObs,
610
+ sourceEvents: [event],
611
+ defaultSourceEventIds: [turnEventId],
612
+ evidenceText: sourceText,
613
+ requireEvidenceSpans: true,
614
+ });
615
+ if (capsules.rejected.length > 0 || capsules.accepted.length !== validObs.length) {
616
+ throw new Error(`Explicit memory refinement failed evidence validation: ${JSON.stringify(capsules.rejected)}`);
617
+ }
618
+ const existingIds = new Set(engine.store.listObservations().map(observation => observation.id));
619
+ const turnId = eventTurnId(event, turnEventId);
620
+ const memories = [];
621
+ const retiredMemoryIds = [];
622
+ const operations = [];
623
+ this.getStoreDb().transaction(() => {
624
+ for (const acceptedCapsule of capsules.accepted) {
625
+ const observation = acceptedCapsule.observation;
626
+ const semantic = normalizeSemanticMemory({
627
+ project: this.projectRoot,
628
+ type: observation.type,
629
+ title: observation.title,
630
+ narrative: observation.narrative ?? '',
631
+ concepts: observation.concepts,
632
+ subject: observation.subject,
633
+ predicate: observation.predicate,
634
+ value: observation.value,
635
+ polarity: observation.polarity,
636
+ entities: observation.entities,
637
+ temporal: observation.temporal,
638
+ });
639
+ const write = engine.store.acceptSemanticObservation({
640
+ sessionId,
641
+ project: this.projectRoot,
642
+ type: observation.type,
643
+ title: observation.title,
644
+ narrative: observation.narrative,
645
+ facts: [...new Set([...(observation.facts ?? []), ...(observation.evidenceSpans ?? [])])],
646
+ concepts: mergeSemanticEntities(normalizeBatchConcepts(observation), [semantic.subject, semantic.predicate, semantic.value], semantic.entities.map(normalizeEntityKey)),
647
+ importance: Math.max(8, observation.importance),
648
+ confidence: Math.max(0.9, observation.confidence),
649
+ source: 'explicit_intent',
650
+ sourceEventIds: [turnEventId],
651
+ qualityStatus: 'accepted',
652
+ evidenceStatus: 'verified',
653
+ evidenceDetails: (observation.evidenceSpans ?? []).map(span => ({
654
+ kind: 'host_evidence_span',
655
+ source: turnEventId,
656
+ summary: span,
657
+ })),
658
+ semantic,
659
+ capsule: acceptedCapsule.capsule,
660
+ projection: acceptedCapsule.projection,
661
+ preferIncomingNarrative: true,
662
+ });
663
+ const row = write.row;
664
+ operations.push(write.action.toLocaleLowerCase('en-US'));
665
+ memories.push({
666
+ id: row.id,
667
+ type: row.type,
668
+ title: row.title,
669
+ content: row.narrative,
670
+ source: row.source,
671
+ scope: row.project,
672
+ confidence: row.confidence,
673
+ importance: row.importance,
674
+ score: 1,
675
+ createdAt: row.timestamp,
676
+ indexStatus: row.embedding ? 'indexed' : 'pending',
677
+ });
678
+ engine.store.recordPendingTurnObservation({
679
+ turnEventId,
680
+ turnId,
681
+ sessionId,
682
+ project: this.projectRoot,
683
+ observationId: row.id,
684
+ createdByTurn: !existingIds.has(row.id),
685
+ createdAt: event.created_at,
686
+ });
687
+ }
688
+ const constraintSpans = turnConstraints.flatMap(candidate => candidate.evidenceSpans ?? []);
689
+ const baseline = engine.store.listObservations({ project: this.projectRoot })
690
+ .filter(observation => observation.source === 'explicit_intent')
691
+ .filter(observation => parseLearningStringArray(observation.source_event_ids).includes(turnEventId));
692
+ for (const observation of baseline) {
693
+ if (!constraintSpans.some(span => sameGroundedClause(observation.narrative, span)))
694
+ continue;
695
+ if (engine.store.quarantineObservation(observation.id, 'host_classified_turn_constraint', {
696
+ turnId,
697
+ eventId: turnEventId,
698
+ evidenceSpans: constraintSpans,
699
+ })) {
700
+ retiredMemoryIds.push(observation.id);
701
+ operations.push('retire');
702
+ }
703
+ }
704
+ })();
705
+ emitSemanticControlLog({
706
+ event: 'memory.semantic.refined',
707
+ identity: { projectRoot: this.projectRoot, sessionId, turnId },
708
+ level: 'info',
709
+ timestamp: Date.now(),
710
+ data: {
711
+ candidateCount: observations.length,
712
+ durableCount: durableCandidates.length,
713
+ turnConstraintCount: turnConstraints.length,
714
+ memoryIds: memories.map(memory => memory.id),
715
+ retiredMemoryIds,
716
+ operations,
717
+ },
718
+ });
719
+ return { memories, retiredMemoryIds, operations };
720
+ }
582
721
  async skipTurnEvent(turnEventId, sessionId, reason) {
583
722
  if (!this.engine)
584
723
  await this.forceWarmUp();
@@ -639,7 +778,13 @@ export class MemoryGate {
639
778
  * @deprecated Use saveDistilledObservationsByBatch(batchId, observations) instead.
640
779
  */
641
780
  async saveDistilledObservations(observations, sessionId) {
642
- console.warn('[MemoryGate] saveDistilledObservations by sessionId is deprecated; use saveDistilledObservationsByBatch instead');
781
+ emitSemanticControlLog({
782
+ event: 'memory.write.degraded',
783
+ identity: { projectRoot: this.projectRoot, sessionId },
784
+ level: 'warn',
785
+ timestamp: Date.now(),
786
+ data: { stage: 'legacy_distill', reason: 'deprecated_session_write', fallback: 'legacy_compatibility_path' },
787
+ });
643
788
  if (!this.engine)
644
789
  return 0;
645
790
  const validObs = validateObservationBatch(observations);
@@ -684,10 +829,12 @@ export class MemoryGate {
684
829
  engine.store.markEventsProcessed(unprocessedIds);
685
830
  });
686
831
  transaction();
687
- console.log('[MemoryGate] Distilled observations saved', {
688
- sessionId,
689
- observationCount: validObs.length,
690
- eventsMarked: unprocessedIds.length,
832
+ emitSemanticControlLog({
833
+ event: 'memory.write.completed',
834
+ identity: { projectRoot: this.projectRoot, sessionId },
835
+ level: 'info',
836
+ timestamp: Date.now(),
837
+ data: { stage: 'legacy_distill', observationCount: validObs.length, eventsMarked: unprocessedIds.length },
691
838
  });
692
839
  return validObs.length;
693
840
  }
@@ -716,11 +863,12 @@ export class MemoryGate {
716
863
  const events = this.engine.store.getUnprocessedEvents();
717
864
  if (events.length === 0)
718
865
  return 0;
719
- const groups = groupEventsBySession(events, { log: false });
866
+ const groups = groupEventsBySession(events, { log: false, projectRoot: this.projectRoot });
720
867
  const preserveSessionIds = this.getActiveSessionIds(groups);
721
868
  const { discarded } = triageGroupsWithDisposition(groups, Number.MAX_SAFE_INTEGER, {
722
869
  log: false,
723
870
  preserveSessionIds,
871
+ projectRoot: this.projectRoot,
724
872
  });
725
873
  const ids = discarded.flatMap(group => group.events).map(event => event.id);
726
874
  return this.engine.store.discardEvents(ids, 'low_value_auto_triage', `auto-triage:${Date.now()}`);
@@ -753,7 +901,10 @@ export class MemoryGate {
753
901
  this.projectValid = true;
754
902
  }
755
903
  this.state = 'warming';
756
- console.log('[MemoryGate] State transition', { from: 'cold', to: 'warming' });
904
+ emitSemanticControlLog({
905
+ event: 'memory.engine.transitioned', identity: { projectRoot: this.projectRoot }, level: 'info', timestamp: Date.now(),
906
+ data: { from: 'cold', to: 'warming' },
907
+ });
757
908
  this.engine = new MemoryEngine(this.projectRoot, this.embedder);
758
909
  this.setLeaseStore(new LeaseStore(this.getStoreDb()));
759
910
  // Clean up zombie sessions (>24h active)
@@ -761,7 +912,13 @@ export class MemoryGate {
761
912
  const zombies = this.engine.store.getZombieSessions(24);
762
913
  for (const z of zombies) {
763
914
  this.engine.store.closeSession(z.id);
764
- console.log('[MemoryGate] Zombie session auto-closed', { sessionId: z.id, age: `${Math.round((Date.now() - z.created_at) / 3600000)}h` });
915
+ emitSemanticControlLog({
916
+ event: 'memory.engine.transitioned',
917
+ identity: { projectRoot: this.projectRoot, sessionId: z.id },
918
+ level: 'warn',
919
+ timestamp: Date.now(),
920
+ data: { from: 'active', to: 'closed', reason: 'zombie_session', ageHours: Math.round((Date.now() - z.created_at) / 3600000) },
921
+ });
765
922
  }
766
923
  }
767
924
  catch { /* non-critical */ }
@@ -771,7 +928,10 @@ export class MemoryGate {
771
928
  }
772
929
  catch { /* non-critical */ }
773
930
  this.state = 'hot';
774
- console.log('[MemoryGate] State transition', { from: 'warming', to: 'hot' });
931
+ emitSemanticControlLog({
932
+ event: 'memory.engine.transitioned', identity: { projectRoot: this.projectRoot }, level: 'info', timestamp: Date.now(),
933
+ data: { from: 'warming', to: 'hot' },
934
+ });
775
935
  }
776
936
  // --- IMemoryEngine implementation (delegates to engine when hot) ---
777
937
  requireEngine() {
@@ -854,7 +1014,28 @@ export class MemoryGate {
854
1014
  async saveExplicitMemoryIntent(content, sessionId, sourceEventId) {
855
1015
  if (!this.engine)
856
1016
  await this.forceWarmUp();
857
- return this.requireEngine().saveExplicitMemoryIntent(content, sessionId, sourceEventId);
1017
+ try {
1018
+ const result = await this.requireEngine().saveExplicitMemoryIntent(content, sessionId, sourceEventId);
1019
+ emitSemanticControlLog({
1020
+ event: 'memory.write.completed',
1021
+ identity: { projectRoot: this.projectRoot, sessionId },
1022
+ level: 'info',
1023
+ timestamp: Date.now(),
1024
+ data: {
1025
+ stage: 'explicit_memory', memoryId: result.id, relatedMemoryIds: result.relatedMemoryIds ?? [],
1026
+ semanticStatus: result.semanticDecision?.status,
1027
+ operations: result.semanticDecision?.operations ?? [],
1028
+ },
1029
+ });
1030
+ return result;
1031
+ }
1032
+ catch (error) {
1033
+ emitSemanticControlLog({
1034
+ event: 'memory.write.degraded', identity: { projectRoot: this.projectRoot, sessionId }, level: 'error', timestamp: Date.now(),
1035
+ data: { stage: 'explicit_memory', reason: memoryErrorReason(error) },
1036
+ });
1037
+ throw error;
1038
+ }
858
1039
  }
859
1040
  async enrichExplicitMemoryObservation(observationId) {
860
1041
  if (!this.engine)
@@ -924,15 +1105,28 @@ export class MemoryGate {
924
1105
  const rows = engine.store.listObservationsMissingEmbedding(batchSize, lastId);
925
1106
  if (rows.length === 0)
926
1107
  break;
927
- const embeddings = await this.embedder.embedBatch(rows.map(row => `${row.title}\n${row.narrative}`));
928
- if (embeddings.length !== rows.length) {
929
- throw new Error(`Observation vector count mismatch: ${rows.length} observations, ${embeddings.length} vectors`);
1108
+ const texts = rows.map(row => `${row.title}\n${row.narrative}`);
1109
+ let indexed = [];
1110
+ try {
1111
+ const embeddings = await this.embedder.embedBatch(texts);
1112
+ if (embeddings.length !== rows.length) {
1113
+ throw new Error(`Observation vector count mismatch: ${rows.length} observations, ${embeddings.length} vectors`);
1114
+ }
1115
+ indexed = rows.map((row, index) => ({ id: row.id, embedding: embeddings[index] }));
930
1116
  }
931
- engine.store.insertObservationEmbeddings(rows.map((row, index) => ({
932
- id: row.id,
933
- embedding: embeddings[index],
934
- })));
935
- processed += rows.length;
1117
+ catch {
1118
+ for (let index = 0; index < rows.length; index++) {
1119
+ try {
1120
+ indexed.push({ id: rows[index].id, embedding: await this.embedder.embed(texts[index]) });
1121
+ }
1122
+ catch {
1123
+ // Keep this observation pending; deterministic retrieval remains available.
1124
+ }
1125
+ }
1126
+ }
1127
+ if (indexed.length > 0)
1128
+ engine.store.insertObservationEmbeddings(indexed);
1129
+ processed += indexed.length;
936
1130
  lastId = rows.at(-1).id;
937
1131
  const coverage = engine.store.countObservationVectors();
938
1132
  options.onProgress?.({ processed, remaining: coverage.missing, lastId });
@@ -964,7 +1158,38 @@ export class MemoryGate {
964
1158
  async search(query, options) {
965
1159
  if (!this.engine)
966
1160
  await this.forceWarmUp();
967
- return this.requireEngine().search(query, options);
1161
+ const startedAt = Date.now();
1162
+ try {
1163
+ const results = await this.requireEngine().search(query, options);
1164
+ emitSemanticControlLog({
1165
+ event: 'memory.search.completed',
1166
+ identity: {
1167
+ projectRoot: this.projectRoot,
1168
+ sessionId: options?.sessionId,
1169
+ executionId: options?.executionId,
1170
+ requestId: options?.requestId,
1171
+ },
1172
+ level: 'info',
1173
+ timestamp: Date.now(),
1174
+ data: {
1175
+ status: results.length > 0 ? 'selected' : 'empty', selected: results.length,
1176
+ verified: results.filter(result => result.entry.status === 'approved').length,
1177
+ durationMs: Date.now() - startedAt,
1178
+ vectorReady: results.filter(result => (result.scoreDetails?.semanticScore ?? 0) > 0).length,
1179
+ },
1180
+ });
1181
+ return results;
1182
+ }
1183
+ catch (error) {
1184
+ emitSemanticControlLog({
1185
+ event: 'memory.search.completed',
1186
+ identity: { projectRoot: this.projectRoot, sessionId: options?.sessionId, executionId: options?.executionId, requestId: options?.requestId },
1187
+ level: 'error',
1188
+ timestamp: Date.now(),
1189
+ data: { status: 'failed', selected: 0, durationMs: Date.now() - startedAt, reason: memoryErrorReason(error) },
1190
+ });
1191
+ throw error;
1192
+ }
968
1193
  }
969
1194
  async listMemories(filter) {
970
1195
  if (!this.engine)
@@ -1070,6 +1295,24 @@ function eventTurnId(event, fallbackEventId) {
1070
1295
  }
1071
1296
  return `turn:${fallbackEventId}`;
1072
1297
  }
1298
+ function readCanonicalPrompt(payloadJson, userMessage) {
1299
+ if (payloadJson) {
1300
+ try {
1301
+ const payload = JSON.parse(payloadJson);
1302
+ if (typeof payload.prompt === 'string' && payload.prompt.trim())
1303
+ return payload.prompt.trim();
1304
+ }
1305
+ catch { }
1306
+ }
1307
+ return userMessage?.trim() ?? '';
1308
+ }
1309
+ function sameGroundedClause(narrative, evidenceSpan) {
1310
+ const left = narrative.normalize('NFKC').trim().toLocaleLowerCase('en-US');
1311
+ const right = evidenceSpan.normalize('NFKC').trim().toLocaleLowerCase('en-US');
1312
+ if (left.length < 4 || right.length < 4)
1313
+ return false;
1314
+ return left === right || (right.includes(left) && left.length / right.length >= 0.7);
1315
+ }
1073
1316
  // --- L1 Rule Scorer (backward compat) ---
1074
1317
  function scoreL1Rules(event) {
1075
1318
  let score = 0;
@@ -1131,5 +1374,8 @@ function scoreDialogSignals(message) {
1131
1374
  }
1132
1375
  return maxScore;
1133
1376
  }
1377
+ function memoryErrorReason(error) {
1378
+ return (error instanceof Error ? error.message : String(error)).trim().slice(0, 500) || 'memory_operation_failed';
1379
+ }
1134
1380
  export { scoreL1Rules, scoreDialogSignals };
1135
1381
  //# sourceMappingURL=memory-gate.js.map