From e5eff2e897f2158b97d197c9fb144e38f05bf61a Mon Sep 17 00:00:00 2001 From: lcpdeb <512953872@qq.com> Date: Sat, 14 Mar 2026 12:22:17 +0800 Subject: [PATCH 1/4] This comprehensive update improves the robustness and efficiency of the memos-local memory retrieval system. Key Improvements: - Modularized Intent Logic: Extracted judgment logic into dedicated `intent-filter.ts` and `intent-patterns.ts` to allow for specialized local processing. - Persistent Intent Filtering: Implemented a reliable filter mechanism that survives restarts, ensuring consistent recall behavior. - Advanced Regex Pattern Matching: Replaced brittle regex literals with dynamic `new RegExp()` constructors to fix UTF-8 encoding issues and support multi-language scenarios. - LLM-Powered Judgment: Integrated an LLM-based intent analyzer with configurable timeouts and fallbacks to reduce irrelevant memory searches. - Performance Optimization: Added query normalization (trimming) and filter-out logic for conversational fillers/real-time queries. - Configuration Integration: Exposed `llmTimeoutMs` and `autoRecallMaxResults` settings to the plugin configuration interface. - Quality Assurance: Introduced a full Vitest suite in `tests/intent-filter.test.ts` to validate intent branching and edge cases. These changes collectively reduce context pollution and improve the overall "intelligence" of the auto-recall feature. --- apps/memos-local-openclaw/index.ts | 20 +- .../memos-local-openclaw/src/intent-filter.ts | 310 ++++++++++++++++++ .../src/intent-patterns.ts | 97 ++++++ .../tests/intent-filter.test.ts | 170 ++++++++++ 4 files changed, 596 insertions(+), 1 deletion(-) create mode 100644 apps/memos-local-openclaw/src/intent-filter.ts create mode 100644 apps/memos-local-openclaw/src/intent-patterns.ts create mode 100644 apps/memos-local-openclaw/tests/intent-filter.test.ts diff --git a/apps/memos-local-openclaw/index.ts b/apps/memos-local-openclaw/index.ts index d84d94dcd..250ec4642 100644 --- a/apps/memos-local-openclaw/index.ts +++ b/apps/memos-local-openclaw/index.ts @@ -22,6 +22,7 @@ import { SkillInstaller } from "./src/skill/installer"; import { Summarizer } from "./src/ingest/providers"; import { MEMORY_GUIDE_SKILL_MD } from "./src/skill/bundled-memory-guide"; import { Telemetry } from "./src/telemetry"; +import { executeIntentJudge, resolveAutoRecallMaxResults } from "./src/intent-filter"; /** Remove near-duplicate hits based on summary word overlap (>70%). Keeps first (highest-scored) hit. */ @@ -157,6 +158,7 @@ const memosLocalPlugin = { } const pluginCfg = (api.pluginConfig ?? {}) as Record; + const intentFilterOptions = ((pluginCfg as any).intentFilter ?? {}) as any; const stateDir = api.resolvePath("~/.openclaw"); const ctx = buildContext(stateDir, process.cwd(), pluginCfg as any, { debug: (msg: string) => api.logger.info(`[debug] ${msg}`), @@ -890,7 +892,23 @@ const memosLocalPlugin = { } ctx.log.debug(`auto-recall: query="${query.slice(0, 80)}"`); - const result = await engine.search({ query, maxResults: 20, minScore: 0.45, ownerFilter: recallOwnerFilter }); + const { shouldSearch } = await executeIntentJudge({ + query, + summarizer, + ctx, + store, + recallT0, + performance, + options: intentFilterOptions, + }); + if (!shouldSearch) return; + + const result = await engine.search({ + query, + maxResults: resolveAutoRecallMaxResults(intentFilterOptions), + minScore: 0.45, + ownerFilter: recallOwnerFilter, + }); if (result.hits.length === 0) { ctx.log.debug("auto-recall: no candidates found"); const dur = performance.now() - recallT0; diff --git a/apps/memos-local-openclaw/src/intent-filter.ts b/apps/memos-local-openclaw/src/intent-filter.ts new file mode 100644 index 000000000..b004649e5 --- /dev/null +++ b/apps/memos-local-openclaw/src/intent-filter.ts @@ -0,0 +1,310 @@ +/** + * Intent Filter Module + * Determines if a user query requires memory retrieval. + * + * Design Principle: Rather skip than pollute context with irrelevant hits. + */ + +import { + compilePatterns, + matchPatterns, + MEMORY_QUERY_PATTERN_SOURCES, + SKIP_RECALL_PATTERN_SOURCES, +} from "./intent-patterns"; + +const DEFAULT_AUTO_RECALL_MAX_RESULTS = 10; + +export type IntentFilterOptions = { + /** LLM intent judgment timeout (ms) */ + llmTimeoutMs?: number; + /** Fallback strategy on LLM error/timeout */ + onLlmError?: "skip" | "search"; + /** Minimum confidence level to trigger search */ + minConfidenceForSearch?: "high" | "medium"; + /** Maximum LLM output length, exceeds treated as abnormal */ + maxLlmOutputLength?: number; + /** Max results for auto-recall (provided as a config interface for callers) */ + autoRecallMaxResults?: number; +}; + +export const DEFAULT_INTENT_FILTER_OPTIONS: Required< + Pick +> = { + llmTimeoutMs: 2000, + onLlmError: "skip", + minConfidenceForSearch: "high", + maxLlmOutputLength: 400, +}; + +// ====== Pattern Matching Rules ====== + +/** Conversation continuation: No memory/search needed, let LLM reply based on current context */ +const compiledSkipRecallPatterns = compilePatterns(SKIP_RECALL_PATTERN_SOURCES); + +/** Explicit memory retrieval: High confidence memory queries */ +const compiledMemoryQueryPatterns = compilePatterns(MEMORY_QUERY_PATTERN_SOURCES); + +// ====== LLM Prompt ====== + +const intentPromptTemplate = (query: string) => `You are a query intent analyzer. Determine if the following user query requires "Memory Retrieval". + +Judgment Criteria (Strictly Follow): +- "Memory Retrieval (High)": User explicitly refers to specific past conversation details (e.g., "the bug fix we discussed last time", "the pricing strategy mentioned yesterday"). Must contain a specific subject. +- "Memory Retrieval (Medium)": User refers to the past but vaguely (e.g., "last news", "previous issues"). Clear temporal pointer but blurry content. +- "Real-time Search": User wants current data (weather, news, stock prices) or just general search (e.g., "search for news"). +- "Skip": Vague or ambiguous queries (e.g., "optimise this", "what should I do", "continue", single-word replies). + +User Query: "${query}" + +Output Format (Strictly Follow): +Intent: +Confidence: +Reason: `; + +// ====== Type Definitions ====== + +export type IntentResult = { + action: 'skip' | 'search' | 'llm_judge'; + reason: string; + /** LLM prompt (only if action='llm_judge') */ + llmPrompt?: string; +}; + +export type LLMJudgeResult = { + action: 'skip' | 'search'; + reason: string; + /** Raw LLM output (for debugging) */ + raw?: string; +}; + +function normalizeText(text: string): string { + return text + .replace(/\r\n/g, "\n") + .replace(/[:﹕︰]/g, ":") + .trim(); +} + +function pickFieldFromObject(obj: Record, keys: string[]): string { + for (const k of keys) { + const v = obj[k]; + if (typeof v === "string" && v.trim()) return v.trim(); + } + return ""; +} + +function parseJsonLikeIntent(output: string): { intent: string; confidence: string } | null { + // Try full string first + try { + const full = JSON.parse(output) as Record; + return { + intent: pickFieldFromObject(full, ["intent", "意图", "label", "category"]), + confidence: pickFieldFromObject(full, ["confidence", "置信度", "conf"]), + }; + } catch { + // Fallback to JSON snippets (non-greedy) for mixed text output. + } + + for (const m of output.matchAll(/\{[\s\S]*?\}/g)) { + try { + const parsed = JSON.parse(m[0]) as Record; + const intent = pickFieldFromObject(parsed, ["intent", "意图", "label", "category"]); + const confidence = pickFieldFromObject(parsed, ["confidence", "置信度", "conf"]); + if (intent || confidence) return { intent, confidence }; + } catch { + // continue + } + } + return null; +} + +function extractIntentAndConfidence(output: string): { intent: string; confidence: string } { + const jsonParsed = parseJsonLikeIntent(output); + if (jsonParsed) return jsonParsed; + + const intentLine = output.match(/^(?:意图|intent)\s*:\s*(.+)$/im)?.[1]?.trim() ?? ""; + const confidenceLine = output.match(/^(?:置信度|confidence)\s*:\s*(.+)$/im)?.[1]?.trim() ?? ""; + return { intent: intentLine, confidence: confidenceLine }; +} + +function isMemoryIntent(intent: string): boolean { + const v = intent.toLowerCase(); + return ( + intent.includes("记忆检索") || + intent.includes("回忆") || + /memory\s*(retrieval|search|recall)/i.test(v) || + /search\s*memory/i.test(v) + ); +} + +function confidenceRank(confidence: string): number { + const v = confidence.toLowerCase(); + if (confidence.includes("高") || /high/.test(v)) return 3; + if (confidence.includes("中") || /medium|med/.test(v)) return 2; + if (confidence.includes("低") || /low/.test(v)) return 1; + return 0; +} + +function requiredConfidenceRank(level: "high" | "medium"): number { + return level === "high" ? 3 : 2; +} + +// ====== Main Functions ====== + +/** + * Determines if query should be skipped, searched directly, or judged by LLM + * @param query User query + * @returns Result + */ +export function shouldSkipOrSearch(query: string): IntentResult { + const normalizedQuery = query.trim(); + + // 1. Continuation command -> Skip + if (matchPatterns(normalizedQuery, compiledSkipRecallPatterns)) { + return { action: 'skip', reason: 'skip_continue_command' }; + } + + // 2. Explicit memory query -> Search (skip LLM) + if (matchPatterns(normalizedQuery, compiledMemoryQueryPatterns)) { + return { action: 'search', reason: 'explicit_memory_query' }; + } + + // 3. Others -> LLM judge + return { + action: 'llm_judge', + reason: 'needs_llm_judgment', + llmPrompt: intentPromptTemplate(normalizedQuery), + }; +} + +/** + * Parses LLM intent judgment output + * @param llmOutput Raw LLM output + * @param query User query (for logging) + * @returns Judgment result + */ +export function parseLLMIntent(llmOutput: string, query: string, options?: IntentFilterOptions): LLMJudgeResult { + const output = normalizeText(llmOutput ?? ''); + const maxOutputLength = options?.maxLlmOutputLength ?? DEFAULT_INTENT_FILTER_OPTIONS.maxLlmOutputLength; + + // Error tolerance: LLM failed (returns prompt or too long) + const llmFailed = + output.includes('You are a query intent analyzer') || + output.toLowerCase().includes('you are a query intent analyzer') || + output.length > maxOutputLength; + if (llmFailed) { + return { action: 'skip', reason: 'llm_failed_skipped', raw: output }; + } + + // Parse fields + const { intent, confidence } = extractIntentAndConfidence(output); + const threshold = options?.minConfidenceForSearch ?? DEFAULT_INTENT_FILTER_OPTIONS.minConfidenceForSearch; + const shouldSearchMemory = + isMemoryIntent(intent) && + confidenceRank(confidence) >= requiredConfidenceRank(threshold); + + if (shouldSearchMemory) { + return { action: 'search', reason: `intent=${intent},confidence=${confidence}`, raw: output }; + } + + const reason = intent || confidence + ? `intent=${intent},confidence=${confidence}` + : `intent=unknown,confidence=unknown,query=${query.slice(0, 40)}`; + return { action: 'skip', reason, raw: output }; +} + +/** + * Executes intent judgment logic (used in index.ts) + */ +export async function executeIntentJudge(params: { + query: string; + summarizer: { summarize: (prompt: string) => Promise }; + ctx: { log: { debug: (m: string) => void; info: (m: string) => void; warn: (m: string) => void } }; + store: { + recordToolCall: (name: string, duration: number, success: boolean) => void; + recordApiLog: (name: string, payload: any, result: string, duration: number, success: boolean) => void; + }; + recallT0: number; + performance: { now: () => number }; + options?: IntentFilterOptions; +}): Promise<{ shouldSearch: boolean }> { + const { query, summarizer, ctx, store, recallT0, performance, options } = params; + const policy = { + ...DEFAULT_INTENT_FILTER_OPTIONS, + ...(options ?? {}), + }; + const timerApi = globalThis as any; + + const intentCheck = shouldSkipOrSearch(query); + + // 1. Direct Skip + if (intentCheck.action === 'skip') { + ctx.log.debug(`auto-recall: skipped query "${query}" reason=${intentCheck.reason}`); + const dur = performance.now() - recallT0; + store.recordToolCall("memory_search", dur, true); + store.recordApiLog("memory_search", { type: "auto_recall", query, reason: intentCheck.reason }, "skipped", dur, true); + return { shouldSearch: false }; + } + + // 2. Explicit memory query -> Search + if (intentCheck.action === 'search') { + ctx.log.debug(`auto-recall: explicit memory query "${query}"`); + return { shouldSearch: true }; + } + + // 3. Others -> LLM Judge + try { + const timeoutError = new Error("intent_judge_timeout"); + let tid: any; + const timeoutPromise = new Promise((_, reject) => { + tid = timerApi.setTimeout(() => reject(timeoutError), policy.llmTimeoutMs); + }); + + const intentResult = await Promise.race([ + summarizer.summarize(intentCheck.llmPrompt!), + timeoutPromise, + ]).finally(() => { + if (tid !== undefined) timerApi.clearTimeout(tid); + }); + + ctx.log.debug(`auto-recall: LLM intent result="${intentResult}"`); + + const parsed = parseLLMIntent(intentResult ?? '', query, policy); + + if (parsed.action === 'skip') { + if (parsed.reason === 'llm_failed_skipped') { + ctx.log.warn(`auto-recall: LLM call failed, skipping memory retrieval by default (fallback policy)`); + } else { + ctx.log.info(`auto-recall: skipped query "${query.slice(0, 50)}" reason=${parsed.reason}`); + } + const dur = performance.now() - recallT0; + store.recordToolCall("memory_search", dur, true); + store.recordApiLog("memory_search", { type: "auto_recall", query, reason: parsed.reason }, "skipped", dur, true); + return { shouldSearch: false }; + } + + return { shouldSearch: true }; + } catch (intentErr) { + if (policy.onLlmError === "search") { + ctx.log.warn(`auto-recall: LLM intent judgment failed, proceeding with retrieval (config policy): ${intentErr}`); + return { shouldSearch: true }; + } + ctx.log.warn(`auto-recall: LLM intent judgment failed: ${intentErr}`); + const dur = performance.now() - recallT0; + store.recordToolCall("memory_search", dur, true); + store.recordApiLog("memory_search", { type: "auto_recall", query, reason: "llm_error_skipped" }, "skipped", dur, true); + return { shouldSearch: false }; + } +} + +/** + * Resolves auto-recall max results configuration + */ +export function resolveAutoRecallMaxResults(options?: IntentFilterOptions): number { + const raw = options?.autoRecallMaxResults; + if (typeof raw !== "number" || !Number.isFinite(raw)) return DEFAULT_AUTO_RECALL_MAX_RESULTS; + const n = Math.floor(raw); + if (n < 1) return 1; + if (n > 20) return 20; + return n; +} + diff --git a/apps/memos-local-openclaw/src/intent-patterns.ts b/apps/memos-local-openclaw/src/intent-patterns.ts new file mode 100644 index 000000000..f2f347e17 --- /dev/null +++ b/apps/memos-local-openclaw/src/intent-patterns.ts @@ -0,0 +1,97 @@ +export interface RegexPatternSource { + pattern: string + flags?: string +} + +export interface CompiledRegexPattern { + regex: RegExp +} + +/** + * Skip memory recall patterns + */ +export const SKIP_RECALL_PATTERN_SOURCES: RegexPatternSource[] = [ + // continuation + { + pattern: "^(继续|接着|然后|下一步|continue|next|go on)$", + flags: "i", + }, + + // acknowledgement + { + pattern: "^(好(的)?|行|嗯+|啊+|哦+|ok(ay)?|sure|got it|收到|明白了)$", + flags: "i", + }, + + // real-time information queries + { + pattern: + "^(今天|今日|刚刚|刚才|最新|最近).{0,12}(新闻|消息|资讯|情况|发生|动态)", + }, + + // english realtime queries + { + pattern: + "^(latest|today|recent).{0,12}(news|updates|events)", + flags: "i", + }, +] + +/** + * Explicit memory recall patterns + */ +export const MEMORY_QUERY_PATTERN_SOURCES: RegexPatternSource[] = [ + // reference to past conversation + { + pattern: + "(上次|之前|以前|刚才|刚刚).{0,10}(说|讲|问|聊|讨论|提到|写|给我|那个)", + }, + + // asking if assistant remembers + { + pattern: + "(还记得|记不记得|记得吗|remember|do you remember)", + flags: "i", + }, + + // explicit history search + { + pattern: + "(聊天|对话|历史).{0,6}(记录|内容)?.{0,6}(查|找|看|搜索|查询)", + }, + + // english past reference + { + pattern: + "(last time|previously|earlier|before we)", + flags: "i", + }, + + // implicit reference (那个代码 / 刚才那个函数) + { + pattern: + "(那个|刚才那个|刚刚那个).{0,8}(代码|函数|问题|回答|内容)", + }, +] + +/** + * compile regex patterns + */ +export function compilePatterns( + sources: RegexPatternSource[] +): CompiledRegexPattern[] { + return sources.map((s) => ({ + regex: new RegExp(s.pattern, s.flags ?? ""), + })) +} + +/** + * test input against compiled patterns + */ +export function matchPatterns( + input: string, + patterns: CompiledRegexPattern[] +): boolean { + return patterns.some((p) => p.regex.test(input)) +} + diff --git a/apps/memos-local-openclaw/tests/intent-filter.test.ts b/apps/memos-local-openclaw/tests/intent-filter.test.ts new file mode 100644 index 000000000..559e22331 --- /dev/null +++ b/apps/memos-local-openclaw/tests/intent-filter.test.ts @@ -0,0 +1,170 @@ +import { describe, it, expect } from "vitest"; +import { + shouldSkipOrSearch, + parseLLMIntent, + executeIntentJudge, + resolveAutoRecallMaxResults, +} from "../src/intent-filter"; + +describe("intent-filter: shouldSkipOrSearch", () => { + it("should skip simple continue command (zh)", () => { + expect(shouldSkipOrSearch("继续").action).toBe("skip"); + }); + + it("should skip continue command with surrounding spaces", () => { + expect(shouldSkipOrSearch(" continue ").action).toBe("skip"); + }); + + it("should skip simple continue command (en)", () => { + expect(shouldSkipOrSearch("continue").action).toBe("skip"); + }); + + it("should search for explicit memory query (en)", () => { + expect(shouldSkipOrSearch("Do you remember what we discussed last time about pricing?").action).toBe("search"); + }); +}); + +describe("intent-filter: parseLLMIntent", () => { + it("parses Chinese label output", () => { + const out = `意图: 记忆检索\n置信度: 高\n原因: 明确提到过去对话`; + const parsed = parseLLMIntent(out, "explicit search (zh)"); + expect(parsed.action).toBe("search"); + }); + + it("parses English label output", () => { + const out = `Intent: Memory Retrieval\nConfidence: High\nReason: asks about previous discussion`; + const parsed = parseLLMIntent(out, "last discussion"); + expect(parsed.action).toBe("search"); + }); + + it("parses JSON output", () => { + const out = JSON.stringify({ intent: "Memory Retrieval", confidence: "High", reason: "explicit past reference" }); + const parsed = parseLLMIntent(out, "previous topic"); + expect(parsed.action).toBe("search"); + }); + + it("parses JSON snippet from mixed text", () => { + const out = `Some preface text\n{"intent":"Memory Retrieval","confidence":"High"}\nSome suffix text`; + const parsed = parseLLMIntent(out, "previous topic"); + expect(parsed.action).toBe("search"); + }); + + it("supports medium threshold by option", () => { + const out = `Intent: Memory Retrieval\nConfidence: medium\nReason: mentions previous issue`; + const parsedDefault = parseLLMIntent(out, "previous issue"); + const parsedMedium = parseLLMIntent(out, "previous issue", { minConfidenceForSearch: "medium" }); + expect(parsedDefault.action).toBe("skip"); + expect(parsedMedium.action).toBe("search"); + }); +}); + +describe("intent-filter: executeIntentJudge", () => { + it("returns shouldSearch=false when LLM fails and fallback=skip", async () => { + const summarizer = { + summarize: async () => { + throw new Error("network"); + }, + }; + const logs: string[] = []; + const ctx = { + log: { + debug: (_m: string) => {}, + info: (_m: string) => {}, + warn: (m: string) => logs.push(m), + }, + }; + const store = { + recordToolCall: (_name: string, _dur: number, _success: boolean) => {}, + recordApiLog: (_name: string, _payload: unknown, _result: string, _dur: number, _success: boolean) => {}, + }; + const perf = { now: () => 1000 }; + + const result = await executeIntentJudge({ + query: "optimise this", + summarizer, + ctx, + store, + recallT0: 900, + performance: perf, + options: { onLlmError: "skip" }, + }); + + expect(result.shouldSearch).toBe(false); + expect(logs.length).toBeGreaterThan(0); + }); + + it("returns shouldSearch=true when LLM fails and fallback=search", async () => { + const summarizer = { + summarize: async () => { + throw new Error("timeout"); + }, + }; + const ctx = { + log: { + debug: (_m: string) => {}, + info: (_m: string) => {}, + warn: (_m: string) => {}, + }, + }; + const store = { + recordToolCall: (_name: string, _dur: number, _success: boolean) => {}, + recordApiLog: (_name: string, _payload: unknown, _result: string, _dur: number, _success: boolean) => {}, + }; + const perf = { now: () => 1000 }; + + const result = await executeIntentJudge({ + query: "optimise this", + summarizer, + ctx, + store, + recallT0: 900, + performance: perf, + options: { onLlmError: "search" }, + }); + + expect(result.shouldSearch).toBe(true); + }); + + it("returns shouldSearch=false on timeout when fallback=skip", async () => { + const summarizer = { + summarize: async () => await new Promise(() => {}), + }; + const ctx = { + log: { + debug: (_m: string) => {}, + info: (_m: string) => {}, + warn: (_m: string) => {}, + }, + }; + const store = { + recordToolCall: (_name: string, _dur: number, _success: boolean) => {}, + recordApiLog: (_name: string, _payload: unknown, _result: string, _dur: number, _success: boolean) => {}, + }; + const perf = { now: () => 1000 }; + + const result = await executeIntentJudge({ + query: "please optimize this", + summarizer, + ctx, + store, + recallT0: 900, + performance: perf, + options: { llmTimeoutMs: 1, onLlmError: "skip" }, + }); + + expect(result.shouldSearch).toBe(false); + }); +}); + +describe("intent-filter: resolveAutoRecallMaxResults", () => { + it("clamps range to 1..20", () => { + expect(resolveAutoRecallMaxResults({ autoRecallMaxResults: 0 })).toBe(1); + expect(resolveAutoRecallMaxResults({ autoRecallMaxResults: 99 })).toBe(20); + expect(resolveAutoRecallMaxResults({ autoRecallMaxResults: 12 })).toBe(12); + }); + + it("uses default on invalid input", () => { + expect(resolveAutoRecallMaxResults({ autoRecallMaxResults: Number.NaN })).toBe(10); + expect(resolveAutoRecallMaxResults()).toBe(10); + }); +}); From 87b38bd88eeb767321cb65f8aeb35562429b4546 Mon Sep 17 00:00:00 2001 From: lcpdeb <47087196+lcpdeb@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:36:56 +0800 Subject: [PATCH 2/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- apps/memos-local-openclaw/src/intent-filter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/memos-local-openclaw/src/intent-filter.ts b/apps/memos-local-openclaw/src/intent-filter.ts index b004649e5..f9fac80c2 100644 --- a/apps/memos-local-openclaw/src/intent-filter.ts +++ b/apps/memos-local-openclaw/src/intent-filter.ts @@ -192,7 +192,8 @@ export function parseLLMIntent(llmOutput: string, query: string, options?: Inten output.toLowerCase().includes('you are a query intent analyzer') || output.length > maxOutputLength; if (llmFailed) { - return { action: 'skip', reason: 'llm_failed_skipped', raw: output }; + const fallbackAction = options?.onLlmError ?? 'skip'; + return { action: fallbackAction, reason: 'llm_failed_skipped', raw: output }; } // Parse fields From 80ee4c1e89743736f66ca94285878095c2e19cd6 Mon Sep 17 00:00:00 2001 From: lcpdeb <512953872@qq.com> Date: Sat, 14 Mar 2026 13:15:37 +0800 Subject: [PATCH 3/4] fix: add intent filter configuration, fix LLM timeout fallback action and auto-recall settings --- apps/memos-local-openclaw/index.ts | 20 +++++++++++++++++++ .../memos-local-openclaw/src/intent-filter.ts | 14 +++++-------- .../src/intent-patterns.ts | 15 +++++++------- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/apps/memos-local-openclaw/index.ts b/apps/memos-local-openclaw/index.ts index 250ec4642..f91e25e96 100644 --- a/apps/memos-local-openclaw/index.ts +++ b/apps/memos-local-openclaw/index.ts @@ -63,6 +63,26 @@ const pluginConfigSchema = { }, }, }, + intentFilter: { + type: "object" as const, + description: + "Intent filter configuration. Controls LLM timeout and automatic recall behavior for memory search.", + additionalProperties: true, + properties: { + llmTimeoutMs: { + type: "number" as const, + description: + "Timeout in milliseconds for the intent-judging LLM call used by the intent filter (default: implementation-specific).", + minimum: 0, + }, + autoRecallMaxResults: { + type: "number" as const, + description: + "Maximum number of memory results that automatic recall may return when the intent filter triggers recall.", + minimum: 0, + }, + }, + }, }, }; diff --git a/apps/memos-local-openclaw/src/intent-filter.ts b/apps/memos-local-openclaw/src/intent-filter.ts index f9fac80c2..d0733733b 100644 --- a/apps/memos-local-openclaw/src/intent-filter.ts +++ b/apps/memos-local-openclaw/src/intent-filter.ts @@ -234,15 +234,14 @@ export async function executeIntentJudge(params: { ...(options ?? {}), }; const timerApi = globalThis as any; - + const intentCheck = shouldSkipOrSearch(query); // 1. Direct Skip if (intentCheck.action === 'skip') { ctx.log.debug(`auto-recall: skipped query "${query}" reason=${intentCheck.reason}`); const dur = performance.now() - recallT0; - store.recordToolCall("memory_search", dur, true); - store.recordApiLog("memory_search", { type: "auto_recall", query, reason: intentCheck.reason }, "skipped", dur, true); + store.recordApiLog("auto_recall_intent_skip", { type: "auto_recall", query, reason: intentCheck.reason }, "skipped", dur, true); return { shouldSearch: false }; } @@ -278,11 +277,10 @@ export async function executeIntentJudge(params: { ctx.log.info(`auto-recall: skipped query "${query.slice(0, 50)}" reason=${parsed.reason}`); } const dur = performance.now() - recallT0; - store.recordToolCall("memory_search", dur, true); - store.recordApiLog("memory_search", { type: "auto_recall", query, reason: parsed.reason }, "skipped", dur, true); + store.recordApiLog("auto_recall_intent_skip", { type: "auto_recall", query, reason: parsed.reason }, "skipped", dur, true); return { shouldSearch: false }; } - + return { shouldSearch: true }; } catch (intentErr) { if (policy.onLlmError === "search") { @@ -291,8 +289,7 @@ export async function executeIntentJudge(params: { } ctx.log.warn(`auto-recall: LLM intent judgment failed: ${intentErr}`); const dur = performance.now() - recallT0; - store.recordToolCall("memory_search", dur, true); - store.recordApiLog("memory_search", { type: "auto_recall", query, reason: "llm_error_skipped" }, "skipped", dur, true); + store.recordApiLog("auto_recall_intent_skip", { type: "auto_recall", query, reason: "llm_error_skipped" }, "skipped", dur, true); return { shouldSearch: false }; } } @@ -308,4 +305,3 @@ export function resolveAutoRecallMaxResults(options?: IntentFilterOptions): numb if (n > 20) return 20; return n; } - diff --git a/apps/memos-local-openclaw/src/intent-patterns.ts b/apps/memos-local-openclaw/src/intent-patterns.ts index f2f347e17..f1b7c7f01 100644 --- a/apps/memos-local-openclaw/src/intent-patterns.ts +++ b/apps/memos-local-openclaw/src/intent-patterns.ts @@ -1,10 +1,10 @@ export interface RegexPatternSource { - pattern: string - flags?: string + pattern: string; + flags?: string; } export interface CompiledRegexPattern { - regex: RegExp + regex: RegExp; } /** @@ -35,7 +35,7 @@ export const SKIP_RECALL_PATTERN_SOURCES: RegexPatternSource[] = [ "^(latest|today|recent).{0,12}(news|updates|events)", flags: "i", }, -] +]; /** * Explicit memory recall patterns @@ -72,7 +72,7 @@ export const MEMORY_QUERY_PATTERN_SOURCES: RegexPatternSource[] = [ pattern: "(那个|刚才那个|刚刚那个).{0,8}(代码|函数|问题|回答|内容)", }, -] +]; /** * compile regex patterns @@ -82,7 +82,7 @@ export function compilePatterns( ): CompiledRegexPattern[] { return sources.map((s) => ({ regex: new RegExp(s.pattern, s.flags ?? ""), - })) + })); } /** @@ -92,6 +92,5 @@ export function matchPatterns( input: string, patterns: CompiledRegexPattern[] ): boolean { - return patterns.some((p) => p.regex.test(input)) + return patterns.some((p) => p.regex.test(input)); } - From f75fefacd7702b31d2b3f32e33629e3af7ff9fbe Mon Sep 17 00:00:00 2001 From: lcpdeb <512953872@qq.com> Date: Sat, 14 Mar 2026 23:09:43 +0800 Subject: [PATCH 4/4] add up start session system prompt and slash command for skipping patterns --- .../src/intent-patterns.ts | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/apps/memos-local-openclaw/src/intent-patterns.ts b/apps/memos-local-openclaw/src/intent-patterns.ts index f1b7c7f01..2ab5df1e9 100644 --- a/apps/memos-local-openclaw/src/intent-patterns.ts +++ b/apps/memos-local-openclaw/src/intent-patterns.ts @@ -13,26 +13,36 @@ export interface CompiledRegexPattern { export const SKIP_RECALL_PATTERN_SOURCES: RegexPatternSource[] = [ // continuation { - pattern: "^(继续|接着|然后|下一步|continue|next|go on)$", + pattern: "^(继续|接着|然后|下一步|continue|next|go\\s+on)$", flags: "i", }, // acknowledgement { - pattern: "^(好(的)?|行|嗯+|啊+|哦+|ok(ay)?|sure|got it|收到|明白了)$", + pattern: "^(好(的)?|行|嗯+|啊+|哦+|ok(ay)?|sure|got\\s+it|收到|明白了)$", flags: "i", }, + // session start events - should never trigger memory search + { + pattern: "(new session|Session Startup|greet the user|read the required files)", + flags: "i", + }, + + // slash commands - should never trigger memory search + { + pattern: "/(new|reset|status|reasoning|model|help|clear|undo|continue)(\\s|$)", + flags: "i", + }, // real-time information queries { pattern: - "^(今天|今日|刚刚|刚才|最新|最近).{0,12}(新闻|消息|资讯|情况|发生|动态)", + "(今|刚|最新|最近|新闻|消息|资讯|情况|发生|动态|网|浏览)", }, // english realtime queries { - pattern: - "^(latest|today|recent).{0,12}(news|updates|events)", + pattern: "^(latest|today|recent).{0,12}(news|updates|events)", flags: "i", }, ]; @@ -43,27 +53,23 @@ export const SKIP_RECALL_PATTERN_SOURCES: RegexPatternSource[] = [ export const MEMORY_QUERY_PATTERN_SOURCES: RegexPatternSource[] = [ // reference to past conversation { - pattern: - "(上次|之前|以前|刚才|刚刚).{0,10}(说|讲|问|聊|讨论|提到|写|给我|那个)", + pattern: "(上次|之前|以前|刚才|刚刚).{0,10}(说|讲|问|聊|讨论|提到)", }, // asking if assistant remembers { - pattern: - "(还记得|记不记得|记得吗|remember|do you remember)", + pattern: "(还记得|记得吗|do\\s+you\\s+remember)", flags: "i", }, // explicit history search { - pattern: - "(聊天|对话|历史).{0,6}(记录|内容)?.{0,6}(查|找|看|搜索|查询)", + pattern: "(聊天|对话|历史).{0,6}(记录|内容)?.{0,6}(查|找|看|搜索|查询)", }, // english past reference { - pattern: - "(last time|previously|earlier|before we)", + pattern: "(last\\s+time|previously|earlier|before\\s+we)", flags: "i", },