fix(openclaw): inject recalled memories as system context (#548)

Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>
This commit is contained in:
Stable Genius 2026-03-12 09:09:13 -07:00 committed by GitHub
parent e210953d05
commit b17f338e17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 28 additions and 19 deletions

View file

@ -26,7 +26,7 @@ That's it! The plugin will automatically start capturing and recalling memories.
## Features ## Features
- **Auto-capture** and **auto-recall** of memories each turn - **Auto-capture** and **auto-recall** of memories each turn, injected into system prompt space so recalled memories stay out of the visible chat transcript
- **Memory isolation** — configurable per agent, channel, user, or provider via `dynamicBankGranularity` - **Memory isolation** — configurable per agent, channel, user, or provider via `dynamicBankGranularity`
- **Retention controls** — choose which message roles to retain and toggle auto-retain on/off - **Retention controls** — choose which message roles to retain and toggle auto-retain on/off
@ -61,7 +61,7 @@ Optional settings in `~/.openclaw/openclaw.json` under `plugins.entries.hindsigh
| `recallTopK` | — | Max number of memories to inject per turn. Applied after API response as a hard cap. | | `recallTopK` | — | Max number of memories to inject per turn. Applied after API response as a hard cap. |
| `recallContextTurns` | `1` | Number of user turns to include when composing recall query context. `1` keeps latest-message-only behavior. | | `recallContextTurns` | `1` | Number of user turns to include when composing recall query context. `1` keeps latest-message-only behavior. |
| `recallMaxQueryChars` | `800` | Maximum character length for the composed recall query before calling recall. | | `recallMaxQueryChars` | `800` | Maximum character length for the composed recall query before calling recall. |
| `recallPromptPreamble` | built-in string | Prompt text placed above recalled memories in the injected `<hindsight_memories>` block. | | `recallPromptPreamble` | built-in string | Prompt text placed above recalled memories in the injected `<hindsight_memories>` system-context block. |
| `hindsightApiUrl` | — | External Hindsight API URL (skips local daemon) | | `hindsightApiUrl` | — | External Hindsight API URL (skips local daemon) |
| `hindsightApiToken` | — | Auth token for external API | | `hindsightApiToken` | — | Auth token for external API |

View file

@ -1143,8 +1143,9 @@ ${memoriesFormatted}
debug(`[Hindsight] Auto-recall: Injecting ${results.length} memories from bank ${bankId}`); debug(`[Hindsight] Auto-recall: Injecting ${results.length} memories from bank ${bankId}`);
// Inject context before the user message // Inject recalled memories into system prompt space so they stay hidden from
return { prependContext: contextMessage }; // the end-user transcript/UI while still being available to the model.
return { prependSystemContext: contextMessage };
} 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 ${RECALL_TIMEOUT_MS}ms, skipping memory injection`);

View file

@ -1,10 +1,15 @@
// Moltbot plugin API types (minimal subset needed for this plugin) // Moltbot plugin API types (minimal subset needed for this plugin)
export interface PluginPromptHookResult {
prependContext?: string;
prependSystemContext?: string;
}
export interface MoltbotPluginAPI { export interface MoltbotPluginAPI {
config: MoltbotConfig; config: MoltbotConfig;
registerService(config: ServiceConfig): void; registerService(config: ServiceConfig): void;
// OpenClaw hook handler signature: (event, ctx?) where ctx contains channel/sender info // OpenClaw hook handler signature: (event, ctx?) where ctx contains channel/sender info
on(event: string, handler: (event: any, ctx?: any) => void | Promise<void | { prependContext?: string }>): void; on(event: string, handler: (event: any, ctx?: any) => void | Promise<void | PluginPromptHookResult>): void;
// Add more as needed // Add more as needed
} }

View file

