@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 +1 -1
- package/src/core/retrieval.ts +132 -15
- package/src/utils/logger.ts +3 -1
package/package.json
CHANGED
package/src/core/retrieval.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 /
|
|
579
|
-
relevance_score: item.signals.count /
|
|
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,
|
|
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
|
},
|
package/src/utils/logger.ts
CHANGED
|
@@ -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
|