@vectorize-io/hindsight-openclaw 0.5.1 → 0.6.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/README.md +147 -18
- package/dist/backfill-lib.d.ts +63 -0
- package/dist/backfill-lib.js +201 -0
- package/dist/backfill.d.ts +22 -0
- package/dist/backfill.js +473 -0
- package/dist/index.d.ts +49 -2
- package/dist/index.js +612 -344
- package/dist/retain-queue.d.ts +54 -0
- package/dist/retain-queue.js +105 -0
- package/dist/session-patterns.d.ts +10 -0
- package/dist/session-patterns.js +21 -0
- package/dist/setup-lib.d.ts +80 -0
- package/dist/setup-lib.js +134 -0
- package/dist/setup.d.ts +34 -0
- package/dist/setup.js +425 -0
- package/dist/types.d.ts +40 -40
- package/openclaw.plugin.json +110 -10
- package/package.json +13 -5
- package/dist/client.d.ts +0 -34
- package/dist/client.js +0 -215
- package/dist/embed-manager.d.ts +0 -27
- package/dist/embed-manager.js +0 -210
package/dist/backfill.js
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, realpathSync } from 'fs';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { HindsightServer } from '@vectorize-io/hindsight-all';
|
|
6
|
+
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
|
7
|
+
import { detectExternalApi, detectLLMConfig } from './index.js';
|
|
8
|
+
import { buildBackfillPlan, checkpointKey, defaultCheckpointPath, defaultOpenClawRoot, loadCheckpoint, loadPluginConfigFromOpenClawRoot, saveCheckpoint, } from './backfill-lib.js';
|
|
9
|
+
function usage() {
|
|
10
|
+
return [
|
|
11
|
+
'Usage: hindsight-openclaw-backfill [options]',
|
|
12
|
+
'',
|
|
13
|
+
'Options:',
|
|
14
|
+
' --openclaw-root <path> OpenClaw root directory (default: ~/.openclaw)',
|
|
15
|
+
' --profile <name> Logical profile name for reporting (default: openclaw)',
|
|
16
|
+
' --agent <id> Restrict import to a specific agent (repeatable)',
|
|
17
|
+
' --include-archive Include migration archives (default)',
|
|
18
|
+
' --exclude-archive Exclude migration archives',
|
|
19
|
+
' --limit <n> Stop after enqueueing N sessions',
|
|
20
|
+
' --dry-run Build and print the import plan without enqueueing',
|
|
21
|
+
' --json Print final summary as JSON',
|
|
22
|
+
' --resume Skip entries already marked completed in the checkpoint',
|
|
23
|
+
' --checkpoint <path> Path to checkpoint JSON',
|
|
24
|
+
' --bank-strategy <mode> mirror-config | agent | fixed',
|
|
25
|
+
' --fixed-bank <id> Required when bank strategy is fixed',
|
|
26
|
+
' --api-url <url> Hindsight API base URL override',
|
|
27
|
+
' --api-token <token> Hindsight API bearer token override',
|
|
28
|
+
' --max-pending-operations <n> Wait until target bank queue is <= n before enqueueing',
|
|
29
|
+
' --wait-until-drained Wait for touched banks to drain and finalize checkpoint state',
|
|
30
|
+
' -h, --help Show this help',
|
|
31
|
+
].join('\n');
|
|
32
|
+
}
|
|
33
|
+
function parseArgs(argv) {
|
|
34
|
+
const args = {
|
|
35
|
+
openclawRoot: defaultOpenClawRoot(),
|
|
36
|
+
profile: 'openclaw',
|
|
37
|
+
agents: [],
|
|
38
|
+
includeArchive: true,
|
|
39
|
+
dryRun: false,
|
|
40
|
+
json: false,
|
|
41
|
+
resume: false,
|
|
42
|
+
checkpointPath: '',
|
|
43
|
+
bankStrategy: 'mirror-config',
|
|
44
|
+
waitUntilDrained: false,
|
|
45
|
+
};
|
|
46
|
+
for (let i = 0; i < argv.length; i++) {
|
|
47
|
+
const arg = argv[i];
|
|
48
|
+
const next = () => {
|
|
49
|
+
const value = argv[++i];
|
|
50
|
+
if (!value) {
|
|
51
|
+
throw new Error(`missing value for ${arg}`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
};
|
|
55
|
+
switch (arg) {
|
|
56
|
+
case '--openclaw-root':
|
|
57
|
+
args.openclawRoot = resolve(next());
|
|
58
|
+
break;
|
|
59
|
+
case '--profile':
|
|
60
|
+
args.profile = next();
|
|
61
|
+
break;
|
|
62
|
+
case '--agent':
|
|
63
|
+
args.agents.push(next());
|
|
64
|
+
break;
|
|
65
|
+
case '--include-archive':
|
|
66
|
+
args.includeArchive = true;
|
|
67
|
+
break;
|
|
68
|
+
case '--exclude-archive':
|
|
69
|
+
args.includeArchive = false;
|
|
70
|
+
break;
|
|
71
|
+
case '--limit':
|
|
72
|
+
args.limit = Number(next());
|
|
73
|
+
break;
|
|
74
|
+
case '--dry-run':
|
|
75
|
+
args.dryRun = true;
|
|
76
|
+
break;
|
|
77
|
+
case '--json':
|
|
78
|
+
args.json = true;
|
|
79
|
+
break;
|
|
80
|
+
case '--resume':
|
|
81
|
+
args.resume = true;
|
|
82
|
+
break;
|
|
83
|
+
case '--checkpoint':
|
|
84
|
+
args.checkpointPath = resolve(next());
|
|
85
|
+
break;
|
|
86
|
+
case '--bank-strategy': {
|
|
87
|
+
const value = next();
|
|
88
|
+
if (value !== 'mirror-config' && value !== 'agent' && value !== 'fixed') {
|
|
89
|
+
throw new Error(`invalid bank strategy: ${value}`);
|
|
90
|
+
}
|
|
91
|
+
args.bankStrategy = value;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case '--fixed-bank':
|
|
95
|
+
args.fixedBank = next();
|
|
96
|
+
break;
|
|
97
|
+
case '--api-url':
|
|
98
|
+
args.apiUrl = next();
|
|
99
|
+
break;
|
|
100
|
+
case '--api-token':
|
|
101
|
+
args.apiToken = next();
|
|
102
|
+
break;
|
|
103
|
+
case '--max-pending-operations':
|
|
104
|
+
args.maxPendingOperations = Number(next());
|
|
105
|
+
break;
|
|
106
|
+
case '--wait-until-drained':
|
|
107
|
+
args.waitUntilDrained = true;
|
|
108
|
+
break;
|
|
109
|
+
case '-h':
|
|
110
|
+
case '--help':
|
|
111
|
+
console.log(usage());
|
|
112
|
+
process.exit(0);
|
|
113
|
+
default:
|
|
114
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!args.checkpointPath) {
|
|
118
|
+
args.checkpointPath = defaultCheckpointPath(args.openclawRoot);
|
|
119
|
+
}
|
|
120
|
+
if (args.bankStrategy === 'fixed' && !args.fixedBank) {
|
|
121
|
+
throw new Error('--fixed-bank is required when --bank-strategy fixed is used');
|
|
122
|
+
}
|
|
123
|
+
return args;
|
|
124
|
+
}
|
|
125
|
+
function inferApiSettings(pluginConfig, explicitApiUrl, explicitApiToken) {
|
|
126
|
+
const apiUrl = explicitApiUrl
|
|
127
|
+
|| pluginConfig.hindsightApiUrl
|
|
128
|
+
|| `http://127.0.0.1:${pluginConfig.apiPort || 9077}`;
|
|
129
|
+
const apiToken = explicitApiToken || pluginConfig.hindsightApiToken;
|
|
130
|
+
return { apiUrl, apiToken: apiToken || undefined };
|
|
131
|
+
}
|
|
132
|
+
async function checkHealth(apiUrl, apiToken) {
|
|
133
|
+
try {
|
|
134
|
+
const response = await fetch(`${apiUrl.replace(/\/$/, '')}/health`, {
|
|
135
|
+
method: 'GET',
|
|
136
|
+
headers: apiToken ? { Authorization: `Bearer ${apiToken}` } : undefined,
|
|
137
|
+
signal: AbortSignal.timeout(5000),
|
|
138
|
+
});
|
|
139
|
+
return response.ok;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export function filterEntriesForResume(entries, checkpoint, resume) {
|
|
146
|
+
if (!resume) {
|
|
147
|
+
return entries;
|
|
148
|
+
}
|
|
149
|
+
return entries.filter((entry) => checkpoint.entries[checkpointKey(entry)]?.status !== 'completed');
|
|
150
|
+
}
|
|
151
|
+
export function splitResumeEntries(entries, checkpoint, waitUntilDrained) {
|
|
152
|
+
const entriesToEnqueue = [];
|
|
153
|
+
const alreadyEnqueuedKeys = [];
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
const status = checkpoint.entries[checkpointKey(entry)]?.status;
|
|
156
|
+
if (status === 'enqueued') {
|
|
157
|
+
if (waitUntilDrained) {
|
|
158
|
+
alreadyEnqueuedKeys.push(checkpointKey(entry));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
entriesToEnqueue.push(entry);
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
entriesToEnqueue.push(entry);
|
|
166
|
+
}
|
|
167
|
+
return { entriesToEnqueue, alreadyEnqueuedKeys };
|
|
168
|
+
}
|
|
169
|
+
export function applyDrainResults(checkpoint, touchedEntriesByBank, finalStatsByBank, initialFailedOperationsByBank) {
|
|
170
|
+
let completed = 0;
|
|
171
|
+
let unresolved = 0;
|
|
172
|
+
const warnings = [];
|
|
173
|
+
for (const [bankId, entryKeys] of touchedEntriesByBank.entries()) {
|
|
174
|
+
const stats = finalStatsByBank.get(bankId);
|
|
175
|
+
const initialFailed = initialFailedOperationsByBank.get(bankId) ?? 0;
|
|
176
|
+
const hasNewFailures = !!stats && stats.failed_operations > initialFailed;
|
|
177
|
+
if (hasNewFailures) {
|
|
178
|
+
warnings.push(`bank ${bankId} reported ${stats.failed_operations - initialFailed} new failed operations during drain; leaving ${entryKeys.length} checkpoint entries enqueued`);
|
|
179
|
+
}
|
|
180
|
+
else if (!stats || stats.pending_operations > 0) {
|
|
181
|
+
warnings.push(`bank ${bankId} did not finish draining cleanly; leaving ${entryKeys.length} checkpoint entries enqueued`);
|
|
182
|
+
}
|
|
183
|
+
for (const entryKey of entryKeys) {
|
|
184
|
+
const existing = checkpoint.entries[entryKey];
|
|
185
|
+
if (!existing || existing.status !== 'enqueued') {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (!hasNewFailures && stats && stats.pending_operations === 0) {
|
|
189
|
+
checkpoint.entries[entryKey] = {
|
|
190
|
+
...existing,
|
|
191
|
+
status: 'completed',
|
|
192
|
+
updatedAt: new Date().toISOString(),
|
|
193
|
+
error: undefined,
|
|
194
|
+
};
|
|
195
|
+
completed += 1;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
unresolved += 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { completed, unresolved, warnings };
|
|
203
|
+
}
|
|
204
|
+
export async function createBackfillRuntime(pluginConfig, explicitApiUrl, explicitApiToken) {
|
|
205
|
+
const explicit = inferApiSettings(pluginConfig, explicitApiUrl, explicitApiToken);
|
|
206
|
+
const externalApi = detectExternalApi(pluginConfig);
|
|
207
|
+
const useExternalApi = !!(explicitApiUrl || explicitApiToken || externalApi.apiUrl || pluginConfig.hindsightApiUrl);
|
|
208
|
+
if (useExternalApi) {
|
|
209
|
+
return {
|
|
210
|
+
apiUrl: explicit.apiUrl,
|
|
211
|
+
apiToken: explicit.apiToken,
|
|
212
|
+
async stop() { },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (await checkHealth(explicit.apiUrl, explicit.apiToken)) {
|
|
216
|
+
return {
|
|
217
|
+
apiUrl: explicit.apiUrl,
|
|
218
|
+
apiToken: explicit.apiToken,
|
|
219
|
+
async stop() { },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const llmConfig = detectLLMConfig(pluginConfig);
|
|
223
|
+
const manager = new HindsightServer({
|
|
224
|
+
profile: 'openclaw',
|
|
225
|
+
port: pluginConfig.apiPort || 9077,
|
|
226
|
+
embedVersion: pluginConfig.embedVersion,
|
|
227
|
+
embedPackagePath: pluginConfig.embedPackagePath,
|
|
228
|
+
env: {
|
|
229
|
+
HINDSIGHT_API_LLM_PROVIDER: llmConfig.provider || '',
|
|
230
|
+
HINDSIGHT_API_LLM_API_KEY: llmConfig.apiKey || '',
|
|
231
|
+
HINDSIGHT_API_LLM_MODEL: llmConfig.model,
|
|
232
|
+
HINDSIGHT_API_LLM_BASE_URL: llmConfig.baseUrl,
|
|
233
|
+
HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT: String(pluginConfig.daemonIdleTimeout ?? 0),
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
await manager.start();
|
|
237
|
+
return {
|
|
238
|
+
apiUrl: manager.getBaseUrl(),
|
|
239
|
+
apiToken: undefined,
|
|
240
|
+
async stop() {
|
|
241
|
+
await manager.stop();
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Fetch stats for a single bank over HTTP. The high-level `HindsightClient`
|
|
247
|
+
* doesn't yet wrap this endpoint, so we go direct — it's one call.
|
|
248
|
+
*/
|
|
249
|
+
async function fetchBankStats(baseUrl, apiToken, bankId) {
|
|
250
|
+
const headers = {};
|
|
251
|
+
if (apiToken)
|
|
252
|
+
headers.Authorization = `Bearer ${apiToken}`;
|
|
253
|
+
const res = await fetch(`${baseUrl}/v1/default/banks/${encodeURIComponent(bankId)}/stats`, { headers });
|
|
254
|
+
if (!res.ok) {
|
|
255
|
+
throw new Error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
256
|
+
}
|
|
257
|
+
return res.json();
|
|
258
|
+
}
|
|
259
|
+
async function waitForBankQueue(apiUrl, apiToken, bankId, maxPendingOperations) {
|
|
260
|
+
for (;;) {
|
|
261
|
+
try {
|
|
262
|
+
const stats = await fetchBankStats(apiUrl, apiToken, bankId);
|
|
263
|
+
if (stats.pending_operations <= maxPendingOperations) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (error instanceof Error && error.message.includes('HTTP 404')) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function getInitialBankStats(apiUrl, apiToken, bankId) {
|
|
277
|
+
try {
|
|
278
|
+
return await fetchBankStats(apiUrl, apiToken, bankId);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (error instanceof Error && error.message.includes('HTTP 404')) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function waitForBanksToDrain(apiUrl, apiToken, bankIds) {
|
|
288
|
+
const ids = Array.from(bankIds);
|
|
289
|
+
for (;;) {
|
|
290
|
+
const stats = await Promise.all(ids.map(async (bankId) => ({ bankId, stats: await fetchBankStats(apiUrl, apiToken, bankId) })));
|
|
291
|
+
const statsByBank = new Map(stats.map(({ bankId, stats: bankStats }) => [bankId, bankStats]));
|
|
292
|
+
const pending = stats.filter(({ stats: bankStats }) => bankStats.pending_operations > 0);
|
|
293
|
+
if (pending.length === 0) {
|
|
294
|
+
return statsByBank;
|
|
295
|
+
}
|
|
296
|
+
console.log(pending
|
|
297
|
+
.map(({ bankId, stats: bankStats }) => `${bankId}\tpending_operations=${bankStats.pending_operations}\tfailed_operations=${bankStats.failed_operations}\tpending_consolidation=${bankStats.pending_consolidation}`)
|
|
298
|
+
.join('\n'));
|
|
299
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
303
|
+
const args = parseArgs(argv);
|
|
304
|
+
if (!existsSync(join(args.openclawRoot, 'openclaw.json'))) {
|
|
305
|
+
throw new Error(`could not find openclaw.json under ${args.openclawRoot}`);
|
|
306
|
+
}
|
|
307
|
+
const pluginConfig = loadPluginConfigFromOpenClawRoot(args.openclawRoot);
|
|
308
|
+
const backfillOptions = {
|
|
309
|
+
openclawRoot: args.openclawRoot,
|
|
310
|
+
includeArchive: args.includeArchive,
|
|
311
|
+
selectedAgents: args.agents.length ? new Set(args.agents) : undefined,
|
|
312
|
+
limit: args.limit,
|
|
313
|
+
bankStrategy: args.bankStrategy,
|
|
314
|
+
fixedBank: args.fixedBank,
|
|
315
|
+
};
|
|
316
|
+
const checkpoint = loadCheckpoint(args.checkpointPath);
|
|
317
|
+
const { entries, discoveredSessions, skippedEmpty } = buildBackfillPlan(pluginConfig, backfillOptions);
|
|
318
|
+
const plannedEntries = filterEntriesForResume(entries, checkpoint, args.resume);
|
|
319
|
+
const { entriesToEnqueue, alreadyEnqueuedKeys } = splitResumeEntries(plannedEntries, checkpoint, args.waitUntilDrained);
|
|
320
|
+
if (args.dryRun) {
|
|
321
|
+
for (const entry of plannedEntries) {
|
|
322
|
+
console.log(`${entry.agentId}\t${entry.bankId}\t${entry.sessionId}\tmsgs=${entry.messageCount}\tchars=${entry.transcript.length}`);
|
|
323
|
+
}
|
|
324
|
+
const summary = {
|
|
325
|
+
profile: args.profile,
|
|
326
|
+
dry_run: true,
|
|
327
|
+
discovered_sessions: discoveredSessions,
|
|
328
|
+
planned_sessions: plannedEntries.length,
|
|
329
|
+
skipped_empty: skippedEmpty,
|
|
330
|
+
bank_strategy: args.bankStrategy,
|
|
331
|
+
checkpoint_path: args.checkpointPath,
|
|
332
|
+
};
|
|
333
|
+
console.log(args.json ? JSON.stringify(summary, null, 2) : JSON.stringify(summary));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const runtime = await createBackfillRuntime(pluginConfig, args.apiUrl, args.apiToken);
|
|
337
|
+
// Single shared client — hindsight-client takes bankId as a parameter on
|
|
338
|
+
// every call, so there's no reason to cache per-bank clients anymore.
|
|
339
|
+
const client = new HindsightClient({ baseUrl: runtime.apiUrl, apiKey: runtime.apiToken });
|
|
340
|
+
const bankRuntimes = new Map();
|
|
341
|
+
let imported = 0;
|
|
342
|
+
let failed = 0;
|
|
343
|
+
let finalized = 0;
|
|
344
|
+
try {
|
|
345
|
+
for (const entryKey of alreadyEnqueuedKeys) {
|
|
346
|
+
const checkpointEntry = checkpoint.entries[entryKey];
|
|
347
|
+
if (!checkpointEntry)
|
|
348
|
+
continue;
|
|
349
|
+
let bankRuntime = bankRuntimes.get(checkpointEntry.bankId);
|
|
350
|
+
if (!bankRuntime) {
|
|
351
|
+
bankRuntime = {
|
|
352
|
+
bankId: checkpointEntry.bankId,
|
|
353
|
+
touchedEntryKeys: [],
|
|
354
|
+
initialFailedOperations: (await getInitialBankStats(runtime.apiUrl, runtime.apiToken, checkpointEntry.bankId))?.failed_operations ?? 0,
|
|
355
|
+
missionApplied: false,
|
|
356
|
+
};
|
|
357
|
+
bankRuntimes.set(checkpointEntry.bankId, bankRuntime);
|
|
358
|
+
}
|
|
359
|
+
bankRuntime.touchedEntryKeys.push(entryKey);
|
|
360
|
+
}
|
|
361
|
+
for (const entry of entriesToEnqueue) {
|
|
362
|
+
let bankRuntime = bankRuntimes.get(entry.bankId);
|
|
363
|
+
if (!bankRuntime) {
|
|
364
|
+
bankRuntime = {
|
|
365
|
+
bankId: entry.bankId,
|
|
366
|
+
touchedEntryKeys: [],
|
|
367
|
+
initialFailedOperations: (await getInitialBankStats(runtime.apiUrl, runtime.apiToken, entry.bankId))?.failed_operations ?? 0,
|
|
368
|
+
missionApplied: false,
|
|
369
|
+
};
|
|
370
|
+
bankRuntimes.set(entry.bankId, bankRuntime);
|
|
371
|
+
}
|
|
372
|
+
if (!bankRuntime.missionApplied && pluginConfig.bankMission) {
|
|
373
|
+
await client.createBank(entry.bankId, { reflectMission: pluginConfig.bankMission });
|
|
374
|
+
}
|
|
375
|
+
if (typeof args.maxPendingOperations === 'number' && args.maxPendingOperations >= 0) {
|
|
376
|
+
await waitForBankQueue(runtime.apiUrl, runtime.apiToken, entry.bankId, args.maxPendingOperations);
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
const metadata = {
|
|
380
|
+
source: 'openclaw-backfill',
|
|
381
|
+
file_path: entry.filePath,
|
|
382
|
+
agent_id: entry.agentId,
|
|
383
|
+
session_id: entry.sessionId,
|
|
384
|
+
retained_at: new Date().toISOString(),
|
|
385
|
+
};
|
|
386
|
+
if (entry.startedAt) {
|
|
387
|
+
metadata.session_started_at = entry.startedAt;
|
|
388
|
+
}
|
|
389
|
+
await client.retain(entry.bankId, entry.transcript, {
|
|
390
|
+
documentId: entry.documentId,
|
|
391
|
+
metadata,
|
|
392
|
+
async: true,
|
|
393
|
+
});
|
|
394
|
+
checkpoint.entries[checkpointKey(entry)] = {
|
|
395
|
+
status: 'enqueued',
|
|
396
|
+
bankId: entry.bankId,
|
|
397
|
+
filePath: entry.filePath,
|
|
398
|
+
sessionId: entry.sessionId,
|
|
399
|
+
updatedAt: new Date().toISOString(),
|
|
400
|
+
};
|
|
401
|
+
bankRuntime.touchedEntryKeys.push(checkpointKey(entry));
|
|
402
|
+
if (!bankRuntime.missionApplied && pluginConfig.bankMission) {
|
|
403
|
+
await client.createBank(entry.bankId, { reflectMission: pluginConfig.bankMission });
|
|
404
|
+
bankRuntime.missionApplied = true;
|
|
405
|
+
}
|
|
406
|
+
saveCheckpoint(args.checkpointPath, checkpoint);
|
|
407
|
+
console.log(`${entry.agentId}\t${entry.bankId}\t${entry.sessionId}\tenqueued`);
|
|
408
|
+
imported += 1;
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
checkpoint.entries[checkpointKey(entry)] = {
|
|
412
|
+
status: 'failed',
|
|
413
|
+
bankId: entry.bankId,
|
|
414
|
+
filePath: entry.filePath,
|
|
415
|
+
sessionId: entry.sessionId,
|
|
416
|
+
updatedAt: new Date().toISOString(),
|
|
417
|
+
error: error instanceof Error ? error.message : String(error),
|
|
418
|
+
};
|
|
419
|
+
saveCheckpoint(args.checkpointPath, checkpoint);
|
|
420
|
+
failed += 1;
|
|
421
|
+
console.error(`${entry.agentId}\t${entry.bankId}\t${entry.sessionId}\tfailed\t${error instanceof Error ? error.message : String(error)}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (args.waitUntilDrained && bankRuntimes.size > 0) {
|
|
425
|
+
const finalStatsByBank = await waitForBanksToDrain(runtime.apiUrl, runtime.apiToken, bankRuntimes.keys());
|
|
426
|
+
const touchedEntriesByBank = new Map(Array.from(bankRuntimes.entries()).map(([bankId, value]) => [bankId, value.touchedEntryKeys]));
|
|
427
|
+
const initialFailedByBank = new Map(Array.from(bankRuntimes.entries()).map(([bankId, value]) => [bankId, value.initialFailedOperations]));
|
|
428
|
+
const finalization = applyDrainResults(checkpoint, touchedEntriesByBank, finalStatsByBank, initialFailedByBank);
|
|
429
|
+
finalized = finalization.completed;
|
|
430
|
+
for (const warning of finalization.warnings) {
|
|
431
|
+
console.warn(warning);
|
|
432
|
+
}
|
|
433
|
+
saveCheckpoint(args.checkpointPath, checkpoint);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
await runtime.stop();
|
|
438
|
+
}
|
|
439
|
+
const summary = {
|
|
440
|
+
profile: args.profile,
|
|
441
|
+
api_url: runtime.apiUrl,
|
|
442
|
+
discovered_sessions: discoveredSessions,
|
|
443
|
+
planned_sessions: plannedEntries.length,
|
|
444
|
+
imported_sessions: imported,
|
|
445
|
+
finalized_sessions: finalized,
|
|
446
|
+
failed_sessions: failed,
|
|
447
|
+
skipped_empty: skippedEmpty,
|
|
448
|
+
bank_strategy: args.bankStrategy,
|
|
449
|
+
checkpoint_path: args.checkpointPath,
|
|
450
|
+
};
|
|
451
|
+
console.log(args.json ? JSON.stringify(summary, null, 2) : JSON.stringify(summary));
|
|
452
|
+
}
|
|
453
|
+
function canonicalizeExecutionPath(path) {
|
|
454
|
+
const resolved = resolve(path);
|
|
455
|
+
try {
|
|
456
|
+
return realpathSync(resolved);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
return resolved;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
export function isDirectExecution(entrypoint = process.argv[1], moduleUrl = import.meta.url) {
|
|
463
|
+
if (!entrypoint) {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
return canonicalizeExecutionPath(entrypoint) === canonicalizeExecutionPath(fileURLToPath(moduleUrl));
|
|
467
|
+
}
|
|
468
|
+
if (isDirectExecution()) {
|
|
469
|
+
runCli().catch((error) => {
|
|
470
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
471
|
+
process.exit(1);
|
|
472
|
+
});
|
|
473
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
import type { MoltbotPluginAPI, PluginConfig, PluginHookAgentContext, MemoryResult } from './types.js';
|
|
2
|
-
import { HindsightClient } from '
|
|
1
|
+
import type { MoltbotPluginAPI, PluginConfig, PluginHookAgentContext, MemoryResult, RetainRequest } from './types.js';
|
|
2
|
+
import { HindsightClient, type HindsightClientOptions } from '@vectorize-io/hindsight-client';
|
|
3
|
+
import type { RecallResponse } from './types.js';
|
|
4
|
+
export interface BankScopedClient {
|
|
5
|
+
readonly bankId: string;
|
|
6
|
+
retain(req: RetainRequest): Promise<void>;
|
|
7
|
+
recall(req: {
|
|
8
|
+
query: string;
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
budget?: 'low' | 'mid' | 'high';
|
|
11
|
+
types?: Array<'world' | 'experience' | 'observation'>;
|
|
12
|
+
}, timeoutMs?: number): Promise<RecallResponse>;
|
|
13
|
+
setMission(mission: string): Promise<void>;
|
|
14
|
+
}
|
|
3
15
|
/**
|
|
4
16
|
* Strip plugin-injected memory tags from content to prevent retain feedback loop.
|
|
5
17
|
* Removes <hindsight_memories> and <relevant_memories> blocks that were injected
|
|
@@ -30,7 +42,42 @@ export declare function composeRecallQuery(latestQuery: string, messages: any[]
|
|
|
30
42
|
export declare function truncateRecallQuery(query: string, latestQuery: string, maxChars: number): string;
|
|
31
43
|
export declare function deriveBankId(ctx: PluginHookAgentContext | undefined, pluginConfig: PluginConfig): string;
|
|
32
44
|
export declare function formatMemories(results: MemoryResult[]): string;
|
|
45
|
+
export declare function detectLLMConfig(pluginConfig?: PluginConfig): {
|
|
46
|
+
provider?: string;
|
|
47
|
+
apiKey?: string;
|
|
48
|
+
model?: string;
|
|
49
|
+
baseUrl?: string;
|
|
50
|
+
source: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Detect external Hindsight API configuration from plugin config.
|
|
54
|
+
*/
|
|
55
|
+
export declare function detectExternalApi(pluginConfig?: PluginConfig): {
|
|
56
|
+
apiUrl: string | null;
|
|
57
|
+
apiToken: string | null;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Build HindsightClientOptions for the generated hindsight-client. In
|
|
61
|
+
* external-API mode we use the configured URL/token; in local daemon mode
|
|
62
|
+
* the caller overrides with the daemon's base URL after start().
|
|
63
|
+
* The llmConfig parameter is currently only consumed by the daemon manager
|
|
64
|
+
* (via env vars); it's kept on the client builder signature so callers
|
|
65
|
+
* don't need to branch and so future features can forward it.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildClientOptions(_llmConfig: {
|
|
68
|
+
provider?: string;
|
|
69
|
+
apiKey?: string;
|
|
70
|
+
model?: string;
|
|
71
|
+
}, _pluginCfg: PluginConfig, externalApi: {
|
|
72
|
+
apiUrl: string | null;
|
|
73
|
+
apiToken: string | null;
|
|
74
|
+
}): HindsightClientOptions;
|
|
33
75
|
export default function (api: MoltbotPluginAPI): void;
|
|
76
|
+
export declare function buildRetainRequest(transcript: string, messageCount: number, effectiveCtx: PluginHookAgentContext | undefined, pluginConfig: PluginConfig, now?: number, options?: {
|
|
77
|
+
retentionScope?: 'turn' | 'window' | 'manual';
|
|
78
|
+
windowTurns?: number;
|
|
79
|
+
turnIndex?: number;
|
|
80
|
+
}): RetainRequest;
|
|
34
81
|
export declare function prepareRetentionTranscript(messages: any[], pluginConfig: PluginConfig, retainFullWindow?: boolean): {
|
|
35
82
|
transcript: string;
|
|
36
83
|
messageCount: number;
|