fix(openclaw): add recallTimeoutMs config option for auto-recall (#736)

The auto-recall timeout was hardcoded to 10s but recall with budget=high
can take 13s+. This adds a configurable recallTimeoutMs option (default:
10000ms) so users can increase the timeout when using higher recall budgets.

Also adds recallInjectionPosition to the plugin schema (it was already
implemented in code but missing from the JSON schema validation, causing
config rejection).

Co-authored-by: Marco Rutsch <marco@rutimka.de>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Rutimka 2026-03-28 17:59:23 +01:00 committed by GitHub
parent 7a3dbc1958
commit cd4d449f8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 27 additions and 4 deletions

View file

@ -188,6 +188,18 @@
"description": "Text shown above recalled memories in the injected context block.", "description": "Text shown above recalled memories in the injected context block.",
"default": "Relevant memories from past conversations (prioritize recent when conflicting). Only use memories that are directly useful to continue this conversation; ignore the rest:" "default": "Relevant memories from past conversations (prioritize recent when conflicting). Only use memories that are directly useful to continue this conversation; ignore the rest:"
}, },
"recallTimeoutMs": {
"type": "integer",
"minimum": 1000,
"description": "Timeout for auto-recall in milliseconds. Increase if recall times out with high budget.",
"default": 10000
},
"recallInjectionPosition": {
"type": "string",
"enum": ["prepend", "append", "user"],
"description": "Where to inject recalled memories. 'prepend' = start of system prompt (default), 'append' = end of system prompt (preserves prompt cache), 'user' = before user message.",
"default": "prepend"
},
"debug": { "debug": {
"type": "boolean", "type": "boolean",
"description": "Enable debug logging for Hindsight plugin operations.", "description": "Enable debug logging for Hindsight plugin operations.",
@ -309,6 +321,14 @@
"label": "Recall Prompt Preamble", "label": "Recall Prompt Preamble",
"placeholder": "Instruction shown above recalled memories in injected context" "placeholder": "Instruction shown above recalled memories in injected context"
}, },
"recallTimeoutMs": {
"label": "Recall Timeout (ms)",
"placeholder": "10000 (default)"
},
"recallInjectionPosition": {
"label": "Recall Injection Position",
"placeholder": "prepend, append, or user"
},
"debug": { "debug": {
"label": "Debug" "label": "Debug"
} }

View file

