From 714459a954fd02bbf113c741fc873e15bedfbf52 Mon Sep 17 00:00:00 2001 From: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 22:04:04 +0000 Subject: [PATCH] fix(active-memory): fast-fail stalled recall paths --- CHANGELOG.md | 1 + extensions/active-memory/index.test.ts | 43 ++++++++++++++++++++++++++ extensions/active-memory/index.ts | 11 +++---- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d14897f3192..5392de22b4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai - CLI/plugins: stop treating the non-plugin `auth` command root as a bundled plugin id, so restrictive `plugins.allow` configs no longer tell users to add stale `auth` plugin entries. - Doctor/plugins: update configured plugin installs whose stale manifests still declare channels without `channelConfigs`, so beta upgrades repair old Discord-style package payloads during `doctor --fix`. +- Active Memory: keep non-empty `memory_search` results from being fast-failed as empty when debug telemetry reports zero hits. - Plugins/externalization: repair missing configured plugin installs from npm by default, reserve ClawHub downloads for explicit `clawhubSpec` metadata, and cover agent-runtime/env-selected plugin repair. Thanks @vincentkoc. - Upgrade/config: validate configured web-search providers and statically suppressed model/provider pairs against the active plugin set at config load, so stale plugin state fails loud before runtime fallback. - Status/update: resolve beta update-channel checks from the installed version when config still says `stable`, and let `status --deep` reuse live gateway channel credential state instead of warning on command-path-only token misses. diff --git a/extensions/active-memory/index.test.ts b/extensions/active-memory/index.test.ts index a78827c1370..a4cdea89ff8 100644 --- a/extensions/active-memory/index.test.ts +++ b/extensions/active-memory/index.test.ts @@ -2342,6 +2342,49 @@ describe("active-memory plugin", () => { ]); }); + it("does not fast-fail memory_search results solely because debug hits is zero", async () => { + __testing.setMinimumTimeoutMsForTests(1); + __testing.setSetupGraceTimeoutMsForTests(0); + api.pluginConfig = { + agents: ["main"], + timeoutMs: 500, + logging: true, + }; + plugin.register(api as unknown as OpenClawPluginApi); + const sessionKey = "agent:main:terminal-zero-hit-with-results"; + hoisted.sessionStore[sessionKey] = { + sessionId: "s-terminal-zero-hit-with-results", + updatedAt: 0, + }; + runEmbeddedPiAgent.mockImplementationOnce(async (params: { sessionFile: string }) => { + await writeTranscriptJsonl(params.sessionFile, [ + { + message: { + role: "toolResult", + toolName: "memory_search", + details: { + results: [{ path: "memory/food.md", text: "User usually orders ramen." }], + debug: { backend: "qmd", hits: 0, searchMs: 8 }, + }, + }, + }, + ]); + await new Promise((resolve) => setTimeout(resolve, 50)); + return { payloads: [{ text: "User usually orders ramen." }] }; + }); + + const result = await hooks.before_prompt_build( + { prompt: "what food do i usually order? zero hit with results", messages: [] }, + { agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" }, + ); + + expect(result?.prependContext).toContain("User usually orders ramen."); + expect(getActiveMemoryLines(sessionKey)).toEqual([ + expect.stringContaining("🧩 Active Memory: status=ok"), + expect.stringContaining("🔎 Active Memory Debug: backend=qmd searchMs=8 hits=0"), + ]); + }); + it("fast-fails unavailable memory_search results without injecting provider errors", async () => { const CONFIGURED_TIMEOUT_MS = 1_000; __testing.setMinimumTimeoutMsForTests(1); diff --git a/extensions/active-memory/index.ts b/extensions/active-memory/index.ts index 743b68b0dc2..825f8e6a82c 100644 --- a/extensions/active-memory/index.ts +++ b/extensions/active-memory/index.ts @@ -1595,13 +1595,10 @@ function extractTerminalMemorySearchResultFromSessionRecord( const disabled = details?.disabled === true; const unavailable = disabled || Boolean(debug?.warning) || Boolean(debug?.error) || Boolean(details?.error); - const hits = - typeof debug?.hits === "number" && Number.isFinite(debug.hits) - ? debug.hits - : results - ? results.length - : undefined; - if (unavailable || hits === 0) { + const debugHits = + typeof debug?.hits === "number" && Number.isFinite(debug.hits) ? debug.hits : undefined; + const zeroHitSearch = results !== undefined ? results.length === 0 : debugHits === 0; + if (unavailable || zeroHitSearch) { return { status: "empty", searchDebug: debug }; } return undefined;