hippo-memory 1.17.0 → 1.19.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.
Files changed (106) hide show
  1. package/bin/hippo.js +2 -2
  2. package/dist/api.d.ts +43 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +109 -7
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +109 -11
  8. package/dist/cli.js.map +1 -1
  9. package/dist/connectors/github/backfill.js +4 -4
  10. package/dist/connectors/github/cli-impl.js +6 -6
  11. package/dist/connectors/github/dlq.js +14 -14
  12. package/dist/connectors/slack/backfill.js +1 -1
  13. package/dist/connectors/slack/dlq.js +10 -10
  14. package/dist/connectors/slack/workspaces.js +4 -4
  15. package/dist/customer-notes.d.ts.map +1 -1
  16. package/dist/customer-notes.js +5 -1
  17. package/dist/customer-notes.js.map +1 -1
  18. package/dist/dag.js +6 -6
  19. package/dist/dashboard.js +7 -7
  20. package/dist/decisions.d.ts.map +1 -1
  21. package/dist/decisions.js +9 -1
  22. package/dist/decisions.js.map +1 -1
  23. package/dist/goals.d.ts +11 -0
  24. package/dist/goals.d.ts.map +1 -1
  25. package/dist/goals.js +61 -49
  26. package/dist/goals.js.map +1 -1
  27. package/dist/graph-extract.d.ts.map +1 -1
  28. package/dist/graph-extract.js +32 -12
  29. package/dist/graph-extract.js.map +1 -1
  30. package/dist/graph.d.ts +46 -3
  31. package/dist/graph.d.ts.map +1 -1
  32. package/dist/graph.js +116 -8
  33. package/dist/graph.js.map +1 -1
  34. package/dist/hooks.js +24 -24
  35. package/dist/physics-state.js +27 -27
  36. package/dist/policies.d.ts.map +1 -1
  37. package/dist/policies.js +5 -1
  38. package/dist/policies.js.map +1 -1
  39. package/dist/predictions.js +67 -67
  40. package/dist/project-briefs.d.ts.map +1 -1
  41. package/dist/project-briefs.js +6 -1
  42. package/dist/project-briefs.js.map +1 -1
  43. package/dist/refine-llm.js +13 -13
  44. package/dist/search.d.ts +33 -0
  45. package/dist/search.d.ts.map +1 -1
  46. package/dist/search.js.map +1 -1
  47. package/dist/server.d.ts.map +1 -1
  48. package/dist/server.js +7 -0
  49. package/dist/server.js.map +1 -1
  50. package/dist/sleep-redact.d.ts +1 -0
  51. package/dist/sleep-redact.d.ts.map +1 -1
  52. package/dist/sleep-redact.js +6 -0
  53. package/dist/sleep-redact.js.map +1 -1
  54. package/dist/src/api.js +109 -7
  55. package/dist/src/api.js.map +1 -1
  56. package/dist/src/cli.js +109 -11
  57. package/dist/src/cli.js.map +1 -1
  58. package/dist/src/connectors/github/backfill.js +4 -4
  59. package/dist/src/connectors/github/cli-impl.js +6 -6
  60. package/dist/src/connectors/github/dlq.js +14 -14
  61. package/dist/src/connectors/slack/backfill.js +1 -1
  62. package/dist/src/connectors/slack/dlq.js +10 -10
  63. package/dist/src/connectors/slack/workspaces.js +4 -4
  64. package/dist/src/customer-notes.js +5 -1
  65. package/dist/src/customer-notes.js.map +1 -1
  66. package/dist/src/dag.js +6 -6
  67. package/dist/src/dashboard.js +7 -7
  68. package/dist/src/decisions.js +9 -1
  69. package/dist/src/decisions.js.map +1 -1
  70. package/dist/src/goals.js +61 -49
  71. package/dist/src/goals.js.map +1 -1
  72. package/dist/src/graph-extract.js +32 -12
  73. package/dist/src/graph-extract.js.map +1 -1
  74. package/dist/src/graph.js +116 -8
  75. package/dist/src/graph.js.map +1 -1
  76. package/dist/src/hooks.js +24 -24
  77. package/dist/src/physics-state.js +27 -27
  78. package/dist/src/policies.js +5 -1
  79. package/dist/src/policies.js.map +1 -1
  80. package/dist/src/predictions.js +67 -67
  81. package/dist/src/project-briefs.js +6 -1
  82. package/dist/src/project-briefs.js.map +1 -1
  83. package/dist/src/refine-llm.js +13 -13
  84. package/dist/src/search.js.map +1 -1
  85. package/dist/src/server.js +7 -0
  86. package/dist/src/server.js.map +1 -1
  87. package/dist/src/sleep-redact.js +6 -0
  88. package/dist/src/sleep-redact.js.map +1 -1
  89. package/dist/src/store.js +261 -260
  90. package/dist/src/store.js.map +1 -1
  91. package/dist/src/version.js +1 -1
  92. package/dist/src/working-memory.js +19 -19
  93. package/dist/store.d.ts +6 -0
  94. package/dist/store.d.ts.map +1 -1
  95. package/dist/store.js +261 -260
  96. package/dist/store.js.map +1 -1
  97. package/dist/version.d.ts +1 -1
  98. package/dist/version.js +1 -1
  99. package/dist/working-memory.js +19 -19
  100. package/dist-ui/index.html +12 -12
  101. package/extensions/openclaw-plugin/index.ts +650 -650
  102. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  103. package/extensions/openclaw-plugin/package.json +1 -1
  104. package/openclaw.plugin.json +1 -1
  105. package/package.json +1 -1
  106. package/dist/benchmarks/e1.3/scenarios.json +0 -2587
@@ -1,207 +1,207 @@
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
-
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
10
  import { execFileSync, spawn } from 'child_process';
