From 41025c3b7c25d980de1a399f15bee947af677c77 Mon Sep 17 00:00:00 2001 From: Kagura Date: Tue, 31 Mar 2026 15:10:13 +0800 Subject: [PATCH] fix(openclaw): defer heavy init to service.start() to avoid CLI slowdown (#764) OpenClaw loads plugins on every CLI command (status, models auth add, config validate, etc.), not just gateway start. The plugin was starting LLM detection, daemon initialization, and API health checks immediately in the default export, causing unnecessary resource usage and terminal noise on routine CLI operations. Move all heavy initialization (detectLLMConfig, embedManager.start(), checkExternalApiHealth, client creation) into service.start() which is only called when the gateway starts. The default export now only does lightweight config parsing and service/hook registration. Hooks (before_prompt_build, agent_end) gracefully no-op when called before service.start() via the waitForReady guard. Closes #746 --- hindsight-integrations/openclaw/src/index.ts | 328 ++++++++++--------- 1 file changed, 168 insertions(+), 160 deletions(-) diff --git a/hindsight-integrations/openclaw/src/index.ts b/hindsight-integrations/openclaw/src/index.ts index abddaa3f..aa6dc782 100644 --- a/hindsight-integrations/openclaw/src/index.ts +++ b/hindsight-integrations/openclaw/src/index.ts @@ -66,21 +66,25 @@ function formatCurrentTimeForRecall(date = new Date()): string { * Lazy re-initialization after startup failure. * Called by waitForReady when initPromise rejected but API may now be reachable. * Throttled to one attempt per 30s to avoid hammering a down service. + * Only works if initialization was attempted at least once (isInitialized guard). */ async function lazyReinit(): Promise { const now = Date.now(); if (now - lastReinitAttempt < REINIT_COOLDOWN_MS || isReinitInProgress) { return; } + + // Only attempt lazy reinit if we've already done initial setup + // (i.e., service.start() was called at least once) + if (!currentPluginConfig) { + debug('[Hindsight] lazyReinit skipped - no plugin config (service.start() not called yet)'); + return; + } + isReinitInProgress = true; lastReinitAttempt = now; const config = currentPluginConfig; - if (!config) { - isReinitInProgress = false; - return; - } - const externalApi = detectExternalApi(config); if (!externalApi.apiUrl) { isReinitInProgress = false; @@ -127,16 +131,20 @@ if (typeof global !== 'undefined') { getClient: () => client, waitForReady: async () => { if (isInitialized) {return;} - if (initPromise) { - try { - await initPromise; - } catch { - // Init failed (e.g., health check timeout at startup). - // Attempt lazy re-initialization so Hindsight recovers - // once the API becomes reachable again. - if (!isInitialized) { - await lazyReinit(); - } + // If initPromise is null, it means service.start() hasn't been called yet + // (CLI mode, not gateway mode). Hooks should gracefully no-op. + if (!initPromise) { + debug('[Hindsight] waitForReady called but initPromise is null (gateway not started)'); + return; + } + try { + await initPromise; + } catch { + // Init failed (e.g., health check timeout at startup). + // Attempt lazy re-initialization so Hindsight recovers + // once the API becomes reachable again. + if (!isInitialized) { + await lazyReinit(); } } }, @@ -738,7 +746,7 @@ export default function (api: MoltbotPluginAPI) { try { debug('[Hindsight] Plugin loading...'); - // Get plugin config first (needed for LLM detection and debug flag) + // Get plugin config first (needed for debug flag and service registration) const pluginConfig = getPluginConfig(api); // If logLevel is 'debug', also enable legacy debug flag debugEnabled = pluginConfig.debug ?? (pluginConfig.logLevel === 'debug'); @@ -753,158 +761,158 @@ export default function (api: MoltbotPluginAPI) { // Store config globally for bank ID derivation in hooks currentPluginConfig = pluginConfig; - // Detect LLM configuration (env vars > plugin config > auto-detect) - debug('[Hindsight] Detecting LLM config...'); - const llmConfig = detectLLMConfig(pluginConfig); - - const baseUrlInfo = llmConfig.baseUrl ? `, base URL: ${llmConfig.baseUrl}` : ''; - const modelInfo = llmConfig.model || 'default'; - - if (llmConfig.provider === 'ollama') { - debug(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${modelInfo} (${llmConfig.source})`); - } else { - debug(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${modelInfo} (${llmConfig.source}${baseUrlInfo})`); - } - if (pluginConfig.bankMission) { - debug(`[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})` : ''; - 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}`); - } - - // Detect external API mode - const externalApi = detectExternalApi(pluginConfig); - - // Get API port from config (default: 9077) - const apiPort = pluginConfig.apiPort || 9077; - - if (externalApi.apiUrl) { - // External API mode - skip local daemon - usingExternalApi = true; - debug(`[Hindsight] ✓ Using external API: ${externalApi.apiUrl}`); - - // Set env vars so CLI commands (uvx hindsight-embed) use external API - process.env.HINDSIGHT_EMBED_API_URL = externalApi.apiUrl; - if (externalApi.apiToken) { - process.env.HINDSIGHT_EMBED_API_TOKEN = externalApi.apiToken; - debug('[Hindsight] API token configured'); - } - } else { - debug(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`); - debug(`[Hindsight] API Port: ${apiPort}`); - } - - // Initialize in background (non-blocking) - debug('[Hindsight] Starting initialization in background...'); - initPromise = (async () => { - try { - if (usingExternalApi && externalApi.apiUrl) { - // External API mode - check health, skip daemon startup - debug('[Hindsight] External API mode - skipping local daemon...'); - await checkExternalApiHealth(externalApi.apiUrl, externalApi.apiToken); - - // Initialize client with direct HTTP mode - debug('[Hindsight] Creating HindsightClient (HTTP mode)...'); - clientOptions = buildClientOptions(llmConfig, pluginConfig, externalApi); - clientsByBankId.clear(); - banksWithMissionSet.clear(); - client = new HindsightClient(clientOptions); - - // Set default bank (will be overridden per-request when dynamic bank IDs are enabled) - const defaultBankId = deriveBankId(undefined, pluginConfig); - debug(`[Hindsight] Default bank: ${defaultBankId}`); - client.setBankId(defaultBankId); - - // 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) { - debug(`[Hindsight] Setting bank mission...`); - await client.setBankMission(pluginConfig.bankMission); - } - - if (!isInitialized) { - const mode = 'external API'; - const autoRecall = pluginConfig.autoRecall !== false; - const autoRetain = pluginConfig.autoRetain !== false; - log.info(`initialized (mode: ${mode}, bank: ${defaultBankId}, autoRecall: ${autoRecall}, autoRetain: ${autoRetain})`); - } - isInitialized = true; - debug('[Hindsight] ✓ Ready (external API mode)'); - } else { - // Local daemon mode - start hindsight-embed daemon - debug('[Hindsight] Creating HindsightEmbedManager...'); - embedManager = new HindsightEmbedManager( - apiPort, - llmConfig.provider || "", - llmConfig.apiKey || "", - llmConfig.model, - llmConfig.baseUrl, - pluginConfig.daemonIdleTimeout, - pluginConfig.embedVersion, - pluginConfig.embedPackagePath - ); - - // Start the embedded server - debug('[Hindsight] Starting embedded server...'); - await embedManager.start(); - - // Initialize client (local daemon mode — no apiUrl) - debug('[Hindsight] Creating HindsightClient (subprocess mode)...'); - clientOptions = buildClientOptions(llmConfig, pluginConfig, { apiUrl: null, apiToken: null }); - clientsByBankId.clear(); - banksWithMissionSet.clear(); - client = new HindsightClient(clientOptions); - - // Set default bank (will be overridden per-request when dynamic bank IDs are enabled) - const defaultBankId = deriveBankId(undefined, pluginConfig); - debug(`[Hindsight] Default bank: ${defaultBankId}`); - client.setBankId(defaultBankId); - - // 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) { - debug(`[Hindsight] Setting bank mission...`); - await client.setBankMission(pluginConfig.bankMission); - } - - if (!isInitialized) { - const mode = 'local daemon'; - const autoRecall = pluginConfig.autoRecall !== false; - const autoRetain = pluginConfig.autoRetain !== false; - log.info(`initialized (mode: ${mode}, bank: ${defaultBankId}, autoRecall: ${autoRecall}, autoRetain: ${autoRetain})`); - } - isInitialized = true; - debug('[Hindsight] ✓ Ready'); - } - } catch (error) { - log.error('initialization error', error); - throw error; - } - })(); - - // Suppress unhandled rejection — service.start() will await and handle errors - initPromise.catch(() => {}); + debug('[Hindsight] Plugin loaded successfully (deferred heavy init to gateway start)'); // Register background service for cleanup + // IMPORTANT: Heavy initialization (LLM detection, daemon start, API health checks) + // happens in service.start() which is ONLY called on gateway start, + // not on every CLI command. debug('[Hindsight] Registering service...'); api.registerService({ id: 'hindsight-memory', async start() { - debug('[Hindsight] Service start called...'); + debug('[Hindsight] Service start called - beginning heavy initialization...'); - // Wait for background init if still pending - if (initPromise) { - try { - await initPromise; - } catch (error) { - log.error('initial initialization failed', error); - // Continue to health check below + // Detect LLM configuration (env vars > plugin config > auto-detect) + debug('[Hindsight] Detecting LLM config...'); + const llmConfig = detectLLMConfig(pluginConfig); + + const baseUrlInfo = llmConfig.baseUrl ? `, base URL: ${llmConfig.baseUrl}` : ''; + const modelInfo = llmConfig.model || 'default'; + + if (llmConfig.provider === 'ollama') { + debug(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${modelInfo} (${llmConfig.source})`); + } else { + debug(`[Hindsight] ✓ Using provider: ${llmConfig.provider}, model: ${modelInfo} (${llmConfig.source}${baseUrlInfo})`); + } + if (pluginConfig.bankMission) { + debug(`[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})` : ''; + 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}`); + } + + // Detect external API mode + const externalApi = detectExternalApi(pluginConfig); + + // Get API port from config (default: 9077) + const apiPort = pluginConfig.apiPort || 9077; + + if (externalApi.apiUrl) { + // External API mode - skip local daemon + usingExternalApi = true; + debug(`[Hindsight] ✓ Using external API: ${externalApi.apiUrl}`); + + // Set env vars so CLI commands (uvx hindsight-embed) use external API + process.env.HINDSIGHT_EMBED_API_URL = externalApi.apiUrl; + if (externalApi.apiToken) { + process.env.HINDSIGHT_EMBED_API_TOKEN = externalApi.apiToken; + debug('[Hindsight] API token configured'); } + } else { + debug(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`); + debug(`[Hindsight] API Port: ${apiPort}`); + } + + // Initialize (runs synchronously in service.start()) + debug('[Hindsight] Starting initialization...'); + initPromise = (async () => { + try { + if (usingExternalApi && externalApi.apiUrl) { + // External API mode - check health, skip daemon startup + debug('[Hindsight] External API mode - skipping local daemon...'); + await checkExternalApiHealth(externalApi.apiUrl, externalApi.apiToken); + + // Initialize client with direct HTTP mode + debug('[Hindsight] Creating HindsightClient (HTTP mode)...'); + clientOptions = buildClientOptions(llmConfig, pluginConfig, externalApi); + clientsByBankId.clear(); + banksWithMissionSet.clear(); + client = new HindsightClient(clientOptions); + + // Set default bank (will be overridden per-request when dynamic bank IDs are enabled) + const defaultBankId = deriveBankId(undefined, pluginConfig); + debug(`[Hindsight] Default bank: ${defaultBankId}`); + client.setBankId(defaultBankId); + + // 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) { + debug(`[Hindsight] Setting bank mission...`); + await client.setBankMission(pluginConfig.bankMission); + } + + if (!isInitialized) { + const mode = 'external API'; + const autoRecall = pluginConfig.autoRecall !== false; + const autoRetain = pluginConfig.autoRetain !== false; + log.info(`initialized (mode: ${mode}, bank: ${defaultBankId}, autoRecall: ${autoRecall}, autoRetain: ${autoRetain})`); + } + isInitialized = true; + debug('[Hindsight] ✓ Ready (external API mode)'); + } else { + // Local daemon mode - start hindsight-embed daemon + debug('[Hindsight] Creating HindsightEmbedManager...'); + embedManager = new HindsightEmbedManager( + apiPort, + llmConfig.provider || "", + llmConfig.apiKey || "", + llmConfig.model, + llmConfig.baseUrl, + pluginConfig.daemonIdleTimeout, + pluginConfig.embedVersion, + pluginConfig.embedPackagePath + ); + + // Start the embedded server + debug('[Hindsight] Starting embedded server...'); + await embedManager.start(); + + // Initialize client (local daemon mode — no apiUrl) + debug('[Hindsight] Creating HindsightClient (subprocess mode)...'); + clientOptions = buildClientOptions(llmConfig, pluginConfig, { apiUrl: null, apiToken: null }); + clientsByBankId.clear(); + banksWithMissionSet.clear(); + client = new HindsightClient(clientOptions); + + // Set default bank (will be overridden per-request when dynamic bank IDs are enabled) + const defaultBankId = deriveBankId(undefined, pluginConfig); + debug(`[Hindsight] Default bank: ${defaultBankId}`); + client.setBankId(defaultBankId); + + // 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) { + debug(`[Hindsight] Setting bank mission...`); + await client.setBankMission(pluginConfig.bankMission); + } + + if (!isInitialized) { + const mode = 'local daemon'; + const autoRecall = pluginConfig.autoRecall !== false; + const autoRetain = pluginConfig.autoRetain !== false; + log.info(`initialized (mode: ${mode}, bank: ${defaultBankId}, autoRecall: ${autoRecall}, autoRetain: ${autoRetain})`); + } + isInitialized = true; + debug('[Hindsight] ✓ Ready'); + } + } catch (error) { + log.error('initialization error', error); + throw error; + } + })(); + + // Wait for initialization to complete + try { + await initPromise; + } catch (error) { + log.error('initial initialization failed', error); + // Continue to health check below } // External API mode: check external API health