@vectorize-io/hindsight-openclaw 0.4.19 → 0.5.0
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 +8 -7
- package/dist/index.js +78 -42
- package/dist/logger.d.ts +40 -0
- package/dist/logger.js +127 -0
- package/dist/types.d.ts +10 -0
- package/openclaw.plugin.json +41 -3
- package/package.json +3 -2
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
|
-
|
|
82
|
+
log.verbose('bank mission set via HTTP');
|
|
82
83
|
}
|
|
83
84
|
catch (error) {
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
? (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
432
|
-
{ name: 'anthropic', keyEnv: 'ANTHROPIC_API_KEY'
|
|
433
|
-
{ name: 'gemini', keyEnv: 'GEMINI_API_KEY'
|
|
434
|
-
{ name: 'groq', keyEnv: 'GROQ_API_KEY'
|
|
435
|
-
{ name: 'ollama', keyEnv: ''
|
|
436
|
-
{ name: 'openai-codex', keyEnv: ''
|
|
437
|
-
{ name: 'claude-code', keyEnv: ''
|
|
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
|
|
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
|
|
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
|
|
502
|
-
baseUrl: overrideBaseUrl,
|
|
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
|
|
522
|
-
` export ANTHROPIC_API_KEY=your-key
|
|
523
|
-
` export GEMINI_API_KEY=your-key
|
|
524
|
-
` export GROQ_API_KEY=your-key
|
|
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"
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1017
|
+
log.warn(`[Hindsight] Auto-recall aborted after ${pluginConfig.recallTimeoutMs ?? DEFAULT_RECALL_TIMEOUT_MS}ms, skipping memory injection`);
|
|
983
1018
|
}
|
|
984
1019
|
else {
|
|
985
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1126
|
+
log.error('error retaining messages', error);
|
|
1091
1127
|
}
|
|
1092
1128
|
});
|
|
1093
1129
|
debug('[Hindsight] Hooks registered');
|
|
1094
1130
|
}
|
|
1095
1131
|
catch (error) {
|
|
1096
|
-
|
|
1132
|
+
log.error('plugin loading error', error);
|
|
1097
1133
|
if (error instanceof Error) {
|
|
1098
|
-
|
|
1134
|
+
log.error('error stack', error.stack);
|
|
1099
1135
|
}
|
|
1100
1136
|
throw error;
|
|
1101
1137
|
}
|
package/dist/logger.d.ts
ADDED
|
@@ -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;
|
package/openclaw.plugin.json
CHANGED
|
@@ -188,11 +188,34 @@
|
|
|
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
|
|
195
|
-
}
|
|
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
|
|
218
|
+
},
|
|
196
219
|
},
|
|
197
220
|
"additionalProperties": false
|
|
198
221
|
},
|
|
@@ -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"
|
|
314
|
-
}
|
|
345
|
+
},
|
|
346
|
+
"logLevel": {
|
|
347
|
+
"label": "Log Level"
|
|
348
|
+
},
|
|
349
|
+
"logSummaryIntervalMs": {
|
|
350
|
+
"label": "Log Summary Interval (ms)",
|
|
351
|
+
"placeholder": "300000"
|
|
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.
|
|
3
|
+
"version": "0.5.0",
|
|
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
|
}
|