@fathippo/fathippo-context-engine 0.1.1
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 +76 -0
- package/dist/api/client.d.ts +75 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +176 -0
- package/dist/api/client.js.map +1 -0
- package/dist/cognitive/sanitize.d.ts +3 -0
- package/dist/cognitive/sanitize.d.ts.map +1 -0
- package/dist/cognitive/sanitize.js +17 -0
- package/dist/cognitive/sanitize.js.map +1 -0
- package/dist/cognitive/trace-capture.d.ts +51 -0
- package/dist/cognitive/trace-capture.d.ts.map +1 -0
- package/dist/cognitive/trace-capture.js +659 -0
- package/dist/cognitive/trace-capture.js.map +1 -0
- package/dist/engine.d.ts +147 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1092 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +103 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/filtering.d.ts +24 -0
- package/dist/utils/filtering.d.ts.map +1 -0
- package/dist/utils/filtering.js +95 -0
- package/dist/utils/filtering.js.map +1 -0
- package/dist/utils/formatting.d.ts +21 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/formatting.js +86 -0
- package/dist/utils/formatting.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/openclaw.plugin.json +123 -0
- package/package.json +45 -0
package/dist/engine.js
ADDED
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FatHippo Context Engine
|
|
3
|
+
*
|
|
4
|
+
* Implements OpenClaw's ContextEngine interface for hosted or local agent memory.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { createLocalMemoryStore, invalidateAllLocalResultsForUser, localRetrieve, localStoreResult, } from "@fathippo/local";
|
|
9
|
+
import { FatHippoClient } from "./api/client.js";
|
|
10
|
+
import { buildStructuredTrace, getMessageText, shouldCaptureCodingTrace } from "./cognitive/trace-capture.js";
|
|
11
|
+
import { CONTEXT_ENGINE_ID, CONTEXT_ENGINE_VERSION } from "./version.js";
|
|
12
|
+
import { formatMemoriesForInjection, dedupeMemories, estimateTokens, } from "./utils/formatting.js";
|
|
13
|
+
import { detectPromptInjection, matchesCapturePatterns, sanitizeContent, } from "./utils/filtering.js";
|
|
14
|
+
/**
|
|
15
|
+
* FatHippo Context Engine implementation
|
|
16
|
+
*/
|
|
17
|
+
export class FatHippoContextEngine {
|
|
18
|
+
info = {
|
|
19
|
+
id: CONTEXT_ENGINE_ID,
|
|
20
|
+
name: "FatHippo Context Engine",
|
|
21
|
+
version: CONTEXT_ENGINE_VERSION,
|
|
22
|
+
ownsCompaction: true, // We handle compaction via Dream Cycle
|
|
23
|
+
};
|
|
24
|
+
client;
|
|
25
|
+
config;
|
|
26
|
+
mode;
|
|
27
|
+
localStore;
|
|
28
|
+
cachedCritical = null;
|
|
29
|
+
// Cognitive engine state
|
|
30
|
+
sessionStartTimes = new Map();
|
|
31
|
+
sessionApplicationIds = new Map();
|
|
32
|
+
sessionLocalProfiles = new Map();
|
|
33
|
+
sessionHippoNodState = new Map();
|
|
34
|
+
cognitiveEnabled;
|
|
35
|
+
static TRIVIAL_ACKS = new Set([
|
|
36
|
+
"ok",
|
|
37
|
+
"thanks",
|
|
38
|
+
"yes",
|
|
39
|
+
"no",
|
|
40
|
+
"sure",
|
|
41
|
+
"cool",
|
|
42
|
+
"nice",
|
|
43
|
+
"got it",
|
|
44
|
+
"k",
|
|
45
|
+
"ty",
|
|
46
|
+
"thx",
|
|
47
|
+
]);
|
|
48
|
+
static MIN_VECTOR_SIMILARITY = 0.75;
|
|
49
|
+
static MIN_CRITICAL_RELEVANCE = 0.7;
|
|
50
|
+
static HIPPO_NOD_COOLDOWN_MS = 15 * 60 * 1000;
|
|
51
|
+
static HIPPO_NOD_MIN_MESSAGE_GAP = 6;
|
|
52
|
+
constructor(config) {
|
|
53
|
+
this.config = config;
|
|
54
|
+
this.mode = config.mode === "local" || (!config.apiKey && config.mode !== "hosted") ? "local" : "hosted";
|
|
55
|
+
if (this.mode === "hosted" && !config.apiKey) {
|
|
56
|
+
throw new Error("FatHippo hosted mode requires an API key. Pass apiKey or switch mode to local/auto.");
|
|
57
|
+
}
|
|
58
|
+
this.client = this.mode === "hosted" ? new FatHippoClient(config) : null;
|
|
59
|
+
this.localStore =
|
|
60
|
+
this.mode === "local"
|
|
61
|
+
? createLocalMemoryStore({
|
|
62
|
+
storagePath: config.localStoragePath,
|
|
63
|
+
})
|
|
64
|
+
: null;
|
|
65
|
+
// Enable cognitive features if configured (default: true)
|
|
66
|
+
this.cognitiveEnabled = this.mode === "hosted" && config.cognitiveEnabled !== false;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Initialize engine state for a session
|
|
70
|
+
*/
|
|
71
|
+
async bootstrap(params) {
|
|
72
|
+
try {
|
|
73
|
+
this.sessionStartTimes.set(params.sessionId, Date.now());
|
|
74
|
+
if (this.mode === "local") {
|
|
75
|
+
this.sessionLocalProfiles.set(params.sessionId, this.deriveLocalProfileId(params.sessionId, params.sessionFile));
|
|
76
|
+
return {
|
|
77
|
+
bootstrapped: true,
|
|
78
|
+
importedMessages: 0,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Prefetch critical memories for this session
|
|
82
|
+
const critical = await this.client?.getCriticalMemories({
|
|
83
|
+
limit: 30,
|
|
84
|
+
excludeAbsorbed: true,
|
|
85
|
+
});
|
|
86
|
+
if (!critical) {
|
|
87
|
+
return {
|
|
88
|
+
bootstrapped: true,
|
|
89
|
+
importedMessages: 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
this.cachedCritical = {
|
|
93
|
+
memories: critical.memories,
|
|
94
|
+
syntheses: critical.syntheses,
|
|
95
|
+
fetchedAt: Date.now(),
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
bootstrapped: true,
|
|
99
|
+
importedMessages: critical.memories.length + critical.syntheses.length,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error("[FatHippo] Bootstrap error:", error);
|
|
104
|
+
return {
|
|
105
|
+
bootstrapped: false,
|
|
106
|
+
reason: error instanceof Error ? error.message : "Unknown error",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Ingest a single message into FatHippo
|
|
112
|
+
*/
|
|
113
|
+
async ingest(params) {
|
|
114
|
+
// Skip heartbeat messages
|
|
115
|
+
if (params.isHeartbeat) {
|
|
116
|
+
return { ingested: false };
|
|
117
|
+
}
|
|
118
|
+
// Only capture user messages (configurable)
|
|
119
|
+
if (this.config.captureUserOnly !== false &&
|
|
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
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Ingest a batch of messages
|
|
165
|
+
*/
|
|
166
|
+
async ingestBatch(params) {
|
|
167
|
+
if (params.isHeartbeat) {
|
|
168
|
+
return { ingestedCount: 0 };
|
|
169
|
+
}
|
|
170
|
+
let ingestedCount = 0;
|
|
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 };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Post-turn lifecycle processing
|
|
184
|
+
*/
|
|
185
|
+
async afterTurn(params) {
|
|
186
|
+
if (params.isHeartbeat) {
|
|
187
|
+
if (this.cognitiveEnabled && this.config.cognitiveHeartbeatEnabled !== false) {
|
|
188
|
+
await this.runCognitiveHeartbeat();
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Invalidate critical cache after turns (may have new memories)
|
|
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
|
|
201
|
+
if (this.mode === "local") {
|
|
202
|
+
await this.captureLocalTrace({
|
|
203
|
+
sessionId: params.sessionId,
|
|
204
|
+
sessionFile: params.sessionFile,
|
|
205
|
+
messages: params.messages,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
else if (this.cognitiveEnabled) {
|
|
209
|
+
await this.captureStructuredTrace({
|
|
210
|
+
sessionId: params.sessionId,
|
|
211
|
+
sessionFile: params.sessionFile,
|
|
212
|
+
messages: params.messages,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
detectToolsUsed(messages) {
|
|
217
|
+
const tools = new Set();
|
|
218
|
+
for (const message of messages) {
|
|
219
|
+
// Check for tool result messages
|
|
220
|
+
const msg = message;
|
|
221
|
+
if (msg.role === 'toolResult' || msg.type === 'toolResult') {
|
|
222
|
+
if (typeof msg.toolName === 'string') {
|
|
223
|
+
tools.add(msg.toolName);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return [...tools];
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Assemble context for the model
|
|
231
|
+
*/
|
|
232
|
+
async assemble(params) {
|
|
233
|
+
const lastUserMessage = this.findLastUserMessage(params.messages)?.trim() ?? "";
|
|
234
|
+
if (this.mode === "local") {
|
|
235
|
+
return this.assembleLocalContext(params, lastUserMessage);
|
|
236
|
+
}
|
|
237
|
+
const client = this.client;
|
|
238
|
+
if (!client) {
|
|
239
|
+
return {
|
|
240
|
+
messages: params.messages,
|
|
241
|
+
estimatedTokens: this.estimateMessageTokens(params.messages),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// Always fetch indexed summaries (they're compact)
|
|
245
|
+
let indexedContext = "";
|
|
246
|
+
try {
|
|
247
|
+
const indexed = await client.getIndexedSummaries();
|
|
248
|
+
if (indexed.count > 0) {
|
|
249
|
+
indexedContext = `\n## Indexed Memory (use GET /indexed/:key for full content)\n${indexed.contextFormat}\n`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Indexed memories are optional, don't fail on error
|
|
254
|
+
}
|
|
255
|
+
if (!lastUserMessage || this.isTrivialQuery(lastUserMessage)) {
|
|
256
|
+
// Still include indexed summaries even for trivial queries
|
|
257
|
+
const baseTokens = this.estimateMessageTokens(params.messages);
|
|
258
|
+
return {
|
|
259
|
+
messages: params.messages,
|
|
260
|
+
estimatedTokens: baseTokens + estimateTokens(indexedContext),
|
|
261
|
+
systemPromptAddition: indexedContext || undefined,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
// Fetch relevant memories based on last user message
|
|
265
|
+
let memories = [];
|
|
266
|
+
let syntheses = [];
|
|
267
|
+
let memoryHippoCue = null;
|
|
268
|
+
let hasRelevantCriticalMatch = false;
|
|
269
|
+
try {
|
|
270
|
+
// Search for relevant memories based on query
|
|
271
|
+
const results = await client.search({
|
|
272
|
+
query: lastUserMessage,
|
|
273
|
+
limit: this.config.injectLimit || 20,
|
|
274
|
+
excludeAbsorbed: true,
|
|
275
|
+
});
|
|
276
|
+
const qualifyingResults = results.filter((r) => r.score >= FatHippoContextEngine.MIN_VECTOR_SIMILARITY);
|
|
277
|
+
const searchedMemories = qualifyingResults.map((r) => r.memory);
|
|
278
|
+
memories = dedupeMemories(searchedMemories);
|
|
279
|
+
// Inject critical only for non-trivial queries with critical relevance.
|
|
280
|
+
hasRelevantCriticalMatch = results.some((r) => r.memory.importanceTier === "critical" &&
|
|
281
|
+
r.score > FatHippoContextEngine.MIN_CRITICAL_RELEVANCE);
|
|
282
|
+
if (this.config.injectCritical !== false && hasRelevantCriticalMatch) {
|
|
283
|
+
let criticalMemories;
|
|
284
|
+
let criticalSyntheses;
|
|
285
|
+
if (this.cachedCritical &&
|
|
286
|
+
Date.now() - this.cachedCritical.fetchedAt < 5 * 60 * 1000) {
|
|
287
|
+
criticalMemories = this.cachedCritical.memories;
|
|
288
|
+
criticalSyntheses = this.cachedCritical.syntheses;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const critical = await client.getCriticalMemories({
|
|
292
|
+
limit: 15,
|
|
293
|
+
excludeAbsorbed: true,
|
|
294
|
+
});
|
|
295
|
+
criticalMemories = critical.memories;
|
|
296
|
+
criticalSyntheses = critical.syntheses;
|
|
297
|
+
this.cachedCritical = {
|
|
298
|
+
memories: criticalMemories,
|
|
299
|
+
syntheses: criticalSyntheses,
|
|
300
|
+
fetchedAt: Date.now(),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
memories = dedupeMemories([...criticalMemories, ...memories]);
|
|
304
|
+
syntheses = criticalSyntheses;
|
|
305
|
+
}
|
|
306
|
+
if (hasRelevantCriticalMatch || syntheses.length > 0) {
|
|
307
|
+
memoryHippoCue = {
|
|
308
|
+
kind: "memory",
|
|
309
|
+
reason: "Fathippo recalled relevant memory for this reply.",
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
console.error("[FatHippo] Assemble error:", error);
|
|
315
|
+
}
|
|
316
|
+
const baseMessageTokens = this.estimateMessageTokens(params.messages);
|
|
317
|
+
if (typeof params.tokenBudget === "number" && params.tokenBudget > 0) {
|
|
318
|
+
const contextBudget = Math.max(0, params.tokenBudget - baseMessageTokens);
|
|
319
|
+
const constrained = this.constrainContextToBudget(memories, syntheses, contextBudget);
|
|
320
|
+
memories = constrained.memories;
|
|
321
|
+
syntheses = constrained.syntheses;
|
|
322
|
+
}
|
|
323
|
+
// Format memories for injection, include indexed summaries
|
|
324
|
+
const memoryBlock = formatMemoriesForInjection(memories, syntheses);
|
|
325
|
+
// Fetch constraints (always inject - these are critical rules)
|
|
326
|
+
let constraintsContext = "";
|
|
327
|
+
if (this.cognitiveEnabled) {
|
|
328
|
+
try {
|
|
329
|
+
constraintsContext = await this.fetchConstraints();
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
console.error("[FatHippo] Constraints fetch error:", error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Fetch cognitive context (traces + patterns) for coding sessions
|
|
336
|
+
let cognitiveContext = "";
|
|
337
|
+
let cognitiveHippoCue = null;
|
|
338
|
+
if (this.cognitiveEnabled && this.looksLikeCodingQuery(lastUserMessage)) {
|
|
339
|
+
try {
|
|
340
|
+
const cognitive = await this.fetchCognitiveContext(params.sessionId, lastUserMessage);
|
|
341
|
+
if (cognitive.context) {
|
|
342
|
+
cognitiveContext = cognitive.context;
|
|
343
|
+
cognitiveHippoCue = cognitive.hippoCue;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
console.error("[FatHippo] Cognitive context error:", error);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const hippoNodInstruction = this.buildHippoNodInstruction({
|
|
351
|
+
sessionId: params.sessionId,
|
|
352
|
+
messageCount: params.messages.length,
|
|
353
|
+
lastUserMessage,
|
|
354
|
+
cue: cognitiveHippoCue ?? memoryHippoCue,
|
|
355
|
+
});
|
|
356
|
+
const fullContext = typeof params.tokenBudget === "number" && params.tokenBudget > 0
|
|
357
|
+
? this.fitContextToBudget({
|
|
358
|
+
sections: [
|
|
359
|
+
constraintsContext,
|
|
360
|
+
memoryBlock ? `${memoryBlock}\n` : "",
|
|
361
|
+
indexedContext,
|
|
362
|
+
cognitiveContext,
|
|
363
|
+
hippoNodInstruction,
|
|
364
|
+
],
|
|
365
|
+
contextBudget: Math.max(0, params.tokenBudget - baseMessageTokens),
|
|
366
|
+
})
|
|
367
|
+
: constraintsContext + (memoryBlock ? memoryBlock + "\n" : "") + indexedContext + cognitiveContext + hippoNodInstruction;
|
|
368
|
+
const tokens = estimateTokens(fullContext) + baseMessageTokens;
|
|
369
|
+
return {
|
|
370
|
+
messages: params.messages,
|
|
371
|
+
estimatedTokens: tokens,
|
|
372
|
+
systemPromptAddition: fullContext.trim() || undefined,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async assembleLocalContext(params, lastUserMessage) {
|
|
376
|
+
const profileId = this.getLocalProfileId(params.sessionId);
|
|
377
|
+
const indexed = await this.localStore?.getIndexedSummaries({
|
|
378
|
+
profileId,
|
|
379
|
+
limit: 18,
|
|
380
|
+
});
|
|
381
|
+
const indexedContext = indexed && indexed.count > 0
|
|
382
|
+
? `\n## Indexed Local Memory\n${indexed.contextFormat}\n`
|
|
383
|
+
: "";
|
|
384
|
+
const baseTokens = this.estimateMessageTokens(params.messages);
|
|
385
|
+
if (!lastUserMessage || this.isTrivialQuery(lastUserMessage)) {
|
|
386
|
+
return {
|
|
387
|
+
messages: params.messages,
|
|
388
|
+
estimatedTokens: baseTokens + estimateTokens(indexedContext),
|
|
389
|
+
systemPromptAddition: indexedContext || undefined,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
let memories = [];
|
|
393
|
+
let localCognitiveContext = null;
|
|
394
|
+
try {
|
|
395
|
+
let localMemories = [];
|
|
396
|
+
const cached = await localRetrieve(lastUserMessage, profileId);
|
|
397
|
+
if (cached.hit) {
|
|
398
|
+
localMemories = await this.localStore?.getMemoriesByIds(profileId, cached.memoryIds) ?? [];
|
|
399
|
+
}
|
|
400
|
+
if (localMemories.length === 0) {
|
|
401
|
+
const searchResults = await this.localStore?.search({
|
|
402
|
+
profileId,
|
|
403
|
+
query: lastUserMessage,
|
|
404
|
+
limit: Math.max(3, Math.min(this.config.injectLimit || 12, 12)),
|
|
405
|
+
}) ?? [];
|
|
406
|
+
localMemories = searchResults.map((result) => result.memory);
|
|
407
|
+
if (searchResults.length > 0) {
|
|
408
|
+
const avgScore = searchResults.reduce((sum, result) => sum + result.score, 0) / searchResults.length;
|
|
409
|
+
localStoreResult(profileId, lastUserMessage, searchResults.map((result) => result.memory.id), avgScore);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const critical = this.config.injectCritical !== false
|
|
413
|
+
? await this.localStore?.getCriticalMemories({ profileId, limit: 10 }) ?? []
|
|
414
|
+
: [];
|
|
415
|
+
localCognitiveContext = await this.localStore?.getCognitiveContext({
|
|
416
|
+
profileId,
|
|
417
|
+
problem: lastUserMessage,
|
|
418
|
+
limit: 3,
|
|
419
|
+
}) ?? null;
|
|
420
|
+
memories = dedupeMemories([
|
|
421
|
+
...critical.map((memory) => this.mapLocalMemory(memory, profileId)),
|
|
422
|
+
...localMemories.map((memory) => this.mapLocalMemory(memory, profileId)),
|
|
423
|
+
]).slice(0, this.config.injectLimit || 20);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
console.error("[FatHippo] Local assemble error:", error);
|
|
427
|
+
}
|
|
428
|
+
const memoryBlock = formatMemoriesForInjection(memories, []);
|
|
429
|
+
const workflowBlock = localCognitiveContext?.workflow
|
|
430
|
+
? `## Recommended Workflow\n${localCognitiveContext.workflow.steps.map((step) => `- ${step}`).join("\n")}\n\nStrategy: ${localCognitiveContext.workflow.title} (${localCognitiveContext.workflow.rationale})\n`
|
|
431
|
+
: "";
|
|
432
|
+
const patternBlock = localCognitiveContext && localCognitiveContext.patterns.length > 0
|
|
433
|
+
? `## Local Learned Fixes\n${localCognitiveContext.patterns
|
|
434
|
+
.map((pattern) => `- ${pattern.title}: ${pattern.approach}`)
|
|
435
|
+
.join("\n")}\n`
|
|
436
|
+
: "";
|
|
437
|
+
const hippoNodInstruction = this.buildHippoNodInstruction({
|
|
438
|
+
sessionId: params.sessionId,
|
|
439
|
+
messageCount: params.messages.length,
|
|
440
|
+
lastUserMessage,
|
|
441
|
+
cue: localCognitiveContext?.workflow
|
|
442
|
+
? {
|
|
443
|
+
kind: "workflow",
|
|
444
|
+
reason: "Fathippo reused a locally learned workflow for this reply.",
|
|
445
|
+
}
|
|
446
|
+
: (localCognitiveContext?.patterns.length ?? 0) > 0
|
|
447
|
+
? {
|
|
448
|
+
kind: "learned_fix",
|
|
449
|
+
reason: "Fathippo reused a locally learned fix for this reply.",
|
|
450
|
+
}
|
|
451
|
+
: memories.length > 0
|
|
452
|
+
? {
|
|
453
|
+
kind: "memory",
|
|
454
|
+
reason: "Fathippo recalled local memory for this reply.",
|
|
455
|
+
}
|
|
456
|
+
: null,
|
|
457
|
+
});
|
|
458
|
+
const fullContext = typeof params.tokenBudget === "number" && params.tokenBudget > 0
|
|
459
|
+
? this.fitContextToBudget({
|
|
460
|
+
sections: [
|
|
461
|
+
workflowBlock,
|
|
462
|
+
patternBlock,
|
|
463
|
+
memoryBlock ? `${memoryBlock}\n` : "",
|
|
464
|
+
indexedContext,
|
|
465
|
+
hippoNodInstruction,
|
|
466
|
+
],
|
|
467
|
+
contextBudget: Math.max(0, params.tokenBudget - baseTokens),
|
|
468
|
+
})
|
|
469
|
+
: workflowBlock + patternBlock + (memoryBlock ? memoryBlock + "\n" : "") + indexedContext + hippoNodInstruction;
|
|
470
|
+
return {
|
|
471
|
+
messages: params.messages,
|
|
472
|
+
estimatedTokens: baseTokens + estimateTokens(fullContext),
|
|
473
|
+
systemPromptAddition: fullContext.trim() || undefined,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Check if query looks like a coding task
|
|
478
|
+
*/
|
|
479
|
+
looksLikeCodingQuery(query) {
|
|
480
|
+
const codingKeywords = [
|
|
481
|
+
'bug', 'error', 'fix', 'debug', 'implement', 'build', 'create', 'refactor',
|
|
482
|
+
'function', 'class', 'api', 'endpoint', 'database', 'query', 'test',
|
|
483
|
+
'deploy', 'config', 'install', 'code', 'script', 'compile', 'run'
|
|
484
|
+
];
|
|
485
|
+
const queryLower = query.toLowerCase();
|
|
486
|
+
return codingKeywords.some(kw => queryLower.includes(kw));
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Fetch active constraints (always injected)
|
|
490
|
+
*/
|
|
491
|
+
async fetchConstraints() {
|
|
492
|
+
const baseUrl = this.getApiBaseUrl();
|
|
493
|
+
const response = await fetch(`${baseUrl}/v1/cognitive/constraints`, {
|
|
494
|
+
method: 'GET',
|
|
495
|
+
headers: this.getHostedHeaders(false),
|
|
496
|
+
});
|
|
497
|
+
if (!response.ok)
|
|
498
|
+
return '';
|
|
499
|
+
const data = await response.json();
|
|
500
|
+
return data.contextFormat || '';
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Auto-detect and store constraints from user message
|
|
504
|
+
*/
|
|
505
|
+
async maybeStoreConstraint(message) {
|
|
506
|
+
const baseUrl = this.getApiBaseUrl();
|
|
507
|
+
try {
|
|
508
|
+
await fetch(`${baseUrl}/v1/cognitive/constraints`, {
|
|
509
|
+
method: 'POST',
|
|
510
|
+
headers: this.getHostedHeaders(),
|
|
511
|
+
body: JSON.stringify({ message }),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// Constraint detection is best-effort
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Fetch relevant traces and patterns from cognitive API
|
|
520
|
+
*/
|
|
521
|
+
async fetchCognitiveContext(sessionId, problem) {
|
|
522
|
+
const baseUrl = this.getApiBaseUrl();
|
|
523
|
+
const response = await fetch(`${baseUrl}/v1/cognitive/traces/relevant`, {
|
|
524
|
+
method: 'POST',
|
|
525
|
+
headers: this.getHostedHeaders(),
|
|
526
|
+
body: JSON.stringify({
|
|
527
|
+
sessionId,
|
|
528
|
+
endpoint: "context-engine.assemble",
|
|
529
|
+
problem,
|
|
530
|
+
limit: 3,
|
|
531
|
+
adaptivePolicy: this.config.adaptivePolicyEnabled !== false,
|
|
532
|
+
}),
|
|
533
|
+
});
|
|
534
|
+
if (!response.ok) {
|
|
535
|
+
return { context: null, hippoCue: null };
|
|
536
|
+
}
|
|
537
|
+
const data = await response.json();
|
|
538
|
+
if (data.applicationId) {
|
|
539
|
+
this.sessionApplicationIds.set(sessionId, data.applicationId);
|
|
540
|
+
}
|
|
541
|
+
const sections = new Map();
|
|
542
|
+
if (data.workflow && data.workflow.steps.length > 0) {
|
|
543
|
+
const stepLines = data.workflow.steps.map((step) => `- ${step}`).join("\n");
|
|
544
|
+
const explorationNote = data.workflow.exploration ? " exploratory" : "";
|
|
545
|
+
sections.set("workflow", `## Recommended Workflow\n${stepLines}\n\nStrategy: ${data.workflow.title} (${data.workflow.rationale}${explorationNote})`);
|
|
546
|
+
}
|
|
547
|
+
const localPatterns = (data.patterns ?? []).filter((pattern) => pattern.scope !== "global");
|
|
548
|
+
const globalPatterns = (data.patterns ?? []).filter((pattern) => pattern.scope === "global");
|
|
549
|
+
if (localPatterns.length > 0) {
|
|
550
|
+
const patternLines = localPatterns
|
|
551
|
+
.map((pattern) => {
|
|
552
|
+
const score = typeof pattern.score === "number" ? `, score ${pattern.score.toFixed(1)}` : "";
|
|
553
|
+
return `- [${pattern.domain}] ${pattern.approach.slice(0, 200)} (${Math.round(pattern.confidence * 100)}% confidence${score})`;
|
|
554
|
+
})
|
|
555
|
+
.join("\n");
|
|
556
|
+
sections.set("local_patterns", `## Learned Coding Patterns\n${patternLines}`);
|
|
557
|
+
}
|
|
558
|
+
if (globalPatterns.length > 0) {
|
|
559
|
+
const patternLines = globalPatterns
|
|
560
|
+
.map((pattern) => {
|
|
561
|
+
const score = typeof pattern.score === "number" ? `, score ${pattern.score.toFixed(1)}` : "";
|
|
562
|
+
return `- [${pattern.domain}] ${pattern.approach.slice(0, 200)} (${Math.round(pattern.confidence * 100)}% confidence${score})`;
|
|
563
|
+
})
|
|
564
|
+
.join("\n");
|
|
565
|
+
sections.set("global_patterns", `## Shared Global Patterns\n${patternLines}`);
|
|
566
|
+
}
|
|
567
|
+
if (data.traces && data.traces.length > 0) {
|
|
568
|
+
const traceLines = data.traces.map(t => {
|
|
569
|
+
const icon = t.outcome === 'success' ? '✓' : t.outcome === 'failed' ? '✗' : '~';
|
|
570
|
+
const solution = t.solution ? ` → ${t.solution.slice(0, 80)}...` : '';
|
|
571
|
+
return `- ${icon} ${t.problem.slice(0, 80)}${solution}`;
|
|
572
|
+
}).join('\n');
|
|
573
|
+
sections.set("traces", `## Past Similar Problems\n${traceLines}`);
|
|
574
|
+
}
|
|
575
|
+
if (data.skills && data.skills.length > 0) {
|
|
576
|
+
const skillLines = data.skills
|
|
577
|
+
.map((skill) => `- [${skill.scope}] ${skill.name}: ${skill.description} (${Math.round(skill.successRate * 100)}% success)`)
|
|
578
|
+
.join("\n");
|
|
579
|
+
sections.set("skills", `## Synthesized Skills\n${skillLines}`);
|
|
580
|
+
}
|
|
581
|
+
let hippoCue = null;
|
|
582
|
+
if (data.workflow && data.workflow.steps.length > 0) {
|
|
583
|
+
hippoCue = {
|
|
584
|
+
kind: "workflow",
|
|
585
|
+
reason: "Fathippo surfaced a learned workflow for this task.",
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
else if ((data.skills?.length ?? 0) > 0) {
|
|
589
|
+
hippoCue = {
|
|
590
|
+
kind: "learned_fix",
|
|
591
|
+
reason: "Fathippo surfaced a synthesized skill for this task.",
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
else if ((localPatterns.length + globalPatterns.length + (data.traces?.length ?? 0)) >= 2) {
|
|
595
|
+
hippoCue = {
|
|
596
|
+
kind: "learned_fix",
|
|
597
|
+
reason: "Fathippo surfaced learned fixes and similar past problems for this task.",
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
if (sections.size === 0) {
|
|
601
|
+
return { context: null, hippoCue: null };
|
|
602
|
+
}
|
|
603
|
+
const orderedSections = [
|
|
604
|
+
sections.get("workflow"),
|
|
605
|
+
...(data.policy?.sectionOrder ?? ["local_patterns", "global_patterns", "traces", "skills"]).map((key) => sections.get(key)),
|
|
606
|
+
]
|
|
607
|
+
.filter((section) => typeof section === "string" && section.length > 0);
|
|
608
|
+
if (orderedSections.length === 0) {
|
|
609
|
+
return { context: null, hippoCue: null };
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
context: '\n' + orderedSections.join('\n\n') + '\n',
|
|
613
|
+
hippoCue,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
async captureStructuredTrace(params) {
|
|
617
|
+
if (!shouldCaptureCodingTrace(params.messages)) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (!this.sessionStartTimes.has(params.sessionId)) {
|
|
621
|
+
this.sessionStartTimes.set(params.sessionId, Date.now() - 60_000);
|
|
622
|
+
}
|
|
623
|
+
const payload = buildStructuredTrace({
|
|
624
|
+
sessionId: params.sessionId,
|
|
625
|
+
messages: params.messages,
|
|
626
|
+
toolsUsed: this.detectToolsUsed(params.messages),
|
|
627
|
+
filesModified: this.detectFilesModified(params.sessionFile, params.messages),
|
|
628
|
+
workspaceRoot: this.detectWorkspaceRoot(params.sessionFile),
|
|
629
|
+
startTime: this.sessionStartTimes.get(params.sessionId) ?? Date.now() - 60_000,
|
|
630
|
+
endTime: Math.min(Date.now(), (this.sessionStartTimes.get(params.sessionId) ?? Date.now()) + 30 * 60 * 1000),
|
|
631
|
+
});
|
|
632
|
+
if (!payload) {
|
|
633
|
+
this.sessionStartTimes.set(params.sessionId, Date.now());
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
const baseUrl = this.getApiBaseUrl();
|
|
638
|
+
const response = await fetch(`${baseUrl}/v1/cognitive/traces`, {
|
|
639
|
+
method: "POST",
|
|
640
|
+
headers: this.getHostedHeaders(),
|
|
641
|
+
body: JSON.stringify({
|
|
642
|
+
...payload,
|
|
643
|
+
applicationId: this.sessionApplicationIds.get(params.sessionId) ?? null,
|
|
644
|
+
shareEligible: this.config.shareEligibleByDefault !== false && payload.shareEligible,
|
|
645
|
+
}),
|
|
646
|
+
});
|
|
647
|
+
if (!response.ok) {
|
|
648
|
+
throw new Error(`Trace capture failed with status ${response.status}`);
|
|
649
|
+
}
|
|
650
|
+
this.sessionStartTimes.delete(params.sessionId);
|
|
651
|
+
this.sessionApplicationIds.delete(params.sessionId);
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
console.error("[FatHippo] Trace capture error:", error);
|
|
655
|
+
this.sessionStartTimes.set(params.sessionId, Date.now());
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async runCognitiveHeartbeat() {
|
|
659
|
+
const baseUrl = this.getApiBaseUrl();
|
|
660
|
+
const headers = this.getHostedHeaders();
|
|
661
|
+
try {
|
|
662
|
+
const response = await fetch(`${baseUrl}/v1/cognitive/patterns/extract`, {
|
|
663
|
+
method: "POST",
|
|
664
|
+
headers,
|
|
665
|
+
body: JSON.stringify({}),
|
|
666
|
+
});
|
|
667
|
+
if (!response.ok) {
|
|
668
|
+
throw new Error(`Pattern extraction failed with status ${response.status}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
console.error("[FatHippo] Pattern extraction heartbeat error:", error);
|
|
673
|
+
}
|
|
674
|
+
try {
|
|
675
|
+
const response = await fetch(`${baseUrl}/v1/cognitive/skills/synthesize`, {
|
|
676
|
+
method: "POST",
|
|
677
|
+
headers,
|
|
678
|
+
body: JSON.stringify({}),
|
|
679
|
+
});
|
|
680
|
+
if (!response.ok) {
|
|
681
|
+
throw new Error(`Skill synthesis failed with status ${response.status}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
console.error("[FatHippo] Skill synthesis heartbeat error:", error);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
detectFilesModified(sessionFile, messages) {
|
|
689
|
+
const files = new Set();
|
|
690
|
+
if (sessionFile) {
|
|
691
|
+
files.add(sessionFile);
|
|
692
|
+
}
|
|
693
|
+
const filePattern = /(?:^|[\s("'`])((?:[\w.-]+\/)*[\w.-]+\.(?:ts|tsx|js|jsx|json|md|sql|py|go|rs|java|rb|sh|yaml|yml))(?:$|[\s)"'`:,])/g;
|
|
694
|
+
for (const message of messages) {
|
|
695
|
+
const text = getMessageText(message);
|
|
696
|
+
for (const match of text.matchAll(filePattern)) {
|
|
697
|
+
if (match[1]) {
|
|
698
|
+
files.add(match[1]);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return [...files].slice(0, 25);
|
|
703
|
+
}
|
|
704
|
+
detectWorkspaceRoot(sessionFile) {
|
|
705
|
+
if (!sessionFile) {
|
|
706
|
+
return undefined;
|
|
707
|
+
}
|
|
708
|
+
const resolved = path.resolve(sessionFile);
|
|
709
|
+
let candidate = path.extname(resolved) ? path.dirname(resolved) : resolved;
|
|
710
|
+
const markers = [
|
|
711
|
+
".git",
|
|
712
|
+
"package.json",
|
|
713
|
+
"pnpm-workspace.yaml",
|
|
714
|
+
"yarn.lock",
|
|
715
|
+
"package-lock.json",
|
|
716
|
+
"bun.lockb",
|
|
717
|
+
"turbo.json",
|
|
718
|
+
"nx.json",
|
|
719
|
+
"deno.json",
|
|
720
|
+
];
|
|
721
|
+
while (candidate && candidate !== path.dirname(candidate)) {
|
|
722
|
+
if (markers.some((marker) => existsSync(path.join(candidate, marker)))) {
|
|
723
|
+
return candidate;
|
|
724
|
+
}
|
|
725
|
+
candidate = path.dirname(candidate);
|
|
726
|
+
}
|
|
727
|
+
return path.extname(resolved) ? path.dirname(resolved) : resolved;
|
|
728
|
+
}
|
|
729
|
+
deriveLocalProfileId(sessionId, sessionFile) {
|
|
730
|
+
if (this.config.localProfileId) {
|
|
731
|
+
return this.config.localProfileId;
|
|
732
|
+
}
|
|
733
|
+
if (this.config.conversationId) {
|
|
734
|
+
return this.config.conversationId;
|
|
735
|
+
}
|
|
736
|
+
const workspaceRoot = sessionFile ? this.detectWorkspaceRoot(sessionFile) : undefined;
|
|
737
|
+
return workspaceRoot || this.sessionLocalProfiles.get(sessionId) || "openclaw-local-default";
|
|
738
|
+
}
|
|
739
|
+
getLocalProfileId(sessionId) {
|
|
740
|
+
return this.sessionLocalProfiles.get(sessionId) || this.deriveLocalProfileId(sessionId);
|
|
741
|
+
}
|
|
742
|
+
buildLocalTitle(content) {
|
|
743
|
+
const normalized = content.trim().replace(/\s+/g, " ");
|
|
744
|
+
return normalized.length > 72 ? `${normalized.slice(0, 69)}...` : normalized;
|
|
745
|
+
}
|
|
746
|
+
mapLocalMemory(memory, profileId) {
|
|
747
|
+
return {
|
|
748
|
+
id: memory.id,
|
|
749
|
+
title: memory.title,
|
|
750
|
+
content: memory.content,
|
|
751
|
+
userId: profileId,
|
|
752
|
+
createdAt: memory.createdAt,
|
|
753
|
+
updatedAt: memory.updatedAt,
|
|
754
|
+
accessCount: memory.accessCount,
|
|
755
|
+
importanceTier: memory.importanceTier,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
toLocalToolSignals(payload) {
|
|
759
|
+
return [...payload.toolCalls, ...payload.toolResults]
|
|
760
|
+
.filter((signal) => Boolean(signal) && typeof signal === "object" && !Array.isArray(signal))
|
|
761
|
+
.map((signal) => ({
|
|
762
|
+
category: typeof signal.category === "string" ? signal.category : undefined,
|
|
763
|
+
command: typeof signal.command === "string" ? signal.command : undefined,
|
|
764
|
+
success: typeof signal.success === "boolean" ? signal.success : undefined,
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
767
|
+
async captureLocalTrace(params) {
|
|
768
|
+
if (!shouldCaptureCodingTrace(params.messages)) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (!this.sessionStartTimes.has(params.sessionId)) {
|
|
772
|
+
this.sessionStartTimes.set(params.sessionId, Date.now() - 60_000);
|
|
773
|
+
}
|
|
774
|
+
const payload = buildStructuredTrace({
|
|
775
|
+
sessionId: params.sessionId,
|
|
776
|
+
messages: params.messages,
|
|
777
|
+
toolsUsed: this.detectToolsUsed(params.messages),
|
|
778
|
+
filesModified: this.detectFilesModified(params.sessionFile, params.messages),
|
|
779
|
+
workspaceRoot: this.detectWorkspaceRoot(params.sessionFile),
|
|
780
|
+
startTime: this.sessionStartTimes.get(params.sessionId) ?? Date.now() - 60_000,
|
|
781
|
+
endTime: Math.min(Date.now(), (this.sessionStartTimes.get(params.sessionId) ?? Date.now()) + 30 * 60 * 1000),
|
|
782
|
+
});
|
|
783
|
+
if (!payload) {
|
|
784
|
+
this.sessionStartTimes.set(params.sessionId, Date.now());
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const profileId = this.deriveLocalProfileId(params.sessionId, params.sessionFile);
|
|
788
|
+
this.sessionLocalProfiles.set(params.sessionId, profileId);
|
|
789
|
+
try {
|
|
790
|
+
await this.localStore?.learnTrace({
|
|
791
|
+
profileId,
|
|
792
|
+
type: payload.type,
|
|
793
|
+
problem: payload.problem,
|
|
794
|
+
reasoning: payload.reasoning,
|
|
795
|
+
solution: payload.solution,
|
|
796
|
+
outcome: payload.outcome,
|
|
797
|
+
technologies: payload.context.technologies,
|
|
798
|
+
errorMessages: payload.context.errorMessages,
|
|
799
|
+
verificationCommands: payload.verificationCommands,
|
|
800
|
+
filesModified: payload.filesModified,
|
|
801
|
+
durationMs: payload.durationMs,
|
|
802
|
+
toolSignals: this.toLocalToolSignals(payload),
|
|
803
|
+
});
|
|
804
|
+
this.sessionStartTimes.delete(params.sessionId);
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
console.error("[FatHippo] Local trace capture error:", error);
|
|
808
|
+
this.sessionStartTimes.set(params.sessionId, Date.now());
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
getApiBaseUrl() {
|
|
812
|
+
const baseUrl = this.config.baseUrl || "https://fathippo.ai/api";
|
|
813
|
+
return baseUrl.replace(/\/v1$/, "");
|
|
814
|
+
}
|
|
815
|
+
getHostedHeaders(includeContentType = true) {
|
|
816
|
+
const headers = {
|
|
817
|
+
"X-Fathippo-Plugin-Id": this.config.pluginId ?? CONTEXT_ENGINE_ID,
|
|
818
|
+
"X-Fathippo-Plugin-Version": this.config.pluginVersion ?? CONTEXT_ENGINE_VERSION,
|
|
819
|
+
"X-Fathippo-Plugin-Mode": this.mode,
|
|
820
|
+
};
|
|
821
|
+
if (this.config.apiKey) {
|
|
822
|
+
headers.Authorization = `Bearer ${this.config.apiKey}`;
|
|
823
|
+
}
|
|
824
|
+
if (includeContentType) {
|
|
825
|
+
headers["Content-Type"] = "application/json";
|
|
826
|
+
}
|
|
827
|
+
return headers;
|
|
828
|
+
}
|
|
829
|
+
buildHippoNodInstruction(params) {
|
|
830
|
+
if (this.config.hippoNodsEnabled === false || !params.cue) {
|
|
831
|
+
return "";
|
|
832
|
+
}
|
|
833
|
+
if (this.isHighUrgencyOrFormalMoment(params.lastUserMessage)) {
|
|
834
|
+
return "";
|
|
835
|
+
}
|
|
836
|
+
const prior = this.sessionHippoNodState.get(params.sessionId);
|
|
837
|
+
if (prior) {
|
|
838
|
+
const withinCooldown = Date.now() - prior.lastOfferedAt < FatHippoContextEngine.HIPPO_NOD_COOLDOWN_MS;
|
|
839
|
+
const withinMessageGap = params.messageCount - prior.lastMessageCount < FatHippoContextEngine.HIPPO_NOD_MIN_MESSAGE_GAP;
|
|
840
|
+
if (withinCooldown || withinMessageGap) {
|
|
841
|
+
return "";
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
this.sessionHippoNodState.set(params.sessionId, {
|
|
845
|
+
lastOfferedAt: Date.now(),
|
|
846
|
+
lastMessageCount: params.messageCount,
|
|
847
|
+
});
|
|
848
|
+
return [
|
|
849
|
+
"## Optional Fathippo Cue",
|
|
850
|
+
params.cue.reason,
|
|
851
|
+
'If it fits naturally, you may include one very brief acknowledgement such as "🦛 Noted." or end one short sentence with "🦛".',
|
|
852
|
+
"Rules:",
|
|
853
|
+
"- This is optional, not required.",
|
|
854
|
+
"- Use it at most once in this reply.",
|
|
855
|
+
"- Only use it if the tone is friendly, calm, or neutral.",
|
|
856
|
+
"- Skip it for urgent, frustrated, highly formal, or sensitive situations.",
|
|
857
|
+
"- Do not mention internal scoring, retrieval policies, or training.",
|
|
858
|
+
"- Keep the rest of the reply normal, direct, and useful.",
|
|
859
|
+
"",
|
|
860
|
+
].join("\n");
|
|
861
|
+
}
|
|
862
|
+
isHighUrgencyOrFormalMoment(message) {
|
|
863
|
+
const normalized = message.toLowerCase();
|
|
864
|
+
return [
|
|
865
|
+
"urgent",
|
|
866
|
+
"asap",
|
|
867
|
+
"immediately",
|
|
868
|
+
"prod down",
|
|
869
|
+
"production down",
|
|
870
|
+
"sev1",
|
|
871
|
+
"sev 1",
|
|
872
|
+
"security incident",
|
|
873
|
+
"breach",
|
|
874
|
+
"privacy request",
|
|
875
|
+
"gdpr",
|
|
876
|
+
"legal",
|
|
877
|
+
"compliance",
|
|
878
|
+
].some((token) => normalized.includes(token));
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Handle compaction via Dream Cycle
|
|
882
|
+
*/
|
|
883
|
+
async compact(params) {
|
|
884
|
+
void params;
|
|
885
|
+
if (this.mode === "local") {
|
|
886
|
+
return { ok: true, compacted: false, reason: "local mode has no hosted Dream Cycle" };
|
|
887
|
+
}
|
|
888
|
+
if (this.config.dreamCycleOnCompact === false) {
|
|
889
|
+
// Fall back to default compaction
|
|
890
|
+
return { ok: true, compacted: false, reason: "dreamCycleOnCompact disabled" };
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
const result = await this.client?.runDreamCycle({
|
|
894
|
+
processCompleted: true,
|
|
895
|
+
processEphemeral: true,
|
|
896
|
+
synthesizeCritical: true,
|
|
897
|
+
applyDecay: true,
|
|
898
|
+
updateGraph: true,
|
|
899
|
+
});
|
|
900
|
+
if (!result) {
|
|
901
|
+
return { ok: false, compacted: false, reason: "hosted client unavailable" };
|
|
902
|
+
}
|
|
903
|
+
// Invalidate cache after dream cycle
|
|
904
|
+
this.cachedCritical = null;
|
|
905
|
+
return {
|
|
906
|
+
ok: result.ok,
|
|
907
|
+
compacted: true,
|
|
908
|
+
reason: `Dream Cycle: ${result.synthesized || 0} synthesized, ${result.decayed || 0} decayed`,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
catch (error) {
|
|
912
|
+
console.error("[FatHippo] Dream Cycle error:", error);
|
|
913
|
+
return {
|
|
914
|
+
ok: false,
|
|
915
|
+
compacted: false,
|
|
916
|
+
reason: error instanceof Error ? error.message : "Unknown error",
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Prepare context for subagent spawn
|
|
922
|
+
*/
|
|
923
|
+
async prepareSubagentSpawn(_params) {
|
|
924
|
+
void _params;
|
|
925
|
+
// For now, subagents inherit parent's memory scope
|
|
926
|
+
// Future: could create isolated memory scope for subagent
|
|
927
|
+
return {
|
|
928
|
+
rollback: async () => {
|
|
929
|
+
// Nothing to rollback currently
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Handle subagent completion
|
|
935
|
+
*/
|
|
936
|
+
async onSubagentEnded(_params) {
|
|
937
|
+
void _params;
|
|
938
|
+
// Future: extract learnings from subagent session
|
|
939
|
+
// and store them in parent's memory
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Cleanup resources
|
|
943
|
+
*/
|
|
944
|
+
async dispose() {
|
|
945
|
+
this.cachedCritical = null;
|
|
946
|
+
this.sessionApplicationIds.clear();
|
|
947
|
+
this.sessionLocalProfiles.clear();
|
|
948
|
+
this.sessionStartTimes.clear();
|
|
949
|
+
this.sessionHippoNodState.clear();
|
|
950
|
+
}
|
|
951
|
+
// --- Helper methods ---
|
|
952
|
+
extractContent(message) {
|
|
953
|
+
const msg = message;
|
|
954
|
+
if (typeof msg.content === "string") {
|
|
955
|
+
return msg.content;
|
|
956
|
+
}
|
|
957
|
+
if (typeof msg.text === "string") {
|
|
958
|
+
return msg.text;
|
|
959
|
+
}
|
|
960
|
+
if (Array.isArray(msg.content)) {
|
|
961
|
+
// Handle multi-part content (text blocks)
|
|
962
|
+
const textParts = msg.content
|
|
963
|
+
.filter((p) => typeof p === "object" && p !== null && "type" in p && p.type === "text")
|
|
964
|
+
.map((p) => p.text);
|
|
965
|
+
return textParts.join("\n") || null;
|
|
966
|
+
}
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
findLastUserMessage(messages) {
|
|
970
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
971
|
+
const msg = messages[i];
|
|
972
|
+
if (this.isRoleMessage(msg) && msg.role === "user") {
|
|
973
|
+
return this.extractContent(msg);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
isTrivialQuery(message) {
|
|
979
|
+
const trimmed = message.trim();
|
|
980
|
+
if (!trimmed) {
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
if (trimmed.length < 3) {
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
const normalized = trimmed.toLowerCase().replace(/\s+/g, " ");
|
|
987
|
+
if (FatHippoContextEngine.TRIVIAL_ACKS.has(normalized)) {
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
if (/^[\p{P}\s]+$/u.test(trimmed)) {
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
if (/^(?:\p{Extended_Pictographic}|\p{Emoji_Component}|\u200D|\uFE0F|\s)+$/u.test(trimmed)) {
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
isRoleMessage(message) {
|
|
999
|
+
return (typeof message === "object" &&
|
|
1000
|
+
message !== null &&
|
|
1001
|
+
"role" in message &&
|
|
1002
|
+
typeof message.role === "string");
|
|
1003
|
+
}
|
|
1004
|
+
estimateMessageTokens(messages) {
|
|
1005
|
+
const plainText = messages
|
|
1006
|
+
.map((message) => this.extractContent(message))
|
|
1007
|
+
.filter((content) => Boolean(content))
|
|
1008
|
+
.join("\n");
|
|
1009
|
+
return estimateTokens(plainText);
|
|
1010
|
+
}
|
|
1011
|
+
constrainContextToBudget(memories, syntheses, contextBudget) {
|
|
1012
|
+
if (contextBudget <= 0) {
|
|
1013
|
+
return { memories: [], syntheses: [] };
|
|
1014
|
+
}
|
|
1015
|
+
let remaining = contextBudget;
|
|
1016
|
+
const selectedSyntheses = [];
|
|
1017
|
+
const selectedMemories = [];
|
|
1018
|
+
const pushIfFits = (tokens) => {
|
|
1019
|
+
if (tokens > remaining) {
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
remaining -= tokens;
|
|
1023
|
+
return true;
|
|
1024
|
+
};
|
|
1025
|
+
for (const synthesis of syntheses) {
|
|
1026
|
+
const tokens = estimateTokens(`${synthesis.title}\n${synthesis.content}`);
|
|
1027
|
+
if (!pushIfFits(tokens)) {
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
selectedSyntheses.push(synthesis);
|
|
1031
|
+
}
|
|
1032
|
+
const critical = memories.filter((memory) => memory.importanceTier === "critical");
|
|
1033
|
+
const high = memories.filter((memory) => memory.importanceTier === "high");
|
|
1034
|
+
const normal = memories.filter((memory) => memory.importanceTier === "normal" || !memory.importanceTier);
|
|
1035
|
+
for (const group of [critical, high, normal]) {
|
|
1036
|
+
for (const memory of group) {
|
|
1037
|
+
const tokens = estimateTokens(`${memory.title}\n${memory.content}`);
|
|
1038
|
+
if (!pushIfFits(tokens)) {
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
selectedMemories.push(memory);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return {
|
|
1045
|
+
memories: selectedMemories,
|
|
1046
|
+
syntheses: selectedSyntheses,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
fitContextToBudget(params) {
|
|
1050
|
+
if (params.contextBudget <= 0) {
|
|
1051
|
+
return "";
|
|
1052
|
+
}
|
|
1053
|
+
let remaining = params.contextBudget;
|
|
1054
|
+
const selected = [];
|
|
1055
|
+
for (const section of params.sections.map((value) => value.trim()).filter(Boolean)) {
|
|
1056
|
+
const sectionTokens = estimateTokens(section);
|
|
1057
|
+
if (sectionTokens <= remaining) {
|
|
1058
|
+
selected.push(section);
|
|
1059
|
+
remaining -= sectionTokens;
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
if (remaining <= 16) {
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
const truncated = this.truncateContextSection(section, remaining);
|
|
1066
|
+
if (truncated) {
|
|
1067
|
+
selected.push(truncated);
|
|
1068
|
+
}
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
return selected.join("\n\n");
|
|
1072
|
+
}
|
|
1073
|
+
truncateContextSection(section, tokenBudget) {
|
|
1074
|
+
let low = 0;
|
|
1075
|
+
let high = section.length;
|
|
1076
|
+
let best = "";
|
|
1077
|
+
while (low <= high) {
|
|
1078
|
+
const mid = Math.floor((low + high) / 2);
|
|
1079
|
+
const candidate = `${section.slice(0, mid).trimEnd()}\n...`;
|
|
1080
|
+
const tokens = estimateTokens(candidate);
|
|
1081
|
+
if (tokens <= tokenBudget) {
|
|
1082
|
+
best = candidate;
|
|
1083
|
+
low = mid + 1;
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
high = mid - 1;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return best;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
//# sourceMappingURL=engine.js.map
|