feat(openclaw): add external Hindsight API support (#289)

Add support for connecting to an external Hindsight API instead of
starting a local daemon. This enables:
- Shared memory across multiple OpenClaw instances
- Centralized Hindsight deployment (e.g., on GKE)
- Reduced resource usage (no local daemon per instance)

Configuration:
- HINDSIGHT_EMBED_API_URL env var or hindsightApiUrl in plugin config
- HINDSIGHT_EMBED_API_TOKEN env var or hindsightApiToken for auth

When external API is configured:
- Skip local daemon startup
- Health check external API on startup
- Pass API URL/token to CLI commands via env vars

Falls back to local daemon mode when not configured.
This commit is contained in:
Anton Evseev 2026-02-04 19:24:33 +10:00 committed by GitHub
parent 63e2964a4c
commit 6b346925e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 199 additions and 66 deletions

View file

@ -46,6 +46,15 @@
"type": "number",
"description": "Port for the openclaw profile daemon (default: 9077)",
"default": 9077
},
"hindsightApiUrl": {
"type": "string",
"description": "External Hindsight API URL (e.g. 'https://mcp.hindsight.devcraft.team'). When set, skips local daemon and connects directly to this API.",
"format": "uri"
},
"hindsightApiToken": {
"type": "string",
"description": "API token for external Hindsight API authentication. Required if the external API has authentication enabled."
}
},
"additionalProperties": false
@ -86,6 +95,14 @@
"apiPort": {
"label": "API Port",
"placeholder": "9077 (default)"
},
"hindsightApiUrl": {
"label": "External Hindsight API URL",
"placeholder": "e.g. https://mcp.hindsight.devcraft.team (leave empty for local daemon)"
},
"hindsightApiToken": {
"label": "External API Token",
"placeholder": "API token if external API requires authentication"
}
}
}

View file

