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.
- package/bin/hippo.js +2 -2
- package/dist/api.d.ts +43 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +109 -7
- package/dist/api.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +109 -11
- package/dist/cli.js.map +1 -1
- package/dist/connectors/github/backfill.js +4 -4
- package/dist/connectors/github/cli-impl.js +6 -6
- package/dist/connectors/github/dlq.js +14 -14
- package/dist/connectors/slack/backfill.js +1 -1
- package/dist/connectors/slack/dlq.js +10 -10
- package/dist/connectors/slack/workspaces.js +4 -4
- package/dist/customer-notes.d.ts.map +1 -1
- package/dist/customer-notes.js +5 -1
- package/dist/customer-notes.js.map +1 -1
- package/dist/dag.js +6 -6
- package/dist/dashboard.js +7 -7
- package/dist/decisions.d.ts.map +1 -1
- package/dist/decisions.js +9 -1
- package/dist/decisions.js.map +1 -1
- package/dist/goals.d.ts +11 -0
- package/dist/goals.d.ts.map +1 -1
- package/dist/goals.js +61 -49
- package/dist/goals.js.map +1 -1
- package/dist/graph-extract.d.ts.map +1 -1
- package/dist/graph-extract.js +32 -12
- package/dist/graph-extract.js.map +1 -1
- package/dist/graph.d.ts +46 -3
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +116 -8
- package/dist/graph.js.map +1 -1
- package/dist/hooks.js +24 -24
- package/dist/physics-state.js +27 -27
- package/dist/policies.d.ts.map +1 -1
- package/dist/policies.js +5 -1
- package/dist/policies.js.map +1 -1
- package/dist/predictions.js +67 -67
- package/dist/project-briefs.d.ts.map +1 -1
- package/dist/project-briefs.js +6 -1
- package/dist/project-briefs.js.map +1 -1
- package/dist/refine-llm.js +13 -13
- package/dist/search.d.ts +33 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +7 -0
- package/dist/server.js.map +1 -1
- package/dist/sleep-redact.d.ts +1 -0
- package/dist/sleep-redact.d.ts.map +1 -1
- package/dist/sleep-redact.js +6 -0
- package/dist/sleep-redact.js.map +1 -1
- package/dist/src/api.js +109 -7
- package/dist/src/api.js.map +1 -1
- package/dist/src/cli.js +109 -11
- package/dist/src/cli.js.map +1 -1
- package/dist/src/connectors/github/backfill.js +4 -4
- package/dist/src/connectors/github/cli-impl.js +6 -6
- package/dist/src/connectors/github/dlq.js +14 -14
- package/dist/src/connectors/slack/backfill.js +1 -1
- package/dist/src/connectors/slack/dlq.js +10 -10
- package/dist/src/connectors/slack/workspaces.js +4 -4
- package/dist/src/customer-notes.js +5 -1
- package/dist/src/customer-notes.js.map +1 -1
- package/dist/src/dag.js +6 -6
- package/dist/src/dashboard.js +7 -7
- package/dist/src/decisions.js +9 -1
- package/dist/src/decisions.js.map +1 -1
- package/dist/src/goals.js +61 -49
- package/dist/src/goals.js.map +1 -1
- package/dist/src/graph-extract.js +32 -12
- package/dist/src/graph-extract.js.map +1 -1
- package/dist/src/graph.js +116 -8
- package/dist/src/graph.js.map +1 -1
- package/dist/src/hooks.js +24 -24
- package/dist/src/physics-state.js +27 -27
- package/dist/src/policies.js +5 -1
- package/dist/src/policies.js.map +1 -1
- package/dist/src/predictions.js +67 -67
- package/dist/src/project-briefs.js +6 -1
- package/dist/src/project-briefs.js.map +1 -1
- package/dist/src/refine-llm.js +13 -13
- package/dist/src/search.js.map +1 -1
- package/dist/src/server.js +7 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/sleep-redact.js +6 -0
- package/dist/src/sleep-redact.js.map +1 -1
- package/dist/src/store.js +261 -260
- package/dist/src/store.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/working-memory.js +19 -19
- package/dist/store.d.ts +6 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +261 -260
- package/dist/store.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.js +19 -19
- package/dist-ui/index.html +12 -12
- package/extensions/openclaw-plugin/index.ts +650 -650
- 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
- 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
|
+
}
|