From 644405553d17be76c5ceb0f207a062a62990924a Mon Sep 17 00:00:00 2001 From: autodev-bot Date: Tue, 30 Jun 2026 17:44:57 +0800 Subject: [PATCH] fix(memos-local-openclaw): honor recall config in auto-recall hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `before_prompt_build` auto-recall hook in `apps/memos-local-openclaw/index.ts` hardcoded `maxResults: 10` for both the local `engine.search` and `hubSearchMemories` calls, so the configured `recall.maxResultsDefault` was effectively ignored on the auto-recall path. Users who lowered `maxResultsDefault` (e.g. 3 or 5) to reduce auto-injected token cost still got ~10+10 candidates per turn. Adds a new optional `recall.autoRecallMaxResults` config key (per issue #1514 option 2) with the resolution order: autoRecallMaxResults -> maxResultsDefault -> 10 (defensive). This is a strict superset of the simpler "just read maxResultsDefault" fix — when `autoRecallMaxResults` is not set the hook falls through to `maxResultsDefault`, while operators who want a richer `memory_search` result set and a leaner auto-recall set can configure them separately. Tests: new resolveConfig cases cover default-undefined / explicit override / independent control alongside maxResultsDefault. The hook change is a literal-to-variable substitution covered by the type and resolver contract. Refs: #1514 Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/memos-local-openclaw/README.md | 1 + apps/memos-local-openclaw/index.ts | 15 ++++++- apps/memos-local-openclaw/src/config.ts | 4 ++ apps/memos-local-openclaw/src/types.ts | 8 ++++ .../memos-local-openclaw/tests/config.test.ts | 40 +++++++++++++++++++ 5 files changed, 66 insertions(+), 2 deletions(-) diff --git a/apps/memos-local-openclaw/README.md b/apps/memos-local-openclaw/README.md index f2a9df64f..073ad7eb9 100644 --- a/apps/memos-local-openclaw/README.md +++ b/apps/memos-local-openclaw/README.md @@ -514,6 +514,7 @@ All optional — shown with defaults: "recall": { "maxResultsDefault": 6, // Default search results "maxResultsMax": 20, // Max search results + "autoRecallMaxResults": 6, // Optional: override max results for the auto-recall hook only. Defaults to `maxResultsDefault`. Set lower (e.g. 3 or 5) to reduce auto-injected tokens while keeping `memory_search` results richer. "minScoreDefault": 0.45, // Default min score threshold "minScoreFloor": 0.35, // Lowest allowed min score "rrfK": 60, // RRF fusion constant diff --git a/apps/memos-local-openclaw/index.ts b/apps/memos-local-openclaw/index.ts index 2f46052ee..4562b50be 100644 --- a/apps/memos-local-openclaw/index.ts +++ b/apps/memos-local-openclaw/index.ts @@ -1889,9 +1889,20 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`, ctx.log.debug(`auto-recall: query="${query.slice(0, 80)}"`); // ── Phase 1: Local search ∥ Hub search (parallel) ── - const arLocalP = engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter }); + // Issue #1514: previously hardcoded maxResults: 10 here, which made + // `recall.maxResultsDefault` and the new `recall.autoRecallMaxResults` + // ineffective for the auto-recall path. Resolution order: + // 1. `recall.autoRecallMaxResults` (explicit auto-recall cap) + // 2. `recall.maxResultsDefault` (shared default with memory_search) + // 3. literal 10 (defensive — `resolveConfig` + // always fills `maxResultsDefault`, so this fires only if a + // caller built a context without going through `resolveConfig`). + const recallCfg = ctx.config.recall ?? {}; + const autoRecallMax = recallCfg.autoRecallMaxResults ?? recallCfg.maxResultsDefault ?? 10; + + const arLocalP = engine.search({ query, maxResults: autoRecallMax, minScore: 0.45, ownerFilter: recallOwnerFilter }); const arHubP = ctx.config?.sharing?.enabled - ? hubSearchMemories(store, ctx, { query, maxResults: 10, scope: "all" }) + ? hubSearchMemories(store, ctx, { query, maxResults: autoRecallMax, scope: "all" }) .catch((err: any) => { ctx.log.debug(`auto-recall: hub search failed (${err})`); return { hits: [] as any[], meta: {} }; }) : Promise.resolve({ hits: [] as any[], meta: {} }); diff --git a/apps/memos-local-openclaw/src/config.ts b/apps/memos-local-openclaw/src/config.ts index 150b09cc4..a84096500 100644 --- a/apps/memos-local-openclaw/src/config.ts +++ b/apps/memos-local-openclaw/src/config.ts @@ -60,6 +60,10 @@ export function resolveConfig(raw: Partial | undefined, stateD recall: { maxResultsDefault: cfg.recall?.maxResultsDefault ?? DEFAULTS.maxResultsDefault, maxResultsMax: cfg.recall?.maxResultsMax ?? DEFAULTS.maxResultsMax, + // Optional override for the `before_prompt_build` auto-recall hook. + // Left undefined when the user does not configure it so the hook can + // fall through to `maxResultsDefault` at call time (issue #1514). + autoRecallMaxResults: cfg.recall?.autoRecallMaxResults, minScoreDefault: cfg.recall?.minScoreDefault ?? DEFAULTS.minScoreDefault, minScoreFloor: cfg.recall?.minScoreFloor ?? DEFAULTS.minScoreFloor, rrfK: cfg.recall?.rrfK ?? DEFAULTS.rrfK, diff --git a/apps/memos-local-openclaw/src/types.ts b/apps/memos-local-openclaw/src/types.ts index cb08eb1cf..e0f1ec56b 100644 --- a/apps/memos-local-openclaw/src/types.ts +++ b/apps/memos-local-openclaw/src/types.ts @@ -305,6 +305,14 @@ export interface MemosLocalConfig { recall?: { maxResultsDefault?: number; maxResultsMax?: number; + /** + * Override the maximum number of candidates the `before_prompt_build` + * auto-recall hook fetches per source (local + Hub). When undefined the + * hook falls back to `maxResultsDefault`. Use this to make auto-recall + * leaner (e.g. 3 or 5) without shrinking the result set of explicit + * `memory_search` tool calls. See issue #1514. + */ + autoRecallMaxResults?: number; minScoreDefault?: number; minScoreFloor?: number; rrfK?: number; diff --git a/apps/memos-local-openclaw/tests/config.test.ts b/apps/memos-local-openclaw/tests/config.test.ts index 072728b9d..73765e3de 100644 --- a/apps/memos-local-openclaw/tests/config.test.ts +++ b/apps/memos-local-openclaw/tests/config.test.ts @@ -48,6 +48,46 @@ describe("resolveConfig", () => { }); }); + describe("recall.autoRecallMaxResults", () => { + it("leaves autoRecallMaxResults undefined when no recall config is provided (fall-through to maxResultsDefault)", () => { + const resolved = resolveConfig(undefined, "/tmp/memos-config-recall-default"); + + // Default policy: auto-recall path inherits maxResultsDefault when not overridden. + expect(resolved.recall?.maxResultsDefault).toBe(6); + expect(resolved.recall?.autoRecallMaxResults).toBeUndefined(); + }); + + it("preserves an explicit autoRecallMaxResults value when set", () => { + const resolved = resolveConfig( + { + recall: { + maxResultsDefault: 6, + autoRecallMaxResults: 3, + }, + } as any, + "/tmp/memos-config-recall-override", + ); + + expect(resolved.recall?.maxResultsDefault).toBe(6); + expect(resolved.recall?.autoRecallMaxResults).toBe(3); + }); + + it("allows independent control: maxResultsDefault for memory_search, autoRecallMaxResults for auto-recall", () => { + const resolved = resolveConfig( + { + recall: { + maxResultsDefault: 10, + autoRecallMaxResults: 5, + }, + } as any, + "/tmp/memos-config-recall-split", + ); + + expect(resolved.recall?.maxResultsDefault).toBe(10); + expect(resolved.recall?.autoRecallMaxResults).toBe(5); + }); + }); + it("preserves explicit user providers when host capabilities are enabled", () => { const resolved = resolveConfig( {