@ -9,6 +9,7 @@ let embedManager: HindsightEmbedManager | null = null;
let client: HindsightClient | null = null;
let initPromise: Promise<void> | null = null;
let isInitialized = false;
let usingExternalApi = false; // Track if using external API (skip daemon management)
// Global access for hooks (Moltbot loads hooks separately)
if (typeof global !== 'undefined') {
@ -148,6 +149,37 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
);
}
/**
* Detect external Hindsight API configuration.
* Priority: env vars > plugin config
*/
function detectExternalApi(pluginConfig?: PluginConfig): {
apiUrl: string | null;
apiToken: string | null;
} {
const apiUrl = process.env.HINDSIGHT_EMBED_API_URL || pluginConfig?.hindsightApiUrl || null;
const apiToken = process.env.HINDSIGHT_EMBED_API_TOKEN || pluginConfig?.hindsightApiToken || null;
return { apiUrl, apiToken };
}
/**
* Health check for external Hindsight API.
*/
async function checkExternalApiHealth(apiUrl: string): Promise<void> {
const healthUrl = `${apiUrl.replace(/\/$/, '')}/health`;
console.log(`[Hindsight] Checking external API health at ${healthUrl}...`);
try {
const response = await fetch(healthUrl, { signal: AbortSignal.timeout(10000) });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json() as { status?: string };
console.log(`[Hindsight] External API health: ${JSON.stringify(data)}`);
} catch (error) {
throw new Error(`Cannot connect to external Hindsight API at ${apiUrl}: ${error}`);
}
}
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.';
@ -161,6 +193,8 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
llmProvider: config.llmProvider,
llmModel: config.llmModel,
llmApiKeyEnv: config.llmApiKeyEnv,
hindsightApiUrl: config.hindsightApiUrl,
hindsightApiToken: config.hindsightApiToken,
};
}
@ -187,49 +221,88 @@ export default function (api: MoltbotPluginAPI) {
if (pluginConfig.bankMission) {
console.log(`[Hindsight] Custom bank mission configured: "${pluginConfig.bankMission.substring(0, 50)}..."`);
}
console.log(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`);
// Detect external API mode
const externalApi = detectExternalApi(pluginConfig);
// Get API port from config (default: 9077)
const apiPort = pluginConfig.apiPort || 9077;
console.log(`[Hindsight] API Port: ${apiPort}`);
if (externalApi.apiUrl) {
// External API mode - skip local daemon
usingExternalApi = true;
console.log(`[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;
console.log('[Hindsight] API token configured');
}
} else {
console.log(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`);
console.log(`[Hindsight] API Port: ${apiPort}`);
}
// Initialize in background (non-blocking)
console.log('[Hindsight] Starting initialization in background...');
initPromise = (async () => {
try {
// Initialize embed manager
console.log('[Hindsight] Creating HindsightEmbedManager...');
embedManager = new HindsightEmbedManager(
apiPort,
llmConfig.provider,
llmConfig.apiKey,
llmConfig.model,
llmConfig.baseUrl,
pluginConfig.daemonIdleTimeout,
pluginConfig.embedVersion,
pluginConfig.embedPackagePath
);
if (usingExternalApi && externalApi.apiUrl) {
// External API mode - check health, skip daemon startup
console.log('[Hindsight] External API mode - skipping local daemon...');
await checkExternalApiHealth(externalApi.apiUrl);
// Start the embedded server
console.log('[Hindsight] Starting embedded server...');
await embedManager.start();
// Initialize client (CLI commands will use external API via env vars)
console.log('[Hindsight] Creating HindsightClient...');
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
// Initialize client
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);
// Use openclaw bank
console.log(`[Hindsight] Using bank: ${BANK_NAME}`);
client.setBankId(BANK_NAME);
// Set bank mission
if (pluginConfig.bankMission) {
console.log(`[Hindsight] Setting bank mission...`);
await client.setBankMission(pluginConfig.bankMission);
}
// Set bank mission
if (pluginConfig.bankMission) {
console.log(`[Hindsight] Setting bank mission...`);
await client.setBankMission(pluginConfig.bankMission);
isInitialized = true;
console.log('[Hindsight] ✓ Ready (external API mode)');
} else {
// Local daemon mode - start hindsight-embed daemon
console.log('[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
console.log('[Hindsight] Starting embedded server...');
await embedManager.start();
// Initialize client
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 bank mission
if (pluginConfig.bankMission) {
console.log(`[Hindsight] Setting bank mission...`);
await client.setBankMission(pluginConfig.bankMission);
}
isInitialized = true;
console.log('[Hindsight] ✓ Ready');
}
isInitialized = true;
console.log('[Hindsight] ✓ Ready');
} catch (error) {
console.error('[Hindsight] Initialization error:', error);
throw error;
@ -243,7 +316,7 @@ export default function (api: MoltbotPluginAPI) {
api.registerService({
id: 'hindsight-memory',
async start() {
console.log('[Hindsight] Service start called - checking daemon health...');
console.log('[Hindsight] Service start called...');
// Wait for background init if still pending
if (initPromise) {
@ -255,50 +328,90 @@ export default function (api: MoltbotPluginAPI) {
}
}
// Check if daemon is actually healthy (handles SIGUSR1 restart case)
if (embedManager && isInitialized) {
const healthy = await embedManager.checkHealth();
if (healthy) {
console.log('[Hindsight] Daemon is healthy');
return;
// External API mode: check external API health
if (usingExternalApi) {
const externalApi = detectExternalApi(pluginConfig);
if (externalApi.apiUrl && isInitialized) {
try {
await checkExternalApiHealth(externalApi.apiUrl);
console.log('[Hindsight] External API is healthy');
return;
} catch (error) {
console.error('[Hindsight] External API health check failed:', error);
// Reset state for reinitialization attempt
client = null;
isInitialized = false;
}
}
} else {
// Local daemon mode: check daemon health (handles SIGUSR1 restart case)
if (embedManager && isInitialized) {
const healthy = await embedManager.checkHealth();
if (healthy) {
console.log('[Hindsight] Daemon is healthy');
return;
}
console.log('[Hindsight] Daemon is not responding - reinitializing...');
// Reset state for reinitialization
embedManager = null;
client = null;
isInitialized = false;
console.log('[Hindsight] Daemon is not responding - reinitializing...');
// Reset state for reinitialization
embedManager = null;
client = null;
isInitialized = false;
}
}
// Reinitialize if needed (fresh start or recovery from dead daemon)
// Reinitialize if needed (fresh start or recovery)
if (!isInitialized) {
console.log('[Hindsight] Reinitializing daemon...');
const pluginConfig = getPluginConfig(api);
const llmConfig = detectLLMConfig(pluginConfig);
const apiPort = pluginConfig.apiPort || 9077;
console.log('[Hindsight] Reinitializing...');
const reinitPluginConfig = getPluginConfig(api);
const llmConfig = detectLLMConfig(reinitPluginConfig);
const externalApi = detectExternalApi(reinitPluginConfig);
const apiPort = reinitPluginConfig.apiPort || 9077;
embedManager = new HindsightEmbedManager(
apiPort,
llmConfig.provider,
llmConfig.apiKey,
llmConfig.model,
llmConfig.baseUrl,
pluginConfig.daemonIdleTimeout,
pluginConfig.embedVersion,
pluginConfig.embedPackagePath
);
if (externalApi.apiUrl) {
// External API mode
usingExternalApi = true;
process.env.HINDSIGHT_EMBED_API_URL = externalApi.apiUrl;
if (externalApi.apiToken) {
process.env.HINDSIGHT_EMBED_API_TOKEN = externalApi.apiToken;
}
await embedManager.start();
await checkExternalApiHealth(externalApi.apiUrl);
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
client.setBankId(BANK_NAME);
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath);
client.setBankId(BANK_NAME);
if (pluginConfig.bankMission) {
await client.setBankMission(pluginConfig.bankMission);
if (reinitPluginConfig.bankMission) {
await client.setBankMission(reinitPluginConfig.bankMission);
}
isInitialized = true;
console.log('[Hindsight] Reinitialization complete (external API mode)');
} else {
// Local daemon mode
embedManager = new HindsightEmbedManager(
apiPort,
llmConfig.provider,
llmConfig.apiKey,
llmConfig.model,
llmConfig.baseUrl,
reinitPluginConfig.daemonIdleTimeout,
reinitPluginConfig.embedVersion,
reinitPluginConfig.embedPackagePath
);
await embedManager.start();
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath);
client.setBankId(BANK_NAME);
if (reinitPluginConfig.bankMission) {
await client.setBankMission(reinitPluginConfig.bankMission);
}
isInitialized = true;
console.log('[Hindsight] Reinitialization complete');
}
isInitialized = true;
console.log('[Hindsight] Reinitialization complete');
}
},
@ -306,7 +419,8 @@ export default function (api: MoltbotPluginAPI) {
try {
console.log('[Hindsight] Service stopping...');
if (embedManager) {
// Only stop daemon if in local mode
if (!usingExternalApi && embedManager) {
await embedManager.stop();
embedManager = null;
}

View file

@ -37,6 +37,8 @@ export interface PluginConfig {
llmModel?: string; // LLM model override (e.g. 'gpt-4o-mini', 'claude-3-5-haiku-20241022')
llmApiKeyEnv?: string; // Env var name holding the API key (e.g. 'MY_CUSTOM_KEY')
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
}
export interface ServiceConfig {