@rlabs-inc/memory 0.4.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rlabs-inc/memory",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "AI Memory System - Consciousness continuity through intelligent memory curation and retrieval",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,6 +27,7 @@ interface ActivationSignals {
27
27
  domain: boolean // Domain word found in message
28
28
  feature: boolean // Feature word found in message
29
29
  content: boolean // Key content words found in message
30
+ files: boolean // Related file path matched
30
31
  count: number // Total signals activated
31
32
  triggerStrength: number // How strong the trigger match was (0-1)
32
33
  tagCount: number // How many tags matched
@@ -99,8 +100,54 @@ export class SmartVectorRetrieval {
99
100
  return new Set(words)
100
101
  }
101
102
 
103
+ /**
104
+ * Extract file paths from message
105
+ * Matches patterns like /path/to/file.ts, src/core/engine.ts, ./relative/path
106
+ */
107
+ private _extractFilePaths(text: string): Set<string> {
108
+ const paths = new Set<string>()
109
+ // Match file paths (with / or \ separators, optional extension)
110
+ const pathPattern = /(?:^|[\s'"(])([.\/\\]?(?:[\w.-]+[\/\\])+[\w.-]+(?:\.\w+)?)/g
111
+ let match
112
+ while ((match = pathPattern.exec(text)) !== null) {
113
+ const path = match[1].toLowerCase()
114
+ paths.add(path)
115
+ // Also add just the filename for partial matching
116
+ const filename = path.split(/[\/\\]/).pop()
117
+ if (filename) paths.add(filename)
118
+ }
119
+ return paths
120
+ }
121
+
122
+ /**
123
+ * Check if related_files activate for this message
124
+ */
125
+ private _checkFilesActivation(
126
+ messagePaths: Set<string>,
127
+ relatedFiles: string[] | undefined
128
+ ): boolean {
129
+ if (!relatedFiles?.length || !messagePaths.size) return false
130
+
131
+ for (const file of relatedFiles) {
132
+ const fileLower = file.toLowerCase()
133
+ // Check if any message path matches this file
134
+ for (const msgPath of messagePaths) {
135
+ if (fileLower.includes(msgPath) || msgPath.includes(fileLower)) {
136
+ return true
137
+ }
138
+ }
139
+ // Also check just the filename
140
+ const filename = fileLower.split(/[\/\\]/).pop()
141
+ if (filename && messagePaths.has(filename)) {
142
+ return true
143
+ }
144
+ }
145
+ return false
146
+ }
147
+
102
148
  /**
103
149
  * Pre-filter: Binary exclusions based on v2 lifecycle fields
150
+ * NOTE: superseded_by memories are NOT filtered here - they get redirected in Phase 1
104
151
  */
105
152
  private _preFilter(
106
153
  memories: StoredMemory[],
@@ -110,7 +157,7 @@ export class SmartVectorRetrieval {
110
157
  return memories.filter(memory => {
111
158
  if (memory.status && memory.status !== 'active') return false
112
159
  if (memory.exclude_from_retrieval === true) return false
113
- if (memory.superseded_by) return false
160
+ // NOTE: Don't filter superseded_by here - we handle redirects in Phase 1
114
161
  const isGlobal = memory.scope === 'global' || memory.project_id === 'global'
115
162
  if (!isGlobal && memory.project_id !== currentProjectId) return false
116
163
  if (memory.anti_triggers?.length) {
@@ -355,6 +402,13 @@ export class SmartVectorRetrieval {
355
402
 
356
403
  const messageLower = currentMessage.toLowerCase()
357
404
  const messageWords = this._extractSignificantWords(currentMessage)
405
+ const messagePaths = this._extractFilePaths(currentMessage)
406
+
407
+ // Build lookup map for redirect resolution
408
+ const memoryById = new Map<string, StoredMemory>()
409
+ for (const m of allMemories) {
410
+ memoryById.set(m.id, m)
411
+ }
358
412
 
359
413
  // ================================================================
360
414
  // PHASE 0: PRE-FILTER (Binary exclusions)
@@ -368,13 +422,13 @@ export class SmartVectorRetrieval {
368
422
  // PHASE 1: ACTIVATION SIGNALS
369
423
  // Count how many signals agree this memory should activate
370
424
  // A memory is relevant if >= MIN_ACTIVATION_SIGNALS fire
425
+ // Handles redirects for superseded_by/resolved_by
371
426
  // ================================================================
372
427
  const activatedMemories: ActivatedMemory[] = []
428
+ const activatedIds = new Set<string>()
373
429
  let rejectedCount = 0
374
430
 
375
431
  for (const memory of candidates) {
376
- const isGlobal = memory.scope === 'global' || memory.project_id === 'global'
377
-
378
432
  // Check each activation signal
379
433
  const triggerResult = this._checkTriggerActivation(
380
434
  messageLower, messageWords, memory.trigger_phrases ?? []
@@ -389,15 +443,17 @@ export class SmartVectorRetrieval {
389
443
  messageLower, messageWords, memory.feature
390
444
  )
391
445
  const contentActivated = this._checkContentActivation(messageWords, memory)
446
+ const filesActivated = this._checkFilesActivation(messagePaths, memory.related_files)
392
447
  const vectorSimilarity = this._calculateVectorSimilarity(queryEmbedding, memory.embedding)
393
448
 
394
- // Count activated signals
449
+ // Count activated signals (now 7 possible signals)
395
450
  let signalCount = 0
396
451
  if (triggerResult.activated) signalCount++
397
452
  if (tagResult.activated) signalCount++
398
453
  if (domainActivated) signalCount++
399
454
  if (featureActivated) signalCount++
400
455
  if (contentActivated) signalCount++
456
+ if (filesActivated) signalCount++
401
457
  // Vector similarity as bonus signal only if very high
402
458
  if (vectorSimilarity >= 0.40) signalCount++
403
459
 
@@ -407,6 +463,7 @@ export class SmartVectorRetrieval {
407
463
  domain: domainActivated,
408
464
  feature: featureActivated,
409
465
  content: contentActivated,
466
+ files: filesActivated,
410
467
  count: signalCount,
411
468
  triggerStrength: triggerResult.strength,
412
469
  tagCount: tagResult.count,
@@ -419,15 +476,34 @@ export class SmartVectorRetrieval {
419
476
  continue
420
477
  }
421
478
 
479
+ // REDIRECT: If superseded_by or resolved_by exists, surface the replacement instead
480
+ let memoryToSurface = memory
481
+ if (memory.superseded_by || memory.resolved_by) {
482
+ const replacementId = memory.superseded_by ?? memory.resolved_by
483
+ const replacement = replacementId ? memoryById.get(replacementId) : undefined
484
+ if (replacement && replacement.status !== 'archived' && replacement.status !== 'deprecated') {
485
+ memoryToSurface = replacement
486
+ logger.debug(`Redirect: ${memory.id.slice(-8)} → ${replacement.id.slice(-8)} (${memory.superseded_by ? 'superseded' : 'resolved'})`, 'retrieval')
487
+ }
488
+ }
489
+
490
+ // Skip if we already added this memory (could happen from multiple redirects)
491
+ if (activatedIds.has(memoryToSurface.id)) {
492
+ continue
493
+ }
494
+
495
+ const isGlobal = memoryToSurface.scope === 'global' || memoryToSurface.project_id === 'global'
496
+
422
497
  // Calculate importance for ranking (Phase 2) - uses ALL rich metadata
423
- const importanceScore = this._calculateImportanceScore(memory, signalCount, messageLower, messageWords)
498
+ const importanceScore = this._calculateImportanceScore(memoryToSurface, signalCount, messageLower, messageWords)
424
499
 
425
500
  activatedMemories.push({
426
- memory,
501
+ memory: memoryToSurface,
427
502
  signals,
428
503
  importanceScore,
429
504
  isGlobal,
430
505
  })
506
+ activatedIds.add(memoryToSurface.id)
431
507
  }
432
508
 
433
509
  // Log diagnostics
@@ -517,23 +593,60 @@ export class SmartVectorRetrieval {
517
593
  selectedIds.add(item.memory.id)
518
594
  }
519
595
 
520
- // PHASE 4: RELATED MEMORIES (if space remains)
596
+ // PHASE 4: LINKED MEMORIES (related_to, blocked_by, blocks)
597
+ // Pull in linked memories when space remains
521
598
  if (selected.length < maxMemories) {
522
- const relatedIds = new Set<string>()
599
+ const linkedIds = new Set<string>()
600
+
523
601
  for (const item of selected) {
602
+ // related_to - explicit relationships
524
603
  for (const relatedId of item.memory.related_to ?? []) {
525
604
  if (!selectedIds.has(relatedId)) {
526
- relatedIds.add(relatedId)
605
+ linkedIds.add(relatedId)
606
+ }
607
+ }
608
+ // blocked_by - show what's blocking this
609
+ if (item.memory.blocked_by && !selectedIds.has(item.memory.blocked_by)) {
610
+ linkedIds.add(item.memory.blocked_by)
611
+ }
612
+ // blocks - show what this blocks
613
+ for (const blockedId of item.memory.blocks ?? []) {
614
+ if (!selectedIds.has(blockedId)) {
615
+ linkedIds.add(blockedId)
527
616
  }
528
617
  }
529
618
  }
530
619
 
620
+ // First try to find linked memories in the activated list
531
621
  for (const item of activatedMemories) {
532
622
  if (selected.length >= maxMemories) break
533
623
  if (selectedIds.has(item.memory.id)) continue
534
- if (relatedIds.has(item.memory.id)) {
624
+ if (linkedIds.has(item.memory.id)) {
535
625
  selected.push(item)
536
626
  selectedIds.add(item.memory.id)
627
+ logger.debug(`Linked: ${item.memory.id.slice(-8)} pulled by relationship`, 'retrieval')
628
+ }
629
+ }
630
+
631
+ // If linked memories weren't in activated list, pull them directly from allMemories
632
+ // (they might not have activated on their own but are important for context)
633
+ if (selected.length < maxMemories) {
634
+ for (const linkedId of linkedIds) {
635
+ if (selected.length >= maxMemories) break
636
+ if (selectedIds.has(linkedId)) continue
637
+
638
+ const linkedMemory = memoryById.get(linkedId)
639
+ if (linkedMemory && linkedMemory.status !== 'archived' && linkedMemory.status !== 'deprecated') {
640
+ const isGlobal = linkedMemory.scope === 'global' || linkedMemory.project_id === 'global'
641
+ selected.push({
642
+ memory: linkedMemory,
643
+ signals: { trigger: false, tags: false, domain: false, feature: false, content: false, files: false, count: 0, triggerStrength: 0, tagCount: 0, vectorSimilarity: 0 },
644
+ importanceScore: linkedMemory.importance_weight ?? 0.5,
645
+ isGlobal,
646
+ })
647
+ selectedIds.add(linkedId)
648
+ logger.debug(`Linked (direct): ${linkedId.slice(-8)} pulled for context`, 'retrieval')
649
+ }
537
650
  }
538
651
  }
539
652
  }
@@ -566,6 +679,7 @@ export class SmartVectorRetrieval {
566
679
  domain: item.signals.domain,
567
680
  feature: item.signals.feature,
568
681
  content: item.signals.content,
682
+ files: item.signals.files,
569
683
  vector: item.signals.vectorSimilarity >= 0.40,
570
684
  vectorSimilarity: item.signals.vectorSimilarity,
571
685
  },
@@ -575,8 +689,8 @@ export class SmartVectorRetrieval {
575
689
  // Convert to RetrievalResult format
576
690
  return selected.map(item => ({
577
691
  ...item.memory,
578
- score: item.signals.count / 6,
579
- relevance_score: item.signals.count / 6,
692
+ score: item.signals.count / 7, // 7 possible signals
693
+ relevance_score: item.signals.count / 7,
580
694
  value_score: item.importanceScore,
581
695
  }))
582
696
  }
@@ -592,6 +706,7 @@ export class SmartVectorRetrieval {
592
706
  if (signals.domain) reasons.push('domain')
593
707
  if (signals.feature) reasons.push('feature')
594
708
  if (signals.content) reasons.push('content')
709
+ if (signals.files) reasons.push('files')
595
710
  if (signals.vectorSimilarity >= 0.40) reasons.push(`vector:${(signals.vectorSimilarity * 100).toFixed(0)}%`)
596
711
 
597
712
  return reasons.length
@@ -608,20 +723,21 @@ export class SmartVectorRetrieval {
608
723
  rejectedCount: number
609
724
  ): void {
610
725
  const signalBuckets: Record<string, number> = {
611
- '2 signals': 0, '3 signals': 0, '4 signals': 0, '5 signals': 0, '6 signals': 0
726
+ '2 signals': 0, '3 signals': 0, '4 signals': 0, '5 signals': 0, '6 signals': 0, '7 signals': 0
612
727
  }
613
728
  for (const mem of activated) {
614
- const key = `${Math.min(mem.signals.count, 6)} signals`
729
+ const key = `${Math.min(mem.signals.count, 7)} signals`
615
730
  signalBuckets[key] = (signalBuckets[key] ?? 0) + 1
616
731
  }
617
732
 
618
- let triggerCount = 0, tagCount = 0, domainCount = 0, featureCount = 0, contentCount = 0, vectorCount = 0
733
+ let triggerCount = 0, tagCount = 0, domainCount = 0, featureCount = 0, contentCount = 0, filesCount = 0, vectorCount = 0
619
734
  for (const mem of activated) {
620
735
  if (mem.signals.trigger) triggerCount++
621
736
  if (mem.signals.tags) tagCount++
622
737
  if (mem.signals.domain) domainCount++
623
738
  if (mem.signals.feature) featureCount++
624
739
  if (mem.signals.content) contentCount++
740
+ if (mem.signals.files) filesCount++
625
741
  if (mem.signals.vectorSimilarity >= 0.40) vectorCount++
626
742
  }
627
743
 
@@ -645,6 +761,7 @@ export class SmartVectorRetrieval {
645
761
  domain: domainCount,
646
762
  feature: featureCount,
647
763
  content: contentCount,
764
+ files: filesCount,
648
765
  vector: vectorCount,
649
766
  total: activated.length,
650
767
  },
@@ -574,6 +574,7 @@ export const logger = {
574
574
  domain: number
575
575
  feature: number
576
576
  content: number
577
+ files: number
577
578
  vector: number
578
579
  total: number
579
580
  }
@@ -600,6 +601,7 @@ export const logger = {
600
601
  { name: 'domain', count: signalBreakdown.domain },
601
602
  { name: 'feature', count: signalBreakdown.feature },
602
603
  { name: 'content', count: signalBreakdown.content },
604
+ { name: 'files', count: signalBreakdown.files },
603
605
  { name: 'vector', count: signalBreakdown.vector },
604
606
  ]
605
607
  for (const sig of signals) {
@@ -620,7 +622,7 @@ export const logger = {
620
622
  if (Object.keys(buckets).length > 0) {
621
623
  console.log(` ${style('bold', 'Distribution:')}`)
622
624
  const maxBucketCount = Math.max(...Object.values(buckets), 1)
623
- const bucketOrder = ['2 signals', '3 signals', '4 signals', '5 signals', '6 signals']
625
+ const bucketOrder = ['2 signals', '3 signals', '4 signals', '5 signals', '6 signals', '7 signals']
624
626
 
625
627
  for (const bucket of bucketOrder) {
626
628
  const count = buckets[bucket] ?? 0