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
- **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 `<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) |
| `hindsightApiToken` | — | Auth token for external API |

View file

@ -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`);

View file

@ -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 | { prependContext?: string }>): void;
on(event: string, handler: (event: any, ctx?: any) => void | Promise<void | PluginPromptHookResult>): void;
// Add more as needed
}

View file

@ -223,7 +223,7 @@ describe('before_prompt_build hook', () => {
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;
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('<hindsight_memories>');
expect(result.prependContext).toContain('User likes Python');
expect(result.prependContext).toContain('</hindsight_memories>');
expect(result.prependContext).toBeUndefined();
expect(result.prependSystemContext).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;
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('<hindsight_memories>');
expect(result.prependContext).toContain('</hindsight_memories>');
expect(result.prependContext).toBeUndefined();
expect(result.prependSystemContext).toContain('- User prefers dark mode');
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 () => {
@ -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('<hindsight_memories>');
expect(result.prependContext).toBeUndefined();
expect(result.prependSystemContext).toContain('User loves hiking');
expect(result.prependSystemContext).toContain('<hindsight_memories>');
});
});