From b17f338e17279750a91aa8728866c85faa8a0137 Mon Sep 17 00:00:00 2001 From: Stable Genius Date: Thu, 12 Mar 2026 09:09:13 -0700 Subject: [PATCH] fix(openclaw): inject recalled memories as system context (#548) Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com> --- hindsight-integrations/openclaw/README.md | 4 +-- hindsight-integrations/openclaw/src/index.ts | 5 +-- hindsight-integrations/openclaw/src/types.ts | 7 ++++- .../openclaw/tests/hooks.integration.test.ts | 31 ++++++++++--------- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/hindsight-integrations/openclaw/README.md b/hindsight-integrations/openclaw/README.md index a304f00f..dc9e59c4 100644 --- a/hindsight-integrations/openclaw/README.md +++ b/hindsight-integrations/openclaw/README.md @@ -26,7 +26,7 @@ That's it! The plugin will automatically start capturing and recalling memories. ## 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` - **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. | | `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. | -| `recallPromptPreamble` | built-in string | Prompt text placed above recalled memories in the injected `` block. | +| `recallPromptPreamble` | built-in string | Prompt text placed above recalled memories in the injected `` system-context block. | | `hindsightApiUrl` | — | External Hindsight API URL (skips local daemon) | | `hindsightApiToken` | — | Auth token for external API | diff --git a/hindsight-integrations/openclaw/src/index.ts b/hindsight-integrations/openclaw/src/index.ts index 4db9db0b..96d112fd 100644 --- a/hindsight-integrations/openclaw/src/index.ts +++ b/hindsight-integrations/openclaw/src/index.ts @@ -1143,8 +1143,9 @@ ${memoriesFormatted} debug(`[Hindsight] Auto-recall: Injecting ${results.length} memories from bank ${bankId}`); - // Inject context before the user message - return { prependContext: contextMessage }; + // Inject recalled memories into system prompt space so they stay hidden from + // the end-user transcript/UI while still being available to the model. + return { prependSystemContext: contextMessage }; } catch (error) { if (error instanceof DOMException && error.name === 'TimeoutError') { console.warn(`[Hindsight] Auto-recall timed out after ${RECALL_TIMEOUT_MS}ms, skipping memory injection`); diff --git a/hindsight-integrations/openclaw/src/types.ts b/hindsight-integrations/openclaw/src/types.ts index 4ffff9e8..5ce8f7b3 100644 --- a/hindsight-integrations/openclaw/src/types.ts +++ b/hindsight-integrations/openclaw/src/types.ts @@ -1,10 +1,15 @@ // Moltbot plugin API types (minimal subset needed for this plugin) +export interface PluginPromptHookResult { + prependContext?: string; + prependSystemContext?: string; +} + export interface MoltbotPluginAPI { config: MoltbotConfig; registerService(config: ServiceConfig): void; // OpenClaw hook handler signature: (event, ctx?) where ctx contains channel/sender info - on(event: string, handler: (event: any, ctx?: any) => void | Promise): void; + on(event: string, handler: (event: any, ctx?: any) => void | Promise): void; // Add more as needed } diff --git a/hindsight-integrations/openclaw/tests/hooks.integration.test.ts b/hindsight-integrations/openclaw/tests/hooks.integration.test.ts index 734353fa..67ee5d42 100644 --- a/hindsight-integrations/openclaw/tests/hooks.integration.test.ts +++ b/hindsight-integrations/openclaw/tests/hooks.integration.test.ts @@ -223,7 +223,7 @@ describe('before_prompt_build hook', () => { expect(result).toBeUndefined(); }); - it('returns { prependContext } with when recall returns results', async () => { + it('returns { prependSystemContext } with when recall returns results', async () => { if (!apiReachable) return; recallSpy.mockResolvedValue({ results: [makeMemoryResult('User likes Python')], @@ -236,15 +236,16 @@ describe('before_prompt_build hook', () => { 'before_prompt_build', { rawMessage: 'What programming language do I prefer?', prompt: '', messages: [] }, { messageProvider: 'telegram', senderId: 'U003' }, - )) as { prependContext: string }; + )) as { prependSystemContext: string; prependContext?: string }; expect(result).toBeDefined(); - expect(result.prependContext).toContain(''); - expect(result.prependContext).toContain('User likes Python'); - expect(result.prependContext).toContain(''); + expect(result.prependContext).toBeUndefined(); + expect(result.prependSystemContext).toContain(''); + expect(result.prependSystemContext).toContain('User likes Python'); + expect(result.prependSystemContext).toContain(''); }); - it('injects all memory result fields in the prependContext', async () => { + it('injects all memory result fields in the prependSystemContext', async () => { if (!apiReachable) return; const mem = makeMemoryResult('User prefers dark mode'); mem.tags = ['preference']; @@ -260,12 +261,13 @@ describe('before_prompt_build hook', () => { 'before_prompt_build', { rawMessage: 'Do I prefer dark or light mode?', prompt: '', messages: [] }, { messageProvider: 'telegram', senderId: 'U004' }, - )) as { prependContext: string }; + )) as { prependSystemContext: string; prependContext?: string }; // formatMemories returns a bullet list, not JSON - expect(result.prependContext).toContain('- User prefers dark mode'); - expect(result.prependContext).toContain(''); - expect(result.prependContext).toContain(''); + expect(result.prependContext).toBeUndefined(); + expect(result.prependSystemContext).toContain('- User prefers dark mode'); + expect(result.prependSystemContext).toContain(''); + expect(result.prependSystemContext).toContain(''); }); 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); }); - it('includes recalled memories in the prependContext block', async () => { + it('includes recalled memories in the prependSystemContext block', async () => { if (!apiReachable) return; recallSpy.mockResolvedValue({ results: [makeMemoryResult('User loves hiking')], @@ -341,10 +343,11 @@ describe('before_prompt_build hook', () => { 'before_prompt_build', { rawMessage: 'What outdoor activities do I enjoy?', prompt: '', messages: [] }, { messageProvider: 'telegram', senderId: 'U007' }, - )) as { prependContext: string }; + )) as { prependSystemContext: string; prependContext?: string }; - expect(result.prependContext).toContain('User loves hiking'); - expect(result.prependContext).toContain(''); + expect(result.prependContext).toBeUndefined(); + expect(result.prependSystemContext).toContain('User loves hiking'); + expect(result.prependSystemContext).toContain(''); }); });