@psiclawops/hypermem 0.9.2 → 0.9.4
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/CHANGELOG.md +16 -0
- package/INSTALL.md +73 -70
- package/README.md +33 -51
- package/assets/default-config.json +47 -0
- package/bin/hypermem-doctor.mjs +76 -2
- package/bin/hypermem-status.mjs +255 -7
- package/dist/adaptive-lifecycle.d.ts +39 -0
- package/dist/adaptive-lifecycle.d.ts.map +1 -1
- package/dist/adaptive-lifecycle.js +87 -9
- package/dist/background-indexer.d.ts.map +1 -1
- package/dist/background-indexer.js +7 -5
- package/dist/compositor.d.ts.map +1 -1
- package/dist/compositor.js +239 -20
- package/dist/hybrid-retrieval.d.ts +8 -0
- package/dist/hybrid-retrieval.d.ts.map +1 -1
- package/dist/hybrid-retrieval.js +112 -10
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/message-store.d.ts +62 -1
- package/dist/message-store.d.ts.map +1 -1
- package/dist/message-store.js +355 -2
- package/dist/open-domain.d.ts.map +1 -1
- package/dist/open-domain.js +3 -2
- package/dist/proactive-pass.d.ts +42 -2
- package/dist/proactive-pass.d.ts.map +1 -1
- package/dist/proactive-pass.js +294 -39
- package/dist/topic-synthesizer.d.ts.map +1 -1
- package/dist/topic-synthesizer.js +9 -3
- package/dist/types.d.ts +99 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-store.d.ts +10 -1
- package/dist/vector-store.d.ts.map +1 -1
- package/dist/vector-store.js +45 -9
- package/docs/DIAGNOSTICS.md +87 -0
- package/docs/INTEGRATION_VALIDATION.md +40 -1
- package/docs/ROADMAP.md +25 -12
- package/docs/TUNING.md +45 -4
- package/install.sh +5 -60
- package/memory-plugin/dist/index.d.ts +24 -0
- package/memory-plugin/dist/index.js +570 -0
- package/memory-plugin/openclaw.plugin.json +199 -2
- package/memory-plugin/package.json +3 -3
- package/package.json +24 -10
- package/plugin/dist/index.d.ts +210 -0
- package/plugin/dist/index.d.ts.map +1 -0
- package/plugin/dist/index.js +3641 -0
- package/plugin/dist/index.js.map +1 -0
- package/plugin/openclaw.plugin.json +199 -2
- package/plugin/package.json +4 -4
- package/scripts/install-packed-runtime.mjs +99 -0
- package/scripts/install-runtime.mjs +164 -4
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HyperMem Memory Plugin
|
|
3
|
+
*
|
|
4
|
+
* Thin adapter that bridges HyperMem's retrieval capabilities into
|
|
5
|
+
* OpenClaw's memory slot contract (`kind: "memory"`).
|
|
6
|
+
*
|
|
7
|
+
* The context engine plugin (hypercompositor) owns the full lifecycle:
|
|
8
|
+
* ingest, assemble, compact, afterTurn, bootstrap, dispose.
|
|
9
|
+
*
|
|
10
|
+
* This plugin owns the memory slot contract:
|
|
11
|
+
* - registerMemoryCapability() with runtime + publicArtifacts
|
|
12
|
+
* - memory_search tool backing via MemorySearchManager
|
|
13
|
+
* - Public artifacts for memory-wiki bridge
|
|
14
|
+
*
|
|
15
|
+
* Both plugins share the same HyperMem singleton (loaded from repo dist).
|
|
16
|
+
*/
|
|
17
|
+
import { definePluginEntry, emptyPluginConfigSchema } from 'openclaw/plugin-sdk/plugin-entry';
|
|
18
|
+
import { matchTriggers, TRIGGER_REGISTRY } from '@psiclawops/hypermem';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import fs from 'fs/promises';
|
|
21
|
+
import fsSync from 'fs';
|
|
22
|
+
import os from 'os';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
// ─── HyperMem singleton ────────────────────────────────────────
|
|
25
|
+
// HyperMem.create() in the core package now dedupes per absolute dataDir, so
|
|
26
|
+
// whichever of the two plugins (context-engine, memory) calls create() first
|
|
27
|
+
// owns the instance. To avoid a race where this plugin would otherwise win
|
|
28
|
+
// boot with no embedding config and force defaults onto the shared instance,
|
|
29
|
+
// we load the same user config file the context-engine plugin loads and pass
|
|
30
|
+
// the full embedding/reranker config through to create().
|
|
31
|
+
const __pluginDir = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
async function resolveHyperMemPath() {
|
|
33
|
+
try {
|
|
34
|
+
const resolvedUrl = await import.meta.resolve('@psiclawops/hypermem');
|
|
35
|
+
return resolvedUrl.startsWith('file:') ? fileURLToPath(resolvedUrl) : resolvedUrl;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return path.resolve(__pluginDir, '../../dist/index.js');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function loadFileConfig(dataDir) {
|
|
42
|
+
const configPath = path.join(dataDir, 'config.json');
|
|
43
|
+
try {
|
|
44
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
45
|
+
return JSON.parse(raw);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (err.code !== 'ENOENT') {
|
|
49
|
+
console.warn(`[hypermem-memory] Failed to parse config.json (using defaults):`, err.message);
|
|
50
|
+
}
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let _hm = null;
|
|
55
|
+
let _hmInitPromise = null;
|
|
56
|
+
async function getHyperMem() {
|
|
57
|
+
if (_hm)
|
|
58
|
+
return _hm;
|
|
59
|
+
if (_hmInitPromise)
|
|
60
|
+
return _hmInitPromise;
|
|
61
|
+
_hmInitPromise = (async () => {
|
|
62
|
+
const hypermemPath = await resolveHyperMemPath();
|
|
63
|
+
const mod = await import(hypermemPath);
|
|
64
|
+
const HyperMem = mod.HyperMem;
|
|
65
|
+
const dataDir = path.join(os.homedir(), '.openclaw/hypermem');
|
|
66
|
+
const fileConfig = await loadFileConfig(dataDir);
|
|
67
|
+
const createConfig = {
|
|
68
|
+
dataDir,
|
|
69
|
+
cache: {
|
|
70
|
+
keyPrefix: 'hm:',
|
|
71
|
+
sessionTTL: 14400,
|
|
72
|
+
historyTTL: 86400,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
// Forward embedding + reranker so this plugin's create() call produces
|
|
76
|
+
// an equivalent instance to the context-engine plugin's. Other config
|
|
77
|
+
// sections (compositor, indexer, dreaming, etc.) are owned by the
|
|
78
|
+
// context-engine plugin and only matter when it wins the singleton race.
|
|
79
|
+
if (fileConfig.embedding)
|
|
80
|
+
createConfig.embedding = fileConfig.embedding;
|
|
81
|
+
if (fileConfig.reranker)
|
|
82
|
+
createConfig.reranker = fileConfig.reranker;
|
|
83
|
+
const instance = await HyperMem.create(createConfig);
|
|
84
|
+
_hm = instance;
|
|
85
|
+
return instance;
|
|
86
|
+
})();
|
|
87
|
+
return _hmInitPromise;
|
|
88
|
+
}
|
|
89
|
+
const DOCTRINE_COLLECTIONS = new Set([
|
|
90
|
+
'governance/policy',
|
|
91
|
+
'governance/charter',
|
|
92
|
+
'governance/comms',
|
|
93
|
+
'operations/agents',
|
|
94
|
+
]);
|
|
95
|
+
function doctrineScore(chunk, rank) {
|
|
96
|
+
const collectionBoost = chunk.collection.startsWith('governance/') ? 1.25 : 1.1;
|
|
97
|
+
return collectionBoost - Math.min(rank, 9) * 0.03;
|
|
98
|
+
}
|
|
99
|
+
function docChunkToMemoryResult(chunk, rank) {
|
|
100
|
+
return {
|
|
101
|
+
path: chunk.sourcePath,
|
|
102
|
+
startLine: 0,
|
|
103
|
+
endLine: 0,
|
|
104
|
+
score: doctrineScore(chunk, rank),
|
|
105
|
+
snippet: chunk.content.slice(0, 500),
|
|
106
|
+
source: 'memory',
|
|
107
|
+
citation: `[doc:${chunk.collection}:${chunk.sectionPath}]`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Create a MemorySearchManager backed by HyperMem's retrieval pipeline.
|
|
112
|
+
*
|
|
113
|
+
* Uses HyperMem's:
|
|
114
|
+
* - library.db fact search (FTS5 + BM25)
|
|
115
|
+
* - vector store semantic search (when available)
|
|
116
|
+
* - message search (full-text across conversations)
|
|
117
|
+
*/
|
|
118
|
+
function createMemorySearchManager(hm, agentId, workspaceDir) {
|
|
119
|
+
return {
|
|
120
|
+
async search(query, opts) {
|
|
121
|
+
const maxResults = opts?.maxResults ?? 10;
|
|
122
|
+
const minScore = opts?.minScore ?? 0;
|
|
123
|
+
const results = [];
|
|
124
|
+
const seenDocChunks = new Set();
|
|
125
|
+
// 0. Canonical doctrine search. Explicit governance queries should surface
|
|
126
|
+
// policy, charter, comms, and AGENTS chunks before stale daily-memory folklore.
|
|
127
|
+
try {
|
|
128
|
+
const triggers = matchTriggers(query, TRIGGER_REGISTRY)
|
|
129
|
+
.filter(trigger => DOCTRINE_COLLECTIONS.has(trigger.collection))
|
|
130
|
+
.slice(0, 4);
|
|
131
|
+
for (const trigger of triggers) {
|
|
132
|
+
const chunks = hm.queryDocChunks({
|
|
133
|
+
collection: trigger.collection,
|
|
134
|
+
agentId,
|
|
135
|
+
keyword: query,
|
|
136
|
+
limit: Math.max(3, Math.ceil(maxResults / Math.max(1, triggers.length))),
|
|
137
|
+
});
|
|
138
|
+
chunks.forEach((chunk, rank) => {
|
|
139
|
+
const key = `${chunk.sourcePath}:${chunk.sectionPath}:${chunk.sourceHash}`;
|
|
140
|
+
if (seenDocChunks.has(key))
|
|
141
|
+
return;
|
|
142
|
+
seenDocChunks.add(key);
|
|
143
|
+
const result = docChunkToMemoryResult(chunk, rank);
|
|
144
|
+
if (result.score >= minScore)
|
|
145
|
+
results.push(result);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Doctrine search is a precision boost, not a hard dependency.
|
|
151
|
+
}
|
|
152
|
+
// 1. Fact search (FTS5 + BM25 from library.db)
|
|
153
|
+
try {
|
|
154
|
+
const facts = hm.getActiveFacts(agentId, { limit: maxResults * 2 });
|
|
155
|
+
// Simple keyword matching for facts (FTS5 handles this in the DB layer)
|
|
156
|
+
const queryLower = query.toLowerCase();
|
|
157
|
+
const queryTerms = queryLower.split(/\s+/).filter(t => t.length > 2);
|
|
158
|
+
for (const fact of facts) {
|
|
159
|
+
const contentLower = fact.content.toLowerCase();
|
|
160
|
+
const matchCount = queryTerms.filter(t => contentLower.includes(t)).length;
|
|
161
|
+
if (matchCount === 0)
|
|
162
|
+
continue;
|
|
163
|
+
const score = matchCount / queryTerms.length;
|
|
164
|
+
if (score < minScore)
|
|
165
|
+
continue;
|
|
166
|
+
results.push({
|
|
167
|
+
path: `library://facts/${fact.id}`,
|
|
168
|
+
startLine: 0,
|
|
169
|
+
endLine: 0,
|
|
170
|
+
score,
|
|
171
|
+
snippet: fact.content.slice(0, 300),
|
|
172
|
+
source: 'memory',
|
|
173
|
+
citation: fact.domain ? `[fact:${fact.domain}]` : '[fact]',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Fact search non-fatal
|
|
179
|
+
}
|
|
180
|
+
// 2. Vector/semantic search (when available)
|
|
181
|
+
try {
|
|
182
|
+
const vectorStore = hm.getVectorStore();
|
|
183
|
+
if (vectorStore) {
|
|
184
|
+
const vectorResults = await hm.semanticSearch(agentId, query, {
|
|
185
|
+
limit: maxResults,
|
|
186
|
+
maxDistance: 1.5,
|
|
187
|
+
});
|
|
188
|
+
for (const vr of vectorResults) {
|
|
189
|
+
const score = 1.0 - (vr.distance / 2.0); // normalize distance to 0-1 score
|
|
190
|
+
if (score < minScore)
|
|
191
|
+
continue;
|
|
192
|
+
results.push({
|
|
193
|
+
path: `vector://${vr.sourceTable}/${vr.sourceId}`,
|
|
194
|
+
startLine: 0,
|
|
195
|
+
endLine: 0,
|
|
196
|
+
score,
|
|
197
|
+
snippet: vr.content.slice(0, 300),
|
|
198
|
+
source: 'memory',
|
|
199
|
+
citation: `[${vr.sourceTable}:${vr.sourceId}]`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Vector search non-fatal
|
|
206
|
+
}
|
|
207
|
+
// 3. Message search (FTS5 across conversations)
|
|
208
|
+
try {
|
|
209
|
+
const messageResults = hm.search(agentId, query, maxResults);
|
|
210
|
+
for (const msg of messageResults) {
|
|
211
|
+
const content = msg.textContent ?? '';
|
|
212
|
+
results.push({
|
|
213
|
+
path: `messages://${msg.conversationId ?? 'unknown'}/${msg.id}`,
|
|
214
|
+
startLine: 0,
|
|
215
|
+
endLine: 0,
|
|
216
|
+
score: 0.5, // message search doesn't return scores, use mid-range
|
|
217
|
+
snippet: content.slice(0, 300),
|
|
218
|
+
source: 'sessions',
|
|
219
|
+
citation: `[message:${msg.id}]`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Message search non-fatal
|
|
225
|
+
}
|
|
226
|
+
// Deduplicate by content similarity, sort by score, limit
|
|
227
|
+
results.sort((a, b) => b.score - a.score);
|
|
228
|
+
return results.slice(0, maxResults);
|
|
229
|
+
},
|
|
230
|
+
async readFile(params) {
|
|
231
|
+
const absPath = path.resolve(workspaceDir, params.relPath);
|
|
232
|
+
try {
|
|
233
|
+
const content = await fs.readFile(absPath, 'utf-8');
|
|
234
|
+
const lines = content.split('\n');
|
|
235
|
+
const from = params.from ?? 0;
|
|
236
|
+
const count = params.lines ?? lines.length;
|
|
237
|
+
const slice = lines.slice(from, from + count);
|
|
238
|
+
return { text: slice.join('\n'), path: absPath };
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
return { text: `Error reading ${absPath}: ${err.message}`, path: absPath };
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
status() {
|
|
245
|
+
const vectorStore = hm.getVectorStore();
|
|
246
|
+
const vectorStats = vectorStore ? hm.getVectorStats(agentId) : null;
|
|
247
|
+
return {
|
|
248
|
+
backend: 'builtin',
|
|
249
|
+
provider: 'hypermem',
|
|
250
|
+
model: 'hypermem-fts5+vector',
|
|
251
|
+
workspaceDir,
|
|
252
|
+
dbPath: path.join(os.homedir(), '.openclaw/hypermem'),
|
|
253
|
+
sources: ['memory', 'sessions'],
|
|
254
|
+
fts: {
|
|
255
|
+
enabled: true,
|
|
256
|
+
available: true,
|
|
257
|
+
},
|
|
258
|
+
vector: {
|
|
259
|
+
enabled: !!vectorStore,
|
|
260
|
+
available: !!vectorStore,
|
|
261
|
+
dims: vectorStats?.dimensions
|
|
262
|
+
?? vectorStats?.dims
|
|
263
|
+
?? undefined,
|
|
264
|
+
},
|
|
265
|
+
custom: {
|
|
266
|
+
vectorStats: vectorStats ?? undefined,
|
|
267
|
+
factCount: hm.getActiveFacts(agentId, { limit: 1 }).length > 0 ? 'available' : 'empty',
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
async probeEmbeddingAvailability() {
|
|
272
|
+
try {
|
|
273
|
+
const vectorStore = hm.getVectorStore();
|
|
274
|
+
if (!vectorStore)
|
|
275
|
+
return { ok: false, error: 'Vector store not initialized' };
|
|
276
|
+
return { ok: true };
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
return { ok: false, error: err.message };
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
async probeVectorAvailability() {
|
|
283
|
+
return !!hm.getVectorStore();
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// ─── history.query agent tool ───────────────────────────────────
|
|
288
|
+
const HISTORY_QUERY_MODES = [
|
|
289
|
+
'runtime_chain',
|
|
290
|
+
'transcript_tail',
|
|
291
|
+
'tool_events',
|
|
292
|
+
'by_topic',
|
|
293
|
+
'by_context',
|
|
294
|
+
'cross_session',
|
|
295
|
+
];
|
|
296
|
+
const HISTORY_QUERY_TOOL_PARAMETERS = {
|
|
297
|
+
type: 'object',
|
|
298
|
+
additionalProperties: false,
|
|
299
|
+
properties: {
|
|
300
|
+
mode: {
|
|
301
|
+
type: 'string',
|
|
302
|
+
enum: HISTORY_QUERY_MODES,
|
|
303
|
+
description: 'History query mode. cross_session is scoped to the current agent by default.',
|
|
304
|
+
},
|
|
305
|
+
sessionKey: {
|
|
306
|
+
type: 'string',
|
|
307
|
+
description: 'Optional session key. Defaults to the active session when available.',
|
|
308
|
+
},
|
|
309
|
+
conversationId: {
|
|
310
|
+
type: 'number',
|
|
311
|
+
description: 'Optional direct conversation id. Must belong to the current agent.',
|
|
312
|
+
},
|
|
313
|
+
contextId: {
|
|
314
|
+
type: 'number',
|
|
315
|
+
description: 'Required for by_context mode. Must belong to the current agent.',
|
|
316
|
+
},
|
|
317
|
+
topicId: {
|
|
318
|
+
type: 'string',
|
|
319
|
+
description: 'Required for by_topic mode.',
|
|
320
|
+
},
|
|
321
|
+
limit: {
|
|
322
|
+
type: 'number',
|
|
323
|
+
description: 'Optional result limit. HyperMem clamps this to the hard cap for the selected mode.',
|
|
324
|
+
},
|
|
325
|
+
minMessageId: {
|
|
326
|
+
type: 'number',
|
|
327
|
+
description: 'Optional lower message id bound for supported modes.',
|
|
328
|
+
},
|
|
329
|
+
since: {
|
|
330
|
+
type: 'string',
|
|
331
|
+
description: 'Optional ISO timestamp lower bound for cross_session mode.',
|
|
332
|
+
},
|
|
333
|
+
includeArchived: {
|
|
334
|
+
type: 'boolean',
|
|
335
|
+
description: 'Allow archived/forked contexts for by_context mode.',
|
|
336
|
+
},
|
|
337
|
+
includeToolPayloads: {
|
|
338
|
+
type: 'boolean',
|
|
339
|
+
description: 'Return raw tool payloads for tool_events. Requires owner context; default is redacted.',
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
required: ['mode'],
|
|
343
|
+
};
|
|
344
|
+
function asRecord(value) {
|
|
345
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
346
|
+
? value
|
|
347
|
+
: {};
|
|
348
|
+
}
|
|
349
|
+
function optionalString(params, key) {
|
|
350
|
+
const value = params[key];
|
|
351
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
352
|
+
}
|
|
353
|
+
function optionalNumber(params, key) {
|
|
354
|
+
const value = params[key];
|
|
355
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
356
|
+
return undefined;
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
function optionalBoolean(params, key) {
|
|
360
|
+
return typeof params[key] === 'boolean' ? params[key] : undefined;
|
|
361
|
+
}
|
|
362
|
+
function isHistoryQueryMode(value) {
|
|
363
|
+
return typeof value === 'string' && HISTORY_QUERY_MODES.includes(value);
|
|
364
|
+
}
|
|
365
|
+
function historyQueryTelemetryEnabled() {
|
|
366
|
+
return process.env.HYPERMEM_TELEMETRY === '1';
|
|
367
|
+
}
|
|
368
|
+
function emitHistoryQueryTelemetry(event) {
|
|
369
|
+
if (!historyQueryTelemetryEnabled())
|
|
370
|
+
return;
|
|
371
|
+
const telemetryPath = process.env.HYPERMEM_TELEMETRY_PATH || './hypermem-telemetry.jsonl';
|
|
372
|
+
try {
|
|
373
|
+
fsSync.appendFileSync(telemetryPath, `${JSON.stringify(event)}\n`, 'utf8');
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// Telemetry must never break the tool path.
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function createHistoryQueryTool(ctx) {
|
|
380
|
+
return {
|
|
381
|
+
name: 'history_query',
|
|
382
|
+
label: 'history.query',
|
|
383
|
+
description: [
|
|
384
|
+
'Query HyperMem SQLite-backed message history for the current agent.',
|
|
385
|
+
'Use this when exact conversation state is needed instead of semantic recall.',
|
|
386
|
+
'Modes: runtime_chain, transcript_tail, tool_events, by_topic, by_context, cross_session.',
|
|
387
|
+
'Tool payloads are redacted by default; raw payloads require owner context.',
|
|
388
|
+
].join(' '),
|
|
389
|
+
parameters: HISTORY_QUERY_TOOL_PARAMETERS,
|
|
390
|
+
displaySummary: 'Query HyperMem message history',
|
|
391
|
+
async execute(_toolCallId, rawParams) {
|
|
392
|
+
const started = Date.now();
|
|
393
|
+
const params = asRecord(rawParams);
|
|
394
|
+
const mode = params.mode;
|
|
395
|
+
const agentId = ctx.agentId || 'main';
|
|
396
|
+
const includeToolPayloads = optionalBoolean(params, 'includeToolPayloads') === true;
|
|
397
|
+
const baseTelemetry = {
|
|
398
|
+
ts: new Date().toISOString(),
|
|
399
|
+
agentId,
|
|
400
|
+
hasSessionKey: Boolean(optionalString(params, 'sessionKey') ?? ctx.sessionKey),
|
|
401
|
+
hasConversationId: optionalNumber(params, 'conversationId') !== undefined,
|
|
402
|
+
includeToolPayloads,
|
|
403
|
+
};
|
|
404
|
+
if (!isHistoryQueryMode(mode)) {
|
|
405
|
+
emitHistoryQueryTelemetry({
|
|
406
|
+
event: 'history-query',
|
|
407
|
+
status: 'error',
|
|
408
|
+
...baseTelemetry,
|
|
409
|
+
durationMs: Date.now() - started,
|
|
410
|
+
errorCode: 'invalid-mode',
|
|
411
|
+
});
|
|
412
|
+
throw new Error(`history.query: mode must be one of ${HISTORY_QUERY_MODES.join(', ')}`);
|
|
413
|
+
}
|
|
414
|
+
if (includeToolPayloads && !ctx.senderIsOwner) {
|
|
415
|
+
emitHistoryQueryTelemetry({
|
|
416
|
+
event: 'history-query',
|
|
417
|
+
status: 'error',
|
|
418
|
+
mode,
|
|
419
|
+
...baseTelemetry,
|
|
420
|
+
durationMs: Date.now() - started,
|
|
421
|
+
errorCode: 'owner-required',
|
|
422
|
+
});
|
|
423
|
+
throw new Error('history.query: includeToolPayloads requires owner context');
|
|
424
|
+
}
|
|
425
|
+
const query = {
|
|
426
|
+
agentId,
|
|
427
|
+
mode,
|
|
428
|
+
sessionKey: optionalString(params, 'sessionKey') ?? ctx.sessionKey,
|
|
429
|
+
conversationId: optionalNumber(params, 'conversationId'),
|
|
430
|
+
contextId: optionalNumber(params, 'contextId'),
|
|
431
|
+
topicId: optionalString(params, 'topicId'),
|
|
432
|
+
limit: optionalNumber(params, 'limit'),
|
|
433
|
+
minMessageId: optionalNumber(params, 'minMessageId'),
|
|
434
|
+
since: optionalString(params, 'since'),
|
|
435
|
+
includeArchived: optionalBoolean(params, 'includeArchived'),
|
|
436
|
+
includeToolPayloads,
|
|
437
|
+
};
|
|
438
|
+
for (const [key, value] of Object.entries(query)) {
|
|
439
|
+
if (value === undefined)
|
|
440
|
+
delete query[key];
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
const hm = await getHyperMem();
|
|
444
|
+
const result = hm.queryHistory(query);
|
|
445
|
+
emitHistoryQueryTelemetry({
|
|
446
|
+
event: 'history-query',
|
|
447
|
+
status: 'ok',
|
|
448
|
+
mode,
|
|
449
|
+
...baseTelemetry,
|
|
450
|
+
messageCount: result.messages.length,
|
|
451
|
+
truncated: Boolean(result.truncated),
|
|
452
|
+
redacted: Boolean(result.redacted),
|
|
453
|
+
durationMs: Date.now() - started,
|
|
454
|
+
});
|
|
455
|
+
const summary = `history.query ${mode}: ${result.messages.length} message(s)`
|
|
456
|
+
+ (result.truncated ? ' (truncated)' : '')
|
|
457
|
+
+ (result.redacted ? ' (redacted)' : '');
|
|
458
|
+
return {
|
|
459
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
460
|
+
details: { status: 'ok', summary, result },
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
emitHistoryQueryTelemetry({
|
|
465
|
+
event: 'history-query',
|
|
466
|
+
status: 'error',
|
|
467
|
+
mode,
|
|
468
|
+
...baseTelemetry,
|
|
469
|
+
durationMs: Date.now() - started,
|
|
470
|
+
errorCode: 'query-failed',
|
|
471
|
+
});
|
|
472
|
+
throw err;
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
// ─── Manager cache ──────────────────────────────────────────────
|
|
478
|
+
// One manager per agentId; closed on plugin dispose.
|
|
479
|
+
const _managers = new Map();
|
|
480
|
+
// ─── Plugin Entry ───────────────────────────────────────────────
|
|
481
|
+
export default definePluginEntry({
|
|
482
|
+
id: 'hypermem',
|
|
483
|
+
name: 'HyperMem Memory',
|
|
484
|
+
description: 'Bridges HyperMem retrieval (facts, vectors, messages) into the OpenClaw memory slot for memory_search and memory-wiki.',
|
|
485
|
+
kind: 'memory',
|
|
486
|
+
configSchema: emptyPluginConfigSchema(),
|
|
487
|
+
register(api) {
|
|
488
|
+
api.registerTool((ctx) => createHistoryQueryTool(ctx), { name: 'history_query', optional: true });
|
|
489
|
+
api.registerMemoryCapability({
|
|
490
|
+
runtime: {
|
|
491
|
+
async getMemorySearchManager(params) {
|
|
492
|
+
try {
|
|
493
|
+
const hm = await getHyperMem();
|
|
494
|
+
const agentId = params.agentId || 'main';
|
|
495
|
+
// Cache managers per agent
|
|
496
|
+
if (!_managers.has(agentId)) {
|
|
497
|
+
// Resolve workspace dir from agent config
|
|
498
|
+
const agents = params.cfg?.agents?.list ?? [];
|
|
499
|
+
const agentCfg = agents.find((a) => a.id === agentId);
|
|
500
|
+
const workspaceDir = agentCfg?.workspace
|
|
501
|
+
?? path.join(os.homedir(), '.openclaw/workspace');
|
|
502
|
+
_managers.set(agentId, createMemorySearchManager(hm, agentId, workspaceDir));
|
|
503
|
+
}
|
|
504
|
+
return { manager: _managers.get(agentId) };
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
return { manager: null, error: err.message };
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
resolveMemoryBackendConfig(_params) {
|
|
511
|
+
return { backend: 'builtin' };
|
|
512
|
+
},
|
|
513
|
+
async closeAllMemorySearchManagers() {
|
|
514
|
+
_managers.clear();
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
publicArtifacts: {
|
|
518
|
+
async listArtifacts(params) {
|
|
519
|
+
const artifacts = [];
|
|
520
|
+
// List memory files for each agent
|
|
521
|
+
const agents = params.cfg?.agents?.list ?? [];
|
|
522
|
+
for (const agent of agents) {
|
|
523
|
+
const agentId = agent.id;
|
|
524
|
+
if (!agentId)
|
|
525
|
+
continue;
|
|
526
|
+
const workspace = agent.workspace;
|
|
527
|
+
if (!workspace)
|
|
528
|
+
continue;
|
|
529
|
+
const memoryDir = path.join(workspace, 'memory');
|
|
530
|
+
try {
|
|
531
|
+
const files = await fs.readdir(memoryDir);
|
|
532
|
+
for (const file of files) {
|
|
533
|
+
if (!file.endsWith('.md'))
|
|
534
|
+
continue;
|
|
535
|
+
artifacts.push({
|
|
536
|
+
kind: 'memory-daily',
|
|
537
|
+
workspaceDir: workspace,
|
|
538
|
+
relativePath: `memory/${file}`,
|
|
539
|
+
absolutePath: path.join(memoryDir, file),
|
|
540
|
+
agentIds: [agentId],
|
|
541
|
+
contentType: 'markdown',
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
// No memory dir for this agent — skip
|
|
547
|
+
}
|
|
548
|
+
// Also expose MEMORY.md index
|
|
549
|
+
const memoryIndex = path.join(workspace, 'MEMORY.md');
|
|
550
|
+
try {
|
|
551
|
+
await fs.access(memoryIndex);
|
|
552
|
+
artifacts.push({
|
|
553
|
+
kind: 'memory-index',
|
|
554
|
+
workspaceDir: workspace,
|
|
555
|
+
relativePath: 'MEMORY.md',
|
|
556
|
+
absolutePath: memoryIndex,
|
|
557
|
+
agentIds: [agentId],
|
|
558
|
+
contentType: 'markdown',
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
// No MEMORY.md — skip
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return artifacts;
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
},
|
|
570
|
+
});
|