@ -223,7 +223,7 @@ describe('before_prompt_build hook', () => {
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
it('returns { prependContext } with <hindsight_memories> when recall returns results', async () => { it('returns { prependSystemContext } with <hindsight_memories> when recall returns results', async () => {
if (!apiReachable) return; if (!apiReachable) return;
recallSpy.mockResolvedValue({ recallSpy.mockResolvedValue({
results: [makeMemoryResult('User likes Python')], results: [makeMemoryResult('User likes Python')],
@ -236,15 +236,16 @@ describe('before_prompt_build hook', () => {
'before_prompt_build', 'before_prompt_build',
{ rawMessage: 'What programming language do I prefer?', prompt: '', messages: [] }, { rawMessage: 'What programming language do I prefer?', prompt: '', messages: [] },
{ messageProvider: 'telegram', senderId: 'U003' }, { messageProvider: 'telegram', senderId: 'U003' },
)) as { prependContext: string }; )) as { prependSystemContext: string; prependContext?: string };
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(result.prependContext).toContain('<hindsight_memories>'); expect(result.prependContext).toBeUndefined();
expect(result.prependContext).toContain('User likes Python'); expect(result.prependSystemContext).toContain('<hindsight_memories>');
expect(result.prependContext).toContain('</hindsight_memories>'); expect(result.prependSystemContext).toContain('User likes Python');
expect(result.prependSystemContext).toContain('</hindsight_memories>');
}); });
it('injects all memory result fields in the prependContext', async () => { it('injects all memory result fields in the prependSystemContext', async () => {
if (!apiReachable) return; if (!apiReachable) return;
const mem = makeMemoryResult('User prefers dark mode'); const mem = makeMemoryResult('User prefers dark mode');
mem.tags = ['preference']; mem.tags = ['preference'];
@ -260,12 +261,13 @@ describe('before_prompt_build hook', () => {
'before_prompt_build', 'before_prompt_build',
{ rawMessage: 'Do I prefer dark or light mode?', prompt: '', messages: [] }, { rawMessage: 'Do I prefer dark or light mode?', prompt: '', messages: [] },
{ messageProvider: 'telegram', senderId: 'U004' }, { messageProvider: 'telegram', senderId: 'U004' },
)) as { prependContext: string }; )) as { prependSystemContext: string; prependContext?: string };
// formatMemories returns a bullet list, not JSON // formatMemories returns a bullet list, not JSON
expect(result.prependContext).toContain('- User prefers dark mode'); expect(result.prependContext).toBeUndefined();
expect(result.prependContext).toContain('<hindsight_memories>'); expect(result.prependSystemContext).toContain('- User prefers dark mode');
expect(result.prependContext).toContain('</hindsight_memories>'); expect(result.prependSystemContext).toContain('<hindsight_memories>');
expect(result.prependSystemContext).toContain('</hindsight_memories>');
}); });
it('extracts the inner query from an envelope-formatted prompt when rawMessage is absent', async () => { it('extracts the inner query from an envelope-formatted prompt when rawMessage is absent', async () => {
@ -328,7 +330,7 @@ describe('before_prompt_build hook', () => {
expect(callArgs.max_tokens).toBeGreaterThan(0); expect(callArgs.max_tokens).toBeGreaterThan(0);
}); });
it('includes recalled memories in the prependContext block', async () => { it('includes recalled memories in the prependSystemContext block', async () => {
if (!apiReachable) return; if (!apiReachable) return;
recallSpy.mockResolvedValue({ recallSpy.mockResolvedValue({
results: [makeMemoryResult('User loves hiking')], results: [makeMemoryResult('User loves hiking')],
@ -341,10 +343,11 @@ describe('before_prompt_build hook', () => {
'before_prompt_build', 'before_prompt_build',
{ rawMessage: 'What outdoor activities do I enjoy?', prompt: '', messages: [] }, { rawMessage: 'What outdoor activities do I enjoy?', prompt: '', messages: [] },
{ messageProvider: 'telegram', senderId: 'U007' }, { messageProvider: 'telegram', senderId: 'U007' },
)) as { prependContext: string }; )) as { prependSystemContext: string; prependContext?: string };
expect(result.prependContext).toContain('User loves hiking'); expect(result.prependContext).toBeUndefined();
expect(result.prependContext).toContain('<hindsight_memories>'); expect(result.prependSystemContext).toContain('User loves hiking');
expect(result.prependSystemContext).toContain('<hindsight_memories>');
}); });
}); });