···
117
117
eventQuery: boolean
118
118
}
119
119
120
120
+
type MemoryHitSignal = {
121
121
+
overlap: number
122
122
+
strongOverlap: boolean
123
123
+
}
124
124
+
120
125
function normalizeText(value: string): string {
121
126
return value.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim()
122
127
}
···
295
300
}
296
301
297
302
function normalizeSearchInput(raw: string): string {
298
298
-
return raw
299
299
-
.replace(/^\[(wake|incoming|harness restarted)[^\n]*\]\s*/gi, "")
303
303
+
const withoutWakeEnvelope = raw.replace(/^\[(wake|incoming|harness restarted)[^\n]*\]\s*/gi, "").trim()
304
304
+
305
305
+
const blocks = withoutWakeEnvelope
306
306
+
.split(/\n\s*\n/g)
307
307
+
.map((block) => block.trim())
308
308
+
.filter(Boolean)
309
309
+
310
310
+
const bodyCandidate = blocks.length > 0 ? blocks[blocks.length - 1]! : withoutWakeEnvelope
311
311
+
312
312
+
return bodyCandidate
313
313
+
.replace(/^\[discord\/[^\]]+\]\s*@\S+\s+in\s+\d+(?:\s+\(\d+\))?\s*/gi, "")
314
314
+
.replace(/^\[[^\]]+\]\s*/g, "")
315
315
+
.replace(/@[a-z0-9_.-]+/gi, " ")
316
316
+
.replace(/\b\d{6,}\b/g, " ")
300
317
.replace(/[^\p{L}\p{N}\s'-]+/gu, " ")
301
318
.toLowerCase()
319
319
+
.trim()
302
320
}
303
321
304
322
function searchTokens(raw: string): string[] {
···
472
490
return score
473
491
}
474
492
493
493
+
function hitSearchHaystack(hit: MemoryHit): string {
494
494
+
return `${hit.documentTitle} ${hit.title} ${hit.headingPath ?? ""} ${basenameWithoutExt(hit.path)} ${hit.text}`.toLowerCase()
495
495
+
}
496
496
+
497
497
+
function memoryHitSignal(hit: MemoryHit, profile: MemorySearchProfile): MemoryHitSignal {
498
498
+
const haystack = hitSearchHaystack(hit)
499
499
+
let overlap = 0
500
500
+
let strongOverlap = false
501
501
+
502
502
+
for (const token of profile.tokens) {
503
503
+
if (!haystack.includes(token)) continue
504
504
+
overlap += 1
505
505
+
if (token.length >= 5 || /[0-9]/.test(token)) strongOverlap = true
506
506
+
}
507
507
+
508
508
+
return { overlap, strongOverlap }
509
509
+
}
510
510
+
511
511
+
function shouldInjectHits(hits: MemoryHit[], profile: MemorySearchProfile): boolean {
512
512
+
if (hits.length === 0) return false
513
513
+
514
514
+
const topSignal = memoryHitSignal(hits[0]!, profile)
515
515
+
516
516
+
if (profile.personQuery && !profile.eventQuery) {
517
517
+
return topSignal.overlap >= 1 && hits.some((hit) => hit.kind === "people" || hit.kind === "core")
518
518
+
}
519
519
+
520
520
+
if (profile.eventQuery && !profile.personQuery) {
521
521
+
return topSignal.overlap >= 1 && hits.some((hit) => hit.kind === "journal")
522
522
+
}
523
523
+
524
524
+
if (topSignal.overlap >= 2) return true
525
525
+
if (topSignal.overlap >= 1 && topSignal.strongOverlap) return true
526
526
+
return false
527
527
+
}
528
528
+
475
529
function searchMemory(
476
530
profile: MemorySearchProfile,
477
531
cooldowns: Record<number, number>,
···
581
635
582
636
const hits = searchMemory(profile, cooldowns, currentTurn, MEMORY_RECALL_MAX_CHUNKS)
583
637
if (hits.length === 0) return { messages: conversation, recalledChunkIds: [] }
638
638
+
if (!shouldInjectHits(hits, profile)) {
639
639
+
console.log(
640
640
+
`[memory] skipped query=${JSON.stringify(trimForPrompt(normalizeText(latestUser), 120))} personQuery=${profile.personQuery} eventQuery=${profile.eventQuery} reason=weak-match`,
641
641
+
)
642
642
+
return { messages: conversation, recalledChunkIds: [] }
643
643
+
}
584
644
585
645
const recallContent = buildMemoryRecallMessage(hits)
586
646
console.log(