From b0e8ac0f4d493a00ab9bb1a115264f7d9f104d79 Mon Sep 17 00:00:00 2001 From: Aldous Date: Wed, 8 Apr 2026 09:52:32 -0400 Subject: [PATCH] feat(openclaw): add configurable retain tags (#937) Co-authored-by: Aldous the Orchestrator --- hindsight-integrations/openclaw/README.md | 4 +- .../openclaw/openclaw.plugin.json | 20 ++++++++ hindsight-integrations/openclaw/src/client.ts | 1 + .../openclaw/src/index.test.ts | 39 ++++++++++++++++ hindsight-integrations/openclaw/src/index.ts | 46 ++++++++++++------- hindsight-integrations/openclaw/src/types.ts | 3 ++ 6 files changed, 95 insertions(+), 18 deletions(-) diff --git a/hindsight-integrations/openclaw/README.md b/hindsight-integrations/openclaw/README.md index b3121674..59a08a3e 100644 --- a/hindsight-integrations/openclaw/README.md +++ b/hindsight-integrations/openclaw/README.md @@ -28,7 +28,7 @@ That's it! The plugin will automatically start capturing and recalling memories. - **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 +- **Retention controls** — choose which message roles to retain, toggle auto-retain on/off, and stamp retained documents with consistent tags/source metadata ## Configuration @@ -48,6 +48,8 @@ Optional settings in `~/.openclaw/openclaw.json` under `plugins.entries.hindsigh | `dynamicBankId` | `true` | Enable per-context memory banks | | `bankId` | — | Static bank ID used when `dynamicBankId` is `false`. Can also be set with `HINDSIGHT_BANK_ID`. | | `bankIdPrefix` | — | Prefix for bank IDs (e.g. `"prod"`) | +| `retainTags` | `[]` | Tags applied to every retained document, useful for cross-agent/source labeling (e.g. `source_system:openclaw`, `agent:agentname`) | +| `retainSource` | `"openclaw"` | `source` value written into retained document metadata | | `dynamicBankGranularity` | `["agent", "channel", "user"]` | Fields used to derive bank ID. Options: `agent`, `channel`, `user`, `provider` | | `excludeProviders` | `[]` | Message providers to skip for recall/retain (e.g. `slack`, `telegram`, `discord`) | | `autoRecall` | `true` | Auto-inject memories before each turn. Set to `false` when the agent has its own recall tool. | diff --git a/hindsight-integrations/openclaw/openclaw.plugin.json b/hindsight-integrations/openclaw/openclaw.plugin.json index 5d0d705e..1e5259bc 100644 --- a/hindsight-integrations/openclaw/openclaw.plugin.json +++ b/hindsight-integrations/openclaw/openclaw.plugin.json @@ -76,6 +76,18 @@ "type": "string", "description": "Optional prefix for bank IDs (e.g., 'prod' results in 'prod-slack-U123'). Useful for separating environments." }, + "retainTags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags applied to every retained document (e.g. ['source_system:openclaw', 'agent:agentname'])." + }, + "retainSource": { + "type": "string", + "description": "Source value written into retained document metadata. Defaults to 'openclaw'.", + "default": "openclaw" + }, "autoRecall": { "type": "boolean", "description": "Automatically recall memories on every prompt and inject them as context. Set to false when agent has its own recall tool.", @@ -294,6 +306,14 @@ "label": "Bank ID Prefix", "placeholder": "e.g., prod, staging (optional)" }, + "retainTags": { + "label": "Retain Tags", + "placeholder": "e.g. ['source_system:openclaw', 'agent:agentname']" + }, + "retainSource": { + "label": "Retain Source", + "placeholder": "openclaw" + }, "autoRecall": { "label": "Auto-Recall", "placeholder": "true (inject memories on every prompt)" diff --git a/hindsight-integrations/openclaw/src/client.ts b/hindsight-integrations/openclaw/src/client.ts index 3da0bb21..c13a467f 100644 --- a/hindsight-integrations/openclaw/src/client.ts +++ b/hindsight-integrations/openclaw/src/client.ts @@ -142,6 +142,7 @@ export class HindsightClient { document_id: request.document_id || 'conversation', metadata: request.metadata, }], + document_tags: request.tags, async: true, }; diff --git a/hindsight-integrations/openclaw/src/index.test.ts b/hindsight-integrations/openclaw/src/index.test.ts index bc75cc19..3ff959c1 100644 --- a/hindsight-integrations/openclaw/src/index.test.ts +++ b/hindsight-integrations/openclaw/src/index.test.ts @@ -7,6 +7,7 @@ import { sliceLastTurnsByUserBoundary, composeRecallQuery, truncateRecallQuery, + buildRetainRequest, } from './index.js'; import type { PluginConfig, MemoryResult } from './types.js'; @@ -225,6 +226,44 @@ describe('formatMemories', () => { // prepareRetentionTranscript // --------------------------------------------------------------------------- +describe('buildRetainRequest', () => { + it('adds configured source metadata and retain tags', () => { + const request = buildRetainRequest('hello world', 2, { + sessionKey: 'agent:main:main', + messageProvider: 'discord', + channelId: 'channel:123', + senderId: 'user:456', + }, { + retainSource: 'openclaw', + retainTags: ['source_system:openclaw', 'agent:agentname'], + }, 1700000000000); + + expect(request).toEqual({ + content: 'hello world', + document_id: 'agent:main:main-1700000000000', + metadata: { + retained_at: expect.any(String), + message_count: '2', + source: 'openclaw', + channel_type: 'discord', + channel_id: 'channel:123', + sender_id: 'user:456', + }, + tags: ['source_system:openclaw', 'agent:agentname'], + }); + }); + + it('defaults source metadata to openclaw when unset', () => { + const request = buildRetainRequest('hello world', 1, {}, {}, 1700000000000); + expect(request.metadata?.source).toBe('openclaw'); + expect(request.tags).toBeUndefined(); + }); +}); + +// --------------------------------------------------------------------------- +// prepareRetentionTranscript +// --------------------------------------------------------------------------- + describe('prepareRetentionTranscript', () => { const baseConfig: PluginConfig = { dynamicBankId: true, diff --git a/hindsight-integrations/openclaw/src/index.ts b/hindsight-integrations/openclaw/src/index.ts index 63774ba2..8e8d3443 100644 --- a/hindsight-integrations/openclaw/src/index.ts +++ b/hindsight-integrations/openclaw/src/index.ts @@ -1,4 +1,4 @@ -import type { MoltbotPluginAPI, PluginConfig, PluginHookAgentContext, MemoryResult } from './types.js'; +import type { MoltbotPluginAPI, PluginConfig, PluginHookAgentContext, MemoryResult, RetainRequest } from './types.js'; import { HindsightEmbedManager } from './embed-manager.js'; import { HindsightClient, type HindsightClientOptions } from './client.js'; import { RetainQueue } from './retain-queue.js'; @@ -811,6 +811,8 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig { dynamicBankId: config.dynamicBankId !== false, bankId: envBankId || (typeof config.bankId === 'string' && config.bankId.trim().length > 0 ? config.bankId.trim() : undefined), bankIdPrefix: config.bankIdPrefix, + retainTags: Array.isArray(config.retainTags) ? config.retainTags.filter((tag): tag is string => typeof tag === 'string') : undefined, + retainSource: typeof config.retainSource === 'string' && config.retainSource.trim().length > 0 ? config.retainSource.trim() : undefined, excludeProviders: Array.isArray(config.excludeProviders) ? config.excludeProviders : [], autoRecall: config.autoRecall !== false, // Default: true (on) — backward compatible dynamicBankGranularity: Array.isArray(config.dynamicBankGranularity) ? config.dynamicBankGranularity : undefined, @@ -1430,28 +1432,16 @@ ${memoriesFormatted} } - // Use unique document ID per conversation (sessionKey + timestamp) - // Static sessionKey (e.g. "agent:main:main") causes CASCADE delete of old memories - const documentId = `${effectiveCtx?.sessionKey || 'session'}-${Date.now()}`; + const retainNow = Date.now(); + const retainRequest = buildRetainRequest(transcript, messageCount, effectiveCtx, pluginConfig, retainNow); // Retain to Hindsight - debug(`[Hindsight] Retaining to bank ${bankId}, document: ${documentId}, chars: ${transcript.length}\n---\n${transcript.substring(0, 500)}${transcript.length > 500 ? '\n...(truncated)' : ''}\n---`); - const retainRequest = { - content: transcript, - document_id: documentId, - metadata: { - retained_at: new Date().toISOString(), - message_count: String(messageCount), - channel_type: effectiveCtx?.messageProvider, - channel_id: effectiveCtx?.channelId, - sender_id: effectiveCtx?.senderId, - }, - }; + debug(`[Hindsight] Retaining to bank ${bankId}, document: ${retainRequest.document_id}, chars: ${transcript.length}\n---\n${transcript.substring(0, 500)}${transcript.length > 500 ? '\n...(truncated)' : ''}\n---`); try { await client.retain(retainRequest); log.trackRetain(bankId, messageCount); - debug(`[Hindsight] Retained ${messageCount} messages to bank ${bankId} for session ${documentId}`); + debug(`[Hindsight] Retained ${messageCount} messages to bank ${bankId} for session ${retainRequest.document_id}`); // After a successful retain, try flushing any queued items if (retainQueue && retainQueue.size() > 0) { @@ -1483,6 +1473,28 @@ ${memoriesFormatted} // Export client getter for tools +export function buildRetainRequest( + transcript: string, + messageCount: number, + effectiveCtx: PluginHookAgentContext | undefined, + pluginConfig: PluginConfig, + now = Date.now(), +): RetainRequest { + return { + content: transcript, + document_id: `${effectiveCtx?.sessionKey || 'session'}-${now}`, + metadata: { + retained_at: new Date(now).toISOString(), + message_count: String(messageCount), + source: pluginConfig.retainSource || 'openclaw', + channel_type: effectiveCtx?.messageProvider, + channel_id: effectiveCtx?.channelId, + sender_id: effectiveCtx?.senderId, + }, + tags: pluginConfig.retainTags && pluginConfig.retainTags.length > 0 ? pluginConfig.retainTags : undefined, + }; +} + export function prepareRetentionTranscript( messages: any[], pluginConfig: PluginConfig, diff --git a/hindsight-integrations/openclaw/src/types.ts b/hindsight-integrations/openclaw/src/types.ts index 8e6924c0..cf5214f2 100644 --- a/hindsight-integrations/openclaw/src/types.ts +++ b/hindsight-integrations/openclaw/src/types.ts @@ -64,6 +64,8 @@ export interface PluginConfig { dynamicBankId?: boolean; // Enable per-channel memory banks (default: true) bankId?: string; // Static bank ID used when dynamicBankId is false. Can also be set via HINDSIGHT_BANK_ID. bankIdPrefix?: string; // Prefix for bank IDs (e.g. 'prod' -> 'prod-slack-C123') + retainTags?: string[]; // Tags applied to all retained documents (e.g. ['source_system:openclaw', 'agent:agentname']) + retainSource?: string; // Source written into retained document metadata (default: 'openclaw') 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. dynamicBankGranularity?: Array<'agent' | 'provider' | 'channel' | 'user'>; // Fields for bank ID derivation. Default: ['agent', 'channel', 'user'] @@ -101,6 +103,7 @@ export interface RetainRequest { content: string; document_id?: string; metadata?: Record; + tags?: string[]; } export interface RetainResponse {