@vectorize-io/hindsight-openclaw 0.4.8 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -1,4 +1,27 @@
1
1
  import type { RetainRequest, RetainResponse, RecallRequest, RecallResponse } from './types.js';
2
+ /**
3
+ * Escape a string for use as a single-quoted shell argument.
4
+ *
5
+ * In POSIX shells, single-quoted strings treat ALL characters literally
6
+ * except for the single quote itself. To include a literal single quote,
7
+ * we use the pattern: end quote + escaped quote + start quote = '\''
8
+ *
9
+ * Example: "It's $100" becomes 'It'\''s $100'
10
+ * Shell interprets: 'It' + \' + 's $100' = It's $100
11
+ *
12
+ * This handles ALL shell-special characters including:
13
+ * - $ (variable expansion)
14
+ * - ` (command substitution)
15
+ * - ! (history expansion)
16
+ * - ? * [ ] (glob patterns)
17
+ * - ( ) { } (subshell/brace expansion)
18
+ * - < > | & ; (redirection/control)
19
+ * - \ " # ~ newlines
20
+ *
21
+ * @param arg - The string to escape
22
+ * @returns The escaped string (without surrounding quotes - caller adds those)
23
+ */
24
+ export declare function escapeShellArg(arg: string): string;
2
25
  export declare class HindsightClient {
3
26
  private bankId;
4
27
  private llmProvider;
package/dist/client.js CHANGED
@@ -1,6 +1,34 @@
1
1
  import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  const execAsync = promisify(exec);
4
+ /**
5
+ * Escape a string for use as a single-quoted shell argument.
6
+ *
7
+ * In POSIX shells, single-quoted strings treat ALL characters literally
8
+ * except for the single quote itself. To include a literal single quote,
9
+ * we use the pattern: end quote + escaped quote + start quote = '\''
10
+ *
11
+ * Example: "It's $100" becomes 'It'\''s $100'
12
+ * Shell interprets: 'It' + \' + 's $100' = It's $100
13
+ *
14
+ * This handles ALL shell-special characters including:
15
+ * - $ (variable expansion)
16
+ * - ` (command substitution)
17
+ * - ! (history expansion)
18
+ * - ? * [ ] (glob patterns)
19
+ * - ( ) { } (subshell/brace expansion)
20
+ * - < > | & ; (redirection/control)
21
+ * - \ " # ~ newlines
22
+ *
23
+ * @param arg - The string to escape
24
+ * @returns The escaped string (without surrounding quotes - caller adds those)
25
+ */
26
+ export function escapeShellArg(arg) {
27
+ // Replace single quotes with the escape sequence: '\''
28
+ // This ends the current single-quoted string, adds an escaped literal quote,
29
+ // and starts a new single-quoted string.
30
+ return arg.replace(/'/g, "'\\''");
31
+ }
4
32
  export class HindsightClient {
5
33
  bankId = 'default'; // Always use default bank
6
34
  llmProvider;
@@ -36,7 +64,7 @@ export class HindsightClient {
36
64
  if (!mission || mission.trim().length === 0) {
37
65
  return;
38
66
  }
39
- const escapedMission = mission.replace(/'/g, "'\\''"); // Escape single quotes
67
+ const escapedMission = escapeShellArg(mission);
40
68
  const embedCmd = this.getEmbedCommandPrefix();
41
69
  const cmd = `${embedCmd} --profile openclaw bank mission ${this.bankId} '${escapedMission}'`;
42
70
  try {
@@ -49,8 +77,8 @@ export class HindsightClient {
49
77
  }
50
78
  }
51
79
  async retain(request) {
52
- const content = request.content.replace(/'/g, "'\\''"); // Escape single quotes
53
- const docId = request.document_id || 'conversation';
80
+ const content = escapeShellArg(request.content);
81
+ const docId = escapeShellArg(request.document_id || 'conversation');
54
82
  const embedCmd = this.getEmbedCommandPrefix();
55
83
  const cmd = `${embedCmd} --profile openclaw memory retain ${this.bankId} '${content}' --doc-id '${docId}' --async`;
56
84
  try {
@@ -68,7 +96,7 @@ export class HindsightClient {
68
96
  }
69
97
  }
70
98
  async recall(request) {
71
- const query = request.query.replace(/'/g, "'\\''"); // Escape single quotes
99
+ const query = escapeShellArg(request.query);
72
100
  const maxTokens = request.max_tokens || 1024;
73
101
  const embedCmd = this.getEmbedCommandPrefix();
74
102
  const cmd = `${embedCmd} --profile openclaw memory recall ${this.bankId} '${query}' --output json --max-tokens ${maxTokens}`;
package/dist/index.js CHANGED
@@ -7,6 +7,11 @@ let embedManager = null;
7
7
  let client = null;
8
8
  let initPromise = null;
9
9
  let isInitialized = false;
10
+ let usingExternalApi = false; // Track if using external API (skip daemon management)
11
+ // Store the current plugin config for bank ID derivation
12
+ let currentPluginConfig = null;
13
+ // Track which banks have had their mission set (to avoid re-setting on every request)
14
+ const banksWithMissionSet = new Set();
10
15
  // Global access for hooks (Moltbot loads hooks separately)
11
16
  if (typeof global !== 'undefined') {
12
17
  global.__hindsightClient = {
@@ -17,13 +22,59 @@ if (typeof global !== 'undefined') {
17
22
  if (initPromise)
18
23
  await initPromise;
19
24
  },
25
+ /**
26
+ * Get a client configured for a specific agent context.
27
+ * Derives the bank ID from the context for per-channel isolation.
28
+ * Also ensures the bank mission is set on first use.
29
+ */
30
+ getClientForContext: async (ctx) => {
31
+ if (!client)
32
+ return null;
33
+ const config = currentPluginConfig || {};
34
+ const bankId = deriveBankId(ctx, config);
35
+ client.setBankId(bankId);
36
+ // Set bank mission on first use of this bank (if configured)
37
+ if (config.bankMission && config.dynamicBankId && !banksWithMissionSet.has(bankId)) {
38
+ try {
39
+ await client.setBankMission(config.bankMission);
40
+ banksWithMissionSet.add(bankId);
41
+ console.log(`[Hindsight] Set mission for new bank: ${bankId}`);
42
+ }
43
+ catch (error) {
44
+ // Log but don't fail - bank mission is not critical
45
+ console.warn(`[Hindsight] Could not set bank mission for ${bankId}: ${error}`);
46
+ }
47
+ }
48
+ return client;
49
+ },
50
+ getPluginConfig: () => currentPluginConfig,
20
51
  };
21
52
  }
22
53
  // Get directory of current module
23
54
  const __filename = fileURLToPath(import.meta.url);
24
55
  const __dirname = dirname(__filename);
25
- // Default bank name
26
- const BANK_NAME = 'openclaw';
56
+ // Default bank name (fallback when channel context not available)
57
+ const DEFAULT_BANK_NAME = 'openclaw';
58
+ /**
59
+ * Derive a bank ID from the agent context.
60
+ * Creates channel-specific banks: {messageProvider}-{channelId}
61
+ * Falls back to default bank when context is unavailable.
62
+ */
63
+ function deriveBankId(ctx, pluginConfig) {
64
+ // If dynamic bank ID is disabled, use static bank
65
+ if (pluginConfig.dynamicBankId === false) {
66
+ return pluginConfig.bankIdPrefix
67
+ ? `${pluginConfig.bankIdPrefix}-${DEFAULT_BANK_NAME}`
68
+ : DEFAULT_BANK_NAME;
69
+ }
70
+ const channelType = ctx?.messageProvider || 'unknown';
71
+ const channelId = ctx?.channelId || 'default';
72
+ // Build bank ID: {prefix?}-{channelType}-{channelId}
73
+ const baseBankId = `${channelType}-${channelId}`;
74
+ return pluginConfig.bankIdPrefix
75
+ ? `${pluginConfig.bankIdPrefix}-${baseBankId}`
76
+ : baseBankId;
77
+ }
27
78
  // Provider detection from standard env vars
28
79
  const PROVIDER_DETECTION = [
29
80
  { name: 'openai', keyEnv: 'OPENAI_API_KEY', defaultModel: 'gpt-4o-mini' },
@@ -121,6 +172,33 @@ function detectLLMConfig(pluginConfig) {
121
172
  ` export HINDSIGHT_API_LLM_BASE_URL=https://openrouter.ai/api/v1 # Optional\n\n` +
122
173
  `Tip: Use a cheap/fast model for memory extraction (e.g., gpt-4o-mini, claude-3-5-haiku, or free models on OpenRouter)`);
123
174
  }
175
+ /**
176
+ * Detect external Hindsight API configuration.
177
+ * Priority: env vars > plugin config
178
+ */
179
+ function detectExternalApi(pluginConfig) {
180
+ const apiUrl = process.env.HINDSIGHT_EMBED_API_URL || pluginConfig?.hindsightApiUrl || null;
181
+ const apiToken = process.env.HINDSIGHT_EMBED_API_TOKEN || pluginConfig?.hindsightApiToken || null;
182
+ return { apiUrl, apiToken };
183
+ }
184
+ /**
185
+ * Health check for external Hindsight API.
186
+ */
187
+ async function checkExternalApiHealth(apiUrl) {
188
+ const healthUrl = `${apiUrl.replace(/\/$/, '')}/health`;
189
+ console.log(`[Hindsight] Checking external API health at ${healthUrl}...`);
190
+ try {
191
+ const response = await fetch(healthUrl, { signal: AbortSignal.timeout(10000) });
192
+ if (!response.ok) {
193
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
194
+ }
195
+ const data = await response.json();
196
+ console.log(`[Hindsight] External API health: ${JSON.stringify(data)}`);
197
+ }
198
+ catch (error) {
199
+ throw new Error(`Cannot connect to external Hindsight API at ${apiUrl}: ${error}`);
200
+ }
201
+ }
124
202
  function getPluginConfig(api) {
125
203
  const config = api.config.plugins?.entries?.['hindsight-openclaw']?.config || {};
126
204
  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.';
@@ -133,6 +211,12 @@ function getPluginConfig(api) {
133
211
  llmProvider: config.llmProvider,
134
212
  llmModel: config.llmModel,
135
213
  llmApiKeyEnv: config.llmApiKeyEnv,
214
+ hindsightApiUrl: config.hindsightApiUrl,
215
+ hindsightApiToken: config.hindsightApiToken,
216
+ apiPort: config.apiPort || 9077,
217
+ // Dynamic bank ID options (default: enabled)
218
+ dynamicBankId: config.dynamicBankId !== false,
219
+ bankIdPrefix: config.bankIdPrefix,
136
220
  };
137
221
  }
138
222
  export default function (api) {
@@ -141,6 +225,8 @@ export default function (api) {
141
225
  // Get plugin config first (needed for LLM detection)
142
226
  console.log('[Hindsight] Getting plugin config...');
143
227
  const pluginConfig = getPluginConfig(api);
228
+ // Store config globally for bank ID derivation in hooks
229
+ currentPluginConfig = pluginConfig;
144
230
  // Detect LLM configuration (env vars > plugin config > auto-detect)
145
231
  console.log('[Hindsight] Detecting LLM config...');
146
232
  const llmConfig = detectLLMConfig(pluginConfig);
@@ -155,33 +241,80 @@ export default function (api) {
155
241
  if (pluginConfig.bankMission) {
156
242
  console.log(`[Hindsight] Custom bank mission configured: "${pluginConfig.bankMission.substring(0, 50)}..."`);
157
243
  }
158
- console.log(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`);
244
+ // Log dynamic bank ID mode
245
+ if (pluginConfig.dynamicBankId) {
246
+ const prefixInfo = pluginConfig.bankIdPrefix ? ` (prefix: ${pluginConfig.bankIdPrefix})` : '';
247
+ console.log(`[Hindsight] ✓ Dynamic bank IDs enabled${prefixInfo} - each channel gets isolated memory`);
248
+ }
249
+ else {
250
+ console.log(`[Hindsight] Dynamic bank IDs disabled - using static bank: ${DEFAULT_BANK_NAME}`);
251
+ }
252
+ // Detect external API mode
253
+ const externalApi = detectExternalApi(pluginConfig);
159
254
  // Get API port from config (default: 9077)
160
255
  const apiPort = pluginConfig.apiPort || 9077;
161
- console.log(`[Hindsight] API Port: ${apiPort}`);
256
+ if (externalApi.apiUrl) {
257
+ // External API mode - skip local daemon
258
+ usingExternalApi = true;
259
+ console.log(`[Hindsight] ✓ Using external API: ${externalApi.apiUrl}`);
260
+ // Set env vars so CLI commands (uvx hindsight-embed) use external API
261
+ process.env.HINDSIGHT_EMBED_API_URL = externalApi.apiUrl;
262
+ if (externalApi.apiToken) {
263
+ process.env.HINDSIGHT_EMBED_API_TOKEN = externalApi.apiToken;
264
+ console.log('[Hindsight] API token configured');
265
+ }
266
+ }
267
+ else {
268
+ console.log(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`);
269
+ console.log(`[Hindsight] API Port: ${apiPort}`);
270
+ }
162
271
  // Initialize in background (non-blocking)
163
272
  console.log('[Hindsight] Starting initialization in background...');
164
273
  initPromise = (async () => {
165
274
  try {
166
- // Initialize embed manager
167
- console.log('[Hindsight] Creating HindsightEmbedManager...');
168
- embedManager = new HindsightEmbedManager(apiPort, llmConfig.provider, llmConfig.apiKey, llmConfig.model, llmConfig.baseUrl, pluginConfig.daemonIdleTimeout, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
169
- // Start the embedded server
170
- console.log('[Hindsight] Starting embedded server...');
171
- await embedManager.start();
172
- // Initialize client
173
- console.log('[Hindsight] Creating HindsightClient...');
174
- client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
175
- // Use openclaw bank
176
- console.log(`[Hindsight] Using bank: ${BANK_NAME}`);
177
- client.setBankId(BANK_NAME);
178
- // Set bank mission
179
- if (pluginConfig.bankMission) {
180
- console.log(`[Hindsight] Setting bank mission...`);
181
- await client.setBankMission(pluginConfig.bankMission);
275
+ if (usingExternalApi && externalApi.apiUrl) {
276
+ // External API mode - check health, skip daemon startup
277
+ console.log('[Hindsight] External API mode - skipping local daemon...');
278
+ await checkExternalApiHealth(externalApi.apiUrl);
279
+ // Initialize client (CLI commands will use external API via env vars)
280
+ console.log('[Hindsight] Creating HindsightClient...');
281
+ client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
282
+ // Set default bank (will be overridden per-request when dynamic bank IDs are enabled)
283
+ const defaultBankId = deriveBankId(undefined, pluginConfig);
284
+ console.log(`[Hindsight] Default bank: ${defaultBankId}`);
285
+ client.setBankId(defaultBankId);
286
+ // Note: Bank mission will be set per-bank when dynamic bank IDs are enabled
287
+ // For now, set it on the default bank
288
+ if (pluginConfig.bankMission && !pluginConfig.dynamicBankId) {
289
+ console.log(`[Hindsight] Setting bank mission...`);
290
+ await client.setBankMission(pluginConfig.bankMission);
291
+ }
292
+ isInitialized = true;
293
+ console.log('[Hindsight] ✓ Ready (external API mode)');
294
+ }
295
+ else {
296
+ // Local daemon mode - start hindsight-embed daemon
297
+ console.log('[Hindsight] Creating HindsightEmbedManager...');
298
+ embedManager = new HindsightEmbedManager(apiPort, llmConfig.provider, llmConfig.apiKey, llmConfig.model, llmConfig.baseUrl, pluginConfig.daemonIdleTimeout, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
299
+ // Start the embedded server
300
+ console.log('[Hindsight] Starting embedded server...');
301
+ await embedManager.start();
302
+ // Initialize client
303
+ console.log('[Hindsight] Creating HindsightClient...');
304
+ client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
305
+ // Set default bank (will be overridden per-request when dynamic bank IDs are enabled)
306
+ const defaultBankId = deriveBankId(undefined, pluginConfig);
307
+ console.log(`[Hindsight] Default bank: ${defaultBankId}`);
308
+ client.setBankId(defaultBankId);
309
+ // Note: Bank mission will be set per-bank when dynamic bank IDs are enabled
310
+ // For now, set it on the default bank
311
+ if (pluginConfig.bankMission && !pluginConfig.dynamicBankId) {
312
+ console.log(`[Hindsight] Setting bank mission...`);
313
+ await client.setBankMission(pluginConfig.bankMission);
314
+ }
315
+ isInitialized = true;
316
+ console.log('[Hindsight] ✓ Ready');
182
317
  }
183
- isInitialized = true;
184
- console.log('[Hindsight] ✓ Ready');
185
318
  }
186
319
  catch (error) {
187
320
  console.error('[Hindsight] Initialization error:', error);
@@ -194,7 +327,7 @@ export default function (api) {
194
327
  api.registerService({
195
328
  id: 'hindsight-memory',
196
329
  async start() {
197
- console.log('[Hindsight] Service start called - checking daemon health...');
330
+ console.log('[Hindsight] Service start called...');
198
331
  // Wait for background init if still pending
199
332
  if (initPromise) {
200
333
  try {
@@ -205,40 +338,83 @@ export default function (api) {
205
338
  // Continue to health check below
206
339
  }
207
340
  }
208
- // Check if daemon is actually healthy (handles SIGUSR1 restart case)
209
- if (embedManager && isInitialized) {
210
- const healthy = await embedManager.checkHealth();
211
- if (healthy) {
212
- console.log('[Hindsight] Daemon is healthy');
213
- return;
341
+ // External API mode: check external API health
342
+ if (usingExternalApi) {
343
+ const externalApi = detectExternalApi(pluginConfig);
344
+ if (externalApi.apiUrl && isInitialized) {
345
+ try {
346
+ await checkExternalApiHealth(externalApi.apiUrl);
347
+ console.log('[Hindsight] External API is healthy');
348
+ return;
349
+ }
350
+ catch (error) {
351
+ console.error('[Hindsight] External API health check failed:', error);
352
+ // Reset state for reinitialization attempt
353
+ client = null;
354
+ isInitialized = false;
355
+ }
356
+ }
357
+ }
358
+ else {
359
+ // Local daemon mode: check daemon health (handles SIGUSR1 restart case)
360
+ if (embedManager && isInitialized) {
361
+ const healthy = await embedManager.checkHealth();
362
+ if (healthy) {
363
+ console.log('[Hindsight] Daemon is healthy');
364
+ return;
365
+ }
366
+ console.log('[Hindsight] Daemon is not responding - reinitializing...');
367
+ // Reset state for reinitialization
368
+ embedManager = null;
369
+ client = null;
370
+ isInitialized = false;
214
371
  }
215
- console.log('[Hindsight] Daemon is not responding - reinitializing...');
216
- // Reset state for reinitialization
217
- embedManager = null;
218
- client = null;
219
- isInitialized = false;
220
372
  }
221
- // Reinitialize if needed (fresh start or recovery from dead daemon)
373
+ // Reinitialize if needed (fresh start or recovery)
222
374
  if (!isInitialized) {
223
- console.log('[Hindsight] Reinitializing daemon...');
224
- const pluginConfig = getPluginConfig(api);
225
- const llmConfig = detectLLMConfig(pluginConfig);
226
- const apiPort = pluginConfig.apiPort || 9077;
227
- embedManager = new HindsightEmbedManager(apiPort, llmConfig.provider, llmConfig.apiKey, llmConfig.model, llmConfig.baseUrl, pluginConfig.daemonIdleTimeout, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
228
- await embedManager.start();
229
- client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
230
- client.setBankId(BANK_NAME);
231
- if (pluginConfig.bankMission) {
232
- await client.setBankMission(pluginConfig.bankMission);
375
+ console.log('[Hindsight] Reinitializing...');
376
+ const reinitPluginConfig = getPluginConfig(api);
377
+ currentPluginConfig = reinitPluginConfig;
378
+ const llmConfig = detectLLMConfig(reinitPluginConfig);
379
+ const externalApi = detectExternalApi(reinitPluginConfig);
380
+ const apiPort = reinitPluginConfig.apiPort || 9077;
381
+ if (externalApi.apiUrl) {
382
+ // External API mode
383
+ usingExternalApi = true;
384
+ process.env.HINDSIGHT_EMBED_API_URL = externalApi.apiUrl;
385
+ if (externalApi.apiToken) {
386
+ process.env.HINDSIGHT_EMBED_API_TOKEN = externalApi.apiToken;
387
+ }
388
+ await checkExternalApiHealth(externalApi.apiUrl);
389
+ client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath);
390
+ const defaultBankId = deriveBankId(undefined, reinitPluginConfig);
391
+ client.setBankId(defaultBankId);
392
+ if (reinitPluginConfig.bankMission && !reinitPluginConfig.dynamicBankId) {
393
+ await client.setBankMission(reinitPluginConfig.bankMission);
394
+ }
395
+ isInitialized = true;
396
+ console.log('[Hindsight] Reinitialization complete (external API mode)');
397
+ }
398
+ else {
399
+ // Local daemon mode
400
+ embedManager = new HindsightEmbedManager(apiPort, llmConfig.provider, llmConfig.apiKey, llmConfig.model, llmConfig.baseUrl, reinitPluginConfig.daemonIdleTimeout, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath);
401
+ await embedManager.start();
402
+ client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, reinitPluginConfig.embedVersion, reinitPluginConfig.embedPackagePath);
403
+ const defaultBankId = deriveBankId(undefined, reinitPluginConfig);
404
+ client.setBankId(defaultBankId);
405
+ if (reinitPluginConfig.bankMission && !reinitPluginConfig.dynamicBankId) {
406
+ await client.setBankMission(reinitPluginConfig.bankMission);
407
+ }
408
+ isInitialized = true;
409
+ console.log('[Hindsight] Reinitialization complete');
233
410
  }
234
- isInitialized = true;
235
- console.log('[Hindsight] Reinitialization complete');
236
411
  }
237
412
  },
238
413
  async stop() {
239
414
  try {
240
415
  console.log('[Hindsight] Service stopping...');
241
- if (embedManager) {
416
+ // Only stop daemon if in local mode
417
+ if (!usingExternalApi && embedManager) {
242
418
  await embedManager.stop();
243
419
  embedManager = null;
244
420
  }
@@ -253,28 +429,46 @@ export default function (api) {
253
429
  },
254
430
  });
255
431
  console.log('[Hindsight] Plugin loaded successfully');
256
- // Register agent_end hook for auto-retention
257
- console.log('[Hindsight] Registering agent_end hook...');
258
- // Store session key for retention
432
+ // Register agent hooks for auto-recall and auto-retention
433
+ console.log('[Hindsight] Registering agent hooks...');
434
+ // Store session key and context for retention
259
435
  let currentSessionKey;
436
+ let currentAgentContext;
260
437
  // Auto-recall: Inject relevant memories before agent processes the message
261
- api.on('before_agent_start', async (context) => {
438
+ // Hook signature: (event, ctx) where event has {prompt, messages?} and ctx has agent context
439
+ api.on('before_agent_start', async (event, ctx) => {
262
440
  try {
263
- // Capture session key
264
- if (context.sessionKey) {
265
- currentSessionKey = context.sessionKey;
266
- console.log('[Hindsight] Captured session key:', currentSessionKey);
441
+ // Capture session key and context for use in agent_end
442
+ if (ctx?.sessionKey) {
443
+ currentSessionKey = ctx.sessionKey;
267
444
  }
445
+ currentAgentContext = ctx;
446
+ // Derive bank ID from context
447
+ const bankId = deriveBankId(ctx, pluginConfig);
448
+ console.log(`[Hindsight] before_agent_start - bank: ${bankId}, channel: ${ctx?.messageProvider}/${ctx?.channelId}`);
268
449
  // Get the user's latest message for recall
269
- let prompt = context.prompt;
450
+ // Prefer rawMessage (clean user text) over prompt (envelope-formatted)
451
+ let prompt = event.rawMessage ?? event.prompt;
270
452
  if (!prompt || typeof prompt !== 'string' || prompt.length < 5) {
271
453
  return; // Skip very short messages
272
454
  }
273
- // Extract actual message from Telegram format: [Telegram ... GMT+1] actual message
274
- const telegramMatch = prompt.match(/\[Telegram[^\]]+\]\s*(.+)$/);
275
- if (telegramMatch) {
276
- prompt = telegramMatch[1].trim();
455
+ // Strip envelope-formatted prompts from any channel
456
+ // The prompt may contain: System: lines, abort hints, [Channel ...] header, [from: ...] suffix
457
+ let cleaned = prompt;
458
+ // Remove leading "System: ..." lines (from prependSystemEvents)
459
+ cleaned = cleaned.replace(/^(?:System:.*\n)+\n?/, '');
460
+ // Remove session abort hint
461
+ cleaned = cleaned.replace(/^Note: The previous agent run was aborted[^\n]*\n\n/, '');
462
+ // Extract message after [ChannelName ...] envelope header
463
+ // Handles any channel: Telegram, Slack, Discord, WhatsApp, Signal, etc.
464
+ // Uses [\s\S]+ instead of .+ to support multiline messages
465
+ const envelopeMatch = cleaned.match(/\[[A-Z][A-Za-z]*(?:\s[^\]]+)?\]\s*([\s\S]+)$/);
466
+ if (envelopeMatch) {
467
+ cleaned = envelopeMatch[1];
277
468
  }
469
+ // Remove trailing [from: SenderName] metadata (group chats)
470
+ cleaned = cleaned.replace(/\n\[from:[^\]]*\]\s*$/, '');
471
+ prompt = cleaned.trim() || prompt;
278
472
  if (prompt.length < 5) {
279
473
  return; // Skip very short messages after extraction
280
474
  }
@@ -285,16 +479,17 @@ export default function (api) {
285
479
  return;
286
480
  }
287
481
  await clientGlobal.waitForReady();
288
- const client = clientGlobal.getClient();
482
+ // Get client configured for this context's bank (async to handle mission setup)
483
+ const client = await clientGlobal.getClientForContext(ctx);
289
484
  if (!client) {
290
485
  console.log('[Hindsight] Client not initialized, skipping auto-recall');
291
486
  return;
292
487
  }
293
- console.log('[Hindsight] Auto-recall for prompt:', prompt.substring(0, 50));
294
- // Recall relevant memories (up to 512 tokens)
488
+ console.log(`[Hindsight] Auto-recall for bank ${bankId}, prompt: ${prompt.substring(0, 50)}`);
489
+ // Recall relevant memories
295
490
  const response = await client.recall({
296
491
  query: prompt,
297
- max_tokens: 512,
492
+ max_tokens: 2048,
298
493
  });
299
494
  if (!response.results || response.results.length === 0) {
300
495
  console.log('[Hindsight] No memories found for auto-recall');
@@ -308,7 +503,7 @@ ${memoriesJson}
308
503
 
309
504
  User message: ${prompt}
310
505
  </hindsight_memories>`;
311
- console.log(`[Hindsight] Auto-recall: Injecting ${response.results.length} memories`);
506
+ console.log(`[Hindsight] Auto-recall: Injecting ${response.results.length} memories from bank ${bankId}`);
312
507
  // Inject context before the user message
313
508
  return { prependContext: contextMessage };
314
509
  }
@@ -317,9 +512,14 @@ User message: ${prompt}
317
512
  return;
318
513
  }
319
514
  });
320
- api.on('agent_end', async (event) => {
515
+ // Hook signature: (event, ctx) where event has {messages, success, error?, durationMs?}
516
+ api.on('agent_end', async (event, ctx) => {
321
517
  try {
322
- console.log('[Hindsight Hook] agent_end triggered');
518
+ // Use context from this hook, or fall back to context captured in before_agent_start
519
+ const effectiveCtx = ctx || currentAgentContext;
520
+ // Derive bank ID from context
521
+ const bankId = deriveBankId(effectiveCtx, pluginConfig);
522
+ console.log(`[Hindsight Hook] agent_end triggered - bank: ${bankId}`);
323
523
  // Check event success and messages
324
524
  if (!event.success || !Array.isArray(event.messages) || event.messages.length === 0) {
325
525
  console.log('[Hindsight Hook] Skipping: success:', event.success, 'messages:', event.messages?.length);
@@ -332,7 +532,8 @@ User message: ${prompt}
332
532
  return;
333
533
  }
334
534
  await clientGlobal.waitForReady();
335
- const client = clientGlobal.getClient();
535
+ // Get client configured for this context's bank (async to handle mission setup)
536
+ const client = await clientGlobal.getClientForContext(effectiveCtx);
336
537
  if (!client) {
337
538
  console.warn('[Hindsight] Client not initialized, skipping retain');
338
539
  return;
@@ -359,8 +560,9 @@ User message: ${prompt}
359
560
  console.log('[Hindsight Hook] Transcript too short, skipping');
360
561
  return;
361
562
  }
362
- // Use session key as document ID
363
- const documentId = currentSessionKey || 'default-session';
563
+ // Use unique document ID per conversation (sessionKey + timestamp)
564
+ // Static sessionKey (e.g. "agent:main:main") causes CASCADE delete of old memories
565
+ const documentId = `${effectiveCtx?.sessionKey || currentSessionKey || 'session'}-${Date.now()}`;
364
566
  // Retain to Hindsight
365
567
  await client.retain({
366
568
  content: transcript,
@@ -368,15 +570,18 @@ User message: ${prompt}
368
570
  metadata: {
369
571
  retained_at: new Date().toISOString(),
370
572
  message_count: event.messages.length,
573
+ channel_type: effectiveCtx?.messageProvider,
574
+ channel_id: effectiveCtx?.channelId,
575
+ sender_id: effectiveCtx?.senderId,
371
576
  },
372
577
  });
373
- console.log(`[Hindsight] Retained ${event.messages.length} messages for session ${documentId}`);
578
+ console.log(`[Hindsight] Retained ${event.messages.length} messages to bank ${bankId} for session ${documentId}`);
374
579
  }
375
580
  catch (error) {
376
581
  console.error('[Hindsight] Error retaining messages:', error);
377
582
  }
378
583
  });
379
- console.log('[Hindsight] Hook registered');
584
+ console.log('[Hindsight] Hooks registered');
380
585
  }
381
586
  catch (error) {
382
587
  console.error('[Hindsight] Plugin loading error:', error);
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export interface MoltbotPluginAPI {
2
2
  config: MoltbotConfig;
3
3
  registerService(config: ServiceConfig): void;
4
- on(event: string, handler: (context: any) => void | Promise<void | {
4
+ on(event: string, handler: (event: any, ctx?: any) => void | Promise<void | {
5
5
  prependContext?: string;
6
6
  }>): void;
7
7
  }
@@ -34,6 +34,10 @@ export interface PluginConfig {
34
34
  llmModel?: string;
35
35
  llmApiKeyEnv?: string;
36
36
  apiPort?: number;
37
+ hindsightApiUrl?: string;
38
+ hindsightApiToken?: string;
39
+ dynamicBankId?: boolean;
40
+ bankIdPrefix?: string;
37
41
  }
38
42
  export interface ServiceConfig {
39
43
  id: string;
@@ -46,6 +46,23 @@
46
46
  "type": "number",
47
47
  "description": "Port for the openclaw profile daemon (default: 9077)",
48
48
  "default": 9077
49
+ },
50
+ "hindsightApiUrl": {
51
+ "type": "string",
52
+ "description": "External Hindsight API URL (e.g. 'https://mcp.hindsight.devcraft.team'). When set, skips local daemon and connects directly to this API."
53
+ },
54
+ "hindsightApiToken": {
55
+ "type": "string",
56
+ "description": "API token for external Hindsight API authentication. Required if the external API has authentication enabled."
57
+ },
58
+ "dynamicBankId": {
59
+ "type": "boolean",
60
+ "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.",
61
+ "default": true
62
+ },
63
+ "bankIdPrefix": {
64
+ "type": "string",
65
+ "description": "Optional prefix for bank IDs (e.g., 'prod' results in 'prod-slack-C123'). Useful for separating environments."
49
66
  }
50
67
  },
51
68
  "additionalProperties": false
@@ -86,6 +103,22 @@
86
103
  "apiPort": {
87
104
  "label": "API Port",
88
105
  "placeholder": "9077 (default)"
106
+ },
107
+ "hindsightApiUrl": {
108
+ "label": "External Hindsight API URL",
109
+ "placeholder": "e.g. https://mcp.hindsight.devcraft.team (leave empty for local daemon)"
110
+ },
111
+ "hindsightApiToken": {
112
+ "label": "External API Token",
113
+ "placeholder": "API token if external API requires authentication"
114
+ },
115
+ "dynamicBankId": {
116
+ "label": "Dynamic Bank IDs",
117
+ "placeholder": "true (isolate memories per channel)"
118
+ },
119
+ "bankIdPrefix": {
120
+ "label": "Bank ID Prefix",
121
+ "placeholder": "e.g., prod, staging (optional)"
89
122
  }
90
123
  }
91
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectorize-io/hindsight-openclaw",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Hindsight memory plugin for OpenClaw - biomimetic long-term memory with fact extraction",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",