@fathippo/fathippo-context-engine 0.1.1 → 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/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 "./api/client.js";
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
- cachedCritical = null;
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
- // Prefetch critical memories for this session
82
- const critical = await this.client?.getCriticalMemories({
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.cachedCritical = {
93
- memories: critical.memories,
94
- syntheses: critical.syntheses,
95
- fetchedAt: Date.now(),
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: critical.memories.length + critical.syntheses.length,
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
- // Skip heartbeat messages
133
+ void params;
115
134
  if (params.isHeartbeat) {
116
135
  return { ingested: false };
117
136
  }
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
- }
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
- 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 };
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
- // 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
160
+ const turnMessages = this.extractTurnMessages(params.messages, params.prePromptMessageCount);
201
161
  if (this.mode === "local") {
202
- await this.captureLocalTrace({
162
+ await this.captureLocalTurnMemories({
203
163
  sessionId: params.sessionId,
204
164
  sessionFile: params.sessionFile,
205
- messages: params.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) {
@@ -231,80 +212,58 @@ export class FatHippoContextEngine {
231
212
  */
232
213
  async assemble(params) {
233
214
  const lastUserMessage = this.findLastUserMessage(params.messages)?.trim() ?? "";
215
+ const runtimeAwareness = this.buildRuntimeAwarenessInstruction();
234
216
  if (this.mode === "local") {
235
- return this.assembleLocalContext(params, lastUserMessage);
217
+ return this.assembleLocalContext(params, lastUserMessage, runtimeAwareness);
236
218
  }
237
- const client = this.client;
238
- if (!client) {
219
+ const runtimeClient = this.runtimeClient;
220
+ const hostedSession = this.hostedSessions.get(params.sessionId);
221
+ if (!runtimeClient || !hostedSession) {
239
222
  return {
240
223
  messages: params.messages,
241
224
  estimatedTokens: this.estimateMessageTokens(params.messages),
242
225
  };
243
226
  }
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
227
  if (!lastUserMessage || this.isTrivialQuery(lastUserMessage)) {
256
- // Still include indexed summaries even for trivial queries
257
228
  const baseTokens = this.estimateMessageTokens(params.messages);
258
229
  return {
259
230
  messages: params.messages,
260
- estimatedTokens: baseTokens + estimateTokens(indexedContext),
261
- systemPromptAddition: indexedContext || undefined,
231
+ estimatedTokens: baseTokens + estimateTokens(runtimeAwareness),
232
+ systemPromptAddition: runtimeAwareness.trim() || undefined,
262
233
  };
263
234
  }
264
- // Fetch relevant memories based on last user message
265
- let memories = [];
266
- let syntheses = [];
267
- let memoryHippoCue = null;
268
- let hasRelevantCriticalMatch = false;
235
+ let hostedContext = "";
236
+ let cue = null;
269
237
  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,
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
+ }),
275
249
  });
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;
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
+ };
305
256
  }
306
- if (hasRelevantCriticalMatch || syntheses.length > 0) {
307
- memoryHippoCue = {
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 = {
308
267
  kind: "memory",
309
268
  reason: "Fathippo recalled relevant memory for this reply.",
310
269
  };
@@ -314,57 +273,22 @@ export class FatHippoContextEngine {
314
273
  console.error("[FatHippo] Assemble error:", error);
315
274
  }
316
275
  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
276
  const hippoNodInstruction = this.buildHippoNodInstruction({
351
277
  sessionId: params.sessionId,
352
278
  messageCount: params.messages.length,
353
279
  lastUserMessage,
354
- cue: cognitiveHippoCue ?? memoryHippoCue,
280
+ cue,
355
281
  });
356
282
  const fullContext = typeof params.tokenBudget === "number" && params.tokenBudget > 0
357
283
  ? this.fitContextToBudget({
358
284
  sections: [
359
- constraintsContext,
360
- memoryBlock ? `${memoryBlock}\n` : "",
361
- indexedContext,
362
- cognitiveContext,
285
+ runtimeAwareness,
286
+ hostedContext,
363
287
  hippoNodInstruction,
364
288
  ],
365
289
  contextBudget: Math.max(0, params.tokenBudget - baseMessageTokens),
366
290
  })
367
- : constraintsContext + (memoryBlock ? memoryBlock + "\n" : "") + indexedContext + cognitiveContext + hippoNodInstruction;
291
+ : runtimeAwareness + hostedContext + hippoNodInstruction;
368
292
  const tokens = estimateTokens(fullContext) + baseMessageTokens;
369
293
  return {
370
294
  messages: params.messages,
@@ -372,7 +296,7 @@ export class FatHippoContextEngine {
372
296
  systemPromptAddition: fullContext.trim() || undefined,
373
297
  };
374
298
  }
375
- async assembleLocalContext(params, lastUserMessage) {
299
+ async assembleLocalContext(params, lastUserMessage, runtimeAwareness) {
376
300
  const profileId = this.getLocalProfileId(params.sessionId);
377
301
  const indexed = await this.localStore?.getIndexedSummaries({
378
302
  profileId,
@@ -383,10 +307,11 @@ export class FatHippoContextEngine {
383
307
  : "";
384
308
  const baseTokens = this.estimateMessageTokens(params.messages);
385
309
  if (!lastUserMessage || this.isTrivialQuery(lastUserMessage)) {
310
+ const systemPromptAddition = runtimeAwareness + indexedContext;
386
311
  return {
387
312
  messages: params.messages,
388
- estimatedTokens: baseTokens + estimateTokens(indexedContext),
389
- systemPromptAddition: indexedContext || undefined,
313
+ estimatedTokens: baseTokens + estimateTokens(systemPromptAddition),
314
+ systemPromptAddition: systemPromptAddition.trim() || undefined,
390
315
  };
391
316
  }
392
317
  let memories = [];
@@ -458,6 +383,7 @@ export class FatHippoContextEngine {
458
383
  const fullContext = typeof params.tokenBudget === "number" && params.tokenBudget > 0
459
384
  ? this.fitContextToBudget({
460
385
  sections: [
386
+ runtimeAwareness,
461
387
  workflowBlock,
462
388
  patternBlock,
463
389
  memoryBlock ? `${memoryBlock}\n` : "",
@@ -466,220 +392,36 @@ export class FatHippoContextEngine {
466
392
  ],
467
393
  contextBudget: Math.max(0, params.tokenBudget - baseTokens),
468
394
  })
469
- : workflowBlock + patternBlock + (memoryBlock ? memoryBlock + "\n" : "") + indexedContext + hippoNodInstruction;
395
+ : runtimeAwareness + workflowBlock + patternBlock + (memoryBlock ? memoryBlock + "\n" : "") + indexedContext + hippoNodInstruction;
470
396
  return {
471
397
  messages: params.messages,
472
398
  estimatedTokens: baseTokens + estimateTokens(fullContext),
473
399
  systemPromptAddition: fullContext.trim() || undefined,
474
400
  };
475
401
  }
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
- };
402
+ buildRuntimeAwarenessInstruction() {
403
+ return [
404
+ "## FatHippo Runtime",
405
+ "FatHippo context engine is active for this session.",
406
+ "If asked whether FatHippo is active or configured for this chat, answer yes.",
407
+ "Only attribute a specific fact to FatHippo when it appears in a FatHippo-labeled section of this prompt.",
408
+ "If a fact appears in another source such as a workspace file, say FatHippo is active but that specific fact came from the other source.",
409
+ "Do not claim access to runtime traces, logs, dashboards, or hook internals unless they are provided in the conversation.",
410
+ "",
411
+ ].join("\n");
615
412
  }
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());
413
+ async runCognitiveHeartbeat() {
414
+ if (!this.client) {
634
415
  return;
635
416
  }
636
417
  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
- }
418
+ await this.client.extractCognitivePatterns({});
670
419
  }
671
420
  catch (error) {
672
421
  console.error("[FatHippo] Pattern extraction heartbeat error:", error);
673
422
  }
674
423
  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
- }
424
+ await this.client.synthesizeCognitiveSkills({});
683
425
  }
684
426
  catch (error) {
685
427
  console.error("[FatHippo] Skill synthesis heartbeat error:", error);
@@ -808,24 +550,6 @@ export class FatHippoContextEngine {
808
550
  this.sessionStartTimes.set(params.sessionId, Date.now());
809
551
  }
810
552
  }
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
553
  buildHippoNodInstruction(params) {
830
554
  if (this.config.hippoNodsEnabled === false || !params.cue) {
831
555
  return "";
@@ -900,8 +624,6 @@ export class FatHippoContextEngine {
900
624
  if (!result) {
901
625
  return { ok: false, compacted: false, reason: "hosted client unavailable" };
902
626
  }
903
- // Invalidate cache after dream cycle
904
- this.cachedCritical = null;
905
627
  return {
906
628
  ok: result.ok,
907
629
  compacted: true,
@@ -933,17 +655,15 @@ export class FatHippoContextEngine {
933
655
  /**
934
656
  * Handle subagent completion
935
657
  */
936
- async onSubagentEnded(_params) {
937
- void _params;
938
- // Future: extract learnings from subagent session
939
- // and store them in parent's memory
658
+ async onSubagentEnded(params) {
659
+ await this.endHostedSession(params.childSessionKey, this.mapHostedOutcomeFromReason(params.reason));
940
660
  }
941
661
  /**
942
662
  * Cleanup resources
943
663
  */
944
664
  async dispose() {
945
- this.cachedCritical = null;
946
- this.sessionApplicationIds.clear();
665
+ await Promise.allSettled([...this.hostedSessions.keys()].map((sessionId) => this.endHostedSession(sessionId, "abandoned")));
666
+ this.hostedSessions.clear();
947
667
  this.sessionLocalProfiles.clear();
948
668
  this.sessionStartTimes.clear();
949
669
  this.sessionHippoNodState.clear();
@@ -966,6 +686,130 @@ export class FatHippoContextEngine {
966
686
  }
967
687
  return null;
968
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
+ }
969
813
  findLastUserMessage(messages) {
970
814
  for (let i = messages.length - 1; i >= 0; i--) {
971
815
  const msg = messages[i];