@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/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) {
@@ -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 client = this.client;
239
- if (!client) {
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(systemPromptAddition),
263
- systemPromptAddition: systemPromptAddition.trim() || undefined,
231
+ estimatedTokens: baseTokens + estimateTokens(runtimeAwareness),
232
+ systemPromptAddition: runtimeAwareness.trim() || undefined,
264
233
  };
265
234
  }
266
- // Fetch relevant memories based on last user message
267
- let memories = [];
268
- let syntheses = [];
269
- let memoryHippoCue = null;
270
- let hasRelevantCriticalMatch = false;
235
+ let hostedContext = "";
236
+ let cue = null;
271
237
  try {
272
- // Search for relevant memories based on query
273
- const results = await client.search({
274
- query: lastUserMessage,
275
- limit: this.config.injectLimit || 20,
276
- 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
+ }),
277
249
  });
278
- const qualifyingResults = results.filter((r) => r.score >= FatHippoContextEngine.MIN_VECTOR_SIMILARITY);
279
- const searchedMemories = qualifyingResults.map((r) => r.memory);
280
- memories = dedupeMemories(searchedMemories);
281
- // Inject critical only for non-trivial queries with critical relevance.
282
- hasRelevantCriticalMatch = results.some((r) => r.memory.importanceTier === "critical" &&
283
- r.score > FatHippoContextEngine.MIN_CRITICAL_RELEVANCE);
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 (hasRelevantCriticalMatch || syntheses.length > 0) {
309
- 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 = {
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: cognitiveHippoCue ?? memoryHippoCue,
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
- constraintsContext,
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 + constraintsContext + (memoryBlock ? memoryBlock + "\n" : "") + indexedContext + cognitiveContext + hippoNodInstruction;
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
- * Check if query looks like a coding task
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
- const baseUrl = this.getApiBaseUrl();
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
- const response = await fetch(`${baseUrl}/v1/cognitive/skills/synthesize`, {
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(_params) {
953
- void _params;
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.cachedCritical = null;
962
- this.sessionApplicationIds.clear();
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];