hippo-memory 0.8.1 → 0.9.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 +71 -2
- package/dist/cli.d.ts +5 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +581 -211
- package/dist/cli.js.map +1 -1
- package/dist/consolidate.d.ts.map +1 -1
- package/dist/consolidate.js +12 -5
- package/dist/consolidate.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +149 -100
- package/dist/db.js.map +1 -1
- package/dist/handoff.d.ts +29 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +30 -0
- package/dist/handoff.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/search.d.ts +19 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +35 -1
- package/dist/search.js.map +1 -1
- package/dist/store.d.ts +18 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +209 -99
- package/dist/store.js.map +1 -1
- package/dist/working-memory.d.ts +59 -0
- package/dist/working-memory.d.ts.map +1 -0
- package/dist/working-memory.js +149 -0
- package/dist/working-memory.js.map +1 -0
- package/extensions/openclaw-plugin/index.ts +569 -495
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -1,495 +1,569 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hippo Memory - OpenClaw Plugin
|
|
3
|
-
*
|
|
4
|
-
* Auto-injects relevant memory context at session start,
|
|
5
|
-
* captures errors during sessions, and runs consolidation.
|
|
6
|
-
*
|
|
7
|
-
* Config lives under plugins.entries.hippo-memory.config
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { execSync } from 'child_process';
|
|
11
|
-
import { existsSync } from 'fs';
|
|
12
|
-
import { basename, dirname, join } from 'path';
|
|
13
|
-
|
|
14
|
-
interface HippoConfig {
|
|
15
|
-
budget?: number;
|
|
16
|
-
autoContext?: boolean;
|
|
17
|
-
autoLearn?: boolean;
|
|
18
|
-
autoSleep?: boolean;
|
|
19
|
-
framing?: 'observe' | 'suggest' | 'assert';
|
|
20
|
-
root?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type HippoRuntimeContext = {
|
|
24
|
-
workspaceDir?: string;
|
|
25
|
-
agentId?: string;
|
|
26
|
-
sessionId?: string;
|
|
27
|
-
sessionKey?: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const AUTO_SLEEP_SESSION_THRESHOLD = 10;
|
|
31
|
-
const sessionMemoryCounts = new Map<string, number>();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
sessionMemoryCounts.set(key, (sessionMemoryCounts.get(key) ?? 0) + 1);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function consumeSessionMemoryCount(
|
|
101
|
-
ctx: Pick<HippoRuntimeContext, 'sessionId' | 'sessionKey' | 'agentId'>,
|
|
102
|
-
): number {
|
|
103
|
-
const key = getSessionIdentity(ctx);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.
|
|
114
|
-
.replace(
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (params.
|
|
215
|
-
if (params.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
args
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Hippo Memory - OpenClaw Plugin
|
|
3
|
+
*
|
|
4
|
+
* Auto-injects relevant memory context at session start,
|
|
5
|
+
* captures errors during sessions, and runs consolidation.
|
|
6
|
+
*
|
|
7
|
+
* Config lives under plugins.entries.hippo-memory.config
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { basename, dirname, join } from 'path';
|
|
13
|
+
|
|
14
|
+
interface HippoConfig {
|
|
15
|
+
budget?: number;
|
|
16
|
+
autoContext?: boolean;
|
|
17
|
+
autoLearn?: boolean;
|
|
18
|
+
autoSleep?: boolean;
|
|
19
|
+
framing?: 'observe' | 'suggest' | 'assert';
|
|
20
|
+
root?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type HippoRuntimeContext = {
|
|
24
|
+
workspaceDir?: string;
|
|
25
|
+
agentId?: string;
|
|
26
|
+
sessionId?: string;
|
|
27
|
+
sessionKey?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const AUTO_SLEEP_SESSION_THRESHOLD = 10;
|
|
31
|
+
const sessionMemoryCounts = new Map<string, number>();
|
|
32
|
+
const injectedSessions = new Set<string>();
|
|
33
|
+
|
|
34
|
+
function getConfig(api: any): HippoConfig {
|
|
35
|
+
try {
|
|
36
|
+
const entries = api.config?.plugins?.entries?.['hippo-memory'];
|
|
37
|
+
return entries?.config ?? {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findHippoRoot(workspace?: string, configRoot?: string): string | null {
|
|
44
|
+
if (configRoot && existsSync(configRoot)) return configRoot;
|
|
45
|
+
|
|
46
|
+
const candidates = [
|
|
47
|
+
workspace ? join(workspace, '.hippo') : null,
|
|
48
|
+
process.env.HIPPO_ROOT,
|
|
49
|
+
join(process.env.USERPROFILE || process.env.HOME || '', '.hippo'),
|
|
50
|
+
].filter(Boolean) as string[];
|
|
51
|
+
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
if (existsSync(candidate)) return candidate;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getAgentWorkspace(api: any, agentId?: string): string | undefined {
|
|
59
|
+
try {
|
|
60
|
+
const agents = api.config?.agents;
|
|
61
|
+
const list = Array.isArray(agents?.list) ? agents.list : [];
|
|
62
|
+
|
|
63
|
+
if (agentId) {
|
|
64
|
+
const match = list.find((agent: any) => agent?.id === agentId);
|
|
65
|
+
if (typeof match?.workspace === 'string' && match.workspace) return match.workspace;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const defaultAgent = list.find((agent: any) => agent?.default);
|
|
69
|
+
if (typeof defaultAgent?.workspace === 'string' && defaultAgent.workspace) {
|
|
70
|
+
return defaultAgent.workspace;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const fallback = agents?.defaults?.workspace;
|
|
74
|
+
return typeof fallback === 'string' && fallback ? fallback : undefined;
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveHippoCwd(workspace?: string, configRoot?: string): string {
|
|
81
|
+
const hippoRoot = findHippoRoot(workspace, configRoot);
|
|
82
|
+
if (!hippoRoot) return workspace || process.cwd();
|
|
83
|
+
return basename(hippoRoot).toLowerCase() === '.hippo' ? dirname(hippoRoot) : hippoRoot;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveHippoCwdFromContext(api: any, ctx: HippoRuntimeContext, configRoot?: string): string {
|
|
87
|
+
const workspace = ctx.workspaceDir ?? getAgentWorkspace(api, ctx.agentId);
|
|
88
|
+
return resolveHippoCwd(workspace, configRoot);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getSessionIdentity(ctx: Pick<HippoRuntimeContext, 'sessionId' | 'sessionKey' | 'agentId'>): string {
|
|
92
|
+
return ctx.sessionId ?? ctx.sessionKey ?? ctx.agentId ?? `fallback-${Date.now()}-${process.pid}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function recordSessionMemory(ctx: Pick<HippoRuntimeContext, 'sessionId' | 'sessionKey' | 'agentId'>): void {
|
|
96
|
+
const key = getSessionIdentity(ctx);
|
|
97
|
+
sessionMemoryCounts.set(key, (sessionMemoryCounts.get(key) ?? 0) + 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function consumeSessionMemoryCount(
|
|
101
|
+
ctx: Pick<HippoRuntimeContext, 'sessionId' | 'sessionKey' | 'agentId'>,
|
|
102
|
+
): number {
|
|
103
|
+
const key = getSessionIdentity(ctx);
|
|
104
|
+
const count = sessionMemoryCounts.get(key) ?? 0;
|
|
105
|
+
sessionMemoryCounts.delete(key);
|
|
106
|
+
return count;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sanitizeTag(tag?: string): string | undefined {
|
|
110
|
+
if (!tag) return undefined;
|
|
111
|
+
const normalized = tag
|
|
112
|
+
.toLowerCase()
|
|
113
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
114
|
+
.replace(/^-+|-+$/g, '')
|
|
115
|
+
.slice(0, 30);
|
|
116
|
+
return normalized || undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatToolErrorMemory(toolName: string, error: string): string {
|
|
120
|
+
const normalized = error.replace(/\s+/g, ' ').trim();
|
|
121
|
+
const truncated = normalized.slice(0, 500);
|
|
122
|
+
const suffix = normalized.length > truncated.length ? ' [truncated]' : '';
|
|
123
|
+
return `Tool '${toolName}' failed: ${truncated}${suffix}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hippoRememberSucceeded(result: string): boolean {
|
|
127
|
+
return result.includes('Remembered [');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function runHippo(args: string, cwd?: string): string {
|
|
131
|
+
try {
|
|
132
|
+
const result = execSync(`hippo ${args}`, {
|
|
133
|
+
cwd: cwd || process.cwd(),
|
|
134
|
+
timeout: 30000,
|
|
135
|
+
encoding: 'utf-8',
|
|
136
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
137
|
+
});
|
|
138
|
+
return result.trim();
|
|
139
|
+
} catch (err: any) {
|
|
140
|
+
return err.stdout?.trim() || err.message || 'hippo command failed';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default function register(api: any) {
|
|
145
|
+
const logger = api.logger ?? console;
|
|
146
|
+
|
|
147
|
+
// --- Tool: hippo_recall ---
|
|
148
|
+
api.registerTool((ctx: HippoRuntimeContext) => ({
|
|
149
|
+
name: 'hippo_recall',
|
|
150
|
+
description:
|
|
151
|
+
'Retrieve relevant memories from the project memory store. Returns memories ranked by relevance, strength, and recency within the token budget. Use at session start or when you need context about a topic.',
|
|
152
|
+
parameters: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
query: {
|
|
156
|
+
type: 'string',
|
|
157
|
+
description: 'What to search for in memory (natural language)',
|
|
158
|
+
},
|
|
159
|
+
budget: {
|
|
160
|
+
type: 'number',
|
|
161
|
+
description: 'Max tokens to return (default: 1500)',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
required: ['query'],
|
|
165
|
+
},
|
|
166
|
+
async execute(_id: string, params: { query: string; budget?: number }) {
|
|
167
|
+
const cfg = getConfig(api);
|
|
168
|
+
const budget = params.budget ?? cfg.budget ?? 1500;
|
|
169
|
+
const framing = cfg.framing ?? 'observe';
|
|
170
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
171
|
+
const result = runHippo(
|
|
172
|
+
`recall "${params.query.replace(/"/g, '\\"')}" --budget ${budget} --framing ${framing}`,
|
|
173
|
+
hippoCwd,
|
|
174
|
+
);
|
|
175
|
+
return { content: [{ type: 'text', text: result || 'No relevant memories found.' }] };
|
|
176
|
+
},
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
// --- Tool: hippo_remember ---
|
|
180
|
+
api.registerTool((ctx: HippoRuntimeContext) => ({
|
|
181
|
+
name: 'hippo_remember',
|
|
182
|
+
description:
|
|
183
|
+
'Store a new memory. Use when you learn something non-obvious, hit an error, or discover a useful pattern. Memories decay over time unless retrieved. Errors get 2x half-life.',
|
|
184
|
+
parameters: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
text: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'The memory to store (1-2 sentences, specific and concrete)',
|
|
190
|
+
},
|
|
191
|
+
error: {
|
|
192
|
+
type: 'boolean',
|
|
193
|
+
description: 'Mark as error memory (doubles half-life)',
|
|
194
|
+
},
|
|
195
|
+
pin: {
|
|
196
|
+
type: 'boolean',
|
|
197
|
+
description: 'Pin memory (never decays)',
|
|
198
|
+
},
|
|
199
|
+
tag: {
|
|
200
|
+
type: 'string',
|
|
201
|
+
description: 'Optional tag for categorization',
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
required: ['text'],
|
|
205
|
+
},
|
|
206
|
+
async execute(
|
|
207
|
+
_id: string,
|
|
208
|
+
params: { text: string; error?: boolean; pin?: boolean; tag?: string },
|
|
209
|
+
) {
|
|
210
|
+
const cfg = getConfig(api);
|
|
211
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
212
|
+
let args = `remember "${params.text.replace(/"/g, '\\"')}"`;
|
|
213
|
+
if (params.error) args += ' --error';
|
|
214
|
+
if (params.pin) args += ' --pin';
|
|
215
|
+
if (params.tag) args += ` --tag ${params.tag}`;
|
|
216
|
+
const result = runHippo(args, hippoCwd);
|
|
217
|
+
if (hippoRememberSucceeded(result)) {
|
|
218
|
+
recordSessionMemory(ctx);
|
|
219
|
+
}
|
|
220
|
+
return { content: [{ type: 'text', text: result || 'Memory stored.' }] };
|
|
221
|
+
},
|
|
222
|
+
}));
|
|
223
|
+
|
|
224
|
+
// --- Tool: hippo_outcome ---
|
|
225
|
+
api.registerTool((ctx: HippoRuntimeContext) => ({
|
|
226
|
+
name: 'hippo_outcome',
|
|
227
|
+
description:
|
|
228
|
+
'Report whether recalled memories were useful. Strengthens good memories (+5 days half-life) and weakens bad ones (-3 days). Call after completing work.',
|
|
229
|
+
parameters: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
properties: {
|
|
232
|
+
good: {
|
|
233
|
+
type: 'boolean',
|
|
234
|
+
description: 'true = memories helped, false = memories were irrelevant',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
required: ['good'],
|
|
238
|
+
},
|
|
239
|
+
async execute(_id: string, params: { good: boolean }) {
|
|
240
|
+
const cfg = getConfig(api);
|
|
241
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
242
|
+
const flag = params.good ? '--good' : '--bad';
|
|
243
|
+
const result = runHippo(`outcome ${flag}`, hippoCwd);
|
|
244
|
+
return { content: [{ type: 'text', text: result || 'Outcome recorded.' }] };
|
|
245
|
+
},
|
|
246
|
+
}));
|
|
247
|
+
|
|
248
|
+
// --- Tool: hippo_status ---
|
|
249
|
+
api.registerTool(
|
|
250
|
+
(ctx: HippoRuntimeContext) => ({
|
|
251
|
+
name: 'hippo_status',
|
|
252
|
+
description:
|
|
253
|
+
'Check memory health: counts, strengths, at-risk memories, last consolidation time.',
|
|
254
|
+
parameters: {
|
|
255
|
+
type: 'object',
|
|
256
|
+
properties: {},
|
|
257
|
+
},
|
|
258
|
+
async execute() {
|
|
259
|
+
const cfg = getConfig(api);
|
|
260
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
261
|
+
const result = runHippo('status', hippoCwd);
|
|
262
|
+
return { content: [{ type: 'text', text: result || 'No hippo store found.' }] };
|
|
263
|
+
},
|
|
264
|
+
}),
|
|
265
|
+
{ optional: true },
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// --- Tool: hippo_context ---
|
|
269
|
+
api.registerTool(
|
|
270
|
+
(ctx: HippoRuntimeContext) => ({
|
|
271
|
+
name: 'hippo_context',
|
|
272
|
+
description:
|
|
273
|
+
'Smart context injection: auto-detects current task from git state and returns relevant memories. Use at the start of any session.',
|
|
274
|
+
parameters: {
|
|
275
|
+
type: 'object',
|
|
276
|
+
properties: {
|
|
277
|
+
budget: {
|
|
278
|
+
type: 'number',
|
|
279
|
+
description: 'Max tokens (default: 1500)',
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
async execute(_id: string, params: { budget?: number }) {
|
|
284
|
+
const cfg = getConfig(api);
|
|
285
|
+
const budget = params.budget ?? cfg.budget ?? 1500;
|
|
286
|
+
const framing = cfg.framing ?? 'observe';
|
|
287
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
288
|
+
const result = runHippo(
|
|
289
|
+
`context --auto --budget ${budget} --framing ${framing}`,
|
|
290
|
+
hippoCwd,
|
|
291
|
+
);
|
|
292
|
+
return { content: [{ type: 'text', text: result || 'No context available.' }] };
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
{ optional: true },
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// --- Tool: hippo_conflicts ---
|
|
299
|
+
api.registerTool(
|
|
300
|
+
(ctx: HippoRuntimeContext) => ({
|
|
301
|
+
name: 'hippo_conflicts',
|
|
302
|
+
description:
|
|
303
|
+
'List open memory conflicts — contradictory memories that need resolution.',
|
|
304
|
+
parameters: {
|
|
305
|
+
type: 'object',
|
|
306
|
+
properties: {
|
|
307
|
+
json: {
|
|
308
|
+
type: 'boolean',
|
|
309
|
+
description: 'Output as JSON (default: false)',
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
async execute(_id: string, params: { json?: boolean }) {
|
|
314
|
+
const cfg = getConfig(api);
|
|
315
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
316
|
+
const args = params.json ? 'conflicts --json' : 'conflicts';
|
|
317
|
+
const result = runHippo(args, hippoCwd);
|
|
318
|
+
return { content: [{ type: 'text', text: result || 'No conflicts found.' }] };
|
|
319
|
+
},
|
|
320
|
+
}),
|
|
321
|
+
{ optional: true },
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// --- Tool: hippo_resolve ---
|
|
325
|
+
api.registerTool(
|
|
326
|
+
(ctx: HippoRuntimeContext) => ({
|
|
327
|
+
name: 'hippo_resolve',
|
|
328
|
+
description:
|
|
329
|
+
'Resolve a memory conflict by keeping one memory and weakening or deleting the other.',
|
|
330
|
+
parameters: {
|
|
331
|
+
type: 'object',
|
|
332
|
+
properties: {
|
|
333
|
+
conflict_id: {
|
|
334
|
+
type: 'number',
|
|
335
|
+
description: 'The conflict ID to resolve',
|
|
336
|
+
},
|
|
337
|
+
keep: {
|
|
338
|
+
type: 'string',
|
|
339
|
+
description: 'ID of the memory to keep',
|
|
340
|
+
},
|
|
341
|
+
forget: {
|
|
342
|
+
type: 'boolean',
|
|
343
|
+
description: 'Delete the losing memory instead of weakening it (default: false)',
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
required: ['conflict_id', 'keep'],
|
|
347
|
+
},
|
|
348
|
+
async execute(
|
|
349
|
+
_id: string,
|
|
350
|
+
params: { conflict_id: number; keep: string; forget?: boolean },
|
|
351
|
+
) {
|
|
352
|
+
const cfg = getConfig(api);
|
|
353
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
354
|
+
let args = `resolve ${params.conflict_id} --keep ${params.keep}`;
|
|
355
|
+
if (params.forget) args += ' --forget';
|
|
356
|
+
const result = runHippo(args, hippoCwd);
|
|
357
|
+
return { content: [{ type: 'text', text: result || 'Conflict resolved.' }] };
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
360
|
+
{ optional: true },
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// --- Tool: hippo_share ---
|
|
364
|
+
api.registerTool(
|
|
365
|
+
(ctx: HippoRuntimeContext) => ({
|
|
366
|
+
name: 'hippo_share',
|
|
367
|
+
description:
|
|
368
|
+
'Share a memory to the global store for cross-project use. Memories with universal lessons (errors, platform gotchas) transfer well; project-specific ones are filtered.',
|
|
369
|
+
parameters: {
|
|
370
|
+
type: 'object',
|
|
371
|
+
properties: {
|
|
372
|
+
id: {
|
|
373
|
+
type: 'string',
|
|
374
|
+
description: 'Memory ID to share (or "auto" to auto-share all high-scoring memories)',
|
|
375
|
+
},
|
|
376
|
+
force: {
|
|
377
|
+
type: 'boolean',
|
|
378
|
+
description: 'Share even if transfer score is low (default: false)',
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
required: ['id'],
|
|
382
|
+
},
|
|
383
|
+
async execute(
|
|
384
|
+
_id: string,
|
|
385
|
+
params: { id: string; force?: boolean },
|
|
386
|
+
) {
|
|
387
|
+
const cfg = getConfig(api);
|
|
388
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
389
|
+
let args: string;
|
|
390
|
+
if (params.id === 'auto') {
|
|
391
|
+
args = 'share --auto';
|
|
392
|
+
} else {
|
|
393
|
+
args = `share ${params.id}`;
|
|
394
|
+
if (params.force) args += ' --force';
|
|
395
|
+
}
|
|
396
|
+
const result = runHippo(args, hippoCwd);
|
|
397
|
+
return { content: [{ type: 'text', text: result || 'Share complete.' }] };
|
|
398
|
+
},
|
|
399
|
+
}),
|
|
400
|
+
{ optional: true },
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
// --- Tool: hippo_peers ---
|
|
404
|
+
api.registerTool(
|
|
405
|
+
(ctx: HippoRuntimeContext) => ({
|
|
406
|
+
name: 'hippo_peers',
|
|
407
|
+
description:
|
|
408
|
+
'List all projects that have contributed memories to the global shared store.',
|
|
409
|
+
parameters: {
|
|
410
|
+
type: 'object',
|
|
411
|
+
properties: {},
|
|
412
|
+
},
|
|
413
|
+
async execute() {
|
|
414
|
+
const cfg = getConfig(api);
|
|
415
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
416
|
+
const result = runHippo('peers', hippoCwd);
|
|
417
|
+
return { content: [{ type: 'text', text: result || 'No peers found.' }] };
|
|
418
|
+
},
|
|
419
|
+
}),
|
|
420
|
+
{ optional: true },
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
// --- Tool: hippo_wm_push ---
|
|
424
|
+
api.registerTool(
|
|
425
|
+
(ctx: HippoRuntimeContext) => ({
|
|
426
|
+
name: 'hippo_wm_push',
|
|
427
|
+
description:
|
|
428
|
+
'Push a note into working memory — a bounded buffer for current-state context. Entries are scoped, importance-ranked, and auto-evicted when the buffer is full (max 20 per scope).',
|
|
429
|
+
parameters: {
|
|
430
|
+
type: 'object',
|
|
431
|
+
properties: {
|
|
432
|
+
content: {
|
|
433
|
+
type: 'string',
|
|
434
|
+
description: 'Working memory note',
|
|
435
|
+
},
|
|
436
|
+
scope: {
|
|
437
|
+
type: 'string',
|
|
438
|
+
description: 'Scope (default: repo)',
|
|
439
|
+
},
|
|
440
|
+
importance: {
|
|
441
|
+
type: 'number',
|
|
442
|
+
description: 'Priority 0-1 (default: 0.5)',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
required: ['content'],
|
|
446
|
+
},
|
|
447
|
+
async execute(
|
|
448
|
+
_id: string,
|
|
449
|
+
params: { content: string; scope?: string; importance?: number },
|
|
450
|
+
) {
|
|
451
|
+
const cfg = getConfig(api);
|
|
452
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
453
|
+
const scope = params.scope ?? 'repo';
|
|
454
|
+
const importance = params.importance ?? 0.5;
|
|
455
|
+
const escapedContent = params.content.replace(/"/g, '\\"');
|
|
456
|
+
const result = runHippo(
|
|
457
|
+
`wm push --scope ${scope} --content "${escapedContent}" --importance ${importance}`,
|
|
458
|
+
hippoCwd,
|
|
459
|
+
);
|
|
460
|
+
return { content: [{ type: 'text', text: result || 'Working memory entry pushed.' }] };
|
|
461
|
+
},
|
|
462
|
+
}),
|
|
463
|
+
{ optional: true },
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
// --- Hook: auto-inject context at session start ---
|
|
467
|
+
api.on(
|
|
468
|
+
'before_prompt_build',
|
|
469
|
+
(_event: any, ctx: HippoRuntimeContext) => {
|
|
470
|
+
const cfg = getConfig(api);
|
|
471
|
+
if (cfg.autoContext === false) return {};
|
|
472
|
+
|
|
473
|
+
// Dedup guard: skip if this session already got context injected
|
|
474
|
+
const sessionKey = getSessionIdentity(ctx);
|
|
475
|
+
if (sessionKey && injectedSessions.has(sessionKey)) {
|
|
476
|
+
logger.debug?.(`[hippo] skipping duplicate context injection for session ${sessionKey}`);
|
|
477
|
+
return {};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const budget = cfg.budget ?? 1500;
|
|
481
|
+
const framing = cfg.framing ?? 'observe';
|
|
482
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
483
|
+
|
|
484
|
+
// Record session_start event
|
|
485
|
+
try {
|
|
486
|
+
runHippo(
|
|
487
|
+
`session log --id "${sessionKey}" --type session_start --content "Session started" --source openclaw`,
|
|
488
|
+
hippoCwd,
|
|
489
|
+
);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
logger.debug?.('[hippo] session_start event skipped:', err);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
const context = runHippo(
|
|
496
|
+
`context --auto --budget ${budget} --framing ${framing}`,
|
|
497
|
+
hippoCwd,
|
|
498
|
+
);
|
|
499
|
+
if (context && context.length > 10 && !context.includes('No hippo store')) {
|
|
500
|
+
if (sessionKey) injectedSessions.add(sessionKey);
|
|
501
|
+
return {
|
|
502
|
+
appendSystemContext: `\n\n## Project Memory (Hippo)\n${context}`,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
} catch (err) {
|
|
506
|
+
logger.debug?.('[hippo] context injection skipped:', err);
|
|
507
|
+
}
|
|
508
|
+
return {};
|
|
509
|
+
},
|
|
510
|
+
{ priority: 5 },
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
api.on(
|
|
514
|
+
'after_tool_call',
|
|
515
|
+
(event: { toolName: string; error?: string }, ctx: HippoRuntimeContext) => {
|
|
516
|
+
const cfg = getConfig(api);
|
|
517
|
+
if (cfg.autoLearn === false) return;
|
|
518
|
+
if (!event.error?.trim()) return;
|
|
519
|
+
if (event.toolName.startsWith('hippo_')) return;
|
|
520
|
+
|
|
521
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
522
|
+
const toolTag = sanitizeTag(event.toolName);
|
|
523
|
+
let args =
|
|
524
|
+
`remember "${formatToolErrorMemory(event.toolName, event.error).replace(/"/g, '\\"')}"` +
|
|
525
|
+
' --error --observed --tag openclaw';
|
|
526
|
+
if (toolTag) args += ` --tag ${toolTag}`;
|
|
527
|
+
|
|
528
|
+
const result = runHippo(args, hippoCwd);
|
|
529
|
+
if (hippoRememberSucceeded(result)) {
|
|
530
|
+
recordSessionMemory(ctx);
|
|
531
|
+
} else {
|
|
532
|
+
logger.debug?.(`[hippo] autoLearn skipped storing tool error: ${result}`);
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
api.on(
|
|
538
|
+
'session_end',
|
|
539
|
+
(_event: { sessionId: string; messageCount: number }, ctx: HippoRuntimeContext) => {
|
|
540
|
+
// Clear dedup guard so a new session can inject fresh context
|
|
541
|
+
const sessionKey = getSessionIdentity(ctx);
|
|
542
|
+
injectedSessions.delete(sessionKey);
|
|
543
|
+
|
|
544
|
+
const cfg = getConfig(api);
|
|
545
|
+
const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
|
|
546
|
+
|
|
547
|
+
// Record session_end event
|
|
548
|
+
try {
|
|
549
|
+
runHippo(
|
|
550
|
+
`session log --id "${sessionKey}" --type session_end --content "Session ended" --source openclaw`,
|
|
551
|
+
hippoCwd,
|
|
552
|
+
);
|
|
553
|
+
} catch (err) {
|
|
554
|
+
logger.debug?.('[hippo] session_end event skipped:', err);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const newMemories = consumeSessionMemoryCount(ctx);
|
|
558
|
+
if (!cfg.autoSleep || newMemories < AUTO_SLEEP_SESSION_THRESHOLD) return;
|
|
559
|
+
const result = runHippo('sleep', hippoCwd);
|
|
560
|
+
logger.info?.(
|
|
561
|
+
`[hippo] autoSleep ran for session ${ctx.sessionId ?? ctx.sessionKey ?? 'unknown'} ` +
|
|
562
|
+
`after ${newMemories} new memories`,
|
|
563
|
+
);
|
|
564
|
+
logger.debug?.(`[hippo] autoSleep result: ${result}`);
|
|
565
|
+
},
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
logger.info?.('[hippo] Memory plugin registered (tools: hippo_recall, hippo_remember, hippo_outcome, hippo_status, hippo_context, hippo_conflicts, hippo_resolve, hippo_share, hippo_peers, hippo_wm_push)');
|
|
569
|
+
}
|