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
|
- **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`
|
- **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
|
## Configuration
|
||||||
|
|
||||||
|
|
@ -48,6 +48,8 @@ Optional settings in `~/.openclaw/openclaw.json` under `plugins.entries.hindsigh
|
||||||
| `dynamicBankId` | `true` | Enable per-context memory banks |
|
| `dynamicBankId` | `true` | Enable per-context memory banks |
|
||||||
| `bankId` | — | Static bank ID used when `dynamicBankId` is `false`. Can also be set with `HINDSIGHT_BANK_ID`. |
|
| `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"`) |
|
| `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` |
|
| `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`) |
|
| `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. |
|
| `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",
|
"type": "string",
|
||||||
"description": "Optional prefix for bank IDs (e.g., 'prod' results in 'prod-slack-U123'). Useful for separating environments."
|
"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": {
|
"autoRecall": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Automatically recall memories on every prompt and inject them as context. Set to false when agent has its own recall tool.",
|
"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",
|
"label": "Bank ID Prefix",
|
||||||
"placeholder": "e.g., prod, staging (optional)"
|
"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": {
|
"autoRecall": {
|
||||||
"label": "Auto-Recall",
|
"label": "Auto-Recall",
|
||||||
"placeholder": "true (inject memories on every prompt)"
|
"placeholder": "true (inject memories on every prompt)"
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@ export class HindsightClient {
|
||||||
document_id: request.document_id || 'conversation',
|
document_id: request.document_id || 'conversation',
|
||||||
metadata: request.metadata,
|
metadata: request.metadata,
|
||||||
}],
|
}],
|
||||||
|
document_tags: request.tags,
|
||||||
async: true,
|
async: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
sliceLastTurnsByUserBoundary,
|
sliceLastTurnsByUserBoundary,
|
||||||
composeRecallQuery,
|
composeRecallQuery,
|
||||||
truncateRecallQuery,
|
truncateRecallQuery,
|
||||||
|
buildRetainRequest,
|
||||||
} from './index.js';
|
} from './index.js';
|
||||||
import type { PluginConfig, MemoryResult } from './types.js';
|
import type { PluginConfig, MemoryResult } from './types.js';
|
||||||
|
|
||||||
|
|
@ -225,6 +226,44 @@ describe('formatMemories', () => {
|
||||||
// prepareRetentionTranscript
|
// 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', () => {
|
describe('prepareRetentionTranscript', () => {
|
||||||
const baseConfig: PluginConfig = {
|
const baseConfig: PluginConfig = {
|
||||||
dynamicBankId: true,
|
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 { HindsightEmbedManager } from './embed-manager.js';
|
||||||
import { HindsightClient, type HindsightClientOptions } from './client.js';
|
import { HindsightClient, type HindsightClientOptions } from './client.js';
|
||||||
import { RetainQueue } from './retain-queue.js';
|
import { RetainQueue } from './retain-queue.js';
|
||||||
|
|
@ -811,6 +811,8 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
|
||||||
dynamicBankId: config.dynamicBankId !== false,
|
dynamicBankId: config.dynamicBankId !== false,
|
||||||
bankId: envBankId || (typeof config.bankId === 'string' && config.bankId.trim().length > 0 ? config.bankId.trim() : undefined),
|
bankId: envBankId || (typeof config.bankId === 'string' && config.bankId.trim().length > 0 ? config.bankId.trim() : undefined),
|
||||||
bankIdPrefix: config.bankIdPrefix,
|
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 : [],
|
excludeProviders: Array.isArray(config.excludeProviders) ? config.excludeProviders : [],
|
||||||
autoRecall: config.autoRecall !== false, // Default: true (on) — backward compatible
|
autoRecall: config.autoRecall !== false, // Default: true (on) — backward compatible
|
||||||
dynamicBankGranularity: Array.isArray(config.dynamicBankGranularity) ? config.dynamicBankGranularity : undefined,
|
dynamicBankGranularity: Array.isArray(config.dynamicBankGranularity) ? config.dynamicBankGranularity : undefined,
|
||||||
|
|
@ -1430,28 +1432,16 @@ ${memoriesFormatted}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Use unique document ID per conversation (sessionKey + timestamp)
|
const retainNow = Date.now();
|
||||||
// Static sessionKey (e.g. "agent:main:main") causes CASCADE delete of old memories
|
const retainRequest = buildRetainRequest(transcript, messageCount, effectiveCtx, pluginConfig, retainNow);
|
||||||
const documentId = `${effectiveCtx?.sessionKey || 'session'}-${Date.now()}`;
|
|
||||||
|
|
||||||
// Retain to Hindsight
|
// 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---`);
|
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---`);
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.retain(retainRequest);
|
await client.retain(retainRequest);
|
||||||
log.trackRetain(bankId, messageCount);
|
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
|
// After a successful retain, try flushing any queued items
|
||||||
if (retainQueue && retainQueue.size() > 0) {
|
if (retainQueue && retainQueue.size() > 0) {
|
||||||
|
|
@ -1483,6 +1473,28 @@ ${memoriesFormatted}
|
||||||
|
|
||||||
// Export client getter for tools
|
// 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(
|
export function prepareRetentionTranscript(
|
||||||
messages: any[],
|
messages: any[],
|
||||||
pluginConfig: PluginConfig,
|
pluginConfig: PluginConfig,
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ export interface PluginConfig {
|
||||||
dynamicBankId?: boolean; // Enable per-channel memory banks (default: true)
|
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.
|
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')
|
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'])
|
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.
|
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']
|
dynamicBankGranularity?: Array<'agent' | 'provider' | 'channel' | 'user'>; // Fields for bank ID derivation. Default: ['agent', 'channel', 'user']
|
||||||
|
|
@ -101,6 +103,7 @@ export interface RetainRequest {
|
||||||
content: string;
|
content: string;
|
||||||
document_id?: string;
|
document_id?: string;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
|
tags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RetainResponse {
|
export interface RetainResponse {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue