diff --git a/hindsight-integrations/openclaw/openclaw.plugin.json b/hindsight-integrations/openclaw/openclaw.plugin.json index 4bfa1242..3f9ba6da 100644 --- a/hindsight-integrations/openclaw/openclaw.plugin.json +++ b/hindsight-integrations/openclaw/openclaw.plugin.json @@ -55,6 +55,15 @@ "hindsightApiToken": { "type": "string", "description": "API token for external Hindsight API authentication. Required if the external API has authentication enabled." + }, + "dynamicBankId": { + "type": "boolean", + "description": "Enable per-channel memory banks. When true, memories are isolated by channel (e.g., slack-C123, telegram-456). When false, all channels share a single 'openclaw' bank.", + "default": true + }, + "bankIdPrefix": { + "type": "string", + "description": "Optional prefix for bank IDs (e.g., 'prod' results in 'prod-slack-C123'). Useful for separating environments." } }, "additionalProperties": false @@ -103,6 +112,14 @@ "hindsightApiToken": { "label": "External API Token", "placeholder": "API token if external API requires authentication" + }, + "dynamicBankId": { + "label": "Dynamic Bank IDs", + "placeholder": "true (isolate memories per channel)" + }, + "bankIdPrefix": { + "label": "Bank ID Prefix", + "placeholder": "e.g., prod, staging (optional)" } } } diff --git a/hindsight-integrations/openclaw/src/index.ts b/hindsight-integrations/openclaw/src/index.ts index 7996088b..feafb5fc 100644 --- a/hindsight-integrations/openclaw/src/index.ts +++ b/hindsight-integrations/openclaw/src/index.ts @@ -11,6 +11,12 @@ let initPromise: Promise | null = null; let isInitialized = false; let usingExternalApi = false; // Track if using external API (skip daemon management) +// Store the current plugin config for bank ID derivation +let currentPluginConfig: PluginConfig | null = null; + +// Track which banks have had their mission set (to avoid re-setting on every request) +const banksWithMissionSet = new Set(); + // Global access for hooks (Moltbot loads hooks separately) if (typeof global !== 'undefined') { (global as any).__hindsightClient = { @@ -19,6 +25,32 @@ if (typeof global !== 'undefined') { if (isInitialized) return; if (initPromise) await initPromise; }, + /** + * Get a client configured for a specific agent context. + * Derives the bank ID from the context for per-channel isolation. + * Also ensures the bank mission is set on first use. + */ + getClientForContext: async (ctx: PluginHookAgentContext | undefined) => { + if (!client) return null; + const config = currentPluginConfig || {}; + const bankId = deriveBankId(ctx, config); + client.setBankId(bankId); + + // Set bank mission on first use of this bank (if configured) + if (config.bankMission && config.dynamicBankId && !banksWithMissionSet.has(bankId)) { + try { + await client.setBankMission(config.bankMission); + banksWithMissionSet.add(bankId); + console.log(`[Hindsight] Set mission for new bank: ${bankId}`); + } catch (error) { + // Log but don't fail - bank mission is not critical + console.warn(`[Hindsight] Could not set bank mission for ${bankId}: ${error}`); + } + } + + return client; + }, + getPluginConfig: () => currentPluginConfig, }; } @@ -26,8 +58,47 @@ if (typeof global !== 'undefined') { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// Default bank name -const BANK_NAME = 'openclaw'; +// Default bank name (fallback when channel context not available) +const DEFAULT_BANK_NAME = 'openclaw'; + +/** + * Agent context passed to plugin hooks. + * These fields are populated by OpenClaw when invoking hooks. + */ +interface PluginHookAgentContext { + agentId?: string; + sessionKey?: string; + workspaceDir?: string; + messageProvider?: string; + channelId?: string; + senderId?: string; +} + +/** + * Derive a bank ID from the agent context. + * Creates channel-specific banks: {messageProvider}-{channelId} + * Falls back to default bank when context is unavailable. + */ +function deriveBankId( + ctx: PluginHookAgentContext | undefined, + pluginConfig: PluginConfig +): string { + // If dynamic bank ID is disabled, use static bank + if (pluginConfig.dynamicBankId === false) { + return pluginConfig.bankIdPrefix + ? `${pluginConfig.bankIdPrefix}-${DEFAULT_BANK_NAME}` + : DEFAULT_BANK_NAME; + } + + const channelType = ctx?.messageProvider || 'unknown'; + const channelId = ctx?.channelId || 'default'; + + // Build bank ID: {prefix?}-{channelType}-{channelId} + const baseBankId = `${channelType}-${channelId}`; + return pluginConfig.bankIdPrefix + ? `${pluginConfig.bankIdPrefix}-${baseBankId}` + : baseBankId; +} // Provider detection from standard env vars const PROVIDER_DETECTION = [ @@ -195,6 +266,10 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig { llmApiKeyEnv: config.llmApiKeyEnv, hindsightApiUrl: config.hindsightApiUrl, hindsightApiToken: config.hindsightApiToken, + apiPort: config.apiPort || 9077, + // Dynamic bank ID options (default: enabled) + dynamicBankId: config.dynamicBankId !== false, + bankIdPrefix: config.bankIdPrefix, }; } @@ -206,6 +281,9 @@ export default function (api: MoltbotPluginAPI) { console.log('[Hindsight] Getting plugin config...'); const pluginConfig = getPluginConfig(api); + // Store config globally for bank ID derivation in hooks + currentPluginConfig = pluginConfig; + // Detect LLM configuration (env vars > plugin config > auto-detect) console.log('[Hindsight] Detecting LLM config...'); const llmConfig = detectLLMConfig(pluginConfig); @@ -221,6 +299,15 @@ export default function (api: MoltbotPluginAPI) { if (pluginConfig.bankMission) { console.log(`[Hindsight] Custom bank mission configured: "${pluginConfig.bankMission.substring(0, 50)}..."`); } + + // Log dynamic bank ID mode + if (pluginConfig.dynamicBankId) { + const prefixInfo = pluginConfig.bankIdPrefix ? ` (prefix: ${pluginConfig.bankIdPrefix})` : ''; + console.log(`[Hindsight] ✓ Dynamic bank IDs enabled${prefixInfo} - each channel gets isolated memory`); + } else { + console.log(`[Hindsight] Dynamic bank IDs disabled - using static bank: ${DEFAULT_BANK_NAME}`); + } + // Detect external API mode const externalApi = detectExternalApi(pluginConfig); @@ -256,12 +343,14 @@ export default function (api: MoltbotPluginAPI) { console.log('[Hindsight] Creating HindsightClient...'); client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath); - // Use openclaw bank - console.log(`[Hindsight] Using bank: ${BANK_NAME}`); - client.setBankId(BANK_NAME); + // Set default bank (will be overridden per-request when dynamic bank IDs are enabled) + const defaultBankId = deriveBankId(undefined, pluginConfig); + console.log(`[Hindsight] Default bank: ${defaultBankId}`); + client.setBankId(defaultBankId); - // Set bank mission - if (pluginConfig.bankMission) { + // 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) { console.log(`[Hindsight] Setting bank mission...`); await client.setBankMission(pluginConfig.bankMission); } @@ -290,12 +379,14 @@ export default function (api: MoltbotPluginAPI) { console.log('[Hindsight] Creating HindsightClient...'); client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath); - // Use openclaw bank - console.log(`[Hindsight] Using bank: ${BANK_NAME}`); - client.setBankId(BANK_NAME); + // Set default bank (will be overridden per-request when dynamic bank IDs are enabled) + const defaultBankId = deriveBankId(undefined, pluginConfig); + console.log(`[Hindsight] Default bank: ${defaultBankId}`); + client.setBankId(defaultBankId); - // Set bank mission - if (pluginConfig.bankMission) { + // 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) { console.log(`[Hindsight] Setting bank mission...`); await client.setBankMission(pluginConfig.bankMission); } @@ -364,6 +455,7 @@ export default function (api: MoltbotPluginAPI) { if (!isInitialized) { console.log('[Hindsight] Reinitializing...'); const reinitPluginConfig = getPluginConfig(api); + currentPluginConfig = reinitPluginConfig; const llmConfig = detectLLMConfig(reinitPluginConfig); const externalApi = detectExternalApi(reinitPluginConfig); const apiPort = reinitPluginConfig.apiPort || 9077; @@ -379,9 +471,10 @@ export default function (api: MoltbotPluginAPI) { await checkExternalApiHealth(externalApi.apiUrl); client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath); - client.setBankId(BANK_NAME); + const defaultBankId = deriveBankId(undefined, reinitPluginConfig); + client.setBankId(defaultBankId); - if (reinitPluginConfig.bankMission) { + if (reinitPluginConfig.bankMission && !reinitPluginConfig.dynamicBankId) { await client.setBankMission(reinitPluginConfig.bankMission); } @@ -403,9 +496,10 @@ export default function (api: MoltbotPluginAPI) { await embedManager.start(); client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath); - client.setBankId(BANK_NAME); + const defaultBankId = deriveBankId(undefined, reinitPluginConfig); + client.setBankId(defaultBankId); - if (reinitPluginConfig.bankMission) { + if (reinitPluginConfig.bankMission && !reinitPluginConfig.dynamicBankId) { await client.setBankMission(reinitPluginConfig.bankMission); } @@ -438,22 +532,29 @@ export default function (api: MoltbotPluginAPI) { console.log('[Hindsight] Plugin loaded successfully'); - // Register agent_end hook for auto-retention - console.log('[Hindsight] Registering agent_end hook...'); - // Store session key for retention + // Register agent hooks for auto-recall and auto-retention + console.log('[Hindsight] Registering agent hooks...'); + + // Store session key and context for retention let currentSessionKey: string | undefined; + let currentAgentContext: PluginHookAgentContext | undefined; // Auto-recall: Inject relevant memories before agent processes the message - api.on('before_agent_start', async (context: any) => { + // Hook signature: (event, ctx) where event has {prompt, messages?} and ctx has agent context + api.on('before_agent_start', async (event: any, ctx?: PluginHookAgentContext) => { try { - // Capture session key - if (context.sessionKey) { - currentSessionKey = context.sessionKey as string; - console.log('[Hindsight] Captured session key:', currentSessionKey); + // Capture session key and context for use in agent_end + if (ctx?.sessionKey) { + currentSessionKey = ctx.sessionKey; } + currentAgentContext = ctx; + + // Derive bank ID from context + const bankId = deriveBankId(ctx, pluginConfig); + console.log(`[Hindsight] before_agent_start - bank: ${bankId}, channel: ${ctx?.messageProvider}/${ctx?.channelId}`); // Get the user's latest message for recall - let prompt = context.prompt; + let prompt = event.prompt; if (!prompt || typeof prompt !== 'string' || prompt.length < 5) { return; // Skip very short messages } @@ -477,13 +578,14 @@ export default function (api: MoltbotPluginAPI) { await clientGlobal.waitForReady(); - const client = clientGlobal.getClient(); + // Get client configured for this context's bank (async to handle mission setup) + const client = await clientGlobal.getClientForContext(ctx); if (!client) { console.log('[Hindsight] Client not initialized, skipping auto-recall'); return; } - console.log('[Hindsight] Auto-recall for prompt:', prompt.substring(0, 50)); + console.log(`[Hindsight] Auto-recall for bank ${bankId}, prompt: ${prompt.substring(0, 50)}`); // Recall relevant memories (up to 512 tokens) const response = await client.recall({ @@ -506,7 +608,7 @@ ${memoriesJson} User message: ${prompt} `; - console.log(`[Hindsight] Auto-recall: Injecting ${response.results.length} memories`); + console.log(`[Hindsight] Auto-recall: Injecting ${response.results.length} memories from bank ${bankId}`); // Inject context before the user message return { prependContext: contextMessage }; @@ -516,9 +618,15 @@ User message: ${prompt} } }); - api.on('agent_end', async (event: any) => { + // Hook signature: (event, ctx) where event has {messages, success, error?, durationMs?} + api.on('agent_end', async (event: any, ctx?: PluginHookAgentContext) => { try { - console.log('[Hindsight Hook] agent_end triggered'); + // Use context from this hook, or fall back to context captured in before_agent_start + const effectiveCtx = ctx || currentAgentContext; + + // Derive bank ID from context + const bankId = deriveBankId(effectiveCtx, pluginConfig); + console.log(`[Hindsight Hook] agent_end triggered - bank: ${bankId}`); // Check event success and messages if (!event.success || !Array.isArray(event.messages) || event.messages.length === 0) { @@ -535,7 +643,8 @@ User message: ${prompt} await clientGlobal.waitForReady(); - const client = clientGlobal.getClient(); + // Get client configured for this context's bank (async to handle mission setup) + const client = await clientGlobal.getClientForContext(effectiveCtx); if (!client) { console.warn('[Hindsight] Client not initialized, skipping retain'); return; @@ -566,8 +675,8 @@ User message: ${prompt} return; } - // Use session key as document ID - const documentId = currentSessionKey || 'default-session'; + // Use session key as document ID (prefer context over captured value) + const documentId = effectiveCtx?.sessionKey || currentSessionKey || 'default-session'; // Retain to Hindsight await client.retain({ @@ -576,15 +685,18 @@ User message: ${prompt} metadata: { retained_at: new Date().toISOString(), message_count: event.messages.length, + channel_type: effectiveCtx?.messageProvider, + channel_id: effectiveCtx?.channelId, + sender_id: effectiveCtx?.senderId, }, }); - console.log(`[Hindsight] Retained ${event.messages.length} messages for session ${documentId}`); + console.log(`[Hindsight] Retained ${event.messages.length} messages to bank ${bankId} for session ${documentId}`); } catch (error) { console.error('[Hindsight] Error retaining messages:', error); } }); - console.log('[Hindsight] Hook registered'); + console.log('[Hindsight] Hooks registered'); } catch (error) { console.error('[Hindsight] Plugin loading error:', error); if (error instanceof Error) { diff --git a/hindsight-integrations/openclaw/src/types.ts b/hindsight-integrations/openclaw/src/types.ts index e23912b5..ed914361 100644 --- a/hindsight-integrations/openclaw/src/types.ts +++ b/hindsight-integrations/openclaw/src/types.ts @@ -3,7 +3,8 @@ export interface MoltbotPluginAPI { config: MoltbotConfig; registerService(config: ServiceConfig): void; - on(event: string, handler: (context: any) => void | Promise): void; + // OpenClaw hook handler signature: (event, ctx?) where ctx contains channel/sender info + on(event: string, handler: (event: any, ctx?: any) => void | Promise): void; // Add more as needed } @@ -39,6 +40,8 @@ export interface PluginConfig { apiPort?: number; // Port for openclaw profile daemon (default: 9077) 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) + bankIdPrefix?: string; // Prefix for bank IDs (e.g. 'prod' -> 'prod-slack-C123') } export interface ServiceConfig {