feat(openclaw): add configurable retain tags (#937)

Co-authored-by: Aldous the Orchestrator <Aldoustheorchestrator@users.noreply.github.com>
This commit is contained in:
Aldous 2026-04-08 09:52:32 -04:00 committed by GitHub
parent f74b577e02
commit b0e8ac0f4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 95 additions and 18 deletions

View file

@ -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. |

View file

@ -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)"

View file

@ -142,6 +142,7 @@ export class HindsightClient {
document_id: request.document_id || 'conversation',
metadata: request.metadata,
}],
document_tags: request.tags,
async: true,
};

View file

@ -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,

View file

@ -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,

View file

@ -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<string, unknown>;
tags?: string[];
}
export interface RetainResponse {