From ad1660b313a56510e8c9dce65e528bab77203b7c Mon Sep 17 00:00:00 2001 From: Fabio Scarsi <103823600+fabioscarsi@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:44:52 +0100 Subject: [PATCH] feat(openclaw): retain last n+2 turns every n turns (default n=10) (#452) --- hindsight-integrations/openclaw/src/index.ts | 28 +++++++++++++++++--- hindsight-integrations/openclaw/src/types.ts | 1 + 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/hindsight-integrations/openclaw/src/index.ts b/hindsight-integrations/openclaw/src/index.ts index c8209919..08db1404 100644 --- a/hindsight-integrations/openclaw/src/index.ts +++ b/hindsight-integrations/openclaw/src/index.ts @@ -20,6 +20,7 @@ const banksWithMissionSet = new Set(); // In-flight recall deduplication: concurrent recalls for the same bank reuse one promise import type { RecallResponse } from './types.js'; const inflightRecalls = new Map>(); +const turnCountBySession = new Map(); const RECALL_TIMEOUT_MS = 10_000; // Cooldown + guard to prevent concurrent reinit attempts @@ -857,8 +858,29 @@ User message: ${prompt} return; } + // --- Chunked retention: only retain every Nth turn --- + const retainEveryN = pluginConfig.retainEveryNTurns ?? 10; + let messagesToRetain = event.messages; + + if (retainEveryN > 1) { + const sessionTrackingKey = `${bankId}:${effectiveCtx?.sessionKey || currentSessionKey || 'session'}`; + const turnCount = (turnCountBySession.get(sessionTrackingKey) || 0) + 1; + turnCountBySession.set(sessionTrackingKey, turnCount); + + if (turnCount % retainEveryN !== 0) { + const nextRetain = Math.ceil(turnCount / retainEveryN) * retainEveryN; + console.log(`[Hindsight Hook] Skipping retain (turn ${turnCount}, next at ${nextRetain})`); + return; + } + + // Sliding window: N turns of new content + 2-turn overlap for context continuity + const windowSize = retainEveryN * 2 + 4; + messagesToRetain = event.messages.slice(-windowSize); + console.log(`[Hindsight Hook] Chunked retain at turn ${turnCount} \u2014 last ${messagesToRetain.length} msgs`); + } + // Format messages into a transcript - const transcript = event.messages + const transcript = messagesToRetain .map((msg: any) => { const role = msg.role || 'unknown'; let content = ''; @@ -895,14 +917,14 @@ User message: ${prompt} document_id: documentId, metadata: { retained_at: new Date().toISOString(), - message_count: String(event.messages.length), + message_count: String(messagesToRetain.length), channel_type: effectiveCtx?.messageProvider, channel_id: effectiveCtx?.channelId, sender_id: effectiveCtx?.senderId, }, }); - console.log(`[Hindsight] Retained ${event.messages.length} messages to bank ${bankId} for session ${documentId}`); + console.log(`[Hindsight] Retained ${messagesToRetain.length} messages to bank ${bankId} for session ${documentId}`); } catch (error) { console.error('[Hindsight] Error retaining messages:', error); } diff --git a/hindsight-integrations/openclaw/src/types.ts b/hindsight-integrations/openclaw/src/types.ts index 3c9f43ea..aa9a6b17 100644 --- a/hindsight-integrations/openclaw/src/types.ts +++ b/hindsight-integrations/openclaw/src/types.ts @@ -44,6 +44,7 @@ export interface PluginConfig { bankIdPrefix?: string; // Prefix for bank IDs (e.g. 'prod' -> 'prod-slack-C123') excludeProviders?: string[]; // Message providers to exclude from recall/retain (e.g. ['telegram', 'discord']) autoRecall?: boolean; // Auto-recall memories on every prompt (default: true). Set to false when agent has its own recall tool. + retainEveryNTurns?: number; // Retain every Nth turn instead of every turn (default: 10). Reduces O(n²) storage growth for long sessions. } export interface ServiceConfig {