fix(memory-lancedb): improve capture/recall filtering and relevance
Three improvements to autoCapture and autoRecall: 1. Skip system prompts in capture and recall - Heartbeat prompts (HEARTBEAT.md, Read HEARTBEAT.md) - Security webhook messages ([SECURITY DETECTION - ANALYZE]) - Pre-compaction flush prompts - HEARTBEAT_OK / NO_REPLY responses These system-generated messages clutter memory and aren't useful for recall context. 2. Strip injected memory context before capture evaluation - autoRecall prepends <relevant-memories> to user messages - Extract and reuse isSystemPrompt() helper for consistency 3. Improve recall relevance with higher threshold and re-ranking - Raise similarity threshold from 0.3 to 0.5 - Fetch 8 candidates, re-rank by: score × recency × importance - Return top 3 after re-ranking - Recency: recent memories boosted (decays over 60 days) - Importance: uses stored importance field (default 0.7) This results in more relevant memories being surfaced while keeping automated system messages out of the memory store.
This commit is contained in:
parent
01e0d3a320
commit
91c3e49b63
@ -185,18 +185,40 @@ const MEMORY_TRIGGERS = [
|
|||||||
/always|never|important/i,
|
/always|never|important/i,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Strip injected memory context from message text before processing
|
||||||
|
function stripMemoryContext(text: string): string {
|
||||||
|
const memoryBlockEnd = text.indexOf("</relevant-memories>");
|
||||||
|
if (memoryBlockEnd !== -1) {
|
||||||
|
return text.slice(memoryBlockEnd + "</relevant-memories>".length).trim();
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if text is a system prompt that shouldn't be captured
|
||||||
|
function isSystemPrompt(text: string): boolean {
|
||||||
|
if (text.includes("HEARTBEAT.md") || text.includes("Read HEARTBEAT.md")) return true;
|
||||||
|
if (text.includes("[SECURITY DETECTION - ANALYZE]")) return true;
|
||||||
|
if (text.includes("Pre-compaction memory flush")) return true;
|
||||||
|
if (text === "HEARTBEAT_OK" || text === "NO_REPLY") return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function shouldCapture(text: string): boolean {
|
function shouldCapture(text: string): boolean {
|
||||||
if (text.length < 10 || text.length > 500) return false;
|
// Strip any injected memory context first
|
||||||
// Skip injected context from memory recall
|
const cleanText = stripMemoryContext(text);
|
||||||
if (text.includes("<relevant-memories>")) return false;
|
|
||||||
// Skip system-generated content
|
// Skip system prompts
|
||||||
if (text.startsWith("<") && text.includes("</")) return false;
|
if (isSystemPrompt(cleanText)) return false;
|
||||||
|
|
||||||
|
if (cleanText.length < 10 || cleanText.length > 500) return false;
|
||||||
|
// Skip system-generated content (pure XML)
|
||||||
|
if (cleanText.startsWith("<") && cleanText.includes("</")) return false;
|
||||||
// Skip agent summary responses (contain markdown formatting)
|
// Skip agent summary responses (contain markdown formatting)
|
||||||
if (text.includes("**") && text.includes("\n-")) return false;
|
if (cleanText.includes("**") && cleanText.includes("\n-")) return false;
|
||||||
// Skip emoji-heavy responses (likely agent output)
|
// Skip emoji-heavy responses (likely agent output)
|
||||||
const emojiCount = (text.match(/[\u{1F300}-\u{1F9FF}]/gu) || []).length;
|
const emojiCount = (cleanText.match(/[\u{1F300}-\u{1F9FF}]/gu) || []).length;
|
||||||
if (emojiCount > 3) return false;
|
if (emojiCount > 3) return false;
|
||||||
return MEMORY_TRIGGERS.some((r) => r.test(text));
|
return MEMORY_TRIGGERS.some((r) => r.test(cleanText));
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectCategory(text: string): MemoryCategory {
|
function detectCategory(text: string): MemoryCategory {
|
||||||
@ -467,19 +489,40 @@ const memoryPlugin = {
|
|||||||
if (cfg.autoRecall) {
|
if (cfg.autoRecall) {
|
||||||
api.on("before_agent_start", async (event) => {
|
api.on("before_agent_start", async (event) => {
|
||||||
if (!event.prompt || event.prompt.length < 5) return;
|
if (!event.prompt || event.prompt.length < 5) return;
|
||||||
|
|
||||||
|
// Skip recall for system prompts (heartbeats, webhooks, etc.)
|
||||||
|
if (isSystemPrompt(event.prompt)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const vector = await embeddings.embed(event.prompt);
|
const vector = await embeddings.embed(event.prompt);
|
||||||
const results = await db.search(vector, 3, 0.3);
|
// Fetch more candidates with higher threshold, then re-rank
|
||||||
|
const rawResults = await db.search(vector, 8, 0.5);
|
||||||
|
|
||||||
if (results.length === 0) return;
|
if (rawResults.length === 0) return;
|
||||||
|
|
||||||
|
// Re-rank by score * recency * importance
|
||||||
|
const now = Date.now();
|
||||||
|
const dayMs = 24 * 60 * 60 * 1000;
|
||||||
|
const ranked = rawResults.map((r) => {
|
||||||
|
const ageInDays = (now - r.entry.createdAt) / dayMs;
|
||||||
|
// Recency: 1.0 for today, decays to 0.5 over 60 days
|
||||||
|
const recencyFactor = Math.max(0.5, 1 - (ageInDays / 60));
|
||||||
|
// Importance is 0-1, default 0.7
|
||||||
|
const importanceFactor = r.entry.importance || 0.7;
|
||||||
|
const finalScore = r.score * recencyFactor * importanceFactor;
|
||||||
|
return { ...r, finalScore };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by final score, take top 3
|
||||||
|
ranked.sort((a, b) => b.finalScore - a.finalScore);
|
||||||
|
const results = ranked.slice(0, 3);
|
||||||
|
|
||||||
const memoryContext = results
|
const memoryContext = results
|
||||||
.map((r) => `- [${r.entry.category}] ${r.entry.text}`)
|
.map((r) => `- [${r.entry.category}] ${r.entry.text}`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
api.logger.info?.(
|
api.logger.info?.(
|
||||||
`memory-lancedb: injecting ${results.length} memories into context`,
|
`memory-lancedb: injecting ${results.length} memories into context (from ${rawResults.length} candidates)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user