11
- import { existsSync, readdirSync, rmSync } from 'fs';
12
- import { join } from 'path';
13
- import { basename as posixBasename, dirname as posixDirname } from 'path/posix';
14
-
15
- interface HippoConfig {
16
- budget?: number;
17
- autoContext?: boolean;
18
- autoLearn?: boolean;
19
- autoSleep?: boolean;
20
- framing?: 'observe' | 'suggest' | 'assert';
21
- root?: string;
22
- }
23
-
24
- type HippoRuntimeContext = {
25
- workspaceDir?: string;
26
- agentId?: string;
27
- sessionId?: string;
28
- sessionKey?: string;
29
- };
30
-
31
- const AUTO_SLEEP_SESSION_THRESHOLD = 10;
32
- const MAX_ERRORS_PER_SESSION = 5;
33
- const sessionMemoryCounts = new Map<string, number>();
34
- const sessionErrorCounts = new Map<string, number>();
35
- const sessionErrorHashes = new Map<string, Set<string>>();
36
- const injectedSessions = new Set<string>();
37
-
38
- /**
39
- * Infrastructure errors that repeat constantly and never produce useful lessons.
40
- * These are transient operational failures, not domain-specific gotchas.
41
- */
42
- const NOISE_ERROR_PATTERNS: RegExp[] = [
43
- /Local media path is not under an allowed directory/i,
44
- /timed out\.?\s*Restart the OpenClaw gateway/i,
45
- /EISDIR:\s*illegal operation on a directory/i,
46
- /Missing required parameter:\s*path/i,
47
- /ENOENT:\s*no such file or directory/i,
48
- /EACCES:\s*permission denied/i,
49
- /EPERM:\s*operation not permitted/i,
50
- /socket hang up/i,
51
- /ECONNREFUSED/i,
52
- /ECONNRESET/i,
53
- /ERR_SOCKET_CONNECTION_TIMEOUT/i,
54
- /net::ERR_/i,
55
- /Navigation timeout/i,
56
- ];
57
-
58
- function isNoiseError(error: string): boolean {
59
- return NOISE_ERROR_PATTERNS.some((p) => p.test(error));
60
- }
61
-
62
- function hashError(toolName: string, error: string): string {
63
- // Normalize the error to a stable key: tool + first 80 chars of error
64
- const normalized = error.replace(/\s+/g, ' ').trim().slice(0, 80).toLowerCase();
65
- return `${toolName}::${normalized}`;
66
- }
67
-
68
- function getConfig(api: any): HippoConfig {
69
- try {
70
- const entries = api.config?.plugins?.entries?.['hippo-memory'];
71
- return entries?.config ?? {};
72
- } catch {
73
- return {};
74
- }
75
- }
76
-
77
- function findHippoRoot(workspace?: string, configRoot?: string): string | null {
78
- if (configRoot && existsSync(configRoot)) return configRoot;
79
-
80
- const home = process.env.USERPROFILE || process.env.HOME || '';
81
- const candidates = [
82
- workspace ? join(workspace, '.hippo') : null,
83
- process.env.HIPPO_HOME,
84
- process.env.HIPPO_ROOT,
85
- process.env.XDG_DATA_HOME ? join(process.env.XDG_DATA_HOME, 'hippo') : null,
86
- home ? join(home, '.hippo') : null,
87
- ].filter(Boolean) as string[];
88
-
89
- for (const candidate of candidates) {
90
- if (existsSync(candidate)) return candidate;
91
- }
92
- return null;
93
- }
94
-
95
- function getAgentWorkspace(api: any, agentId?: string): string | undefined {
96
- try {
97
- const agents = api.config?.agents;
98
- const list = Array.isArray(agents?.list) ? agents.list : [];
99
-
100
- if (agentId) {
101
- const match = list.find((agent: any) => agent?.id === agentId);
102
- if (typeof match?.workspace === 'string' && match.workspace) return match.workspace;
103
- }
104
-
105
- const defaultAgent = list.find((agent: any) => agent?.default);
106
- if (typeof defaultAgent?.workspace === 'string' && defaultAgent.workspace) {
107
- return defaultAgent.workspace;
108
- }
109
-
110
- const fallback = agents?.defaults?.workspace;
111
- return typeof fallback === 'string' && fallback ? fallback : undefined;
112
- } catch {
113
- return undefined;
114
- }
115
- }
116
-
117
- function resolveHippoCwd(workspace?: string, configRoot?: string): string {
118
- const hippoRoot = findHippoRoot(workspace, configRoot);
119
- if (!hippoRoot) return workspace || process.cwd();
120
- const normalized = hippoRoot.replace(/\\/g, '/');
121
- return posixBasename(normalized).toLowerCase() === '.hippo'
122
- ? posixDirname(normalized)
123
- : hippoRoot;
124
- }
125
-
126
- function resolveHippoCwdFromContext(api: any, ctx: HippoRuntimeContext, configRoot?: string): string {
127
- const workspace = ctx.workspaceDir ?? getAgentWorkspace(api, ctx.agentId);
128
- return resolveHippoCwd(workspace, configRoot);
129
- }
130
-
131
- function getSessionIdentity(ctx: Pick<HippoRuntimeContext, 'sessionId' | 'sessionKey' | 'agentId'>): string {
132
- return ctx.sessionId ?? ctx.sessionKey ?? ctx.agentId ?? `fallback-${Date.now()}-${process.pid}`;
133
- }
134
-
135
- function recordSessionMemory(ctx: Pick<HippoRuntimeContext, 'sessionId' | 'sessionKey' | 'agentId'>): void {
136
- const key = getSessionIdentity(ctx);
137
- sessionMemoryCounts.set(key, (sessionMemoryCounts.get(key) ?? 0) + 1);
138
- }
139
-
140
- function consumeSessionMemoryCount(
141
- ctx: Pick<HippoRuntimeContext, 'sessionId' | 'sessionKey' | 'agentId'>,
142
- ): number {
143
- const key = getSessionIdentity(ctx);
144
- const count = sessionMemoryCounts.get(key) ?? 0;
145
- sessionMemoryCounts.delete(key);
146
- return count;
147
- }
148
-
149
- function sanitizeTag(tag?: string): string | undefined {
150
- if (!tag) return undefined;
151
- const normalized = tag
152
- .toLowerCase()
153
- .replace(/[^a-z0-9-]+/g, '-')
154
- .replace(/^-+|-+$/g, '')
155
- .slice(0, 30);
156
- return normalized || undefined;
157
- }
158
-
159
- function formatToolErrorMemory(toolName: string, error: string): string {
160
- const normalized = error.replace(/\s+/g, ' ').trim();
161
- const truncated = normalized.slice(0, 500);
162
- const suffix = normalized.length > truncated.length ? ' [truncated]' : '';
163
- return `Tool '${toolName}' failed: ${truncated}${suffix}`;
164
- }
165
-
166
- /**
167
- * Remove stale hippo-memory.bak-* directories from the OpenClaw extensions folder.
168
- * These are left behind by plugin updates and cause duplicate plugin ID errors on boot.
169
- */
170
- function cleanupBackupPlugins(logger?: { info?: (...args: unknown[]) => void }): void {
171
- try {
172
- const extensionsDir = join(
173
- process.env.USERPROFILE || process.env.HOME || '',
174
- '.openclaw', 'extensions'
175
- );
176
- if (!existsSync(extensionsDir)) return;
177
-
178
- for (const entry of readdirSync(extensionsDir)) {
179
- if (entry.startsWith('hippo-memory.bak')) {
180
- const fullPath = join(extensionsDir, entry);
181
- rmSync(fullPath, { recursive: true, force: true });
182
- logger?.info?.(`[hippo] Removed stale backup: ${entry}`);
183
- }
184
- }
185
- } catch {
186
- // Best-effort cleanup — don't break boot
187
- }
188
- }
189
-
190
- function hippoRememberSucceeded(result: string): boolean {
191
- return result.includes('Remembered [');
192
- }
193
-
11
+ import { existsSync, readdirSync, rmSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { basename as posixBasename, dirname as posixDirname } from 'path/posix';
14
+
15
+ interface HippoConfig {
16
+ budget?: number;
17
+ autoContext?: boolean;
18
+ autoLearn?: boolean;
19
+ autoSleep?: boolean;
20
+ framing?: 'observe' | 'suggest' | 'assert';
21
+ root?: string;
22
+ }
23
+
24
+ type HippoRuntimeContext = {
25
+ workspaceDir?: string;
26
+ agentId?: string;
27
+ sessionId?: string;
28
+ sessionKey?: string;
29
+ };
30
+
31
+ const AUTO_SLEEP_SESSION_THRESHOLD = 10;
32
+ const MAX_ERRORS_PER_SESSION = 5;
33
+ const sessionMemoryCounts = new Map<string, number>();
34
+ const sessionErrorCounts = new Map<string, number>();
35
+ const sessionErrorHashes = new Map<string, Set<string>>();
36
+ const injectedSessions = new Set<string>();
37
+
38
+ /**
39
+ * Infrastructure errors that repeat constantly and never produce useful lessons.
40
+ * These are transient operational failures, not domain-specific gotchas.
41
+ */
42
+ const NOISE_ERROR_PATTERNS: RegExp[] = [
43
+ /Local media path is not under an allowed directory/i,
44
+ /timed out\.?\s*Restart the OpenClaw gateway/i,
45
+ /EISDIR:\s*illegal operation on a directory/i,
46
+ /Missing required parameter:\s*path/i,
47
+ /ENOENT:\s*no such file or directory/i,
48
+ /EACCES:\s*permission denied/i,
49
+ /EPERM:\s*operation not permitted/i,
50
+ /socket hang up/i,
51
+ /ECONNREFUSED/i,
52
+ /ECONNRESET/i,
53
+ /ERR_SOCKET_CONNECTION_TIMEOUT/i,
54
+ /net::ERR_/i,
55
+ /Navigation timeout/i,
56
+ ];
57
+
58
+ function isNoiseError(error: string): boolean {
59
+ return NOISE_ERROR_PATTERNS.some((p) => p.test(error));
60
+ }
61
+
62
+ function hashError(toolName: string, error: string): string {
63
+ // Normalize the error to a stable key: tool + first 80 chars of error
64
+ const normalized = error.replace(/\s+/g, ' ').trim().slice(0, 80).toLowerCase();
65
+ return `${toolName}::${normalized}`;
66
+ }
67
+
68
+ function getConfig(api: any): HippoConfig {
69
+ try {
70
+ const entries = api.config?.plugins?.entries?.['hippo-memory'];
71
+ return entries?.config ?? {};
72
+ } catch {
73
+ return {};
74
+ }
75
+ }
76
+
77
+ function findHippoRoot(workspace?: string, configRoot?: string): string | null {
78
+ if (configRoot && existsSync(configRoot)) return configRoot;
79
+
80
+ const home = process.env.USERPROFILE || process.env.HOME || '';
81
+ const candidates = [
82
+ workspace ? join(workspace, '.hippo') : null,
83
+ process.env.HIPPO_HOME,
84
+ process.env.HIPPO_ROOT,
85
+ process.env.XDG_DATA_HOME ? join(process.env.XDG_DATA_HOME, 'hippo') : null,
86
+ home ? join(home, '.hippo') : null,
87
+ ].filter(Boolean) as string[];
88
+
89
+ for (const candidate of candidates) {
90
+ if (existsSync(candidate)) return candidate;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function getAgentWorkspace(api: any, agentId?: string): string | undefined {
96
+ try {
97
+ const agents = api.config?.agents;
98
+ const list = Array.isArray(agents?.list) ? agents.list : [];
99
+
100
+ if (agentId) {
101
+ const match = list.find((agent: any) => agent?.id === agentId);
102
+ if (typeof match?.workspace === 'string' && match.workspace) return match.workspace;
103
+ }
104
+
105
+ const defaultAgent = list.find((agent: any) => agent?.default);
106
+ if (typeof defaultAgent?.workspace === 'string' && defaultAgent.workspace) {
107
+ return defaultAgent.workspace;
108
+ }
109
+
110
+ const fallback = agents?.defaults?.workspace;
111
+ return typeof fallback === 'string' && fallback ? fallback : undefined;
112
+ } catch {
113
+ return undefined;
114
+ }
115
+ }
116
+
117
+ function resolveHippoCwd(workspace?: string, configRoot?: string): string {
118
+ const hippoRoot = findHippoRoot(workspace, configRoot);
119
+ if (!hippoRoot) return workspace || process.cwd();
120
+ const normalized = hippoRoot.replace(/\\/g, '/');
121
+ return posixBasename(normalized).toLowerCase() === '.hippo'
122
+ ? posixDirname(normalized)
123
+ : hippoRoot;
124
+ }
125
+
126
+ function resolveHippoCwdFromContext(api: any, ctx: HippoRuntimeContext, configRoot?: string): string {
127
+ const workspace = ctx.workspaceDir ?? getAgentWorkspace(api, ctx.agentId);
128
+ return resolveHippoCwd(workspace, configRoot);
129
+ }
130
+
131
+ function getSessionIdentity(ctx: Pick<HippoRuntimeContext, 'sessionId' | 'sessionKey' | 'agentId'>): string {
132
+ return ctx.sessionId ?? ctx.sessionKey ?? ctx.agentId ?? `fallback-${Date.now()}-${process.pid}`;
133
+ }
134
+
135
+ function recordSessionMemory(ctx: Pick<HippoRuntimeContext, 'sessionId' | 'sessionKey' | 'agentId'>): void {
136
+ const key = getSessionIdentity(ctx);
137
+ sessionMemoryCounts.set(key, (sessionMemoryCounts.get(key) ?? 0) + 1);
138
+ }
139
+
140
+ function consumeSessionMemoryCount(
141
+ ctx: Pick<HippoRuntimeContext, 'sessionId' | 'sessionKey' | 'agentId'>,
142
+ ): number {
143
+ const key = getSessionIdentity(ctx);
144
+ const count = sessionMemoryCounts.get(key) ?? 0;
145
+ sessionMemoryCounts.delete(key);
146
+ return count;
147
+ }
148
+
149
+ function sanitizeTag(tag?: string): string | undefined {
150
+ if (!tag) return undefined;
151
+ const normalized = tag
152
+ .toLowerCase()
153
+ .replace(/[^a-z0-9-]+/g, '-')
154
+ .replace(/^-+|-+$/g, '')
155
+ .slice(0, 30);
156
+ return normalized || undefined;
157
+ }
158
+
159
+ function formatToolErrorMemory(toolName: string, error: string): string {
160
+ const normalized = error.replace(/\s+/g, ' ').trim();
161
+ const truncated = normalized.slice(0, 500);
162
+ const suffix = normalized.length > truncated.length ? ' [truncated]' : '';
163
+ return `Tool '${toolName}' failed: ${truncated}${suffix}`;
164
+ }
165
+
166
+ /**
167
+ * Remove stale hippo-memory.bak-* directories from the OpenClaw extensions folder.
168
+ * These are left behind by plugin updates and cause duplicate plugin ID errors on boot.
169
+ */
170
+ function cleanupBackupPlugins(logger?: { info?: (...args: unknown[]) => void }): void {
171
+ try {
172
+ const extensionsDir = join(
173
+ process.env.USERPROFILE || process.env.HOME || '',
174
+ '.openclaw', 'extensions'
175
+ );
176
+ if (!existsSync(extensionsDir)) return;
177
+
178
+ for (const entry of readdirSync(extensionsDir)) {
179
+ if (entry.startsWith('hippo-memory.bak')) {
180
+ const fullPath = join(extensionsDir, entry);
181
+ rmSync(fullPath, { recursive: true, force: true });
182
+ logger?.info?.(`[hippo] Removed stale backup: ${entry}`);
183
+ }
184
+ }
185
+ } catch {
186
+ // Best-effort cleanup — don't break boot
187
+ }
188
+ }
189
+
190
+ function hippoRememberSucceeded(result: string): boolean {
191
+ return result.includes('Remembered [');
192
+ }
193
+
194
194
  function runHippo(args: readonly string[], cwd?: string): string {
195
195
  try {
196
196
  const result = execFileSync('hippo', args, {
197
197
  cwd: cwd || process.cwd(),
198
198
  encoding: 'utf8',
199
- timeout: 30_000,
200
- stdio: ['pipe', 'pipe', 'pipe'],
201
- });
202
- return typeof result === 'string' ? result.trim() : '';
203
- } catch (err: any) {
204
- return err.stdout?.trim() || err.message || 'hippo command failed';
199
+ timeout: 30_000,
200
+ stdio: ['pipe', 'pipe', 'pipe'],
201
+ });
202
+ return typeof result === 'string' ? result.trim() : '';
203
+ } catch (err: any) {
204
+ return err.stdout?.trim() || err.message || 'hippo command failed';
205
205
  }
206
206
  }
207
207
 
@@ -221,455 +221,455 @@ function spawnHippoDetached(args: readonly string[], cwd?: string): boolean {
221
221
  }
222
222
 
223
223
  let _registered = false;
224
-
225
- export default function register(api: any) {
226
- if (_registered) return;
227
- _registered = true;
228
-
229
- const logger = api.logger ?? console;
230
-
231
- // Clean up stale backup plugins from previous updates
232
- cleanupBackupPlugins(logger);
233
-
234
- // --- Tool: hippo_recall ---
235
- api.registerTool((ctx: HippoRuntimeContext) => ({
236
- name: 'hippo_recall',
237
- description:
238
- '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.',
239
- parameters: {
240
- type: 'object',
241
- properties: {
242
- query: {
243
- type: 'string',
244
- description: 'What to search for in memory (natural language)',
245
- },
246
- budget: {
247
- type: 'number',
248
- description: 'Max tokens to return (default: 1500)',
249
- },
250
- },
251
- required: ['query'],
252
- },
253
- async execute(_id: string, params: { query: string; budget?: number }) {
254
- const cfg = getConfig(api);
255
- const budget = params.budget ?? cfg.budget ?? 1500;
256
- const framing = cfg.framing ?? 'observe';
257
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
258
- const result = runHippo(
259
- ['recall', params.query, '--budget', String(budget), '--framing', framing],
260
- hippoCwd,
261
- );
262
- return { content: [{ type: 'text', text: result || 'No relevant memories found.' }] };
263
- },
264
- }));
265
-
266
- // --- Tool: hippo_remember ---
267
- api.registerTool((ctx: HippoRuntimeContext) => ({
268
- name: 'hippo_remember',
269
- description:
270
- '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.',
271
- parameters: {
272
- type: 'object',
273
- properties: {
274
- text: {
275
- type: 'string',
276
- description: 'The memory to store (1-2 sentences, specific and concrete)',
277
- },
278
- error: {
279
- type: 'boolean',
280
- description: 'Mark as error memory (doubles half-life)',
281
- },
282
- pin: {
283
- type: 'boolean',
284
- description: 'Pin memory (never decays)',
285
- },
286
- tag: {
287
- type: 'string',
288
- description: 'Optional tag for categorization',
289
- },
290
- },
291
- required: ['text'],
292
- },
293
- async execute(
294
- _id: string,
295
- params: { text: string; error?: boolean; pin?: boolean; tag?: string },
296
- ) {
297
- const cfg = getConfig(api);
298
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
299
- const args: string[] = ['remember', params.text];
300
- if (params.error) args.push('--error');
301
- if (params.pin) args.push('--pin');
302
- if (params.tag) {
303
- const safe = sanitizeTag(params.tag);
304
- if (safe) args.push('--tag', safe);
305
- }
306
- const result = runHippo(args, hippoCwd);
307
- if (hippoRememberSucceeded(result)) {
308
- recordSessionMemory(ctx);
309
- }
310
- return { content: [{ type: 'text', text: result || 'Memory stored.' }] };
311
- },
312
- }));
313
-
314
- // --- Tool: hippo_outcome ---
315
- api.registerTool((ctx: HippoRuntimeContext) => ({
316
- name: 'hippo_outcome',
317
- description:
318
- 'Report whether recalled memories were useful. Strengthens good memories (+5 days half-life) and weakens bad ones (-3 days). Call after completing work.',
319
- parameters: {
320
- type: 'object',
321
- properties: {
322
- good: {
323
- type: 'boolean',
324
- description: 'true = memories helped, false = memories were irrelevant',
325
- },
326
- },
327
- required: ['good'],
328
- },
329
- async execute(_id: string, params: { good: boolean }) {
330
- const cfg = getConfig(api);
331
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
332
- const flag = params.good ? '--good' : '--bad';
333
- const result = runHippo(['outcome', flag], hippoCwd);
334
- return { content: [{ type: 'text', text: result || 'Outcome recorded.' }] };
335
- },
336
- }));
337
-
338
- // --- Tool: hippo_status ---
339
- api.registerTool(
340
- (ctx: HippoRuntimeContext) => ({
341
- name: 'hippo_status',
342
- description:
343
- 'Check memory health: counts, strengths, at-risk memories, last consolidation time.',
344
- parameters: {
345
- type: 'object',
346
- properties: {},
347
- },
348
- async execute() {
349
- const cfg = getConfig(api);
350
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
351
- const result = runHippo(['status'], hippoCwd);
352
- return { content: [{ type: 'text', text: result || 'No hippo store found.' }] };
353
- },
354
- }),
355
- { optional: true },
356
- );
357
-
358
- // --- Tool: hippo_context ---
359
- api.registerTool(
360
- (ctx: HippoRuntimeContext) => ({
361
- name: 'hippo_context',
362
- description:
363
- 'Smart context injection: auto-detects current task from git state and returns relevant memories. Use at the start of any session.',
364
- parameters: {
365
- type: 'object',
366
- properties: {
367
- budget: {
368
- type: 'number',
369
- description: 'Max tokens (default: 1500)',
370
- },
371
- },
372
- },
373
- async execute(_id: string, params: { budget?: number }) {
374
- const cfg = getConfig(api);
375
- const budget = params.budget ?? cfg.budget ?? 1500;
376
- const framing = cfg.framing ?? 'observe';
377
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
378
- const result = runHippo(
379
- ['context', '--auto', '--budget', String(budget), '--framing', framing],
380
- hippoCwd,
381
- );
382
- return { content: [{ type: 'text', text: result || 'No context available.' }] };
383
- },
384
- }),
385
- { optional: true },
386
- );
387
-
388
- // --- Tool: hippo_conflicts ---
389
- api.registerTool(
390
- (ctx: HippoRuntimeContext) => ({
391
- name: 'hippo_conflicts',
392
- description:
393
- 'List open memory conflicts — contradictory memories that need resolution.',
394
- parameters: {
395
- type: 'object',
396
- properties: {
397
- json: {
398
- type: 'boolean',
399
- description: 'Output as JSON (default: false)',
400
- },
401
- },
402
- },
403
- async execute(_id: string, params: { json?: boolean }) {
404
- const cfg = getConfig(api);
405
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
406
- const args: string[] = params.json ? ['conflicts', '--json'] : ['conflicts'];
407
- const result = runHippo(args, hippoCwd);
408
- return { content: [{ type: 'text', text: result || 'No conflicts found.' }] };
409
- },
410
- }),
411
- { optional: true },
412
- );
413
-
414
- // --- Tool: hippo_resolve ---
415
- api.registerTool(
416
- (ctx: HippoRuntimeContext) => ({
417
- name: 'hippo_resolve',
418
- description:
419
- 'Resolve a memory conflict by keeping one memory and weakening or deleting the other.',
420
- parameters: {
421
- type: 'object',
422
- properties: {
423
- conflict_id: {
424
- type: 'number',
425
- description: 'The conflict ID to resolve',
426
- },
427
- keep: {
428
- type: 'string',
429
- description: 'ID of the memory to keep',
430
- },
431
- forget: {
432
- type: 'boolean',
433
- description: 'Delete the losing memory instead of weakening it (default: false)',
434
- },
435
- },
436
- required: ['conflict_id', 'keep'],
437
- },
438
- async execute(
439
- _id: string,
440
- params: { conflict_id: number; keep: string; forget?: boolean },
441
- ) {
442
- const cfg = getConfig(api);
443
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
444
- const args: string[] = ['resolve', String(params.conflict_id), '--keep', params.keep];
445
- if (params.forget) args.push('--forget');
446
- const result = runHippo(args, hippoCwd);
447
- return { content: [{ type: 'text', text: result || 'Conflict resolved.' }] };
448
- },
449
- }),
450
- { optional: true },
451
- );
452
-
453
- // --- Tool: hippo_share ---
454
- api.registerTool(
455
- (ctx: HippoRuntimeContext) => ({
456
- name: 'hippo_share',
457
- description:
458
- '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.',
459
- parameters: {
460
- type: 'object',
461
- properties: {
462
- id: {
463
- type: 'string',
464
- description: 'Memory ID to share (or "auto" to auto-share all high-scoring memories)',
465
- },
466
- force: {
467
- type: 'boolean',
468
- description: 'Share even if transfer score is low (default: false)',
469
- },
470
- },
471
- required: ['id'],
472
- },
473
- async execute(
474
- _id: string,
475
- params: { id: string; force?: boolean },
476
- ) {
477
- const cfg = getConfig(api);
478
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
479
- const args: string[] = ['share'];
480
- if (params.id === 'auto') {
481
- args.push('--auto');
482
- } else {
483
- args.push(params.id);
484
- if (params.force) args.push('--force');
485
- }
486
- const result = runHippo(args, hippoCwd);
487
- return { content: [{ type: 'text', text: result || 'Share complete.' }] };
488
- },
489
- }),
490
- { optional: true },
491
- );
492
-
493
- // --- Tool: hippo_peers ---
494
- api.registerTool(
495
- (ctx: HippoRuntimeContext) => ({
496
- name: 'hippo_peers',
497
- description:
498
- 'List all projects that have contributed memories to the global shared store.',
499
- parameters: {
500
- type: 'object',
501
- properties: {},
502
- },
503
- async execute() {
504
- const cfg = getConfig(api);
505
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
506
- const result = runHippo(['peers'], hippoCwd);
507
- return { content: [{ type: 'text', text: result || 'No peers found.' }] };
508
- },
509
- }),
510
- { optional: true },
511
- );
512
-
513
- // --- Tool: hippo_wm_push ---
514
- api.registerTool(
515
- (ctx: HippoRuntimeContext) => ({
516
- name: 'hippo_wm_push',
517
- description:
518
- '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).',
519
- parameters: {
520
- type: 'object',
521
- properties: {
522
- content: {
523
- type: 'string',
524
- description: 'Working memory note',
525
- },
526
- scope: {
527
- type: 'string',
528
- description: 'Scope (default: repo)',
529
- },
530
- importance: {
531
- type: 'number',
532
- description: 'Priority 0-1 (default: 0.5)',
533
- },
534
- },
535
- required: ['content'],
536
- },
537
- async execute(
538
- _id: string,
539
- params: { content: string; scope?: string; importance?: number },
540
- ) {
541
- const cfg = getConfig(api);
542
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
543
- const scope = params.scope ?? 'repo';
544
- const importance = params.importance ?? 0.5;
545
- const result = runHippo(
546
- ['wm', 'push', '--scope', scope, '--content', params.content, '--importance', String(importance)],
547
- hippoCwd,
548
- );
549
- return { content: [{ type: 'text', text: result || 'Working memory entry pushed.' }] };
550
- },
551
- }),
552
- { optional: true },
553
- );
554
-
555
- // --- Hook: auto-inject context at session start ---
556
- api.on(
557
- 'before_prompt_build',
558
- (_event: any, ctx: HippoRuntimeContext) => {
559
- const cfg = getConfig(api);
560
- if (cfg.autoContext === false) return {};
561
-
562
- // Dedup guard: skip if this session already got context injected
563
- const sessionKey = getSessionIdentity(ctx);
564
- if (sessionKey && injectedSessions.has(sessionKey)) {
565
- logger.debug?.(`[hippo] skipping duplicate context injection for session ${sessionKey}`);
566
- return {};
567
- }
568
-
569
- const budget = cfg.budget ?? 1500;
570
- const framing = cfg.framing ?? 'observe';
571
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
572
-
573
- // Record session_start event
574
- try {
575
- runHippo(
576
- ['session', 'log', '--id', sessionKey, '--type', 'session_start', '--content', 'Session started', '--source', 'openclaw'],
577
- hippoCwd,
578
- );
579
- } catch (err) {
580
- logger.debug?.('[hippo] session_start event skipped:', err);
581
- }
582
-
583
- try {
584
- const context = runHippo(
585
- ['context', '--auto', '--budget', String(budget), '--framing', framing],
586
- hippoCwd,
587
- );
588
- if (context && context.length > 10 && !context.includes('No hippo store')) {
589
- if (sessionKey) injectedSessions.add(sessionKey);
590
- return {
591
- appendSystemContext: `\n\n## Project Memory (Hippo)\n${context}`,
592
- };
593
- }
594
- } catch (err) {
595
- logger.debug?.('[hippo] context injection skipped:', err);
596
- }
597
- return {};
598
- },
599
- { priority: 5 },
600
- );
601
-
602
- api.on(
603
- 'after_tool_call',
604
- (event: { toolName: string; error?: string }, ctx: HippoRuntimeContext) => {
605
- const cfg = getConfig(api);
606
- if (cfg.autoLearn === false) return;
607
- if (!event.error?.trim()) return;
608
- if (event.toolName.startsWith('hippo_')) return;
609
-
610
- // --- Filter 1: Skip known infrastructure noise ---
611
- if (isNoiseError(event.error)) {
612
- logger.debug?.(`[hippo] autoLearn skipped noise error from '${event.toolName}'`);
613
- return;
614
- }
615
-
616
- const sessionKey = getSessionIdentity(ctx);
617
-
618
- // --- Filter 2: Rate-limit errors per session ---
619
- const errorCount = sessionErrorCounts.get(sessionKey) ?? 0;
620
- if (errorCount >= MAX_ERRORS_PER_SESSION) {
621
- logger.debug?.(`[hippo] autoLearn rate-limited (${errorCount} errors this session)`);
622
- return;
623
- }
624
-
625
- // --- Filter 3: Deduplicate within session ---
626
- const hash = hashError(event.toolName, event.error);
627
- const seen = sessionErrorHashes.get(sessionKey) ?? new Set<string>();
628
- if (seen.has(hash)) {
629
- logger.debug?.(`[hippo] autoLearn skipped duplicate error from '${event.toolName}'`);
630
- return;
631
- }
632
-
633
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
634
- const toolTag = sanitizeTag(event.toolName);
635
- const args: string[] = [
636
- 'remember', formatToolErrorMemory(event.toolName, event.error),
637
- '--error', '--observed', '--tag', 'openclaw',
638
- ];
639
- if (toolTag) args.push('--tag', toolTag);
640
-
641
- const result = runHippo(args, hippoCwd);
642
- if (hippoRememberSucceeded(result)) {
643
- recordSessionMemory(ctx);
644
- seen.add(hash);
645
- sessionErrorHashes.set(sessionKey, seen);
646
- sessionErrorCounts.set(sessionKey, errorCount + 1);
647
- } else {
648
- logger.debug?.(`[hippo] autoLearn skipped storing tool error: ${result}`);
649
- }
650
- },
651
- );
652
-
653
- api.on(
654
- 'session_end',
655
- (_event: { sessionId: string; messageCount: number }, ctx: HippoRuntimeContext) => {
656
- // Clear dedup guards so a new session starts fresh
657
- const sessionKey = getSessionIdentity(ctx);
658
- injectedSessions.delete(sessionKey);
659
- sessionErrorCounts.delete(sessionKey);
660
- sessionErrorHashes.delete(sessionKey);
661
-
662
- const cfg = getConfig(api);
663
- const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
664
-
665
- // Record session_end event
666
- try {
667
- runHippo(
668
- ['session', 'log', '--id', sessionKey, '--type', 'session_end', '--content', 'Session ended', '--source', 'openclaw'],
669
- hippoCwd,
670
- );
671
- } catch (err) {
672
- logger.debug?.('[hippo] session_end event skipped:', err);
224
+
225
+ export default function register(api: any) {
226
+ if (_registered) return;
227
+ _registered = true;
228
+
229
+ const logger = api.logger ?? console;
230
+
231
+ // Clean up stale backup plugins from previous updates
232
+ cleanupBackupPlugins(logger);
233
+
234
+ // --- Tool: hippo_recall ---
235
+ api.registerTool((ctx: HippoRuntimeContext) => ({
236
+ name: 'hippo_recall',
237
+ description:
238
+ '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.',
239
+ parameters: {
240
+ type: 'object',
241
+ properties: {
242
+ query: {
243
+ type: 'string',
244
+ description: 'What to search for in memory (natural language)',
245
+ },
246
+ budget: {
247
+ type: 'number',
248
+ description: 'Max tokens to return (default: 1500)',
249
+ },
250
+ },
251
+ required: ['query'],
252
+ },
253
+ async execute(_id: string, params: { query: string; budget?: number }) {
254
+ const cfg = getConfig(api);
255
+ const budget = params.budget ?? cfg.budget ?? 1500;
256
+ const framing = cfg.framing ?? 'observe';
257
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
258
+ const result = runHippo(
259
+ ['recall', params.query, '--budget', String(budget), '--framing', framing],
260
+ hippoCwd,
261
+ );
262
+ return { content: [{ type: 'text', text: result || 'No relevant memories found.' }] };
263
+ },
264
+ }));
265
+
266
+ // --- Tool: hippo_remember ---
267
+ api.registerTool((ctx: HippoRuntimeContext) => ({
268
+ name: 'hippo_remember',
269
+ description:
270
+ '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.',
271
+ parameters: {
272
+ type: 'object',
273
+ properties: {
274
+ text: {
275
+ type: 'string',
276
+ description: 'The memory to store (1-2 sentences, specific and concrete)',
277
+ },
278
+ error: {
279
+ type: 'boolean',
280
+ description: 'Mark as error memory (doubles half-life)',
281
+ },
282
+ pin: {
283
+ type: 'boolean',
284
+ description: 'Pin memory (never decays)',
285
+ },
286
+ tag: {
287
+ type: 'string',
288
+ description: 'Optional tag for categorization',
289
+ },
290
+ },
291
+ required: ['text'],
292
+ },
293
+ async execute(
294
+ _id: string,
295
+ params: { text: string; error?: boolean; pin?: boolean; tag?: string },
296
+ ) {
297
+ const cfg = getConfig(api);
298
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
299
+ const args: string[] = ['remember', params.text];
300
+ if (params.error) args.push('--error');
301
+ if (params.pin) args.push('--pin');
302
+ if (params.tag) {
303
+ const safe = sanitizeTag(params.tag);
304
+ if (safe) args.push('--tag', safe);
305
+ }
306
+ const result = runHippo(args, hippoCwd);
307
+ if (hippoRememberSucceeded(result)) {
308
+ recordSessionMemory(ctx);
309
+ }
310
+ return { content: [{ type: 'text', text: result || 'Memory stored.' }] };
311
+ },
312
+ }));
313
+
314
+ // --- Tool: hippo_outcome ---
315
+ api.registerTool((ctx: HippoRuntimeContext) => ({
316
+ name: 'hippo_outcome',
317
+ description:
318
+ 'Report whether recalled memories were useful. Strengthens good memories (+5 days half-life) and weakens bad ones (-3 days). Call after completing work.',
319
+ parameters: {
320
+ type: 'object',
321
+ properties: {
322
+ good: {
323
+ type: 'boolean',
324
+ description: 'true = memories helped, false = memories were irrelevant',
325
+ },
326
+ },
327
+ required: ['good'],
328
+ },
329
+ async execute(_id: string, params: { good: boolean }) {
330
+ const cfg = getConfig(api);
331
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
332
+ const flag = params.good ? '--good' : '--bad';
333
+ const result = runHippo(['outcome', flag], hippoCwd);
334
+ return { content: [{ type: 'text', text: result || 'Outcome recorded.' }] };
335
+ },
336
+ }));
337
+
338
+ // --- Tool: hippo_status ---
339
+ api.registerTool(
340
+ (ctx: HippoRuntimeContext) => ({
341
+ name: 'hippo_status',
342
+ description:
343
+ 'Check memory health: counts, strengths, at-risk memories, last consolidation time.',
344
+ parameters: {
345
+ type: 'object',
346
+ properties: {},
347
+ },
348
+ async execute() {
349
+ const cfg = getConfig(api);
350
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
351
+ const result = runHippo(['status'], hippoCwd);
352
+ return { content: [{ type: 'text', text: result || 'No hippo store found.' }] };
353
+ },
354
+ }),
355
+ { optional: true },
356
+ );
357
+
358
+ // --- Tool: hippo_context ---
359
+ api.registerTool(
360
+ (ctx: HippoRuntimeContext) => ({
361
+ name: 'hippo_context',
362
+ description:
363
+ 'Smart context injection: auto-detects current task from git state and returns relevant memories. Use at the start of any session.',
364
+ parameters: {
365
+ type: 'object',
366
+ properties: {
367
+ budget: {
368
+ type: 'number',
369
+ description: 'Max tokens (default: 1500)',
370
+ },
371
+ },
372
+ },
373
+ async execute(_id: string, params: { budget?: number }) {
374
+ const cfg = getConfig(api);
375
+ const budget = params.budget ?? cfg.budget ?? 1500;
376
+ const framing = cfg.framing ?? 'observe';
377
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
378
+ const result = runHippo(
379
+ ['context', '--auto', '--budget', String(budget), '--framing', framing],
380
+ hippoCwd,
381
+ );
382
+ return { content: [{ type: 'text', text: result || 'No context available.' }] };
383
+ },
384
+ }),
385
+ { optional: true },
386
+ );
387
+
388
+ // --- Tool: hippo_conflicts ---
389
+ api.registerTool(
390
+ (ctx: HippoRuntimeContext) => ({
391
+ name: 'hippo_conflicts',
392
+ description:
393
+ 'List open memory conflicts — contradictory memories that need resolution.',
394
+ parameters: {
395
+ type: 'object',
396
+ properties: {
397
+ json: {
398
+ type: 'boolean',
399
+ description: 'Output as JSON (default: false)',
400
+ },
401
+ },
402
+ },
403
+ async execute(_id: string, params: { json?: boolean }) {
404
+ const cfg = getConfig(api);
405
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
406
+ const args: string[] = params.json ? ['conflicts', '--json'] : ['conflicts'];
407
+ const result = runHippo(args, hippoCwd);
408
+ return { content: [{ type: 'text', text: result || 'No conflicts found.' }] };
409
+ },
410
+ }),
411
+ { optional: true },
412
+ );
413
+
414
+ // --- Tool: hippo_resolve ---
415
+ api.registerTool(
416
+ (ctx: HippoRuntimeContext) => ({
417
+ name: 'hippo_resolve',
418
+ description:
419
+ 'Resolve a memory conflict by keeping one memory and weakening or deleting the other.',
420
+ parameters: {
421
+ type: 'object',
422
+ properties: {
423
+ conflict_id: {
424
+ type: 'number',
425
+ description: 'The conflict ID to resolve',
426
+ },
427
+ keep: {
428
+ type: 'string',
429
+ description: 'ID of the memory to keep',
430
+ },
431
+ forget: {
432
+ type: 'boolean',
433
+ description: 'Delete the losing memory instead of weakening it (default: false)',
434
+ },
435
+ },
436
+ required: ['conflict_id', 'keep'],
437
+ },
438
+ async execute(
439
+ _id: string,
440
+ params: { conflict_id: number; keep: string; forget?: boolean },
441
+ ) {
442
+ const cfg = getConfig(api);
443
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
444
+ const args: string[] = ['resolve', String(params.conflict_id), '--keep', params.keep];
445
+ if (params.forget) args.push('--forget');
446
+ const result = runHippo(args, hippoCwd);
447
+ return { content: [{ type: 'text', text: result || 'Conflict resolved.' }] };
448
+ },
449
+ }),
450
+ { optional: true },
451
+ );
452
+
453
+ // --- Tool: hippo_share ---
454
+ api.registerTool(
455
+ (ctx: HippoRuntimeContext) => ({
456
+ name: 'hippo_share',
457
+ description:
458
+ '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.',
459
+ parameters: {
460
+ type: 'object',
461
+ properties: {
462
+ id: {
463
+ type: 'string',
464
+ description: 'Memory ID to share (or "auto" to auto-share all high-scoring memories)',
465
+ },
466
+ force: {
467
+ type: 'boolean',
468
+ description: 'Share even if transfer score is low (default: false)',
469
+ },
470
+ },
471
+ required: ['id'],
472
+ },
473
+ async execute(
474
+ _id: string,
475
+ params: { id: string; force?: boolean },
476
+ ) {
477
+ const cfg = getConfig(api);
478
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
479
+ const args: string[] = ['share'];
480
+ if (params.id === 'auto') {
481
+ args.push('--auto');
482
+ } else {
483
+ args.push(params.id);
484
+ if (params.force) args.push('--force');
485
+ }
486
+ const result = runHippo(args, hippoCwd);
487
+ return { content: [{ type: 'text', text: result || 'Share complete.' }] };
488
+ },
489
+ }),
490
+ { optional: true },
491
+ );
492
+
493
+ // --- Tool: hippo_peers ---
494
+ api.registerTool(
495
+ (ctx: HippoRuntimeContext) => ({
496
+ name: 'hippo_peers',
497
+ description:
498
+ 'List all projects that have contributed memories to the global shared store.',
499
+ parameters: {
500
+ type: 'object',
501
+ properties: {},
502
+ },
503
+ async execute() {
504
+ const cfg = getConfig(api);
505
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
506
+ const result = runHippo(['peers'], hippoCwd);
507
+ return { content: [{ type: 'text', text: result || 'No peers found.' }] };
508
+ },
509
+ }),
510
+ { optional: true },
511
+ );
512
+
513
+ // --- Tool: hippo_wm_push ---
514
+ api.registerTool(
515
+ (ctx: HippoRuntimeContext) => ({
516
+ name: 'hippo_wm_push',
517
+ description:
518
+ '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).',
519
+ parameters: {
520
+ type: 'object',
521
+ properties: {
522
+ content: {
523
+ type: 'string',
524
+ description: 'Working memory note',
525
+ },
526
+ scope: {
527
+ type: 'string',
528
+ description: 'Scope (default: repo)',
529
+ },
530
+ importance: {
531
+ type: 'number',
532
+ description: 'Priority 0-1 (default: 0.5)',
533
+ },
534
+ },
535
+ required: ['content'],
536
+ },
537
+ async execute(
538
+ _id: string,
539
+ params: { content: string; scope?: string; importance?: number },
540
+ ) {
541
+ const cfg = getConfig(api);
542
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
543
+ const scope = params.scope ?? 'repo';
544
+ const importance = params.importance ?? 0.5;
545
+ const result = runHippo(
546
+ ['wm', 'push', '--scope', scope, '--content', params.content, '--importance', String(importance)],
547
+ hippoCwd,
548
+ );
549
+ return { content: [{ type: 'text', text: result || 'Working memory entry pushed.' }] };
550
+ },
551
+ }),
552
+ { optional: true },
553
+ );
554
+
555
+ // --- Hook: auto-inject context at session start ---
556
+ api.on(
557
+ 'before_prompt_build',
558
+ (_event: any, ctx: HippoRuntimeContext) => {
559
+ const cfg = getConfig(api);
560
+ if (cfg.autoContext === false) return {};
561
+
562
+ // Dedup guard: skip if this session already got context injected
563
+ const sessionKey = getSessionIdentity(ctx);
564
+ if (sessionKey && injectedSessions.has(sessionKey)) {
565
+ logger.debug?.(`[hippo] skipping duplicate context injection for session ${sessionKey}`);
566
+ return {};
567
+ }
568
+
569
+ const budget = cfg.budget ?? 1500;
570
+ const framing = cfg.framing ?? 'observe';
571
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
572
+
573
+ // Record session_start event
574
+ try {
575
+ runHippo(
576
+ ['session', 'log', '--id', sessionKey, '--type', 'session_start', '--content', 'Session started', '--source', 'openclaw'],
577
+ hippoCwd,
578
+ );
579
+ } catch (err) {
580
+ logger.debug?.('[hippo] session_start event skipped:', err);
581
+ }
582
+
583
+ try {
584
+ const context = runHippo(
585
+ ['context', '--auto', '--budget', String(budget), '--framing', framing],
586
+ hippoCwd,
587
+ );
588
+ if (context && context.length > 10 && !context.includes('No hippo store')) {
589
+ if (sessionKey) injectedSessions.add(sessionKey);
590
+ return {
591
+ appendSystemContext: `\n\n## Project Memory (Hippo)\n${context}`,
592
+ };
593
+ }
594
+ } catch (err) {
595
+ logger.debug?.('[hippo] context injection skipped:', err);
596
+ }
597
+ return {};
598
+ },
599
+ { priority: 5 },
600
+ );
601
+
602
+ api.on(
603
+ 'after_tool_call',
604
+ (event: { toolName: string; error?: string }, ctx: HippoRuntimeContext) => {
605
+ const cfg = getConfig(api);
606
+ if (cfg.autoLearn === false) return;
607
+ if (!event.error?.trim()) return;
608
+ if (event.toolName.startsWith('hippo_')) return;
609
+
610
+ // --- Filter 1: Skip known infrastructure noise ---
611
+ if (isNoiseError(event.error)) {
612
+ logger.debug?.(`[hippo] autoLearn skipped noise error from '${event.toolName}'`);
613
+ return;
614
+ }
615
+
616
+ const sessionKey = getSessionIdentity(ctx);
617
+
618
+ // --- Filter 2: Rate-limit errors per session ---
619
+ const errorCount = sessionErrorCounts.get(sessionKey) ?? 0;
620
+ if (errorCount >= MAX_ERRORS_PER_SESSION) {
621
+ logger.debug?.(`[hippo] autoLearn rate-limited (${errorCount} errors this session)`);
622
+ return;
623
+ }
624
+
625
+ // --- Filter 3: Deduplicate within session ---
626
+ const hash = hashError(event.toolName, event.error);
627
+ const seen = sessionErrorHashes.get(sessionKey) ?? new Set<string>();
628
+ if (seen.has(hash)) {
629
+ logger.debug?.(`[hippo] autoLearn skipped duplicate error from '${event.toolName}'`);
630
+ return;
631
+ }
632
+
633
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
634
+ const toolTag = sanitizeTag(event.toolName);
635
+ const args: string[] = [
636
+ 'remember', formatToolErrorMemory(event.toolName, event.error),
637
+ '--error', '--observed', '--tag', 'openclaw',
638
+ ];
639
+ if (toolTag) args.push('--tag', toolTag);
640
+
641
+ const result = runHippo(args, hippoCwd);
642
+ if (hippoRememberSucceeded(result)) {
643
+ recordSessionMemory(ctx);
644
+ seen.add(hash);
645
+ sessionErrorHashes.set(sessionKey, seen);
646
+ sessionErrorCounts.set(sessionKey, errorCount + 1);
647
+ } else {
648
+ logger.debug?.(`[hippo] autoLearn skipped storing tool error: ${result}`);
649
+ }
650
+ },
651
+ );
652
+
653
+ api.on(
654
+ 'session_end',
655
+ (_event: { sessionId: string; messageCount: number }, ctx: HippoRuntimeContext) => {
656
+ // Clear dedup guards so a new session starts fresh
657
+ const sessionKey = getSessionIdentity(ctx);
658
+ injectedSessions.delete(sessionKey);
659
+ sessionErrorCounts.delete(sessionKey);
660
+ sessionErrorHashes.delete(sessionKey);
661
+
662
+ const cfg = getConfig(api);
663
+ const hippoCwd = resolveHippoCwdFromContext(api, ctx, cfg.root);
664
+
665
+ // Record session_end event
666
+ try {
667
+ runHippo(
668
+ ['session', 'log', '--id', sessionKey, '--type', 'session_end', '--content', 'Session ended', '--source', 'openclaw'],
669
+ hippoCwd,
670
+ );
671
+ } catch (err) {
672
+ logger.debug?.('[hippo] session_end event skipped:', err);
673
673
  }
674
674
 
675
675
  const newMemories = consumeSessionMemoryCount(ctx);
@@ -691,6 +691,6 @@ export default function register(api: any) {
691
691
  logger.debug?.(`[hippo] autoSleep result: ${result}`);
692
692
  },
693
693
  );
694
-
695
- 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)');
696
- }
694
+
695
+ 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)');
696
+ }