feat(openclaw): support bankId for static banks (#910)

* feat(openclaw): support exact static bank ids

* test(openclaw): use generic static bank id example

* feat(openclaw): support bankId static bank configuration

---------

Co-authored-by: Aldous the Orchestrator <Aldoustheorchestrator@users.noreply.github.com>
This commit is contained in:
Aldous 2026-04-07 11:13:20 -04:00 committed by GitHub
parent 8a2388a48f
commit 0e81d1a25e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 63 additions and 13 deletions

View file

@ -96,7 +96,8 @@ Optional settings in `~/.openclaw/openclaw.json`:
- `embedVersion` - hindsight-embed version (default: `"latest"`)
- `bankMission` - Agent identity/purpose stored on the memory bank. Helps the memory engine understand context for better fact extraction during retain. Set once per bank on first use — not a recall prompt.
- `dynamicBankId` - Enable per-context memory banks (default: `true`)
- `bankIdPrefix` - Optional prefix for bank IDs (e.g. `"prod"``"prod-slack-C123"`)
- `bankId` - Static bank ID used when `dynamicBankId` is `false`. Can also be set with `HINDSIGHT_BANK_ID`.
- `bankIdPrefix` - Optional prefix for bank IDs (e.g. `"prod"``"prod-slack-C123"` or `"prod-shared-bank"`)
- `dynamicBankGranularity` - Fields used to derive bank ID: `agent`, `channel`, `user`, `provider` (default: `["agent", "channel", "user"]`)
- `excludeProviders` - Message providers to skip for recall/retain (e.g. `["slack"]`, `["telegram"]`, `["discord"]`)
- `autoRecall` - Auto-inject memories before each turn (default: `true`). Set to `false` when the agent has its own recall tool.
@ -144,7 +145,7 @@ Available isolation fields:
- `user` - The user interacting with the agent
- `provider` - The message provider (e.g. Slack, Discord)
Use `bankIdPrefix` to namespace bank IDs across environments (e.g. `"prod"`, `"staging"`). Set `dynamicBankId` to `false` to use a single shared bank for all conversations.
Use `bankIdPrefix` to namespace bank IDs across environments (e.g. `"prod"`, `"staging"`). Set `dynamicBankId` to `false` to use a single shared bank for all conversations. In static mode, the plugin uses `bankId`, then `HINDSIGHT_BANK_ID`, then the default `openclaw` bank name.
### Retention Controls

View file

@ -46,6 +46,7 @@ Optional settings in `~/.openclaw/openclaw.json` under `plugins.entries.hindsigh
| `llmModel` | provider default | LLM model override used with `llmProvider` |
| `llmApiKeyEnv` | provider standard env var | Custom env var name for the provider API key |
| `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"`) |
| `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`) |

View file

