feat(openclaw): add configurable retain tags (#937)
Co-authored-by: Aldous the Orchestrator <Aldoustheorchestrator@users.noreply.github.com>
This commit is contained in:
parent
f74b577e02
commit
b0e8ac0f4d
6 changed files with 95 additions and 18 deletions
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ export class HindsightClient {
|
|||
document_id: request.document_id || 'conversation',
|
||||
metadata: request.metadata,
|
||||
}],
|
||||
document_tags: request.tags,
|
||||
async: true,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue