diff --git a/apps/memos-local-openclaw/index.ts b/apps/memos-local-openclaw/index.ts index 21b12d8e..f93ab37b 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, @@ -656,7 +662,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 +678,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; @@ -1874,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"; @@ -1995,28 +2005,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; @@ -2168,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 150b09cc..d0413782 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, @@ -132,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 cb08eb1c..c1c87def 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; @@ -324,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 26e5284b..955c673c 100644 --- a/apps/memos-local-openclaw/src/viewer/html.ts +++ b/apps/memos-local-openclaw/src/viewer/html.ts @@ -1836,6 +1836,24 @@ 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.
+
+
+ + +
+
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 +2356,12 @@ 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', '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 +3139,12 @@ 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':'数据统计', 'settings.telemetry.enabled':'启用匿名数据统计', 'settings.telemetry.hint':'仅收集工具名称、响应时间和版本号,不涉及任何记忆内容或个人数据。', @@ -7223,6 +7253,12 @@ 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; + const tel=cfg.telemetry||{}; document.getElementById('cfgTelemetryEnabled').checked=tel.enabled!==false; @@ -7528,6 +7564,9 @@ 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}; 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 1750c9cc..2ee41944 100644 --- a/apps/memos-local-openclaw/src/viewer/server.ts +++ b/apps/memos-local-openclaw/src/viewer/server.ts @@ -3094,6 +3094,12 @@ 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 }; + } if (newCfg.telemetry !== undefined) config.telemetry = newCfg.telemetry; if (newCfg.sharing !== undefined) { const existing = (config.sharing as Record) || {};