@ -68,6 +68,10 @@
"description": "Enable per-user memory banks. When true, memories are isolated by user per channel (e.g., slack-U123, telegram-456789). When false, all users share a single 'openclaw' bank.",
"default": true
},
"bankId": {
"type": "string",
"description": "Static bank ID used when dynamicBankId is false. Can also be provided via HINDSIGHT_BANK_ID."
},
"bankIdPrefix": {
"type": "string",
"description": "Optional prefix for bank IDs (e.g., 'prod' results in 'prod-slack-U123'). Useful for separating environments."
@ -282,6 +286,10 @@
"label": "Dynamic Bank IDs",
"placeholder": "true (isolate memories per channel)"
},
"bankId": {
"label": "Static Bank ID",
"placeholder": "e.g. openclaw, shared-bank (used when dynamicBankId is false)"
},
"bankIdPrefix": {
"label": "Bank ID Prefix",
"placeholder": "e.g., prod, staging (optional)"

View file

@ -91,6 +91,17 @@ describe('deriveBankId', () => {
expect(bankId).toBe('openclaw');
});
it('should return configured bankId when dynamicBankId is false', () => {
const config: PluginConfig = {
dynamicBankId: false,
bankId: 'shared-bank',
bankIdPrefix: 'prod',
dynamicBankGranularity: ['provider', 'user'],
};
const bankId = deriveBankId(ctx, config);
expect(bankId).toBe('prod-shared-bank');
});
it('should encode segments to prevent separator collisions', () => {
const ctxWithSeparator: PluginHookAgentContext = {
agentId: 'a::b',

View file

@ -175,7 +175,7 @@ async function lazyReinit(): Promise<void> {
const defaultBankId = deriveBankId(undefined, config);
client.setBankId(defaultBankId);
if (config.bankMission && !config.dynamicBankId) {
if (config.bankMission && usesStaticBank(config)) {
await client.setBankMission(config.bankMission);
}
@ -222,7 +222,7 @@ if (typeof global !== 'undefined') {
getClientForContext: async (ctx: PluginHookAgentContext | undefined) => {
if (!client) {return null;}
const config = currentPluginConfig || {};
if (config.dynamicBankId === false) {
if (usesStaticBank(config)) {
return client;
}
const bankId = deriveBankId(ctx, config);
@ -244,7 +244,7 @@ if (typeof global !== 'undefined') {
}
// Set bank mission on first use of this bank (if configured)
if (config.bankMission && config.dynamicBankId && !banksWithMissionSet.has(bankId)) {
if (config.bankMission && !usesStaticBank(config) && !banksWithMissionSet.has(bankId)) {
try {
await bankClient.setBankMission(config.bankMission);
banksWithMissionSet.add(bankId);
@ -268,6 +268,29 @@ const __dirname = dirname(__filename);
// Default bank name (fallback when channel context not available)
const DEFAULT_BANK_NAME = 'openclaw';
function getConfiguredBankId(pluginConfig: PluginConfig): string | undefined {
if (typeof pluginConfig.bankId !== 'string') {
return undefined;
}
const trimmed = pluginConfig.bankId.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function usesStaticBank(pluginConfig: PluginConfig): boolean {
return pluginConfig.dynamicBankId === false;
}
function getDefaultBankId(pluginConfig: PluginConfig): string {
return pluginConfig.bankIdPrefix ? `${pluginConfig.bankIdPrefix}-${DEFAULT_BANK_NAME}` : DEFAULT_BANK_NAME;
}
function getStaticBankId(pluginConfig: PluginConfig): string {
const configuredBankId = getConfiguredBankId(pluginConfig);
const baseBankId = configuredBankId || DEFAULT_BANK_NAME;
return pluginConfig.bankIdPrefix ? `${pluginConfig.bankIdPrefix}-${baseBankId}` : baseBankId;
}
/**
* Strip plugin-injected memory tags from content to prevent retain feedback loop.
* Removes <hindsight_memories> and <relevant_memories> blocks that were injected
@ -515,12 +538,12 @@ function parseSessionKey(sessionKey: string): { agentId?: string; provider?: str
export function deriveBankId(ctx: PluginHookAgentContext | undefined, pluginConfig: PluginConfig): string {
if (pluginConfig.dynamicBankId === false) {
return pluginConfig.bankIdPrefix ? `${pluginConfig.bankIdPrefix}-openclaw` : 'openclaw';
return getStaticBankId(pluginConfig);
}
// When no context is available, fall back to the static default bank.
if (!ctx) {
return pluginConfig.bankIdPrefix ? `${pluginConfig.bankIdPrefix}-openclaw` : 'openclaw';
return getDefaultBankId(pluginConfig);
}
const fields = pluginConfig.dynamicBankGranularity?.length ? pluginConfig.dynamicBankGranularity : ['agent', 'channel', 'user'];
@ -768,6 +791,9 @@ async function checkExternalApiHealth(apiUrl: string, apiToken?: string | null):
function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
const config = api.config.plugins?.entries?.['hindsight-openclaw']?.config || {};
const defaultMission = 'You are an AI assistant helping users across multiple communication channels (Telegram, Slack, Discord, etc.). Remember user preferences, instructions, and important context from conversations to provide personalized assistance.';
const envBankId = typeof process.env.HINDSIGHT_BANK_ID === 'string' && process.env.HINDSIGHT_BANK_ID.trim().length > 0
? process.env.HINDSIGHT_BANK_ID.trim()
: undefined;
return {
bankMission: config.bankMission || defaultMission,
@ -783,6 +809,7 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
apiPort: config.apiPort || 9077,
// Dynamic bank ID options (default: enabled)
dynamicBankId: config.dynamicBankId !== false,
bankId: envBankId || (typeof config.bankId === 'string' && config.bankId.trim().length > 0 ? config.bankId.trim() : undefined),
bankIdPrefix: config.bankIdPrefix,
excludeProviders: Array.isArray(config.excludeProviders) ? config.excludeProviders : [],
autoRecall: config.autoRecall !== false, // Default: true (on) — backward compatible
@ -855,12 +882,13 @@ export default function (api: MoltbotPluginAPI) {
debug(`[Hindsight] Custom bank mission configured: "${pluginConfig.bankMission.substring(0, 50)}..."`);
}
// Log dynamic bank ID mode
// Log bank ID mode
if (pluginConfig.dynamicBankId) {
const prefixInfo = pluginConfig.bankIdPrefix ? ` (prefix: ${pluginConfig.bankIdPrefix})` : '';
debug(`[Hindsight] ✓ Dynamic bank IDs enabled${prefixInfo} - each channel gets isolated memory`);
} else {
debug(`[Hindsight] Dynamic bank IDs disabled - using static bank: ${DEFAULT_BANK_NAME}`);
const sourceInfo = getConfiguredBankId(pluginConfig) ? 'configured' : 'default';
debug(`[Hindsight] Dynamic bank IDs disabled - using ${sourceInfo} static bank: ${getStaticBankId(pluginConfig)}`);
}
// Detect external API mode
@ -933,7 +961,7 @@ export default function (api: MoltbotPluginAPI) {
// Note: Bank mission will be set per-bank when dynamic bank IDs are enabled
// For now, set it on the default bank
if (pluginConfig.bankMission && !pluginConfig.dynamicBankId) {
if (pluginConfig.bankMission && usesStaticBank(pluginConfig)) {
debug(`[Hindsight] Setting bank mission...`);
await client.setBankMission(pluginConfig.bankMission);
}
@ -978,7 +1006,7 @@ export default function (api: MoltbotPluginAPI) {
// Note: Bank mission will be set per-bank when dynamic bank IDs are enabled
// For now, set it on the default bank
if (pluginConfig.bankMission && !pluginConfig.dynamicBankId) {
if (pluginConfig.bankMission && usesStaticBank(pluginConfig)) {
debug(`[Hindsight] Setting bank mission...`);
await client.setBankMission(pluginConfig.bankMission);
}
@ -1070,7 +1098,7 @@ export default function (api: MoltbotPluginAPI) {
const defaultBankId = deriveBankId(undefined, reinitPluginConfig);
client.setBankId(defaultBankId);
if (reinitPluginConfig.bankMission && !reinitPluginConfig.dynamicBankId) {
if (reinitPluginConfig.bankMission && usesStaticBank(reinitPluginConfig)) {
await client.setBankMission(reinitPluginConfig.bankMission);
}
@ -1098,7 +1126,7 @@ export default function (api: MoltbotPluginAPI) {
const defaultBankId = deriveBankId(undefined, reinitPluginConfig);
client.setBankId(defaultBankId);
if (reinitPluginConfig.bankMission && !reinitPluginConfig.dynamicBankId) {
if (reinitPluginConfig.bankMission && usesStaticBank(reinitPluginConfig)) {
await client.setBankMission(reinitPluginConfig.bankMission);
}

View file

@ -62,6 +62,7 @@ export interface PluginConfig {
hindsightApiUrl?: string; // External Hindsight API URL (skips local daemon when set)
hindsightApiToken?: string; // API token for external Hindsight API authentication
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')
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.