Skip to content

Commit ae82a39

Browse files
fix(active-memory): fast-fail stalled recall paths (#76183)
Summary: - This PR adds Active Memory transcript polling to fast-fail terminal zero-hit or unavailable recall tool results, filters timeout boilerplate, extends focused regressions, and adds a changelog fix entry. - Reproducibility: yes. The PR includes focused regressions that reproduce terminal zero-hit search, unavailab ... rch, non-empty `details.results` with `debug.hits: 0`, memory_get misses, and timeout boilerplate behavior. ClawSweeper fixups: - Included follow-up commit: fix(active-memory): fast-fail stalled recall paths - Included follow-up commit: fix(clawsweeper): address review for automerge-openclaw-openclaw-7576… - Included follow-up commit: fix(clawsweeper): reconcile automerge-openclaw-openclaw-75761 with ma… - Ran the ClawSweeper repair loop before final review. Validation: - ClawSweeper review passed for head e5ea3f1. - Required merge gates passed before the squash merge. Prepared head SHA: e5ea3f1 Review: #76183 (comment) Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: codexGW <9350182+codexGW@users.noreply.github.com>
1 parent 9fdc0e7 commit ae82a39

3 files changed

Lines changed: 403 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
1414

1515
- 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.
1616
- 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`.
17+
- Active Memory: keep non-empty `memory_search` results from being fast-failed as empty when debug telemetry reports zero hits.
1718
- 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.
1819
- 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.
1920
- 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.

extensions/active-memory/index.test.ts

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,6 +1789,50 @@ describe("active-memory plugin", () => {
17891789
expect(lines.some((line) => line.includes("timeout_partial"))).toBe(false);
17901790
});
17911791

1792+
it("does not inject embedded timeout boilerplate from partial transcripts", async () => {
1793+
__testing.setMinimumTimeoutMsForTests(1);
1794+
__testing.setSetupGraceTimeoutMsForTests(0);
1795+
api.pluginConfig = {
1796+
agents: ["main"],
1797+
timeoutMs: 1,
1798+
logging: true,
1799+
};
1800+
plugin.register(api as unknown as OpenClawPluginApi);
1801+
const sessionKey = "agent:main:timeout-boilerplate-transcript";
1802+
hoisted.sessionStore[sessionKey] = {
1803+
sessionId: "s-timeout-boilerplate-transcript",
1804+
updatedAt: 0,
1805+
};
1806+
runEmbeddedPiAgent.mockImplementationOnce(async (params: { sessionFile: string }) => {
1807+
await writeTranscriptJsonl(params.sessionFile, [
1808+
{
1809+
type: "message",
1810+
message: {
1811+
role: "assistant",
1812+
content: "LLM request timed out after 15000 ms.",
1813+
},
1814+
},
1815+
]);
1816+
await new Promise<never>(() => {});
1817+
});
1818+
1819+
const result = await hooks.before_prompt_build(
1820+
{ prompt: "what wings should i order? timeout boilerplate", messages: [] },
1821+
{
1822+
agentId: "main",
1823+
trigger: "user",
1824+
sessionKey,
1825+
messageProvider: "webchat",
1826+
},
1827+
);
1828+
1829+
expect(result).toBeUndefined();
1830+
const lines = getActiveMemoryLines(sessionKey);
1831+
expect(lines).toEqual([expect.stringContaining("🧩 Active Memory: status=timeout")]);
1832+
expect(lines.some((line) => line.includes("timeout_partial"))).toBe(false);
1833+
expect(lines.some((line) => line.includes("LLM request timed out"))).toBe(false);
1834+
});
1835+
17921836
it("returns partial transcript text when an aborted subagent rejects before the race timeout wins", async () => {
17931837
__testing.setMinimumTimeoutMsForTests(1);
17941838
api.pluginConfig = {
@@ -2258,6 +2302,171 @@ describe("active-memory plugin", () => {
22582302
expect(wallClockMs).toBeLessThan(CONFIGURED_TIMEOUT_MS + HARD_DEADLINE_MARGIN_MS);
22592303
});
22602304

2305+
it("fast-fails terminal zero-hit memory_search results without waiting for recall timeout", async () => {
2306+
const CONFIGURED_TIMEOUT_MS = 1_000;
2307+
__testing.setMinimumTimeoutMsForTests(1);
2308+
__testing.setSetupGraceTimeoutMsForTests(0);
2309+
api.pluginConfig = {
2310+
agents: ["main"],
2311+
timeoutMs: CONFIGURED_TIMEOUT_MS,
2312+
logging: true,
2313+
};
2314+
plugin.register(api as unknown as OpenClawPluginApi);
2315+
const sessionKey = "agent:main:terminal-zero-hit";
2316+
hoisted.sessionStore[sessionKey] = { sessionId: "s-terminal-zero-hit", updatedAt: 0 };
2317+
runEmbeddedPiAgent.mockImplementationOnce(async (params: { sessionFile: string }) => {
2318+
await writeTranscriptJsonl(params.sessionFile, [
2319+
{
2320+
message: {
2321+
role: "toolResult",
2322+
toolName: "memory_search",
2323+
details: { results: [], debug: { backend: "qmd", hits: 0, searchMs: 8 } },
2324+
},
2325+
},
2326+
]);
2327+
await new Promise<never>(() => {});
2328+
});
2329+
2330+
const startedAt = Date.now();
2331+
const result = await hooks.before_prompt_build(
2332+
{ prompt: "what food do i usually order? zero hit", messages: [] },
2333+
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
2334+
);
2335+
const wallClockMs = Date.now() - startedAt;
2336+
2337+
expect(result).toBeUndefined();
2338+
expect(wallClockMs).toBeLessThan(CONFIGURED_TIMEOUT_MS);
2339+
expect(getActiveMemoryLines(sessionKey)).toEqual([
2340+
expect.stringContaining("🧩 Active Memory: status=empty"),
2341+
expect.stringContaining("🔎 Active Memory Debug: backend=qmd searchMs=8 hits=0"),
2342+
]);
2343+
});
2344+
2345+
it("does not fast-fail memory_search results solely because debug hits is zero", async () => {
2346+
__testing.setMinimumTimeoutMsForTests(1);
2347+
__testing.setSetupGraceTimeoutMsForTests(0);
2348+
api.pluginConfig = {
2349+
agents: ["main"],
2350+
timeoutMs: 500,
2351+
logging: true,
2352+
};
2353+
plugin.register(api as unknown as OpenClawPluginApi);
2354+
const sessionKey = "agent:main:terminal-zero-hit-with-results";
2355+
hoisted.sessionStore[sessionKey] = {
2356+
sessionId: "s-terminal-zero-hit-with-results",
2357+
updatedAt: 0,
2358+
};
2359+
runEmbeddedPiAgent.mockImplementationOnce(async (params: { sessionFile: string }) => {
2360+
await writeTranscriptJsonl(params.sessionFile, [
2361+
{
2362+
message: {
2363+
role: "toolResult",
2364+
toolName: "memory_search",
2365+
details: {
2366+
results: [{ path: "memory/food.md", text: "User usually orders ramen." }],
2367+
debug: { backend: "qmd", hits: 0, searchMs: 8 },
2368+
},
2369+
},
2370+
},
2371+
]);
2372+
await new Promise((resolve) => setTimeout(resolve, 50));
2373+
return { payloads: [{ text: "User usually orders ramen." }] };
2374+
});
2375+
2376+
const result = await hooks.before_prompt_build(
2377+
{ prompt: "what food do i usually order? zero hit with results", messages: [] },
2378+
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
2379+
);
2380+
2381+
expect(result?.prependContext).toContain("User usually orders ramen.");
2382+
expect(getActiveMemoryLines(sessionKey)).toEqual([
2383+
expect.stringContaining("🧩 Active Memory: status=ok"),
2384+
expect.stringContaining("🔎 Active Memory Debug: backend=qmd searchMs=8 hits=0"),
2385+
]);
2386+
});
2387+
2388+
it("fast-fails unavailable memory_search results without injecting provider errors", async () => {
2389+
const CONFIGURED_TIMEOUT_MS = 1_000;
2390+
__testing.setMinimumTimeoutMsForTests(1);
2391+
__testing.setSetupGraceTimeoutMsForTests(0);
2392+
api.pluginConfig = {
2393+
agents: ["main"],
2394+
timeoutMs: CONFIGURED_TIMEOUT_MS,
2395+
logging: true,
2396+
};
2397+
plugin.register(api as unknown as OpenClawPluginApi);
2398+
const sessionKey = "agent:main:terminal-unavailable";
2399+
hoisted.sessionStore[sessionKey] = { sessionId: "s-terminal-unavailable", updatedAt: 0 };
2400+
runEmbeddedPiAgent.mockImplementationOnce(async (params: { sessionFile: string }) => {
2401+
await writeTranscriptJsonl(params.sessionFile, [
2402+
{
2403+
message: {
2404+
role: "toolResult",
2405+
toolName: "memory_search",
2406+
details: {
2407+
disabled: true,
2408+
warning: "Memory search is unavailable due to an embedding/provider error.",
2409+
action: "Check the embedding provider configuration, then retry memory_search.",
2410+
error: "embedding request failed",
2411+
},
2412+
},
2413+
},
2414+
]);
2415+
await new Promise<never>(() => {});
2416+
});
2417+
2418+
const startedAt = Date.now();
2419+
const result = await hooks.before_prompt_build(
2420+
{ prompt: "what food do i usually order? unavailable", messages: [] },
2421+
{ agentId: "main", trigger: "user", sessionKey, messageProvider: "webchat" },
2422+
);
2423+
const wallClockMs = Date.now() - startedAt;
2424+
2425+
expect(result).toBeUndefined();
2426+
expect(wallClockMs).toBeLessThan(CONFIGURED_TIMEOUT_MS);
2427+
expect(getActiveMemoryLines(sessionKey)).toEqual([
2428+
expect.stringContaining("🧩 Active Memory: status=empty"),
2429+
expect.stringContaining(
2430+
"🔎 Active Memory Debug: Memory search is unavailable due to an embedding/provider error. Check the embedding provider configuration, then retry memory_search.",
2431+
),
2432+
]);
2433+
});
2434+
2435+
it("does not treat memory_get misses as terminal recall results", async () => {
2436+
__testing.setMinimumTimeoutMsForTests(1);
2437+
__testing.setSetupGraceTimeoutMsForTests(0);
2438+
api.pluginConfig = {
2439+
agents: ["main"],
2440+
timeoutMs: 500,
2441+
};
2442+
plugin.register(api as unknown as OpenClawPluginApi);
2443+
runEmbeddedPiAgent.mockImplementationOnce(async (params: { sessionFile: string }) => {
2444+
await writeTranscriptJsonl(params.sessionFile, [
2445+
{
2446+
message: {
2447+
role: "toolResult",
2448+
toolName: "memory_get",
2449+
details: { path: "memory/missing.md", text: "", disabled: true, error: "not found" },
2450+
},
2451+
},
2452+
]);
2453+
await new Promise((resolve) => setTimeout(resolve, 50));
2454+
return { payloads: [{ text: "User usually orders ramen after late flights." }] };
2455+
});
2456+
2457+
const result = await hooks.before_prompt_build(
2458+
{ prompt: "what food do i usually order? memory get miss", messages: [] },
2459+
{
2460+
agentId: "main",
2461+
trigger: "user",
2462+
sessionKey: "agent:main:memory-get-miss",
2463+
messageProvider: "webchat",
2464+
},
2465+
);
2466+
2467+
expect(result?.prependContext).toContain("User usually orders ramen after late flights.");
2468+
});
2469+
22612470
it("returns undefined instead of throwing when an unexpected error escapes prompt building", async () => {
22622471
const result = await hooks.before_prompt_build(
22632472
{ prompt: "what should i eat? escape test", messages: undefined as never },

0 commit comments

Comments
 (0)