Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 37 additions & 21 deletions apps/memos-local-openclaw/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2168,6 +2183,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
const sessionMsgCursor = new Map<string, number>();

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 {
Expand Down
3 changes: 3 additions & 0 deletions apps/memos-local-openclaw/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function resolveConfig(raw: Partial<MemosLocalConfig> | 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,
Expand Down Expand Up @@ -132,6 +133,8 @@ export function resolveConfig(raw: Partial<MemosLocalConfig> | undefined, stateD
} : { hubAddress: "", userToken: "", teamToken: "", pendingUserId: "", nickname: "" };
return { enabled, role, hub, client, capabilities: sharingCapabilities };
})(),
memorySearchEnabled: cfg.memorySearchEnabled ?? true,
memoryAddEnabled: cfg.memoryAddEnabled ?? true,
};
}

Expand Down
6 changes: 6 additions & 0 deletions apps/memos-local-openclaw/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ───
Expand Down
39 changes: 39 additions & 0 deletions apps/memos-local-openclaw/src/viewer/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1836,6 +1836,24 @@ input,textarea,select{font-family:inherit;font-size:inherit}
</div>
</div>
<div class="settings-card-divider"></div>
<div class="settings-toggle">
<label class="toggle-switch"><input type="checkbox" id="cfgMemorySearchEnabled" checked><span class="toggle-slider"></span></label>
<label data-i18n="settings.memorySearch.enabled">Enable Memory Search</label>
</div>
<div class="field-hint" style="margin-top:6px" data-i18n="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.</div>
<div class="settings-card-divider"></div>
<div class="settings-toggle">
<label class="toggle-switch"><input type="checkbox" id="cfgMemoryAddEnabled" checked><span class="toggle-slider"></span></label>
<label data-i18n="settings.memoryAdd.enabled">Enable Memory Capture</label>
</div>
<div class="field-hint" style="margin-top:6px" data-i18n="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.</div>
<div class="settings-card-divider"></div>
<div class="settings-toggle">
<label class="toggle-switch"><input type="checkbox" id="cfgLlmFilterEnabled" checked><span class="toggle-slider"></span></label>
<label data-i18n="settings.llmFilter.enabled">Enable LLM Filtering for Memory Search</label>
</div>
<div class="field-hint" style="margin-top:6px" data-i18n="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.</div>
<div class="settings-card-divider"></div>
<div class="settings-toggle">
<label class="toggle-switch"><input type="checkbox" id="cfgTelemetryEnabled" checked><span class="toggle-slider"></span></label>
<label data-i18n="settings.telemetry.enabled">Enable Anonymous Telemetry</label>
Expand Down Expand Up @@ -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.',
Expand Down Expand Up @@ -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':'仅收集工具名称、响应时间和版本号,不涉及任何记忆内容或个人数据。',
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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');
Expand Down
6 changes: 6 additions & 0 deletions apps/memos-local-openclaw/src/viewer/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) || {};
config.recall = { ...existing, ...newCfg.recall };
}
if (newCfg.telemetry !== undefined) config.telemetry = newCfg.telemetry;
if (newCfg.sharing !== undefined) {
const existing = (config.sharing as Record<string, unknown>) || {};
Expand Down