@ -33,7 +33,7 @@ import type { RecallResponse } from './types.js';
const inflightRecalls = new Map<string, Promise<RecallResponse>>(); const inflightRecalls = new Map<string, Promise<RecallResponse>>();
const turnCountBySession = new Map<string, number>(); const turnCountBySession = new Map<string, number>();
const MAX_TRACKED_SESSIONS = 10_000; const MAX_TRACKED_SESSIONS = 10_000;
const RECALL_TIMEOUT_MS = 10_000; const DEFAULT_RECALL_TIMEOUT_MS = 10_000;
// Cache sender IDs discovered in before_prompt_build (where event.prompt has the metadata // Cache sender IDs discovered in before_prompt_build (where event.prompt has the metadata
// blocks) so agent_end can look them up — event.messages in agent_end is clean history. // blocks) so agent_end can look them up — event.messages in agent_end is clean history.
@ -727,6 +727,7 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
? config.recallPromptPreamble ? config.recallPromptPreamble
: DEFAULT_RECALL_PROMPT_PREAMBLE, : DEFAULT_RECALL_PROMPT_PREAMBLE,
recallInjectionPosition: typeof config.recallInjectionPosition === 'string' && ['prepend', 'append', 'user'].includes(config.recallInjectionPosition) ? config.recallInjectionPosition as PluginConfig['recallInjectionPosition'] : undefined, recallInjectionPosition: typeof config.recallInjectionPosition === 'string' && ['prepend', 'append', 'user'].includes(config.recallInjectionPosition) ? config.recallInjectionPosition as PluginConfig['recallInjectionPosition'] : undefined,
recallTimeoutMs: typeof config.recallTimeoutMs === 'number' && config.recallTimeoutMs >= 1000 ? config.recallTimeoutMs : undefined,
debug: config.debug ?? false, debug: config.debug ?? false,
}; };
} }
@ -1112,7 +1113,8 @@ export default function (api: MoltbotPluginAPI) {
debug(`[Hindsight] Reusing in-flight recall for bank ${bankId}`); debug(`[Hindsight] Reusing in-flight recall for bank ${bankId}`);
recallPromise = existing; recallPromise = existing;
} else { } else {
recallPromise = client.recall({ query: prompt, max_tokens: pluginConfig.recallMaxTokens || 1024, budget: pluginConfig.recallBudget, types: pluginConfig.recallTypes }, RECALL_TIMEOUT_MS); const recallTimeoutMs = pluginConfig.recallTimeoutMs ?? DEFAULT_RECALL_TIMEOUT_MS;
recallPromise = client.recall({ query: prompt, max_tokens: pluginConfig.recallMaxTokens || 1024, budget: pluginConfig.recallBudget, types: pluginConfig.recallTypes }, recallTimeoutMs);
inflightRecalls.set(recallKey, recallPromise); inflightRecalls.set(recallKey, recallPromise);
void recallPromise.catch(() => {}).finally(() => inflightRecalls.delete(recallKey)); void recallPromise.catch(() => {}).finally(() => inflightRecalls.delete(recallKey));
} }
@ -1156,9 +1158,9 @@ ${memoriesFormatted}
} }
} catch (error) { } catch (error) {
if (error instanceof DOMException && error.name === 'TimeoutError') { if (error instanceof DOMException && error.name === 'TimeoutError') {
console.warn(`[Hindsight] Auto-recall timed out after ${RECALL_TIMEOUT_MS}ms, skipping memory injection`); console.warn(`[Hindsight] Auto-recall timed out after ${pluginConfig.recallTimeoutMs ?? DEFAULT_RECALL_TIMEOUT_MS}ms, skipping memory injection`);
} else if (error instanceof Error && error.name === 'AbortError') { } else if (error instanceof Error && error.name === 'AbortError') {
console.warn(`[Hindsight] Auto-recall aborted after ${RECALL_TIMEOUT_MS}ms, skipping memory injection`); console.warn(`[Hindsight] Auto-recall aborted after ${pluginConfig.recallTimeoutMs ?? DEFAULT_RECALL_TIMEOUT_MS}ms, skipping memory injection`);
} else { } else {
console.error('[Hindsight] Auto-recall error:', error); console.error('[Hindsight] Auto-recall error:', error);
} }

View file

@ -70,6 +70,7 @@ export interface PluginConfig {
retainOverlapTurns?: number; // Extra prior turns included when chunked retention fires (default: 0). Window = retainEveryNTurns + retainOverlapTurns. retainOverlapTurns?: number; // Extra prior turns included when chunked retention fires (default: 0). Window = retainEveryNTurns + retainOverlapTurns.
recallTopK?: number; // Max number of memories to inject. Default: unlimited recallTopK?: number; // Max number of memories to inject. Default: unlimited
recallContextTurns?: number; // Number of user turns to include in recall query context. Default: 1 (latest only) recallContextTurns?: number; // Number of user turns to include in recall query context. Default: 1 (latest only)
recallTimeoutMs?: number; // Timeout for auto-recall in milliseconds. Default: 10000
recallMaxQueryChars?: number; // Max chars for composed recall query. Default: 800 recallMaxQueryChars?: number; // Max chars for composed recall query. Default: 800
recallPromptPreamble?: string; // Prompt preamble placed above recalled memories. Default: built-in guidance text. recallPromptPreamble?: string; // Prompt preamble placed above recalled memories. Default: built-in guidance text.
recallInjectionPosition?: 'prepend' | 'append' | 'user'; // Where to inject recalled memories. 'prepend' = start of system prompt (default), 'append' = end of system prompt (preserves prompt cache), 'user' = before user message. recallInjectionPosition?: 'prepend' | 'append' | 'user'; // Where to inject recalled memories. 'prepend' = start of system prompt (default), 'append' = end of system prompt (preserves prompt cache), 'user' = before user message.