Skip to content

Commit ae517be

Browse files
committed
fix(memory): attribute corpus=all timeouts to the slow branch instead of provider
memory_search with corpus=all could time out (15s tool deadline) while the embedding provider probe and each individual corpus searched successfully. The deadline error was funneled through buildMemorySearchUnavailableResult, which unconditionally blamed an embedding/provider error and told users to check provider configuration -- misleading when the real problem is a slow aggregation branch. Detect the deadline timeout message and emit an accurate warning that names the slow branch (memory/session vs wiki/supplement) using the phase the tool already tracks, plus debug.timedOut/phase fields. Quota and genuine provider errors keep their existing messages. Closes #92633
1 parent f65aca6 commit ae517be

5 files changed

Lines changed: 71 additions & 19 deletions

File tree

extensions/memory-core/src/tools.citations.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -526,8 +526,11 @@ describe("memory tools", () => {
526526
const stalledAllResult = await stalledAllResultPromise;
527527
expectUnavailableMemorySearchDetails(stalledAllResult.details, {
528528
error: "memory_search timed out after 15s",
529-
warning: "Memory search is unavailable due to an embedding/provider error.",
530-
action: "Check embedding provider configuration and retry memory_search.",
529+
warning: "Memory search timed out before completing (slow branch: wiki/supplement).",
530+
action:
531+
"Retry memory_search; if timeouts persist, narrow the corpus (e.g. corpus=memory) or check embedding/provider latency.",
532+
timedOut: true,
533+
phase: "supplement",
531534
});
532535

533536
const memoryResult = await tool.execute("call_memory_after_stalled_wiki", {
@@ -574,8 +577,11 @@ describe("memory tools", () => {
574577
const stalledAllResult = await stalledAllResultPromise;
575578
expectUnavailableMemorySearchDetails(stalledAllResult.details, {
576579
error: "memory_search timed out after 15s",
577-
warning: "Memory search is unavailable due to an embedding/provider error.",
578-
action: "Check embedding provider configuration and retry memory_search.",
580+
warning: "Memory search timed out before completing (slow branch: memory/session).",
581+
action:
582+
"Retry memory_search; if timeouts persist, narrow the corpus (e.g. corpus=memory) or check embedding/provider latency.",
583+
timedOut: true,
584+
phase: "memory",
579585
});
580586

581587
const wikiOnlyResult = await tool.execute("call_all_after_stalled_memory", {

extensions/memory-core/src/tools.shared.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,28 +110,52 @@ export function buildMemorySearchUnavailableResult(
110110
overrides?: {
111111
warning?: string;
112112
action?: string;
113+
/**
114+
* The corpus branch that was active when the failure occurred. When the
115+
* failure was a deadline timeout this lets the warning name the slow branch
116+
* instead of blaming the embedding provider for an aggregation stall.
117+
*/
118+
phase?: "memory" | "supplement";
113119
},
114120
) {
115121
const reason = (error ?? "memory search unavailable").trim() || "memory search unavailable";
116122
const normalizedReason = normalizeLowercaseStringOrEmpty(reason);
117123
const isQuotaError = /insufficient_quota|quota|429/.test(normalizedReason);
124+
// The tool-level deadline wrapper reports "... timed out after Ns". A timeout
125+
// is not necessarily an embedding/provider fault: the provider probe and each
126+
// individual corpus can be healthy while corpus=all aggregation stalls. Report
127+
// it as a timeout instead of pointing users at provider configuration.
128+
const isTimeoutError = !isQuotaError && normalizedReason.includes("timed out after");
118129
const isMissingNodeSqlite = /missing node:sqlite|no such built-?in module: node:sqlite/.test(
119130
normalizedReason,
120131
);
132+
const phaseLabel =
133+
overrides?.phase === "memory"
134+
? "memory/session"
135+
: overrides?.phase === "supplement"
136+
? "wiki/supplement"
137+
: undefined;
138+
const timeoutWarning = phaseLabel
139+
? `Memory search timed out before completing (slow branch: ${phaseLabel}).`
140+
: "Memory search timed out before completing.";
121141
const warning =
122142
overrides?.warning ??
123143
(isQuotaError
124144
? "Memory search is unavailable because the embedding provider quota is exhausted."
125-
: isMissingNodeSqlite
126-
? "Memory search is unavailable because this OpenClaw Node runtime does not provide SQLite support."
127-
: "Memory search is unavailable due to an embedding/provider error.");
145+
: isTimeoutError
146+
? timeoutWarning
147+
: isMissingNodeSqlite
148+
? "Memory search is unavailable because this OpenClaw Node runtime does not provide SQLite support."
149+
: "Memory search is unavailable due to an embedding/provider error.");
128150
const action =
129151
overrides?.action ??
130152
(isQuotaError
131153
? "Top up or switch embedding provider, then retry memory_search."
132-
: isMissingNodeSqlite
133-
? "Run OpenClaw with a Node runtime that includes node:sqlite, then retry memory_search."
134-
: "Check embedding provider configuration and retry memory_search.");
154+
: isTimeoutError
155+
? "Retry memory_search; if timeouts persist, narrow the corpus (e.g. corpus=memory) or check embedding/provider latency."
156+
: isMissingNodeSqlite
157+
? "Run OpenClaw with a Node runtime that includes node:sqlite, then retry memory_search."
158+
: "Check embedding provider configuration and retry memory_search.");
135159
return {
136160
results: [],
137161
disabled: true,
@@ -143,6 +167,8 @@ export function buildMemorySearchUnavailableResult(
143167
warning,
144168
action,
145169
error: reason,
170+
...(isTimeoutError ? { timedOut: true } : {}),
171+
...(isTimeoutError && overrides?.phase ? { phase: overrides.phase } : {}),
146172
},
147173
};
148174
}

extensions/memory-core/src/tools.test-helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export function expectUnavailableMemorySearchDetails(
5555
error: string;
5656
warning: string;
5757
action: string;
58+
timedOut?: boolean;
59+
phase?: "memory" | "supplement";
5860
},
5961
) {
6062
expect(details).toEqual({
@@ -68,6 +70,8 @@ export function expectUnavailableMemorySearchDetails(
6870
warning: params.warning,
6971
action: params.action,
7072
error: params.error,
73+
...(params.timedOut ? { timedOut: true } : {}),
74+
...(params.phase ? { phase: params.phase } : {}),
7175
},
7276
});
7377
}

extensions/memory-core/src/tools.test.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,11 @@ describe("memory_search unavailable payloads", () => {
181181
const result = await resultPromise;
182182
expectUnavailableMemorySearchDetails(result.details, {
183183
error: "memory_search timed out after 15s",
184-
warning: "Memory search is unavailable due to an embedding/provider error.",
185-
action: "Check embedding provider configuration and retry memory_search.",
184+
warning: "Memory search timed out before completing (slow branch: memory/session).",
185+
action:
186+
"Retry memory_search; if timeouts persist, narrow the corpus (e.g. corpus=memory) or check embedding/provider latency.",
187+
timedOut: true,
188+
phase: "memory",
186189
});
187190
} finally {
188191
vi.useRealTimers();
@@ -207,16 +210,21 @@ describe("memory_search unavailable payloads", () => {
207210
const result = await resultPromise;
208211
expectUnavailableMemorySearchDetails(result.details, {
209212
error: "memory_search timed out after 15s",
210-
warning: "Memory search is unavailable due to an embedding/provider error.",
211-
action: "Check embedding provider configuration and retry memory_search.",
213+
warning: "Memory search timed out before completing (slow branch: memory/session).",
214+
action:
215+
"Retry memory_search; if timeouts persist, narrow the corpus (e.g. corpus=memory) or check embedding/provider latency.",
216+
timedOut: true,
217+
phase: "memory",
212218
});
213219
// The deadline must abort the orphaned search, not just race past it.
214220
expect(searchSignal?.aborted).toBe(true);
215221
const cooldownResult = await tool.execute("search-cooldown", { query: "hello again" });
216222
expectUnavailableMemorySearchDetails(cooldownResult.details, {
217223
error: "memory_search timed out after 15s",
218-
warning: "Memory search is unavailable due to an embedding/provider error.",
219-
action: "Check embedding provider configuration and retry memory_search.",
224+
warning: "Memory search timed out before completing.",
225+
action:
226+
"Retry memory_search; if timeouts persist, narrow the corpus (e.g. corpus=memory) or check embedding/provider latency.",
227+
timedOut: true,
220228
});
221229
expect(searchCalls).toBe(1);
222230
} finally {
@@ -245,8 +253,11 @@ describe("memory_search unavailable payloads", () => {
245253
const result = await resultPromise;
246254
expectUnavailableMemorySearchDetails(result.details, {
247255
error: "memory_search timed out after 15s",
248-
warning: "Memory search is unavailable due to an embedding/provider error.",
249-
action: "Check embedding provider configuration and retry memory_search.",
256+
warning: "Memory search timed out before completing (slow branch: memory/session).",
257+
action:
258+
"Retry memory_search; if timeouts persist, narrow the corpus (e.g. corpus=memory) or check embedding/provider latency.",
259+
timedOut: true,
260+
phase: "memory",
250261
});
251262
} finally {
252263
vi.useRealTimers();

extensions/memory-core/src/tools.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,12 @@ export function createMemorySearchTool(options: {
642642
if (shouldRecordCooldown) {
643643
recordMemorySearchToolCooldown(cooldownKey, outcome.error);
644644
}
645-
return jsonResult(buildMemorySearchUnavailableResult(outcome.error));
645+
return jsonResult(
646+
buildMemorySearchUnavailableResult(
647+
outcome.error,
648+
unavailablePhase ? { phase: unavailablePhase } : undefined,
649+
),
650+
);
646651
}
647652
return outcome.value;
648653
},

0 commit comments

Comments
 (0)