@fathippo/fathippo-context-engine 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -49
- package/dist/api/client.d.ts +1 -74
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +1 -175
- package/dist/api/client.js.map +1 -1
- package/dist/engine.d.ts +9 -24
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +237 -409
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/openclaw.plugin.json +21 -4
- package/package.json +3 -2
package/dist/engine.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { createLocalMemoryStore, invalidateAllLocalResultsForUser, localRetrieve, localStoreResult, } from "@fathippo/local";
|
|
9
|
-
import { FatHippoClient } from "
|
|
9
|
+
import { FatHippoClient, createFatHippoHostedRuntimeClient, } from "@fathippo/hosted";
|
|
10
10
|
import { buildStructuredTrace, getMessageText, shouldCaptureCodingTrace } from "./cognitive/trace-capture.js";
|
|
11
11
|
import { CONTEXT_ENGINE_ID, CONTEXT_ENGINE_VERSION } from "./version.js";
|
|
12
12
|
import { formatMemoriesForInjection, dedupeMemories, estimateTokens, } from "./utils/formatting.js";
|
|
@@ -22,13 +22,13 @@ export class FatHippoContextEngine {
|
|
|
22
22
|
ownsCompaction: true, // We handle compaction via Dream Cycle
|
|
23
23
|
};
|
|
24
24
|
client;
|
|
25
|
+
runtimeClient;
|
|
25
26
|
config;
|
|
26
27
|
mode;
|
|
27
28
|
localStore;
|
|
28
|
-
|
|
29
|
+
hostedSessions = new Map();
|
|
29
30
|
// Cognitive engine state
|
|
30
31
|
sessionStartTimes = new Map();
|
|
31
|
-
sessionApplicationIds = new Map();
|
|
32
32
|
sessionLocalProfiles = new Map();
|
|
33
33
|
sessionHippoNodState = new Map();
|
|
34
34
|
cognitiveEnabled;
|
|
@@ -45,8 +45,6 @@ export class FatHippoContextEngine {
|
|
|
45
45
|
"ty",
|
|
46
46
|
"thx",
|
|
47
47
|
]);
|
|
48
|
-
static MIN_VECTOR_SIMILARITY = 0.75;
|
|
49
|
-
static MIN_CRITICAL_RELEVANCE = 0.7;
|
|
50
48
|
static HIPPO_NOD_COOLDOWN_MS = 15 * 60 * 1000;
|
|
51
49
|
static HIPPO_NOD_MIN_MESSAGE_GAP = 6;
|
|
52
50
|
constructor(config) {
|
|
@@ -56,6 +54,14 @@ export class FatHippoContextEngine {
|
|
|
56
54
|
throw new Error("FatHippo hosted mode requires an API key. Pass apiKey or switch mode to local/auto.");
|
|
57
55
|
}
|
|
58
56
|
this.client = this.mode === "hosted" ? new FatHippoClient(config) : null;
|
|
57
|
+
this.runtimeClient =
|
|
58
|
+
this.mode === "hosted" && config.apiKey
|
|
59
|
+
? createFatHippoHostedRuntimeClient({
|
|
60
|
+
apiKey: config.apiKey,
|
|
61
|
+
baseUrl: config.baseUrl || "https://fathippo.ai/api",
|
|
62
|
+
runtime: this.buildHostedRuntimeMetadata(),
|
|
63
|
+
})
|
|
64
|
+
: null;
|
|
59
65
|
this.localStore =
|
|
60
66
|
this.mode === "local"
|
|
61
67
|
? createLocalMemoryStore({
|
|
@@ -71,6 +77,7 @@ export class FatHippoContextEngine {
|
|
|
71
77
|
async bootstrap(params) {
|
|
72
78
|
try {
|
|
73
79
|
this.sessionStartTimes.set(params.sessionId, Date.now());
|
|
80
|
+
const workspaceRoot = this.detectWorkspaceRoot(params.sessionFile);
|
|
74
81
|
if (this.mode === "local") {
|
|
75
82
|
this.sessionLocalProfiles.set(params.sessionId, this.deriveLocalProfileId(params.sessionId, params.sessionFile));
|
|
76
83
|
return {
|
|
@@ -78,25 +85,37 @@ export class FatHippoContextEngine {
|
|
|
78
85
|
importedMessages: 0,
|
|
79
86
|
};
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
limit: 30,
|
|
84
|
-
excludeAbsorbed: true,
|
|
85
|
-
});
|
|
86
|
-
if (!critical) {
|
|
88
|
+
const runtimeClient = this.runtimeClient;
|
|
89
|
+
if (!runtimeClient) {
|
|
87
90
|
return {
|
|
88
91
|
bootstrapped: true,
|
|
89
92
|
importedMessages: 0,
|
|
90
93
|
};
|
|
91
94
|
}
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
if (!this.hostedSessions.has(params.sessionId)) {
|
|
96
|
+
const session = await runtimeClient.startSession({
|
|
97
|
+
firstMessage: "",
|
|
98
|
+
namespace: this.config.namespace,
|
|
99
|
+
metadata: {
|
|
100
|
+
openclawSessionId: params.sessionId,
|
|
101
|
+
},
|
|
102
|
+
runtime: this.buildHostedRuntimeMetadata({
|
|
103
|
+
sessionId: params.sessionId,
|
|
104
|
+
sessionFile: params.sessionFile,
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
this.hostedSessions.set(params.sessionId, {
|
|
108
|
+
hostedSessionId: session.sessionId,
|
|
109
|
+
workspaceRoot,
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
bootstrapped: true,
|
|
113
|
+
importedMessages: session.injectedMemories.length,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
97
116
|
return {
|
|
98
117
|
bootstrapped: true,
|
|
99
|
-
importedMessages:
|
|
118
|
+
importedMessages: 0,
|
|
100
119
|
};
|
|
101
120
|
}
|
|
102
121
|
catch (error) {
|
|
@@ -111,73 +130,22 @@ export class FatHippoContextEngine {
|
|
|
111
130
|
* Ingest a single message into FatHippo
|
|
112
131
|
*/
|
|
113
132
|
async ingest(params) {
|
|
114
|
-
|
|
133
|
+
void params;
|
|
115
134
|
if (params.isHeartbeat) {
|
|
116
135
|
return { ingested: false };
|
|
117
136
|
}
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
(!this.isRoleMessage(params.message) || params.message.role !== "user")) {
|
|
121
|
-
return { ingested: false };
|
|
122
|
-
}
|
|
123
|
-
const content = this.extractContent(params.message);
|
|
124
|
-
if (!content) {
|
|
125
|
-
return { ingested: false };
|
|
126
|
-
}
|
|
127
|
-
// Auto-detect constraints from user messages
|
|
128
|
-
if (this.cognitiveEnabled && this.isRoleMessage(params.message) && params.message.role === "user") {
|
|
129
|
-
this.maybeStoreConstraint(content).catch(() => { }); // Fire and forget
|
|
130
|
-
}
|
|
131
|
-
// Filter prompt injection attempts
|
|
132
|
-
if (detectPromptInjection(content)) {
|
|
133
|
-
console.warn("[FatHippo] Blocked prompt injection attempt");
|
|
134
|
-
return { ingested: false };
|
|
135
|
-
}
|
|
136
|
-
// Check if content matches capture patterns
|
|
137
|
-
if (!matchesCapturePatterns(content)) {
|
|
138
|
-
return { ingested: false };
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
if (this.mode === "local") {
|
|
142
|
-
const profileId = this.getLocalProfileId(params.sessionId);
|
|
143
|
-
await this.localStore?.remember({
|
|
144
|
-
profileId,
|
|
145
|
-
content: sanitizeContent(content),
|
|
146
|
-
title: this.buildLocalTitle(content),
|
|
147
|
-
});
|
|
148
|
-
invalidateAllLocalResultsForUser(profileId);
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
await this.client?.remember({
|
|
152
|
-
content: sanitizeContent(content),
|
|
153
|
-
conversationId: this.config.conversationId || params.sessionId,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
return { ingested: true };
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
console.error("[FatHippo] Ingest error:", error);
|
|
160
|
-
return { ingested: false };
|
|
161
|
-
}
|
|
137
|
+
// Full-turn capture happens in afterTurn for both hosted and local modes.
|
|
138
|
+
return { ingested: false };
|
|
162
139
|
}
|
|
163
140
|
/**
|
|
164
141
|
* Ingest a batch of messages
|
|
165
142
|
*/
|
|
166
143
|
async ingestBatch(params) {
|
|
144
|
+
void params;
|
|
167
145
|
if (params.isHeartbeat) {
|
|
168
146
|
return { ingestedCount: 0 };
|
|
169
147
|
}
|
|
170
|
-
|
|
171
|
-
for (const message of params.messages) {
|
|
172
|
-
const result = await this.ingest({
|
|
173
|
-
sessionId: params.sessionId,
|
|
174
|
-
message,
|
|
175
|
-
isHeartbeat: params.isHeartbeat,
|
|
176
|
-
});
|
|
177
|
-
if (result.ingested)
|
|
178
|
-
ingestedCount++;
|
|
179
|
-
}
|
|
180
|
-
return { ingestedCount };
|
|
148
|
+
return { ingestedCount: 0 };
|
|
181
149
|
}
|
|
182
150
|
/**
|
|
183
151
|
* Post-turn lifecycle processing
|
|
@@ -189,28 +157,41 @@ export class FatHippoContextEngine {
|
|
|
189
157
|
}
|
|
190
158
|
return;
|
|
191
159
|
}
|
|
192
|
-
|
|
193
|
-
const cacheAge = this.cachedCritical
|
|
194
|
-
? Date.now() - this.cachedCritical.fetchedAt
|
|
195
|
-
: Infinity;
|
|
196
|
-
if (cacheAge > 5 * 60 * 1000) {
|
|
197
|
-
// 5 minute cache
|
|
198
|
-
this.cachedCritical = null;
|
|
199
|
-
}
|
|
200
|
-
// Capture cognitive trace for coding sessions
|
|
160
|
+
const turnMessages = this.extractTurnMessages(params.messages, params.prePromptMessageCount);
|
|
201
161
|
if (this.mode === "local") {
|
|
202
|
-
await this.
|
|
162
|
+
await this.captureLocalTurnMemories({
|
|
203
163
|
sessionId: params.sessionId,
|
|
204
164
|
sessionFile: params.sessionFile,
|
|
205
|
-
messages:
|
|
165
|
+
messages: turnMessages,
|
|
206
166
|
});
|
|
207
|
-
|
|
208
|
-
else if (this.cognitiveEnabled) {
|
|
209
|
-
await this.captureStructuredTrace({
|
|
167
|
+
await this.captureLocalTrace({
|
|
210
168
|
sessionId: params.sessionId,
|
|
211
169
|
sessionFile: params.sessionFile,
|
|
212
170
|
messages: params.messages,
|
|
213
171
|
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const runtimeClient = this.runtimeClient;
|
|
175
|
+
const hostedSessionId = this.hostedSessions.get(params.sessionId)?.hostedSessionId;
|
|
176
|
+
if (!runtimeClient || !hostedSessionId) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
await runtimeClient.recordTurn({
|
|
181
|
+
sessionId: hostedSessionId,
|
|
182
|
+
messages: turnMessages,
|
|
183
|
+
memoriesUsed: [],
|
|
184
|
+
captureUserOnly: this.config.captureUserOnly === true,
|
|
185
|
+
captureConstraints: this.cognitiveEnabled,
|
|
186
|
+
captureTrace: this.cognitiveEnabled,
|
|
187
|
+
runtime: this.buildHostedRuntimeMetadata({
|
|
188
|
+
sessionId: params.sessionId,
|
|
189
|
+
sessionFile: params.sessionFile,
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.error("[FatHippo] Record turn error:", error);
|
|
214
195
|
}
|
|
215
196
|
}
|
|
216
197
|
detectToolsUsed(messages) {
|
|
@@ -235,78 +216,54 @@ export class FatHippoContextEngine {
|
|
|
235
216
|
if (this.mode === "local") {
|
|
236
217
|
return this.assembleLocalContext(params, lastUserMessage, runtimeAwareness);
|
|
237
218
|
}
|
|
238
|
-
const
|
|
239
|
-
|
|
219
|
+
const runtimeClient = this.runtimeClient;
|
|
220
|
+
const hostedSession = this.hostedSessions.get(params.sessionId);
|
|
221
|
+
if (!runtimeClient || !hostedSession) {
|
|
240
222
|
return {
|
|
241
223
|
messages: params.messages,
|
|
242
224
|
estimatedTokens: this.estimateMessageTokens(params.messages),
|
|
243
225
|
};
|
|
244
226
|
}
|
|
245
|
-
// Always fetch indexed summaries (they're compact)
|
|
246
|
-
let indexedContext = "";
|
|
247
|
-
try {
|
|
248
|
-
const indexed = await client.getIndexedSummaries();
|
|
249
|
-
if (indexed.count > 0) {
|
|
250
|
-
indexedContext = `\n## Indexed Memory (use GET /indexed/:key for full content)\n${indexed.contextFormat}\n`;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
catch {
|
|
254
|
-
// Indexed memories are optional, don't fail on error
|
|
255
|
-
}
|
|
256
227
|
if (!lastUserMessage || this.isTrivialQuery(lastUserMessage)) {
|
|
257
|
-
// Still include indexed summaries even for trivial queries
|
|
258
228
|
const baseTokens = this.estimateMessageTokens(params.messages);
|
|
259
|
-
const systemPromptAddition = runtimeAwareness + indexedContext;
|
|
260
229
|
return {
|
|
261
230
|
messages: params.messages,
|
|
262
|
-
estimatedTokens: baseTokens + estimateTokens(
|
|
263
|
-
systemPromptAddition:
|
|
231
|
+
estimatedTokens: baseTokens + estimateTokens(runtimeAwareness),
|
|
232
|
+
systemPromptAddition: runtimeAwareness.trim() || undefined,
|
|
264
233
|
};
|
|
265
234
|
}
|
|
266
|
-
|
|
267
|
-
let
|
|
268
|
-
let syntheses = [];
|
|
269
|
-
let memoryHippoCue = null;
|
|
270
|
-
let hasRelevantCriticalMatch = false;
|
|
235
|
+
let hostedContext = "";
|
|
236
|
+
let cue = null;
|
|
271
237
|
try {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
238
|
+
const context = await runtimeClient.buildContext({
|
|
239
|
+
sessionId: hostedSession.hostedSessionId,
|
|
240
|
+
messages: this.toRuntimeMessages(params.messages),
|
|
241
|
+
lastUserMessage,
|
|
242
|
+
conversationId: this.config.conversationId || params.sessionId,
|
|
243
|
+
includeIndexed: true,
|
|
244
|
+
includeConstraints: this.cognitiveEnabled,
|
|
245
|
+
includeCognitive: this.cognitiveEnabled,
|
|
246
|
+
runtime: this.buildHostedRuntimeMetadata({
|
|
247
|
+
sessionId: params.sessionId,
|
|
248
|
+
}),
|
|
277
249
|
});
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (this.config.injectCritical !== false && hasRelevantCriticalMatch) {
|
|
285
|
-
let criticalMemories;
|
|
286
|
-
let criticalSyntheses;
|
|
287
|
-
if (this.cachedCritical &&
|
|
288
|
-
Date.now() - this.cachedCritical.fetchedAt < 5 * 60 * 1000) {
|
|
289
|
-
criticalMemories = this.cachedCritical.memories;
|
|
290
|
-
criticalSyntheses = this.cachedCritical.syntheses;
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
const critical = await client.getCriticalMemories({
|
|
294
|
-
limit: 15,
|
|
295
|
-
excludeAbsorbed: true,
|
|
296
|
-
});
|
|
297
|
-
criticalMemories = critical.memories;
|
|
298
|
-
criticalSyntheses = critical.syntheses;
|
|
299
|
-
this.cachedCritical = {
|
|
300
|
-
memories: criticalMemories,
|
|
301
|
-
syntheses: criticalSyntheses,
|
|
302
|
-
fetchedAt: Date.now(),
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
memories = dedupeMemories([...criticalMemories, ...memories]);
|
|
306
|
-
syntheses = criticalSyntheses;
|
|
250
|
+
hostedContext = context.systemPromptAddition ?? "";
|
|
251
|
+
if (hostedContext.includes("## Recommended Workflow")) {
|
|
252
|
+
cue = {
|
|
253
|
+
kind: "workflow",
|
|
254
|
+
reason: "Fathippo surfaced a learned workflow for this task.",
|
|
255
|
+
};
|
|
307
256
|
}
|
|
308
|
-
if (
|
|
309
|
-
|
|
257
|
+
else if (hostedContext.includes("## Learned Coding Patterns") ||
|
|
258
|
+
hostedContext.includes("## Shared Global Patterns") ||
|
|
259
|
+
hostedContext.includes("## Past Similar Problems")) {
|
|
260
|
+
cue = {
|
|
261
|
+
kind: "learned_fix",
|
|
262
|
+
reason: "Fathippo surfaced learned fixes and similar past problems for this task.",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
else if (hostedContext.trim()) {
|
|
266
|
+
cue = {
|
|
310
267
|
kind: "memory",
|
|
311
268
|
reason: "Fathippo recalled relevant memory for this reply.",
|
|
312
269
|
};
|
|
@@ -316,58 +273,22 @@ export class FatHippoContextEngine {
|
|
|
316
273
|
console.error("[FatHippo] Assemble error:", error);
|
|
317
274
|
}
|
|
318
275
|
const baseMessageTokens = this.estimateMessageTokens(params.messages);
|
|
319
|
-
if (typeof params.tokenBudget === "number" && params.tokenBudget > 0) {
|
|
320
|
-
const contextBudget = Math.max(0, params.tokenBudget - baseMessageTokens);
|
|
321
|
-
const constrained = this.constrainContextToBudget(memories, syntheses, contextBudget);
|
|
322
|
-
memories = constrained.memories;
|
|
323
|
-
syntheses = constrained.syntheses;
|
|
324
|
-
}
|
|
325
|
-
// Format memories for injection, include indexed summaries
|
|
326
|
-
const memoryBlock = formatMemoriesForInjection(memories, syntheses);
|
|
327
|
-
// Fetch constraints (always inject - these are critical rules)
|
|
328
|
-
let constraintsContext = "";
|
|
329
|
-
if (this.cognitiveEnabled) {
|
|
330
|
-
try {
|
|
331
|
-
constraintsContext = await this.fetchConstraints();
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
console.error("[FatHippo] Constraints fetch error:", error);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
// Fetch cognitive context (traces + patterns) for coding sessions
|
|
338
|
-
let cognitiveContext = "";
|
|
339
|
-
let cognitiveHippoCue = null;
|
|
340
|
-
if (this.cognitiveEnabled && this.looksLikeCodingQuery(lastUserMessage)) {
|
|
341
|
-
try {
|
|
342
|
-
const cognitive = await this.fetchCognitiveContext(params.sessionId, lastUserMessage);
|
|
343
|
-
if (cognitive.context) {
|
|
344
|
-
cognitiveContext = cognitive.context;
|
|
345
|
-
cognitiveHippoCue = cognitive.hippoCue;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
catch (error) {
|
|
349
|
-
console.error("[FatHippo] Cognitive context error:", error);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
276
|
const hippoNodInstruction = this.buildHippoNodInstruction({
|
|
353
277
|
sessionId: params.sessionId,
|
|
354
278
|
messageCount: params.messages.length,
|
|
355
279
|
lastUserMessage,
|
|
356
|
-
cue
|
|
280
|
+
cue,
|
|
357
281
|
});
|
|
358
282
|
const fullContext = typeof params.tokenBudget === "number" && params.tokenBudget > 0
|
|
359
283
|
? this.fitContextToBudget({
|
|
360
284
|
sections: [
|
|
361
285
|
runtimeAwareness,
|
|
362
|
-
|
|
363
|
-
memoryBlock ? `${memoryBlock}\n` : "",
|
|
364
|
-
indexedContext,
|
|
365
|
-
cognitiveContext,
|
|
286
|
+
hostedContext,
|
|
366
287
|
hippoNodInstruction,
|
|
367
288
|
],
|
|
368
289
|
contextBudget: Math.max(0, params.tokenBudget - baseMessageTokens),
|
|
369
290
|
})
|
|
370
|
-
: runtimeAwareness +
|
|
291
|
+
: runtimeAwareness + hostedContext + hippoNodInstruction;
|
|
371
292
|
const tokens = estimateTokens(fullContext) + baseMessageTokens;
|
|
372
293
|
return {
|
|
373
294
|
messages: params.messages,
|
|
@@ -489,213 +410,18 @@ export class FatHippoContextEngine {
|
|
|
489
410
|
"",
|
|
490
411
|
].join("\n");
|
|
491
412
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
*/
|
|
495
|
-
looksLikeCodingQuery(query) {
|
|
496
|
-
const codingKeywords = [
|
|
497
|
-
'bug', 'error', 'fix', 'debug', 'implement', 'build', 'create', 'refactor',
|
|
498
|
-
'function', 'class', 'api', 'endpoint', 'database', 'query', 'test',
|
|
499
|
-
'deploy', 'config', 'install', 'code', 'script', 'compile', 'run'
|
|
500
|
-
];
|
|
501
|
-
const queryLower = query.toLowerCase();
|
|
502
|
-
return codingKeywords.some(kw => queryLower.includes(kw));
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Fetch active constraints (always injected)
|
|
506
|
-
*/
|
|
507
|
-
async fetchConstraints() {
|
|
508
|
-
const baseUrl = this.getApiBaseUrl();
|
|
509
|
-
const response = await fetch(`${baseUrl}/v1/cognitive/constraints`, {
|
|
510
|
-
method: 'GET',
|
|
511
|
-
headers: this.getHostedHeaders(false),
|
|
512
|
-
});
|
|
513
|
-
if (!response.ok)
|
|
514
|
-
return '';
|
|
515
|
-
const data = await response.json();
|
|
516
|
-
return data.contextFormat || '';
|
|
517
|
-
}
|
|
518
|
-
/**
|
|
519
|
-
* Auto-detect and store constraints from user message
|
|
520
|
-
*/
|
|
521
|
-
async maybeStoreConstraint(message) {
|
|
522
|
-
const baseUrl = this.getApiBaseUrl();
|
|
523
|
-
try {
|
|
524
|
-
await fetch(`${baseUrl}/v1/cognitive/constraints`, {
|
|
525
|
-
method: 'POST',
|
|
526
|
-
headers: this.getHostedHeaders(),
|
|
527
|
-
body: JSON.stringify({ message }),
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
catch {
|
|
531
|
-
// Constraint detection is best-effort
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Fetch relevant traces and patterns from cognitive API
|
|
536
|
-
*/
|
|
537
|
-
async fetchCognitiveContext(sessionId, problem) {
|
|
538
|
-
const baseUrl = this.getApiBaseUrl();
|
|
539
|
-
const response = await fetch(`${baseUrl}/v1/cognitive/traces/relevant`, {
|
|
540
|
-
method: 'POST',
|
|
541
|
-
headers: this.getHostedHeaders(),
|
|
542
|
-
body: JSON.stringify({
|
|
543
|
-
sessionId,
|
|
544
|
-
endpoint: "context-engine.assemble",
|
|
545
|
-
problem,
|
|
546
|
-
limit: 3,
|
|
547
|
-
adaptivePolicy: this.config.adaptivePolicyEnabled !== false,
|
|
548
|
-
}),
|
|
549
|
-
});
|
|
550
|
-
if (!response.ok) {
|
|
551
|
-
return { context: null, hippoCue: null };
|
|
552
|
-
}
|
|
553
|
-
const data = await response.json();
|
|
554
|
-
if (data.applicationId) {
|
|
555
|
-
this.sessionApplicationIds.set(sessionId, data.applicationId);
|
|
556
|
-
}
|
|
557
|
-
const sections = new Map();
|
|
558
|
-
if (data.workflow && data.workflow.steps.length > 0) {
|
|
559
|
-
const stepLines = data.workflow.steps.map((step) => `- ${step}`).join("\n");
|
|
560
|
-
const explorationNote = data.workflow.exploration ? " exploratory" : "";
|
|
561
|
-
sections.set("workflow", `## Recommended Workflow\n${stepLines}\n\nStrategy: ${data.workflow.title} (${data.workflow.rationale}${explorationNote})`);
|
|
562
|
-
}
|
|
563
|
-
const localPatterns = (data.patterns ?? []).filter((pattern) => pattern.scope !== "global");
|
|
564
|
-
const globalPatterns = (data.patterns ?? []).filter((pattern) => pattern.scope === "global");
|
|
565
|
-
if (localPatterns.length > 0) {
|
|
566
|
-
const patternLines = localPatterns
|
|
567
|
-
.map((pattern) => {
|
|
568
|
-
const score = typeof pattern.score === "number" ? `, score ${pattern.score.toFixed(1)}` : "";
|
|
569
|
-
return `- [${pattern.domain}] ${pattern.approach.slice(0, 200)} (${Math.round(pattern.confidence * 100)}% confidence${score})`;
|
|
570
|
-
})
|
|
571
|
-
.join("\n");
|
|
572
|
-
sections.set("local_patterns", `## Learned Coding Patterns\n${patternLines}`);
|
|
573
|
-
}
|
|
574
|
-
if (globalPatterns.length > 0) {
|
|
575
|
-
const patternLines = globalPatterns
|
|
576
|
-
.map((pattern) => {
|
|
577
|
-
const score = typeof pattern.score === "number" ? `, score ${pattern.score.toFixed(1)}` : "";
|
|
578
|
-
return `- [${pattern.domain}] ${pattern.approach.slice(0, 200)} (${Math.round(pattern.confidence * 100)}% confidence${score})`;
|
|
579
|
-
})
|
|
580
|
-
.join("\n");
|
|
581
|
-
sections.set("global_patterns", `## Shared Global Patterns\n${patternLines}`);
|
|
582
|
-
}
|
|
583
|
-
if (data.traces && data.traces.length > 0) {
|
|
584
|
-
const traceLines = data.traces.map(t => {
|
|
585
|
-
const icon = t.outcome === 'success' ? '✓' : t.outcome === 'failed' ? '✗' : '~';
|
|
586
|
-
const solution = t.solution ? ` → ${t.solution.slice(0, 80)}...` : '';
|
|
587
|
-
return `- ${icon} ${t.problem.slice(0, 80)}${solution}`;
|
|
588
|
-
}).join('\n');
|
|
589
|
-
sections.set("traces", `## Past Similar Problems\n${traceLines}`);
|
|
590
|
-
}
|
|
591
|
-
if (data.skills && data.skills.length > 0) {
|
|
592
|
-
const skillLines = data.skills
|
|
593
|
-
.map((skill) => `- [${skill.scope}] ${skill.name}: ${skill.description} (${Math.round(skill.successRate * 100)}% success)`)
|
|
594
|
-
.join("\n");
|
|
595
|
-
sections.set("skills", `## Synthesized Skills\n${skillLines}`);
|
|
596
|
-
}
|
|
597
|
-
let hippoCue = null;
|
|
598
|
-
if (data.workflow && data.workflow.steps.length > 0) {
|
|
599
|
-
hippoCue = {
|
|
600
|
-
kind: "workflow",
|
|
601
|
-
reason: "Fathippo surfaced a learned workflow for this task.",
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
else if ((data.skills?.length ?? 0) > 0) {
|
|
605
|
-
hippoCue = {
|
|
606
|
-
kind: "learned_fix",
|
|
607
|
-
reason: "Fathippo surfaced a synthesized skill for this task.",
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
else if ((localPatterns.length + globalPatterns.length + (data.traces?.length ?? 0)) >= 2) {
|
|
611
|
-
hippoCue = {
|
|
612
|
-
kind: "learned_fix",
|
|
613
|
-
reason: "Fathippo surfaced learned fixes and similar past problems for this task.",
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
if (sections.size === 0) {
|
|
617
|
-
return { context: null, hippoCue: null };
|
|
618
|
-
}
|
|
619
|
-
const orderedSections = [
|
|
620
|
-
sections.get("workflow"),
|
|
621
|
-
...(data.policy?.sectionOrder ?? ["local_patterns", "global_patterns", "traces", "skills"]).map((key) => sections.get(key)),
|
|
622
|
-
]
|
|
623
|
-
.filter((section) => typeof section === "string" && section.length > 0);
|
|
624
|
-
if (orderedSections.length === 0) {
|
|
625
|
-
return { context: null, hippoCue: null };
|
|
626
|
-
}
|
|
627
|
-
return {
|
|
628
|
-
context: '\n' + orderedSections.join('\n\n') + '\n',
|
|
629
|
-
hippoCue,
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
async captureStructuredTrace(params) {
|
|
633
|
-
if (!shouldCaptureCodingTrace(params.messages)) {
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
if (!this.sessionStartTimes.has(params.sessionId)) {
|
|
637
|
-
this.sessionStartTimes.set(params.sessionId, Date.now() - 60_000);
|
|
638
|
-
}
|
|
639
|
-
const payload = buildStructuredTrace({
|
|
640
|
-
sessionId: params.sessionId,
|
|
641
|
-
messages: params.messages,
|
|
642
|
-
toolsUsed: this.detectToolsUsed(params.messages),
|
|
643
|
-
filesModified: this.detectFilesModified(params.sessionFile, params.messages),
|
|
644
|
-
workspaceRoot: this.detectWorkspaceRoot(params.sessionFile),
|
|
645
|
-
startTime: this.sessionStartTimes.get(params.sessionId) ?? Date.now() - 60_000,
|
|
646
|
-
endTime: Math.min(Date.now(), (this.sessionStartTimes.get(params.sessionId) ?? Date.now()) + 30 * 60 * 1000),
|
|
647
|
-
});
|
|
648
|
-
if (!payload) {
|
|
649
|
-
this.sessionStartTimes.set(params.sessionId, Date.now());
|
|
413
|
+
async runCognitiveHeartbeat() {
|
|
414
|
+
if (!this.client) {
|
|
650
415
|
return;
|
|
651
416
|
}
|
|
652
417
|
try {
|
|
653
|
-
|
|
654
|
-
const response = await fetch(`${baseUrl}/v1/cognitive/traces`, {
|
|
655
|
-
method: "POST",
|
|
656
|
-
headers: this.getHostedHeaders(),
|
|
657
|
-
body: JSON.stringify({
|
|
658
|
-
...payload,
|
|
659
|
-
applicationId: this.sessionApplicationIds.get(params.sessionId) ?? null,
|
|
660
|
-
shareEligible: this.config.shareEligibleByDefault !== false && payload.shareEligible,
|
|
661
|
-
}),
|
|
662
|
-
});
|
|
663
|
-
if (!response.ok) {
|
|
664
|
-
throw new Error(`Trace capture failed with status ${response.status}`);
|
|
665
|
-
}
|
|
666
|
-
this.sessionStartTimes.delete(params.sessionId);
|
|
667
|
-
this.sessionApplicationIds.delete(params.sessionId);
|
|
668
|
-
}
|
|
669
|
-
catch (error) {
|
|
670
|
-
console.error("[FatHippo] Trace capture error:", error);
|
|
671
|
-
this.sessionStartTimes.set(params.sessionId, Date.now());
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
async runCognitiveHeartbeat() {
|
|
675
|
-
const baseUrl = this.getApiBaseUrl();
|
|
676
|
-
const headers = this.getHostedHeaders();
|
|
677
|
-
try {
|
|
678
|
-
const response = await fetch(`${baseUrl}/v1/cognitive/patterns/extract`, {
|
|
679
|
-
method: "POST",
|
|
680
|
-
headers,
|
|
681
|
-
body: JSON.stringify({}),
|
|
682
|
-
});
|
|
683
|
-
if (!response.ok) {
|
|
684
|
-
throw new Error(`Pattern extraction failed with status ${response.status}`);
|
|
685
|
-
}
|
|
418
|
+
await this.client.extractCognitivePatterns({});
|
|
686
419
|
}
|
|
687
420
|
catch (error) {
|
|
688
421
|
console.error("[FatHippo] Pattern extraction heartbeat error:", error);
|
|
689
422
|
}
|
|
690
423
|
try {
|
|
691
|
-
|
|
692
|
-
method: "POST",
|
|
693
|
-
headers,
|
|
694
|
-
body: JSON.stringify({}),
|
|
695
|
-
});
|
|
696
|
-
if (!response.ok) {
|
|
697
|
-
throw new Error(`Skill synthesis failed with status ${response.status}`);
|
|
698
|
-
}
|
|
424
|
+
await this.client.synthesizeCognitiveSkills({});
|
|
699
425
|
}
|
|
700
426
|
catch (error) {
|
|
701
427
|
console.error("[FatHippo] Skill synthesis heartbeat error:", error);
|
|
@@ -824,24 +550,6 @@ export class FatHippoContextEngine {
|
|
|
824
550
|
this.sessionStartTimes.set(params.sessionId, Date.now());
|
|
825
551
|
}
|
|
826
552
|
}
|
|
827
|
-
getApiBaseUrl() {
|
|
828
|
-
const baseUrl = this.config.baseUrl || "https://fathippo.ai/api";
|
|
829
|
-
return baseUrl.replace(/\/v1$/, "");
|
|
830
|
-
}
|
|
831
|
-
getHostedHeaders(includeContentType = true) {
|
|
832
|
-
const headers = {
|
|
833
|
-
"X-Fathippo-Plugin-Id": this.config.pluginId ?? CONTEXT_ENGINE_ID,
|
|
834
|
-
"X-Fathippo-Plugin-Version": this.config.pluginVersion ?? CONTEXT_ENGINE_VERSION,
|
|
835
|
-
"X-Fathippo-Plugin-Mode": this.mode,
|
|
836
|
-
};
|
|
837
|
-
if (this.config.apiKey) {
|
|
838
|
-
headers.Authorization = `Bearer ${this.config.apiKey}`;
|
|
839
|
-
}
|
|
840
|
-
if (includeContentType) {
|
|
841
|
-
headers["Content-Type"] = "application/json";
|
|
842
|
-
}
|
|
843
|
-
return headers;
|
|
844
|
-
}
|
|
845
553
|
buildHippoNodInstruction(params) {
|
|
846
554
|
if (this.config.hippoNodsEnabled === false || !params.cue) {
|
|
847
555
|
return "";
|
|
@@ -916,8 +624,6 @@ export class FatHippoContextEngine {
|
|
|
916
624
|
if (!result) {
|
|
917
625
|
return { ok: false, compacted: false, reason: "hosted client unavailable" };
|
|
918
626
|
}
|
|
919
|
-
// Invalidate cache after dream cycle
|
|
920
|
-
this.cachedCritical = null;
|
|
921
627
|
return {
|
|
922
628
|
ok: result.ok,
|
|
923
629
|
compacted: true,
|
|
@@ -949,17 +655,15 @@ export class FatHippoContextEngine {
|
|
|
949
655
|
/**
|
|
950
656
|
* Handle subagent completion
|
|
951
657
|
*/
|
|
952
|
-
async onSubagentEnded(
|
|
953
|
-
|
|
954
|
-
// Future: extract learnings from subagent session
|
|
955
|
-
// and store them in parent's memory
|
|
658
|
+
async onSubagentEnded(params) {
|
|
659
|
+
await this.endHostedSession(params.childSessionKey, this.mapHostedOutcomeFromReason(params.reason));
|
|
956
660
|
}
|
|
957
661
|
/**
|
|
958
662
|
* Cleanup resources
|
|
959
663
|
*/
|
|
960
664
|
async dispose() {
|
|
961
|
-
this.
|
|
962
|
-
this.
|
|
665
|
+
await Promise.allSettled([...this.hostedSessions.keys()].map((sessionId) => this.endHostedSession(sessionId, "abandoned")));
|
|
666
|
+
this.hostedSessions.clear();
|
|
963
667
|
this.sessionLocalProfiles.clear();
|
|
964
668
|
this.sessionStartTimes.clear();
|
|
965
669
|
this.sessionHippoNodState.clear();
|
|
@@ -982,6 +686,130 @@ export class FatHippoContextEngine {
|
|
|
982
686
|
}
|
|
983
687
|
return null;
|
|
984
688
|
}
|
|
689
|
+
buildHostedRuntimeMetadata(params) {
|
|
690
|
+
const workspaceRoot = params?.sessionFile
|
|
691
|
+
? this.detectWorkspaceRoot(params.sessionFile)
|
|
692
|
+
: params?.sessionId
|
|
693
|
+
? this.hostedSessions.get(params.sessionId)?.workspaceRoot
|
|
694
|
+
: undefined;
|
|
695
|
+
return {
|
|
696
|
+
runtime: "openclaw",
|
|
697
|
+
runtimeVersion: CONTEXT_ENGINE_VERSION,
|
|
698
|
+
adapterVersion: CONTEXT_ENGINE_VERSION,
|
|
699
|
+
namespace: this.config.namespace,
|
|
700
|
+
installationId: this.config.installationId,
|
|
701
|
+
workspaceId: this.config.workspaceId ?? workspaceRoot,
|
|
702
|
+
workspaceRoot,
|
|
703
|
+
conversationId: this.config.conversationId ?? params?.sessionId,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
toRuntimeMessages(messages) {
|
|
707
|
+
return messages
|
|
708
|
+
.map((message) => {
|
|
709
|
+
const content = getMessageText(message).trim();
|
|
710
|
+
if (!content) {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
const raw = message;
|
|
714
|
+
const rawRole = typeof raw.role === "string"
|
|
715
|
+
? raw.role.toLowerCase()
|
|
716
|
+
: typeof raw.type === "string"
|
|
717
|
+
? raw.type.toLowerCase()
|
|
718
|
+
: "assistant";
|
|
719
|
+
const role = rawRole === "system"
|
|
720
|
+
? "system"
|
|
721
|
+
: rawRole === "user"
|
|
722
|
+
? "user"
|
|
723
|
+
: rawRole === "tool" || rawRole === "toolresult"
|
|
724
|
+
? "tool"
|
|
725
|
+
: "assistant";
|
|
726
|
+
return {
|
|
727
|
+
role,
|
|
728
|
+
content,
|
|
729
|
+
toolName: typeof raw.toolName === "string" ? raw.toolName : undefined,
|
|
730
|
+
};
|
|
731
|
+
})
|
|
732
|
+
.filter((message) => message !== null);
|
|
733
|
+
}
|
|
734
|
+
extractTurnMessages(messages, prePromptMessageCount) {
|
|
735
|
+
const sliced = Number.isFinite(prePromptMessageCount) && prePromptMessageCount >= 0
|
|
736
|
+
? messages.slice(prePromptMessageCount)
|
|
737
|
+
: messages;
|
|
738
|
+
const turnMessages = this.toRuntimeMessages(sliced);
|
|
739
|
+
return turnMessages.length > 0 ? turnMessages : this.toRuntimeMessages(messages);
|
|
740
|
+
}
|
|
741
|
+
async captureLocalTurnMemories(params) {
|
|
742
|
+
const profileId = this.deriveLocalProfileId(params.sessionId, params.sessionFile);
|
|
743
|
+
this.sessionLocalProfiles.set(params.sessionId, profileId);
|
|
744
|
+
const candidates = new Set();
|
|
745
|
+
const durablePattern = /\b(decide|decided|decision|prefer|preference|always|never|must|remember|rule|workflow|process|plan|configured|set to|resolved|fixed|installed|created|updated)\b/i;
|
|
746
|
+
const toolPattern = /\b(namespace|workspace|project|plugin|database|schema|endpoint|config|mode|version)\b/i;
|
|
747
|
+
for (const message of params.messages) {
|
|
748
|
+
if (this.config.captureUserOnly === true && message.role !== "user") {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
const segments = message.content
|
|
752
|
+
.split(/\n{2,}|(?<=[.!?])\s+/)
|
|
753
|
+
.map((segment) => segment.trim())
|
|
754
|
+
.filter(Boolean);
|
|
755
|
+
for (const segment of segments) {
|
|
756
|
+
if (!segment || detectPromptInjection(segment) || !matchesCapturePatterns(segment)) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (message.role === "tool" &&
|
|
760
|
+
!(durablePattern.test(segment) && toolPattern.test(segment))) {
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (message.role === "assistant" && !durablePattern.test(segment)) {
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
candidates.add(sanitizeContent(segment));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (candidates.size === 0) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
for (const candidate of candidates) {
|
|
773
|
+
await this.localStore?.remember({
|
|
774
|
+
profileId,
|
|
775
|
+
content: candidate,
|
|
776
|
+
title: this.buildLocalTitle(candidate),
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
invalidateAllLocalResultsForUser(profileId);
|
|
780
|
+
}
|
|
781
|
+
mapHostedOutcomeFromReason(reason) {
|
|
782
|
+
const normalized = reason.toLowerCase();
|
|
783
|
+
if (/success|completed|done|finished/.test(normalized)) {
|
|
784
|
+
return "success";
|
|
785
|
+
}
|
|
786
|
+
if (/fail|error|crash/.test(normalized)) {
|
|
787
|
+
return "failure";
|
|
788
|
+
}
|
|
789
|
+
return "abandoned";
|
|
790
|
+
}
|
|
791
|
+
async endHostedSession(sessionId, outcome) {
|
|
792
|
+
const runtimeClient = this.runtimeClient;
|
|
793
|
+
const hostedSession = this.hostedSessions.get(sessionId);
|
|
794
|
+
if (!runtimeClient || !hostedSession) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
await runtimeClient.endSession({
|
|
799
|
+
sessionId: hostedSession.hostedSessionId,
|
|
800
|
+
outcome,
|
|
801
|
+
runtime: this.buildHostedRuntimeMetadata({ sessionId }),
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
console.error("[FatHippo] End session error:", error);
|
|
806
|
+
}
|
|
807
|
+
finally {
|
|
808
|
+
this.hostedSessions.delete(sessionId);
|
|
809
|
+
this.sessionStartTimes.delete(sessionId);
|
|
810
|
+
this.sessionHippoNodState.delete(sessionId);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
985
813
|
findLastUserMessage(messages) {
|
|
986
814
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
987
815
|
const msg = messages[i];
|