From 2a9c7f629f18d3ca4c1637f7224c9a0d79470027 Mon Sep 17 00:00:00 2001 From: jiang Date: Fri, 10 Apr 2026 17:15:46 +0800 Subject: [PATCH 1/2] feat(memos-local-openclaw): add LLM filter toggle for memory search Add a new toggle in Settings > General to enable/disable LLM-based relevance filtering for memory_search and auto-recall. The filter is enabled by default (preserving existing behavior) but can be turned off for faster results that skip the summarizer.filterRelevant call. Changes: - types.ts: add recall.llmFilterEnabled config field - config.ts: default llmFilterEnabled to true - html.ts: add toggle UI, i18n (en/zh), loadConfig & saveGeneralConfig - server.ts: merge-save recall config in handleSaveConfig - index.ts: guard filterRelevant calls behind llmFilterEnabled check --- apps/memos-local-openclaw/index.ts | 50 +++++++++++-------- apps/memos-local-openclaw/src/config.ts | 1 + apps/memos-local-openclaw/src/types.ts | 2 + apps/memos-local-openclaw/src/viewer/html.ts | 14 ++++++ .../memos-local-openclaw/src/viewer/server.ts | 4 ++ 5 files changed, 50 insertions(+), 21 deletions(-) diff --git a/apps/memos-local-openclaw/index.ts b/apps/memos-local-openclaw/index.ts index 21b12d8e1..3e41db667 100644 --- a/apps/memos-local-openclaw/index.ts +++ b/apps/memos-local-openclaw/index.ts @@ -656,7 +656,8 @@ const memosLocalPlugin = { let filteredHubRemoteHits = hubRemoteForFilter; let sufficient = false; - if (mergedCandidates.length > 0) { + const llmFilterOn = ctx.config.recall?.llmFilterEnabled !== false; + if (llmFilterOn && mergedCandidates.length > 0) { const filterResult = await summarizer.filterRelevant(query, mergedCandidates); if (filterResult !== null) { sufficient = filterResult.sufficient; @@ -671,6 +672,8 @@ const memosLocalPlugin = { filteredHubRemoteHits = []; } } + } else if (!llmFilterOn) { + ctx.log.debug(`memory_search: LLM filter disabled by config, returning all ${mergedCandidates.length} candidates`); } const beforeDedup = filteredLocalHits.length; @@ -1995,28 +1998,33 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`, let filteredHits = allRawHits; let sufficient = false; - const filterResult = await summarizer.filterRelevant(query, mergedForFilter); - if (filterResult !== null) { - sufficient = filterResult.sufficient; - if (filterResult.relevant.length > 0) { - const indexSet = new Set(filterResult.relevant); - filteredHits = allRawHits.filter((_, i) => indexSet.has(i + 1)); - } else { - const dur = performance.now() - recallT0; - store.recordToolCall("memory_search", dur, true); - store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({ - candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [], - }), dur, true); - if (query.length > 50) { - const noRecallHint = - "## Memory system — ACTION REQUIRED\n\n" + - "Auto-recall found no relevant results for a long query. " + - "You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " + - "Do NOT skip this step. Do NOT answer without searching first."; - return { prependContext: noRecallHint }; + const autoRecallLlmFilter = ctx.config.recall?.llmFilterEnabled !== false; + if (autoRecallLlmFilter) { + const filterResult = await summarizer.filterRelevant(query, mergedForFilter); + if (filterResult !== null) { + sufficient = filterResult.sufficient; + if (filterResult.relevant.length > 0) { + const indexSet = new Set(filterResult.relevant); + filteredHits = allRawHits.filter((_, i) => indexSet.has(i + 1)); + } else { + const dur = performance.now() - recallT0; + store.recordToolCall("memory_search", dur, true); + store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({ + candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [], + }), dur, true); + if (query.length > 50) { + const noRecallHint = + "## Memory system — ACTION REQUIRED\n\n" + + "Auto-recall found no relevant results for a long query. " + + "You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " + + "Do NOT skip this step. Do NOT answer without searching first."; + return { prependContext: noRecallHint }; + } + return; } - return; } + } else { + ctx.log.debug("auto-recall: LLM filter disabled by config, using all candidates"); } const beforeDedup = filteredHits.length; diff --git a/apps/memos-local-openclaw/src/config.ts b/apps/memos-local-openclaw/src/config.ts index 150b09cc4..cea6450dc 100644 --- a/apps/memos-local-openclaw/src/config.ts +++ b/apps/memos-local-openclaw/src/config.ts @@ -66,6 +66,7 @@ export function resolveConfig(raw: Partial | undefined, stateD mmrLambda: cfg.recall?.mmrLambda ?? DEFAULTS.mmrLambda, recencyHalfLifeDays: cfg.recall?.recencyHalfLifeDays ?? DEFAULTS.recencyHalfLifeDays, vectorSearchMaxChunks: cfg.recall?.vectorSearchMaxChunks ?? DEFAULTS.vectorSearchMaxChunks, + llmFilterEnabled: cfg.recall?.llmFilterEnabled ?? true, }, dedup: { similarityThreshold: cfg.dedup?.similarityThreshold ?? DEFAULTS.dedupSimilarityThreshold, diff --git a/apps/memos-local-openclaw/src/types.ts b/apps/memos-local-openclaw/src/types.ts index cb08eb1cf..1b62586bb 100644 --- a/apps/memos-local-openclaw/src/types.ts +++ b/apps/memos-local-openclaw/src/types.ts @@ -312,6 +312,8 @@ export interface MemosLocalConfig { recencyHalfLifeDays?: number; /** Cap vector search to this many most recent chunks. 0 = no cap (search all; may get slower with 200k+ chunks). If you set a cap for performance, use a large value (e.g. 200000–300000) so older memories are still in the window; FTS always searches all. */ vectorSearchMaxChunks?: number; + /** Whether to use LLM to filter search results for relevance. Default true. */ + llmFilterEnabled?: boolean; }; dedup?: { similarityThreshold?: number; diff --git a/apps/memos-local-openclaw/src/viewer/html.ts b/apps/memos-local-openclaw/src/viewer/html.ts index 26e5284bd..3c86a976b 100644 --- a/apps/memos-local-openclaw/src/viewer/html.ts +++ b/apps/memos-local-openclaw/src/viewer/html.ts @@ -1836,6 +1836,12 @@ input,textarea,select{font-family:inherit;font-size:inherit}
+
+ + +
+
When enabled, an LLM judges the relevance of search results before returning them. Disabling this returns all candidates from the retrieval engine directly, which is faster but may include less relevant results.
+
@@ -2338,6 +2344,8 @@ const I18N={ 'settings.skill.model.hint':'Leave empty to reuse the Summarizer model. Set a dedicated one for higher quality.', 'settings.optional':'Optional', 'settings.skill.usemain':'Use Main Summarizer', + 'settings.llmFilter.enabled':'Enable LLM Filtering for Memory Search', + 'settings.llmFilter.hint':'When enabled, an LLM judges the relevance of search results before returning them. Disabling this returns all candidates from the retrieval engine directly, which is faster but may include less relevant results.', 'settings.telemetry':'Telemetry', 'settings.telemetry.enabled':'Enable Anonymous Telemetry', 'settings.telemetry.hint':'Only collects tool names, latencies and version info. No memory content or personal data.', @@ -3115,6 +3123,8 @@ const I18N={ 'settings.skill.model.hint':'不配置则复用摘要模型。如需更高质量可单独指定。', 'settings.optional':'可选', 'settings.skill.usemain':'使用主摘要模型', + 'settings.llmFilter.enabled':'启用大模型过滤(记忆搜索)', + 'settings.llmFilter.hint':'开启后,搜索结果会经大模型判断相关性后再返回。关闭后将直接返回检索引擎的所有候选结果,速度更快但可能包含不太相关的内容。', 'settings.telemetry':'数据统计', 'settings.telemetry.enabled':'启用匿名数据统计', 'settings.telemetry.hint':'仅收集工具名称、响应时间和版本号,不涉及任何记忆内容或个人数据。', @@ -7223,6 +7233,9 @@ async function loadConfig(){ document.getElementById('cfgViewerPort').value=cfg.viewerPort||''; document.getElementById('cfgTaskAutoFinalizeHours').value=cfg.taskAutoFinalizeHours!=null?cfg.taskAutoFinalizeHours:''; + const recall=cfg.recall||{}; + document.getElementById('cfgLlmFilterEnabled').checked=recall.llmFilterEnabled!==false; + const tel=cfg.telemetry||{}; document.getElementById('cfgTelemetryEnabled').checked=tel.enabled!==false; @@ -7528,6 +7541,7 @@ async function saveGeneralConfig(){ if(vp) cfg.viewerPort=Number(vp); const tafh=document.getElementById('cfgTaskAutoFinalizeHours').value.trim(); cfg.taskAutoFinalizeHours=tafh!==''?Math.max(0,Number(tafh)):4; + cfg.recall={llmFilterEnabled:document.getElementById('cfgLlmFilterEnabled').checked}; cfg.telemetry={enabled:document.getElementById('cfgTelemetryEnabled').checked}; await doSaveConfig(cfg, saveBtn, 'generalSaved'); diff --git a/apps/memos-local-openclaw/src/viewer/server.ts b/apps/memos-local-openclaw/src/viewer/server.ts index 1750c9cc6..4d52b6863 100644 --- a/apps/memos-local-openclaw/src/viewer/server.ts +++ b/apps/memos-local-openclaw/src/viewer/server.ts @@ -3094,6 +3094,10 @@ export class ViewerServer { if (newCfg.skillEvolution) config.skillEvolution = newCfg.skillEvolution; if (newCfg.viewerPort) config.viewerPort = newCfg.viewerPort; if (newCfg.taskAutoFinalizeHours !== undefined) config.taskAutoFinalizeHours = newCfg.taskAutoFinalizeHours; + if (newCfg.recall !== undefined) { + const existing = (config.recall as Record) || {}; + config.recall = { ...existing, ...newCfg.recall }; + } if (newCfg.telemetry !== undefined) config.telemetry = newCfg.telemetry; if (newCfg.sharing !== undefined) { const existing = (config.sharing as Record) || {}; From cc40abbeb686cb98ca7df69f8066b95c51c5282b Mon Sep 17 00:00:00 2001 From: jiang Date: Fri, 10 Apr 2026 17:22:08 +0800 Subject: [PATCH 2/2] feat(memos-local-openclaw): add memory search and memory capture toggles Add two new toggles in Settings > General: - Enable Memory Search (memorySearchEnabled): controls whether memory_search tool and auto-recall retrieve memories. When off, the agent responds without any historical context. - Enable Memory Capture (memoryAddEnabled): controls whether new conversations are captured and written to the database. When off, existing memories can still be retrieved but no new ones are stored. Both default to enabled, preserving existing behavior. --- apps/memos-local-openclaw/index.ts | 8 ++++++ apps/memos-local-openclaw/src/config.ts | 2 ++ apps/memos-local-openclaw/src/types.ts | 4 +++ apps/memos-local-openclaw/src/viewer/html.ts | 25 +++++++++++++++++++ .../memos-local-openclaw/src/viewer/server.ts | 2 ++ 5 files changed, 41 insertions(+) diff --git a/apps/memos-local-openclaw/index.ts b/apps/memos-local-openclaw/index.ts index 3e41db667..f93ab37be 100644 --- a/apps/memos-local-openclaw/index.ts +++ b/apps/memos-local-openclaw/index.ts @@ -553,6 +553,12 @@ const memosLocalPlugin = { userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })), }), execute: trackTool("memory_search", async (_toolCallId: any, params: any) => { + if (ctx.config.memorySearchEnabled === false) { + return { + content: [{ type: "text", text: "Memory search is currently disabled in settings." }], + details: { candidates: [], hubCandidates: [], filtered: [], meta: {} }, + }; + } const { query, scope: rawScope, @@ -1877,6 +1883,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`, api.on("before_prompt_build", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => { if (!allowPromptInjection) return {}; + if (ctx.config.memorySearchEnabled === false) return; if (!event.prompt || event.prompt.length < 3) return; const recallAgentId = hookCtx?.agentId ?? (event as any)?.agentId ?? (event as any)?.profileId ?? "main"; @@ -2176,6 +2183,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`, const sessionMsgCursor = new Map(); api.on("agent_end", async (event: any, hookCtx?: { agentId?: string; sessionKey?: string; sessionId?: string }) => { + if (ctx.config.memoryAddEnabled === false) return; if (!event.success || !event.messages || event.messages.length === 0) return; try { diff --git a/apps/memos-local-openclaw/src/config.ts b/apps/memos-local-openclaw/src/config.ts index cea6450dc..d0413782e 100644 --- a/apps/memos-local-openclaw/src/config.ts +++ b/apps/memos-local-openclaw/src/config.ts @@ -133,6 +133,8 @@ export function resolveConfig(raw: Partial | undefined, stateD } : { hubAddress: "", userToken: "", teamToken: "", pendingUserId: "", nickname: "" }; return { enabled, role, hub, client, capabilities: sharingCapabilities }; })(), + memorySearchEnabled: cfg.memorySearchEnabled ?? true, + memoryAddEnabled: cfg.memoryAddEnabled ?? true, }; } diff --git a/apps/memos-local-openclaw/src/types.ts b/apps/memos-local-openclaw/src/types.ts index 1b62586bb..c1c87defc 100644 --- a/apps/memos-local-openclaw/src/types.ts +++ b/apps/memos-local-openclaw/src/types.ts @@ -326,6 +326,10 @@ export interface MemosLocalConfig { sharing?: SharingConfig; /** Hours of inactivity after which an active task is automatically finalized. 0 = disabled. Default 4. */ taskAutoFinalizeHours?: number; + /** Whether memory_search and auto-recall are enabled. Default true. */ + memorySearchEnabled?: boolean; + /** Whether new memories are captured and written to the database. Default true. */ + memoryAddEnabled?: boolean; } // ─── Defaults ─── diff --git a/apps/memos-local-openclaw/src/viewer/html.ts b/apps/memos-local-openclaw/src/viewer/html.ts index 3c86a976b..955c673ce 100644 --- a/apps/memos-local-openclaw/src/viewer/html.ts +++ b/apps/memos-local-openclaw/src/viewer/html.ts @@ -1836,6 +1836,18 @@ input,textarea,select{font-family:inherit;font-size:inherit}
+
+ + +
+
When enabled, the agent retrieves relevant memories for each conversation. Disabling this skips all memory retrieval — the agent will respond without any historical context.
+
+
+ + +
+
When enabled, conversations are captured and stored as memories. Disabling this stops writing new memories to the database while still allowing retrieval of existing ones.
+
@@ -2344,6 +2356,10 @@ const I18N={ 'settings.skill.model.hint':'Leave empty to reuse the Summarizer model. Set a dedicated one for higher quality.', 'settings.optional':'Optional', 'settings.skill.usemain':'Use Main Summarizer', + 'settings.memorySearch.enabled':'Enable Memory Search', + 'settings.memorySearch.hint':'When enabled, the agent retrieves relevant memories for each conversation. Disabling this skips all memory retrieval — the agent will respond without any historical context.', + 'settings.memoryAdd.enabled':'Enable Memory Capture', + 'settings.memoryAdd.hint':'When enabled, conversations are captured and stored as memories. Disabling this stops writing new memories to the database while still allowing retrieval of existing ones.', 'settings.llmFilter.enabled':'Enable LLM Filtering for Memory Search', 'settings.llmFilter.hint':'When enabled, an LLM judges the relevance of search results before returning them. Disabling this returns all candidates from the retrieval engine directly, which is faster but may include less relevant results.', 'settings.telemetry':'Telemetry', @@ -3123,6 +3139,10 @@ const I18N={ 'settings.skill.model.hint':'不配置则复用摘要模型。如需更高质量可单独指定。', 'settings.optional':'可选', 'settings.skill.usemain':'使用主摘要模型', + 'settings.memorySearch.enabled':'启用记忆检索', + 'settings.memorySearch.hint':'开启后,智能体在每次对话时会检索相关记忆。关闭后将跳过所有记忆检索,智能体将在没有历史上下文的情况下回复。', + 'settings.memoryAdd.enabled':'启用记忆写入', + 'settings.memoryAdd.hint':'开启后,对话内容会被捕获并存储为记忆。关闭后不再向数据库写入新记忆,但仍可检索已有记忆。', 'settings.llmFilter.enabled':'启用大模型过滤(记忆搜索)', 'settings.llmFilter.hint':'开启后,搜索结果会经大模型判断相关性后再返回。关闭后将直接返回检索引擎的所有候选结果,速度更快但可能包含不太相关的内容。', 'settings.telemetry':'数据统计', @@ -7233,6 +7253,9 @@ async function loadConfig(){ document.getElementById('cfgViewerPort').value=cfg.viewerPort||''; document.getElementById('cfgTaskAutoFinalizeHours').value=cfg.taskAutoFinalizeHours!=null?cfg.taskAutoFinalizeHours:''; + document.getElementById('cfgMemorySearchEnabled').checked=cfg.memorySearchEnabled!==false; + document.getElementById('cfgMemoryAddEnabled').checked=cfg.memoryAddEnabled!==false; + const recall=cfg.recall||{}; document.getElementById('cfgLlmFilterEnabled').checked=recall.llmFilterEnabled!==false; @@ -7541,6 +7564,8 @@ async function saveGeneralConfig(){ if(vp) cfg.viewerPort=Number(vp); const tafh=document.getElementById('cfgTaskAutoFinalizeHours').value.trim(); cfg.taskAutoFinalizeHours=tafh!==''?Math.max(0,Number(tafh)):4; + cfg.memorySearchEnabled=document.getElementById('cfgMemorySearchEnabled').checked; + cfg.memoryAddEnabled=document.getElementById('cfgMemoryAddEnabled').checked; cfg.recall={llmFilterEnabled:document.getElementById('cfgLlmFilterEnabled').checked}; cfg.telemetry={enabled:document.getElementById('cfgTelemetryEnabled').checked}; diff --git a/apps/memos-local-openclaw/src/viewer/server.ts b/apps/memos-local-openclaw/src/viewer/server.ts index 4d52b6863..2ee41944f 100644 --- a/apps/memos-local-openclaw/src/viewer/server.ts +++ b/apps/memos-local-openclaw/src/viewer/server.ts @@ -3094,6 +3094,8 @@ export class ViewerServer { if (newCfg.skillEvolution) config.skillEvolution = newCfg.skillEvolution; if (newCfg.viewerPort) config.viewerPort = newCfg.viewerPort; if (newCfg.taskAutoFinalizeHours !== undefined) config.taskAutoFinalizeHours = newCfg.taskAutoFinalizeHours; + if (newCfg.memorySearchEnabled !== undefined) config.memorySearchEnabled = newCfg.memorySearchEnabled; + if (newCfg.memoryAddEnabled !== undefined) config.memoryAddEnabled = newCfg.memoryAddEnabled; if (newCfg.recall !== undefined) { const existing = (config.recall as Record) || {}; config.recall = { ...existing, ...newCfg.recall };