@vectorize-io/hindsight-openclaw 0.4.19 → 0.5.1

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.js CHANGED
@@ -4,6 +4,7 @@ import { writeFile, mkdir, rm } from 'fs/promises';
4
4
  import { tmpdir } from 'os';
5
5
  import { join } from 'path';
6
6
  import { randomBytes } from 'crypto';
7
+ import * as log from './logger.js';
7
8
  const execFileAsync = promisify(execFile);
8
9
  const MAX_BUFFER = 5 * 1024 * 1024; // 5 MB — large transcripts can exceed default 1 MB
9
10
  const DEFAULT_TIMEOUT_MS = 15_000;
@@ -78,10 +79,10 @@ export class HindsightClient {
78
79
  const body = await res.text().catch(() => '');
79
80
  throw new Error(`HTTP ${res.status}: ${body}`);
80
81
  }
81
- console.log(`[Hindsight] Bank mission set via HTTP`);
82
+ log.verbose('bank mission set via HTTP');
82
83
  }
83
84
  catch (error) {
84
- console.warn(`[Hindsight] Could not set bank mission (bank may not exist yet): ${error}`);
85
+ log.warn(`could not set bank mission (bank may not exist yet): ${error}`);
85
86
  }
86
87
  }
87
88
  async setBankMissionSubprocess(mission) {
@@ -89,11 +90,11 @@ export class HindsightClient {
89
90
  const args = [...baseArgs, '--profile', 'openclaw', 'bank', 'mission', this.bankId, sanitize(mission)];
90
91
  try {
91
92
  const { stdout } = await execFileAsync(cmd, args, { maxBuffer: MAX_BUFFER });
92
- console.log(`[Hindsight] Bank mission set: ${stdout.trim()}`);
93
+ log.verbose(`bank mission set: ${stdout.trim()}`);
93
94
  }
94
95
  catch (error) {
95
96
  // Don't fail if mission set fails - bank might not exist yet, will be created on first retain
96
- console.warn(`[Hindsight] Could not set bank mission (bank may not exist yet): ${error}`);
97
+ log.warn(`could not set bank mission (bank may not exist yet): ${error}`);
97
98
  }
98
99
  }
99
100
  // --- retain ---
@@ -124,7 +125,7 @@ export class HindsightClient {
124
125
  throw new Error(`Failed to retain memory (HTTP ${res.status}): ${text}`);
125
126
  }
126
127
  const data = await res.json();
127
- console.log(`[Hindsight] Retained via HTTP (async): ${JSON.stringify(data).substring(0, 200)}`);
128
+ log.verbose(`retained via HTTP (async): ${JSON.stringify(data).substring(0, 200)}`);
128
129
  return {
129
130
  message: 'Memory queued for background processing',
130
131
  document_id: request.document_id || 'conversation',
@@ -144,7 +145,7 @@ export class HindsightClient {
144
145
  const [cmd, ...baseArgs] = this.getEmbedCommand();
145
146
  const args = [...baseArgs, '--profile', 'openclaw', 'memory', 'retain-files', this.bankId, tempFile, '--async'];
146
147
  const { stdout } = await execFileAsync(cmd, args, { maxBuffer: MAX_BUFFER });
147
- console.log(`[Hindsight] Retained (async): ${stdout.trim()}`);
148
+ log.verbose(`retained (async): ${stdout.trim()}`);
148
149
  return {
149
150
  message: 'Memory queued for background processing',
150
151
  document_id: docId,
@@ -170,7 +171,7 @@ export class HindsightClient {
170
171
  // Defense-in-depth: truncate query to stay under API's 500-token limit
171
172
  const MAX_QUERY_CHARS = 800;
172
173
  const query = request.query.length > MAX_QUERY_CHARS
173
- ? (console.warn(`[Hindsight] Truncating recall query from ${request.query.length} to ${MAX_QUERY_CHARS} chars`),
174
+ ? (log.warn(`truncating recall query from ${request.query.length} to ${MAX_QUERY_CHARS} chars`),
174
175
  request.query.substring(0, MAX_QUERY_CHARS))
175
176
  : request.query;
176
177
  const body = {
package/dist/index.js CHANGED
@@ -3,11 +3,13 @@ import { HindsightClient } from './client.js';
3
3
  import { createHash } from 'crypto';
4
4
  import { dirname } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- // Debug logging: silent by default, enable with debug: true in plugin config
6
+ import * as log from './logger.js';
7
+ import { configureLogger, setApiLogger, stopLogger } from './logger.js';
8
+ // Debug logging: silent by default, enable with debug: true or logLevel: 'debug'
7
9
  let debugEnabled = false;
8
10
  const debug = (...args) => {
9
11
  if (debugEnabled)
10
- console.log(...args);
12
+ log.verbose(args.map(a => typeof a === 'string' ? a.replace(/^\[Hindsight\]\s*/, '') : String(a)).join(' '));
11
13
  };
12
14
  // Module-level state
13
15
  let embedManager = null;
@@ -26,7 +28,7 @@ const MAX_TRACKED_BANK_CLIENTS = 10_000;
26
28
  const inflightRecalls = new Map();
27
29
  const turnCountBySession = new Map();
28
30
  const MAX_TRACKED_SESSIONS = 10_000;
29
- const RECALL_TIMEOUT_MS = 10_000;
31
+ const DEFAULT_RECALL_TIMEOUT_MS = 10_000;
30
32
  // Cache sender IDs discovered in before_prompt_build (where event.prompt has the metadata
31
33
  // blocks) so agent_end can look them up — event.messages in agent_end is clean history.
32
34
  const senderIdBySession = new Map();
@@ -93,7 +95,7 @@ async function lazyReinit() {
93
95
  debug('[Hindsight] ✓ Lazy re-initialization succeeded');
94
96
  }
95
97
  catch (error) {
96
- console.warn(`[Hindsight] Lazy re-initialization failed (will retry in ${REINIT_COOLDOWN_MS / 1000}s):`, error instanceof Error ? error.message : error);
98
+ log.warn(`lazy re-init failed (retry in ${REINIT_COOLDOWN_MS / 1000}s): ${error instanceof Error ? error.message : error}`);
97
99
  }
98
100
  finally {
99
101
  isReinitInProgress = false;
@@ -160,7 +162,7 @@ if (typeof global !== 'undefined') {
160
162
  }
161
163
  catch (error) {
162
164
  // Log but don't fail - bank mission is not critical
163
- console.warn(`[Hindsight] Could not set bank mission for ${bankId}: ${error}`);
165
+ log.warn(`could not set bank mission for ${bankId}: ${error}`);
164
166
  }
165
167
  }
166
168
  return bankClient;
@@ -393,7 +395,7 @@ export function deriveBankId(ctx, pluginConfig) {
393
395
  const validFields = new Set(['agent', 'channel', 'user', 'provider']);
394
396
  for (const f of fields) {
395
397
  if (!validFields.has(f)) {
396
- console.warn(`[Hindsight] Unknown dynamicBankGranularity field "${f}" — will resolve to "unknown" in bank ID. Valid fields: agent, channel, user, provider`);
398
+ log.warn(`unknown dynamicBankGranularity field "${f}" — will resolve to "unknown". Valid: agent, channel, user, provider`);
397
399
  }
398
400
  }
399
401
  // Parse sessionKey as fallback when direct context fields are missing
@@ -428,13 +430,13 @@ export function formatMemories(results) {
428
430
  }
429
431
  // Provider detection from standard env vars
430
432
  const PROVIDER_DETECTION = [
431
- { name: 'openai', keyEnv: 'OPENAI_API_KEY', defaultModel: 'gpt-4o-mini' },
432
- { name: 'anthropic', keyEnv: 'ANTHROPIC_API_KEY', defaultModel: 'claude-3-5-haiku-20241022' },
433
- { name: 'gemini', keyEnv: 'GEMINI_API_KEY', defaultModel: 'gemini-2.5-flash' },
434
- { name: 'groq', keyEnv: 'GROQ_API_KEY', defaultModel: 'openai/gpt-oss-20b' },
435
- { name: 'ollama', keyEnv: '', defaultModel: 'llama3.2' },
436
- { name: 'openai-codex', keyEnv: '', defaultModel: 'gpt-5.2-codex' },
437
- { name: 'claude-code', keyEnv: '', defaultModel: 'claude-sonnet-4-5-20250929' },
433
+ { name: 'openai', keyEnv: 'OPENAI_API_KEY' },
434
+ { name: 'anthropic', keyEnv: 'ANTHROPIC_API_KEY' },
435
+ { name: 'gemini', keyEnv: 'GEMINI_API_KEY' },
436
+ { name: 'groq', keyEnv: 'GROQ_API_KEY' },
437
+ { name: 'ollama', keyEnv: '' },
438
+ { name: 'openai-codex', keyEnv: '' },
439
+ { name: 'claude-code', keyEnv: '' },
438
440
  ];
439
441
  function detectLLMConfig(pluginConfig) {
440
442
  // Override values from HINDSIGHT_API_LLM_* env vars (highest priority)
@@ -450,11 +452,10 @@ function detectLLMConfig(pluginConfig) {
450
452
  throw new Error(`HINDSIGHT_API_LLM_PROVIDER is set to "${overrideProvider}" but HINDSIGHT_API_LLM_API_KEY is not set.\n` +
451
453
  `Please set: export HINDSIGHT_API_LLM_API_KEY=your-api-key`);
452
454
  }
453
- const providerInfo = PROVIDER_DETECTION.find(p => p.name === overrideProvider);
454
455
  return {
455
456
  provider: overrideProvider,
456
457
  apiKey: overrideKey || '',
457
- model: overrideModel || (providerInfo?.defaultModel),
458
+ model: overrideModel,
458
459
  baseUrl: overrideBaseUrl,
459
460
  source: 'HINDSIGHT_API_LLM_PROVIDER override',
460
461
  };
@@ -481,7 +482,7 @@ function detectLLMConfig(pluginConfig) {
481
482
  return {
482
483
  provider: pluginConfig.llmProvider,
483
484
  apiKey,
484
- model: pluginConfig.llmModel || overrideModel || providerInfo?.defaultModel,
485
+ model: pluginConfig.llmModel || overrideModel,
485
486
  baseUrl: overrideBaseUrl,
486
487
  source: 'plugin config',
487
488
  };
@@ -498,8 +499,8 @@ function detectLLMConfig(pluginConfig) {
498
499
  return {
499
500
  provider: providerInfo.name,
500
501
  apiKey,
501
- model: overrideModel || providerInfo.defaultModel,
502
- baseUrl: overrideBaseUrl, // Only use explicit HINDSIGHT_API_LLM_BASE_URL
502
+ model: overrideModel,
503
+ baseUrl: overrideBaseUrl,
503
504
  source: `auto-detected from ${providerInfo.keyEnv}`,
504
505
  };
505
506
  }
@@ -518,21 +519,20 @@ function detectLLMConfig(pluginConfig) {
518
519
  }
519
520
  throw new Error(`No LLM configuration found for Hindsight memory plugin.\n\n` +
520
521
  `Option 1: Set a standard provider API key (auto-detect):\n` +
521
- ` export OPENAI_API_KEY=sk-your-key # Uses gpt-4o-mini\n` +
522
- ` export ANTHROPIC_API_KEY=your-key # Uses claude-3-5-haiku\n` +
523
- ` export GEMINI_API_KEY=your-key # Uses gemini-2.5-flash\n` +
524
- ` export GROQ_API_KEY=your-key # Uses openai/gpt-oss-20b\n\n` +
522
+ ` export OPENAI_API_KEY=sk-your-key\n` +
523
+ ` export ANTHROPIC_API_KEY=your-key\n` +
524
+ ` export GEMINI_API_KEY=your-key\n` +
525
+ ` export GROQ_API_KEY=your-key\n\n` +
525
526
  `Option 2: Use Codex or Claude Code (no API key needed):\n` +
526
527
  ` export HINDSIGHT_API_LLM_PROVIDER=openai-codex # Requires 'codex auth login'\n` +
527
528
  ` export HINDSIGHT_API_LLM_PROVIDER=claude-code # Requires Claude Code CLI\n\n` +
528
529
  `Option 3: Set llmProvider in openclaw.json plugin config:\n` +
529
- ` "llmProvider": "openai", "llmModel": "gpt-4o-mini"\n\n` +
530
+ ` "llmProvider": "openai"\n\n` +
530
531
  `Option 4: Override with Hindsight-specific env vars:\n` +
531
532
  ` export HINDSIGHT_API_LLM_PROVIDER=openai\n` +
532
- ` export HINDSIGHT_API_LLM_MODEL=gpt-4o-mini\n` +
533
533
  ` export HINDSIGHT_API_LLM_API_KEY=sk-your-key\n` +
534
534
  ` export HINDSIGHT_API_LLM_BASE_URL=https://openrouter.ai/api/v1 # Optional\n\n` +
535
- `Tip: Use a cheap/fast model for memory extraction (e.g., gpt-4o-mini, claude-3-5-haiku, or free models on OpenRouter)`);
535
+ `The model will be selected automatically by Hindsight. To override: export HINDSIGHT_API_LLM_MODEL=your-model`);
536
536
  }
537
537
  /**
538
538
  * Detect external Hindsight API configuration.
@@ -624,6 +624,8 @@ function getPluginConfig(api) {
624
624
  recallPromptPreamble: typeof config.recallPromptPreamble === 'string' && config.recallPromptPreamble.trim().length > 0
625
625
  ? config.recallPromptPreamble
626
626
  : DEFAULT_RECALL_PROMPT_PREAMBLE,
627
+ recallInjectionPosition: typeof config.recallInjectionPosition === 'string' && ['prepend', 'append', 'user'].includes(config.recallInjectionPosition) ? config.recallInjectionPosition : undefined,
628
+ recallTimeoutMs: typeof config.recallTimeoutMs === 'number' && config.recallTimeoutMs >= 1000 ? config.recallTimeoutMs : undefined,
627
629
  debug: config.debug ?? false,
628
630
  };
629
631
  }
@@ -632,7 +634,15 @@ export default function (api) {
632
634
  debug('[Hindsight] Plugin loading...');
633
635
  // Get plugin config first (needed for LLM detection and debug flag)
634
636
  const pluginConfig = getPluginConfig(api);
635
- debugEnabled = pluginConfig.debug ?? false;
637
+ // If logLevel is 'debug', also enable legacy debug flag
638
+ debugEnabled = pluginConfig.debug ?? (pluginConfig.logLevel === 'debug');
639
+ // Configure structured logger — route through OpenClaw's api.logger for consistent formatting
640
+ if (api.logger)
641
+ setApiLogger(api.logger);
642
+ configureLogger({
643
+ logLevel: pluginConfig.logLevel ?? (pluginConfig.debug ? 'debug' : 'info'),
644
+ logSummaryIntervalMs: pluginConfig.logSummaryIntervalMs,
645
+ });
636
646
  // Store config globally for bank ID derivation in hooks
637
647
  currentPluginConfig = pluginConfig;
638
648
  // Detect LLM configuration (env vars > plugin config > auto-detect)
@@ -700,6 +710,12 @@ export default function (api) {
700
710
  debug(`[Hindsight] Setting bank mission...`);
701
711
  await client.setBankMission(pluginConfig.bankMission);
702
712
  }
713
+ if (!isInitialized) {
714
+ const mode = 'external API';
715
+ const autoRecall = pluginConfig.autoRecall !== false;
716
+ const autoRetain = pluginConfig.autoRetain !== false;
717
+ log.info(`initialized (mode: ${mode}, bank: ${defaultBankId}, autoRecall: ${autoRecall}, autoRetain: ${autoRetain})`);
718
+ }
703
719
  isInitialized = true;
704
720
  debug('[Hindsight] ✓ Ready (external API mode)');
705
721
  }
@@ -726,12 +742,18 @@ export default function (api) {
726
742
  debug(`[Hindsight] Setting bank mission...`);
727
743
  await client.setBankMission(pluginConfig.bankMission);
728
744
  }
745
+ if (!isInitialized) {
746
+ const mode = 'local daemon';
747
+ const autoRecall = pluginConfig.autoRecall !== false;
748
+ const autoRetain = pluginConfig.autoRetain !== false;
749
+ log.info(`initialized (mode: ${mode}, bank: ${defaultBankId}, autoRecall: ${autoRecall}, autoRetain: ${autoRetain})`);
750
+ }
729
751
  isInitialized = true;
730
752
  debug('[Hindsight] ✓ Ready');
731
753
  }
732
754
  }
733
755
  catch (error) {
734
- console.error('[Hindsight] Initialization error:', error);
756
+ log.error('initialization error', error);
735
757
  throw error;
736
758
  }
737
759
  })();
@@ -749,7 +771,7 @@ export default function (api) {
749
771
  await initPromise;
750
772
  }
751
773
  catch (error) {
752
- console.error('[Hindsight] Initial initialization failed:', error);
774
+ log.error('initial initialization failed', error);
753
775
  // Continue to health check below
754
776
  }
755
777
  }
@@ -763,7 +785,7 @@ export default function (api) {
763
785
  return;
764
786
  }
765
787
  catch (error) {
766
- console.error('[Hindsight] External API health check failed:', error);
788
+ log.error('external API health check failed', error);
767
789
  // Reset state for reinitialization attempt
768
790
  client = null;
769
791
  clientOptions = null;
@@ -850,10 +872,11 @@ export default function (api) {
850
872
  clientsByBankId.clear();
851
873
  banksWithMissionSet.clear();
852
874
  isInitialized = false;
875
+ stopLogger();
853
876
  debug('[Hindsight] Service stopped');
854
877
  }
855
878
  catch (error) {
856
- console.error('[Hindsight] Service stop error:', error);
879
+ log.error('service stop error', error);
857
880
  throw error;
858
881
  }
859
882
  },
@@ -949,7 +972,8 @@ export default function (api) {
949
972
  recallPromise = existing;
950
973
  }
951
974
  else {
952
- recallPromise = client.recall({ query: prompt, max_tokens: pluginConfig.recallMaxTokens || 1024, budget: pluginConfig.recallBudget, types: pluginConfig.recallTypes }, RECALL_TIMEOUT_MS);
975
+ const recallTimeoutMs = pluginConfig.recallTimeoutMs ?? DEFAULT_RECALL_TIMEOUT_MS;
976
+ recallPromise = client.recall({ query: prompt, max_tokens: pluginConfig.recallMaxTokens || 1024, budget: pluginConfig.recallBudget, types: pluginConfig.recallTypes }, recallTimeoutMs);
953
977
  inflightRecalls.set(recallKey, recallPromise);
954
978
  void recallPromise.catch(() => { }).finally(() => inflightRecalls.delete(recallKey));
955
979
  }
@@ -970,19 +994,30 @@ Current time - ${formatCurrentTimeForRecall()}
970
994
  ${memoriesFormatted}
971
995
  </hindsight_memories>`;
972
996
  debug(`[Hindsight] Auto-recall: Injecting ${results.length} memories from bank ${bankId}`);
973
- // Inject recalled memories into system prompt space so they stay hidden from
974
- // the end-user transcript/UI while still being available to the model.
975
- return { prependSystemContext: contextMessage };
997
+ log.info(`injecting ${results.length} memories into context (bank: ${bankId})`);
998
+ log.trackRecall(bankId, results.length);
999
+ // Inject recalled memories. Position is configurable to preserve prompt caching
1000
+ // when agents have large static system prompts.
1001
+ const position = pluginConfig.recallInjectionPosition || 'prepend';
1002
+ switch (position) {
1003
+ case 'append':
1004
+ return { appendSystemContext: contextMessage };
1005
+ case 'user':
1006
+ return { prependContext: contextMessage };
1007
+ case 'prepend':
1008
+ default:
1009
+ return { prependSystemContext: contextMessage };
1010
+ }
976
1011
  }
977
1012
  catch (error) {
978
1013
  if (error instanceof DOMException && error.name === 'TimeoutError') {
979
- console.warn(`[Hindsight] Auto-recall timed out after ${RECALL_TIMEOUT_MS}ms, skipping memory injection`);
1014
+ log.warn(`[Hindsight] Auto-recall timed out after ${pluginConfig.recallTimeoutMs ?? DEFAULT_RECALL_TIMEOUT_MS}ms, skipping memory injection`);
980
1015
  }
981
1016
  else if (error instanceof Error && error.name === 'AbortError') {
982
- console.warn(`[Hindsight] Auto-recall aborted after ${RECALL_TIMEOUT_MS}ms, skipping memory injection`);
1017
+ log.warn(`[Hindsight] Auto-recall aborted after ${pluginConfig.recallTimeoutMs ?? DEFAULT_RECALL_TIMEOUT_MS}ms, skipping memory injection`);
983
1018
  }
984
1019
  else {
985
- console.error('[Hindsight] Auto-recall error:', error);
1020
+ log.error('auto-recall error', error);
986
1021
  }
987
1022
  return;
988
1023
  }
@@ -1058,14 +1093,14 @@ ${memoriesFormatted}
1058
1093
  // Wait for client to be ready
1059
1094
  const clientGlobal = global.__hindsightClient;
1060
1095
  if (!clientGlobal) {
1061
- console.warn('[Hindsight] Client global not found, skipping retain');
1096
+ log.warn('client global not found, skipping retain');
1062
1097
  return;
1063
1098
  }
1064
1099
  await clientGlobal.waitForReady();
1065
1100
  // Get client configured for this context's bank (async to handle mission setup)
1066
1101
  const client = await clientGlobal.getClientForContext(effectiveCtxForRetain);
1067
1102
  if (!client) {
1068
- console.warn('[Hindsight] Client not initialized, skipping retain');
1103
+ log.warn('client not initialized, skipping retain');
1069
1104
  return;
1070
1105
  }
1071
1106
  // Use unique document ID per conversation (sessionKey + timestamp)
@@ -1084,18 +1119,19 @@ ${memoriesFormatted}
1084
1119
  sender_id: effectiveCtx?.senderId,
1085
1120
  },
1086
1121
  });
1122
+ log.trackRetain(bankId, messageCount);
1087
1123
  debug(`[Hindsight] Retained ${messageCount} messages to bank ${bankId} for session ${documentId}`);
1088
1124
  }
1089
1125
  catch (error) {
1090
- console.error('[Hindsight] Error retaining messages:', error);
1126
+ log.error('error retaining messages', error);
1091
1127
  }
1092
1128
  });
1093
1129
  debug('[Hindsight] Hooks registered');
1094
1130
  }
1095
1131
  catch (error) {
1096
- console.error('[Hindsight] Plugin loading error:', error);
1132
+ log.error('plugin loading error', error);
1097
1133
  if (error instanceof Error) {
1098
- console.error('[Hindsight] Error stack:', error.stack);
1134
+ log.error('error stack', error.stack);
1099
1135
  }
1100
1136
  throw error;
1101
1137
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Hindsight OpenClaw plugin logger.
3
+ *
4
+ * Routes output through OpenClaw's api.logger for consistent formatting
5
+ * with other plugins (same colors/timestamps as mem0, etc.).
6
+ *
7
+ * Features:
8
+ * - Configurable log level: 'off' | 'error' | 'warning' | 'info' | 'debug'
9
+ * - Batched retain/recall summaries instead of per-event spam
10
+ */
11
+ export type LogLevel = 'off' | 'error' | 'warning' | 'info' | 'debug';
12
+ export interface LoggerConfig {
13
+ /** Minimum severity to print. Default: 'info' */
14
+ logLevel?: LogLevel;
15
+ /** Interval in ms to print batched retain/recall summaries. 0 = print every event. Default: 300000 (5 min) */
16
+ logSummaryIntervalMs?: number;
17
+ }
18
+ /** Bind to OpenClaw's api.logger for consistent output formatting */
19
+ export declare function setApiLogger(logger: {
20
+ info(msg: string): void;
21
+ warn(msg: string): void;
22
+ error(msg: string): void;
23
+ }): void;
24
+ export declare function configureLogger(cfg: LoggerConfig): void;
25
+ /** Info-level log (requires 'info' or higher) */
26
+ export declare function info(msg: string): void;
27
+ /** Debug log (requires 'debug') */
28
+ export declare function verbose(msg: string): void;
29
+ /** Warning (requires 'warning' or higher) */
30
+ export declare function warn(msg: string): void;
31
+ /** Error (requires 'error' or higher) */
32
+ export declare function error(msg: string, err?: unknown): void;
33
+ /** Track a retain event for batched summary */
34
+ export declare function trackRetain(bankId: string, messageCount: number): void;
35
+ /** Track a recall event for batched summary */
36
+ export declare function trackRecall(bankId: string, memoriesFound: number): void;
37
+ /** Flush the batched summary to console */
38
+ export declare function flushSummary(): void;
39
+ /** Cleanup (call on plugin stop) */
40
+ export declare function stopLogger(): void;
package/dist/logger.js ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Hindsight OpenClaw plugin logger.
3
+ *
4
+ * Routes output through OpenClaw's api.logger for consistent formatting
5
+ * with other plugins (same colors/timestamps as mem0, etc.).
6
+ *
7
+ * Features:
8
+ * - Configurable log level: 'off' | 'error' | 'warning' | 'info' | 'debug'
9
+ * - Batched retain/recall summaries instead of per-event spam
10
+ */
11
+ // Muted blue (38;5;103 = slate/dusty blue from 256-color palette)
12
+ const PREFIX = '\x1b[38;5;103mhindsight:\x1b[0m';
13
+ const LEVEL_RANK = {
14
+ off: 0,
15
+ error: 1,
16
+ warning: 2,
17
+ info: 3,
18
+ debug: 4,
19
+ };
20
+ // Output backend — set via setApiLogger, falls back to console
21
+ let apiLogger = {
22
+ info: (msg) => console.log(msg),
23
+ warn: (msg) => console.warn(msg),
24
+ error: (msg) => console.error(msg),
25
+ };
26
+ // Batched summary state
27
+ let retainCount = 0;
28
+ let retainMsgTotal = 0;
29
+ let recallCount = 0;
30
+ let recallMemoriesCount = 0;
31
+ const banksSeen = new Set();
32
+ let lastSummaryTime = Date.now();
33
+ let summaryTimer = null;
34
+ let currentLevel = 'info';
35
+ let currentSummaryIntervalMs = 300_000; // 5 min
36
+ /** Bind to OpenClaw's api.logger for consistent output formatting */
37
+ export function setApiLogger(logger) {
38
+ apiLogger = logger;
39
+ }
40
+ export function configureLogger(cfg) {
41
+ currentLevel = cfg.logLevel ?? 'info';
42
+ currentSummaryIntervalMs = cfg.logSummaryIntervalMs ?? 300_000;
43
+ // Restart summary timer
44
+ if (summaryTimer) {
45
+ clearInterval(summaryTimer);
46
+ summaryTimer = null;
47
+ }
48
+ if (currentSummaryIntervalMs > 0 && LEVEL_RANK[currentLevel] >= LEVEL_RANK['info']) {
49
+ summaryTimer = setInterval(flushSummary, currentSummaryIntervalMs);
50
+ summaryTimer.unref?.(); // don't keep process alive
51
+ }
52
+ }
53
+ function allowed(level) {
54
+ return LEVEL_RANK[currentLevel] >= LEVEL_RANK[level];
55
+ }
56
+ /** Info-level log (requires 'info' or higher) */
57
+ export function info(msg) {
58
+ if (!allowed('info'))
59
+ return;
60
+ apiLogger.info(`${PREFIX} ${msg}`);
61
+ }
62
+ /** Debug log (requires 'debug') */
63
+ export function verbose(msg) {
64
+ if (!allowed('debug'))
65
+ return;
66
+ apiLogger.info(`${PREFIX} ${msg}`);
67
+ }
68
+ /** Warning (requires 'warning' or higher) */
69
+ export function warn(msg) {
70
+ if (!allowed('warning'))
71
+ return;
72
+ apiLogger.warn(`${PREFIX} ${msg}`);
73
+ }
74
+ /** Error (requires 'error' or higher) */
75
+ export function error(msg, err) {
76
+ if (!allowed('error'))
77
+ return;
78
+ const detail = err instanceof Error ? err.message : (err ? String(err) : '');
79
+ apiLogger.error(`${PREFIX} ${detail ? `${msg}: ${detail}` : msg}`);
80
+ }
81
+ /** Track a retain event for batched summary */
82
+ export function trackRetain(bankId, messageCount) {
83
+ retainCount++;
84
+ retainMsgTotal += messageCount;
85
+ banksSeen.add(bankId);
86
+ if (currentSummaryIntervalMs === 0 && allowed('info')) {
87
+ apiLogger.info(`${PREFIX} auto-retained ${messageCount} messages (bank: ${bankId})`);
88
+ }
89
+ }
90
+ /** Track a recall event for batched summary */
91
+ export function trackRecall(bankId, memoriesFound) {
92
+ recallCount++;
93
+ recallMemoriesCount += memoriesFound;
94
+ banksSeen.add(bankId);
95
+ // per-event logging is handled by info() call at the injection site
96
+ }
97
+ /** Flush the batched summary to console */
98
+ export function flushSummary() {
99
+ if (!allowed('info'))
100
+ return;
101
+ if (retainCount === 0 && recallCount === 0)
102
+ return;
103
+ const elapsed = Math.round((Date.now() - lastSummaryTime) / 1000);
104
+ const parts = [];
105
+ if (recallCount > 0)
106
+ parts.push(`${recallCount} recalls (${recallMemoriesCount} memories injected)`);
107
+ if (retainCount > 0)
108
+ parts.push(`${retainCount} retains (${retainMsgTotal} messages captured)`);
109
+ const bankList = [...banksSeen];
110
+ const bankLabel = bankList.length === 1 ? 'bank' : 'banks';
111
+ const banks = bankList.length > 0 ? ` (${bankLabel}: ${bankList.join(', ')})` : '';
112
+ apiLogger.info(`${PREFIX} ${parts.join(', ')} in ${elapsed}s${banks}`);
113
+ retainCount = 0;
114
+ retainMsgTotal = 0;
115
+ recallCount = 0;
116
+ recallMemoriesCount = 0;
117
+ banksSeen.clear();
118
+ lastSummaryTime = Date.now();
119
+ }
120
+ /** Cleanup (call on plugin stop) */
121
+ export function stopLogger() {
122
+ flushSummary();
123
+ if (summaryTimer) {
124
+ clearInterval(summaryTimer);
125
+ summaryTimer = null;
126
+ }
127
+ }
package/dist/types.d.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  export interface PluginPromptHookResult {
2
2
  prependContext?: string;
3
3
  prependSystemContext?: string;
4
+ appendSystemContext?: string;
4
5
  }
5
6
  export interface MoltbotPluginAPI {
6
7
  config: MoltbotConfig;
7
8
  registerService(config: ServiceConfig): void;
8
9
  on(event: string, handler: (event: any, ctx?: any) => void | Promise<void | PluginPromptHookResult>): void;
10
+ logger: {
11
+ info(msg: string): void;
12
+ warn(msg: string): void;
13
+ error(msg: string): void;
14
+ };
9
15
  }
10
16
  export interface MoltbotConfig {
11
17
  agents?: {
@@ -61,9 +67,13 @@ export interface PluginConfig {
61
67
  retainOverlapTurns?: number;
62
68
  recallTopK?: number;
63
69
  recallContextTurns?: number;
70
+ recallTimeoutMs?: number;
64
71
  recallMaxQueryChars?: number;
65
72
  recallPromptPreamble?: string;
73
+ recallInjectionPosition?: 'prepend' | 'append' | 'user';
66
74
  debug?: boolean;
75
+ logLevel?: 'off' | 'error' | 'warning' | 'info' | 'debug';
76
+ logSummaryIntervalMs?: number;
67
77
  }
68
78
  export interface ServiceConfig {
69
79
  id: string;
@@ -188,10 +188,33 @@
188
188
  "description": "Text shown above recalled memories in the injected context block.",
189
189
  "default": "Relevant memories from past conversations (prioritize recent when conflicting). Only use memories that are directly useful to continue this conversation; ignore the rest:"
190
190
  },
191
+ "recallTimeoutMs": {
192
+ "type": "integer",
193
+ "minimum": 1000,
194
+ "description": "Timeout for auto-recall in milliseconds. Increase if recall times out with high budget.",
195
+ "default": 10000
196
+ },
197
+ "recallInjectionPosition": {
198
+ "type": "string",
199
+ "enum": ["prepend", "append", "user"],
200
+ "description": "Where to inject recalled memories. 'prepend' = start of system prompt (default), 'append' = end of system prompt (preserves prompt cache), 'user' = before user message.",
201
+ "default": "prepend"
202
+ },
191
203
  "debug": {
192
204
  "type": "boolean",
193
- "description": "Enable debug logging for Hindsight plugin operations.",
205
+ "description": "Enable debug logging for Hindsight plugin operations. Equivalent to logLevel: 'debug'.",
194
206
  "default": false
207
+ },
208
+ "logLevel": {
209
+ "type": "string",
210
+ "description": "Console log verbosity. 'off' = no output, 'error' = errors only, 'warning' = errors + warnings, 'info' = key events + periodic summaries, 'debug' = all details.",
211
+ "enum": ["off", "error", "warning", "info", "debug"],
212
+ "default": "info"
213
+ },
214
+ "logSummaryIntervalMs": {
215
+ "type": "number",
216
+ "description": "Interval in ms to batch retain/recall log summaries. 0 = log every event individually. Default: 300000 (5 min).",
217
+ "default": 300000
195
218
  }
196
219
  },
197
220
  "additionalProperties": false
@@ -309,8 +332,23 @@
309
332
  "label": "Recall Prompt Preamble",
310
333
  "placeholder": "Instruction shown above recalled memories in injected context"
311
334
  },
335
+ "recallTimeoutMs": {
336
+ "label": "Recall Timeout (ms)",
337
+ "placeholder": "10000 (default)"
338
+ },
339
+ "recallInjectionPosition": {
340
+ "label": "Recall Injection Position",
341
+ "placeholder": "prepend, append, or user"
342
+ },
312
343
  "debug": {
313
344
  "label": "Debug"
345
+ },
346
+ "logLevel": {
347
+ "label": "Log Level"
348
+ },
349
+ "logSummaryIntervalMs": {
350
+ "label": "Log Summary Interval (ms)",
351
+ "placeholder": "300000"
314
352
  }
315
353
  }
316
354
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectorize-io/hindsight-openclaw",
3
- "version": "0.4.19",
3
+ "version": "0.5.1",
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",
@@ -52,6 +52,7 @@
52
52
  "node": ">=22"
53
53
  },
54
54
  "overrides": {
55
- "rollup": "^4.59.0"
55
+ "rollup": "^4.59.0",
56
+ "picomatch": ">=2.3.2 <3.0.0 || >=4.0.4"
56
57
  }
57
58
  }