@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/README.md +48 -49
- package/dist/api/client.d.ts +1 -74
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +1 -175
- package/dist/api/client.js.map +1 -1
- package/dist/engine.d.ts +10 -24
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +256 -412
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/openclaw.plugin.json +21 -4
- package/package.json +6 -3
package/dist/engine.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { createLocalMemoryStore, invalidateAllLocalResultsForUser, localRetrieve, localStoreResult, } from "@fathippo/local";
|
|
9
|
-
import { FatHippoClient } from "
|
|
9
|
+
import { FatHippoClient, createFatHippoHostedRuntimeClient, } from "@fathippo/hosted";
|
|
10
10
|
import { buildStructuredTrace, getMessageText, shouldCaptureCodingTrace } from "./cognitive/trace-capture.js";
|
|
11
11
|
import { CONTEXT_ENGINE_ID, CONTEXT_ENGINE_VERSION } from "./version.js";
|
|
12
12
|
import { formatMemoriesForInjection, dedupeMemories, estimateTokens, } from "./utils/formatting.js";
|
|
@@ -22,13 +22,13 @@ export class FatHippoContextEngine {
|
|
|
22
22
|
ownsCompaction: true, // We handle compaction via Dream Cycle
|
|
23
23
|
};
|
|
24
24
|
client;
|
|
25
|
+
runtimeClient;
|
|
25
26
|
config;
|
|
26
27
|
mode;
|
|
27
28
|
localStore;
|
|
28
|
-
|
|
29
|
+
hostedSessions = new Map();
|
|
29
30
|
// Cognitive engine state
|
|
30
31
|
sessionStartTimes = new Map();
|
|
31
|
-
sessionApplicationIds = new Map();
|
|
32
32
|
sessionLocalProfiles = new Map();
|
|
33
33
|
sessionHippoNodState = new Map();
|
|
34
34
|
cognitiveEnabled;
|
|
@@ -45,8 +45,6 @@ export class FatHippoContextEngine {
|
|
|
45
45
|
"ty",
|
|
46
46
|
"thx",
|
|
47
47
|
]);
|
|
48
|
-
static MIN_VECTOR_SIMILARITY = 0.75;
|
|
49
|
-
static MIN_CRITICAL_RELEVANCE = 0.7;
|
|
50
48
|
static HIPPO_NOD_COOLDOWN_MS = 15 * 60 * 1000;
|
|
51
49
|
static HIPPO_NOD_MIN_MESSAGE_GAP = 6;
|
|
52
50
|
constructor(config) {
|
|
@@ -56,6 +54,14 @@ export class FatHippoContextEngine {
|
|
|
56
54
|
throw new Error("FatHippo hosted mode requires an API key. Pass apiKey or switch mode to local/auto.");
|
|
57
55
|
}
|
|
58
56
|
this.client = this.mode === "hosted" ? new FatHippoClient(config) : null;
|
|
57
|
+
this.runtimeClient =
|
|
58
|
+
this.mode === "hosted" && config.apiKey
|
|
59
|
+
? createFatHippoHostedRuntimeClient({
|
|
60
|
+
apiKey: config.apiKey,
|
|
61
|
+
baseUrl: config.baseUrl || "https://fathippo.ai/api",
|
|
62
|
+
runtime: this.buildHostedRuntimeMetadata(),
|
|
63
|
+
})
|
|
64
|
+
: null;
|
|
59
65
|
this.localStore =
|
|
60
66
|
this.mode === "local"
|
|
61
67
|
? createLocalMemoryStore({
|
|
@@ -71,6 +77,7 @@ export class FatHippoContextEngine {
|
|
|
71
77
|
async bootstrap(params) {
|
|
72
78
|
try {
|
|
73
79
|
this.sessionStartTimes.set(params.sessionId, Date.now());
|
|
80
|
+
const workspaceRoot = this.detectWorkspaceRoot(params.sessionFile);
|
|
74
81
|
if (this.mode === "local") {
|
|
75
82
|
this.sessionLocalProfiles.set(params.sessionId, this.deriveLocalProfileId(params.sessionId, params.sessionFile));
|
|
76
83
|
return {
|
|
@@ -78,25 +85,37 @@ export class FatHippoContextEngine {
|
|
|
78
85
|
importedMessages: 0,
|
|
79
86
|
};
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
limit: 30,
|
|
84
|
-
excludeAbsorbed: true,
|
|
85
|
-
});
|
|
86
|
-
if (!critical) {
|
|
88
|
+
const runtimeClient = this.runtimeClient;
|
|
89
|
+
if (!runtimeClient) {
|
|
87
90
|
return {
|
|
88
91
|
bootstrapped: true,
|
|
89
92
|
importedMessages: 0,
|
|
90
93
|
};
|
|
91
94
|
}
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
if (!this.hostedSessions.has(params.sessionId)) {
|
|
96
|
+
const session = await runtimeClient.startSession({
|
|
97
|
+
firstMessage: "",
|
|
98
|
+
namespace: this.config.namespace,
|
|
99
|
+
metadata: {
|
|
100
|
+
openclawSessionId: params.sessionId,
|
|
101
|
+
},
|
|
102
|
+
runtime: this.buildHostedRuntimeMetadata({
|
|
103
|
+
sessionId: params.sessionId,
|
|
104
|
+
sessionFile: params.sessionFile,
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
this.hostedSessions.set(params.sessionId, {
|
|
108
|
+
hostedSessionId: session.sessionId,
|
|
109
|
+
workspaceRoot,
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
bootstrapped: true,
|
|
113
|
+
importedMessages: session.injectedMemories.length,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
97
116
|
return {
|
|
98
117
|
bootstrapped: true,
|
|
99
|
-
importedMessages:
|
|
118
|
+
importedMessages: 0,
|
|
100
119
|
};
|
|
101
120
|
}
|
|
102
121
|
catch (error) {
|
|
@@ -111,73 +130,22 @@ export class FatHippoContextEngine {
|
|
|
111
130
|
* Ingest a single message into FatHippo
|
|
112
131
|
*/
|
|
113
132
|
async ingest(params) {
|
|
114
|
-
|
|
133
|
+
void params;
|
|
115
134
|
if (params.isHeartbeat) {
|
|
116
135
|
return { ingested: false };
|
|
117
136
|
}
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
(!this.isRoleMessage(params.message) || params.message.role !== "user")) {
|
|
121
|
-
return { ingested: false };
|
|
122
|
-
}
|
|
123
|
-
const content = this.extractContent(params.message);
|
|
124
|
-
if (!content) {
|
|
125
|
-
return { ingested: false };
|
|
126
|
-
}
|
|
127
|
-
// Auto-detect constraints from user messages
|
|
128
|
-
if (this.cognitiveEnabled && this.isRoleMessage(params.message) && params.message.role === "user") {
|
|
129
|
-
this.maybeStoreConstraint(content).catch(() => { }); // Fire and forget
|
|
130
|
-
}
|
|
131
|
-
// Filter prompt injection attempts
|
|
132
|
-
if (detectPromptInjection(content)) {
|
|
133
|
-
console.warn("[FatHippo] Blocked prompt injection attempt");
|
|
134
|
-
return { ingested: false };
|
|
135
|
-
}
|
|
136
|
-
// Check if content matches capture patterns
|
|
137
|
-
if (!matchesCapturePatterns(content)) {
|
|
138
|
-
return { ingested: false };
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
if (this.mode === "local") {
|
|
142
|
-
const profileId = this.getLocalProfileId(params.sessionId);
|
|
143
|
-
await this.localStore?.remember({
|
|
144
|
-
profileId,
|
|
145
|
-
content: sanitizeContent(content),
|
|
146
|
-
title: this.buildLocalTitle(content),
|
|
147
|
-
});
|
|
148
|
-
invalidateAllLocalResultsForUser(profileId);
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
await this.client?.remember({
|
|
152
|
-
content: sanitizeContent(content),
|
|
153
|
-
conversationId: this.config.conversationId || params.sessionId,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
return { ingested: true };
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
console.error("[FatHippo] Ingest error:", error);
|
|
160
|
-
return { ingested: false };
|
|
161
|
-
}
|
|
137
|
+
// Full-turn capture happens in afterTurn for both hosted and local modes.
|
|
138
|
+
return { ingested: false };
|
|
162
139
|
}
|
|
163
140
|
/**
|
|
164
141
|
* Ingest a batch of messages
|
|
165
142
|
*/
|
|
166
143
|
async ingestBatch(params) {
|
|
144
|
+
void params;
|
|
167
145
|
if (params.isHeartbeat) {
|
|
168
146
|
return { ingestedCount: 0 };
|
|
169
147
|
}
|
|
170
|
-
|
|
171
|
-
for (const message of params.messages) {
|
|
172
|
-
const result = await this.ingest({
|
|
173
|
-
sessionId: params.sessionId,
|
|
174
|
-
message,
|
|
175
|
-
isHeartbeat: params.isHeartbeat,
|
|
176
|
-
});
|
|
177
|
-
if (result.ingested)
|
|
178
|
-
ingestedCount++;
|
|
179
|
-
}
|
|
180
|
-
return { ingestedCount };
|
|
148
|
+
return { ingestedCount: 0 };
|
|
181
149
|
}
|
|
182
150
|
/**
|
|
183
151
|
* Post-turn lifecycle processing
|
|
@@ -189,28 +157,41 @@ export class FatHippoContextEngine {
|
|
|
189
157
|
}
|
|
190
158
|
return;
|
|
191
159
|
}
|
|
192
|
-
|
|
193
|
-
const cacheAge = this.cachedCritical
|
|
194
|
-
? Date.now() - this.cachedCritical.fetchedAt
|
|
195
|
-
: Infinity;
|
|
196
|
-
if (cacheAge > 5 * 60 * 1000) {
|
|
197
|
-
// 5 minute cache
|
|
198
|
-
this.cachedCritical = null;
|
|
199
|
-
}
|
|
200
|
-
// Capture cognitive trace for coding sessions
|
|
160
|
+
const turnMessages = this.extractTurnMessages(params.messages, params.prePromptMessageCount);
|
|
201
161
|
if (this.mode === "local") {
|
|
202
|
-
await this.
|
|
162
|
+
await this.captureLocalTurnMemories({
|
|
203
163
|
sessionId: params.sessionId,
|
|
204
164
|
sessionFile: params.sessionFile,
|
|
205
|
-
messages:
|
|
165
|
+
messages: turnMessages,
|
|
206
166
|
});
|
|
207
|
-
|
|
208
|
-
else if (this.cognitiveEnabled) {
|
|
209
|
-
await this.captureStructuredTrace({
|
|
167
|
+
await this.captureLocalTrace({
|
|
210
168
|
sessionId: params.sessionId,
|
|
211
169
|
sessionFile: params.sessionFile,
|
|
212
170
|
messages: params.messages,
|
|
213
171
|
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const runtimeClient = this.runtimeClient;
|
|
175
|
+
const hostedSessionId = this.hostedSessions.get(params.sessionId)?.hostedSessionId;
|
|
176
|
+
if (!runtimeClient || !hostedSessionId) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
await runtimeClient.recordTurn({
|
|
181
|
+
sessionId: hostedSessionId,
|
|
182
|
+
messages: turnMessages,
|
|
183
|
+
memoriesUsed: [],
|
|
184
|
+
captureUserOnly: this.config.captureUserOnly === true,
|
|
185
|
+
captureConstraints: this.cognitiveEnabled,
|
|
186
|
+
captureTrace: this.cognitiveEnabled,
|
|
187
|
+
runtime: this.buildHostedRuntimeMetadata({
|
|
188
|
+
sessionId: params.sessionId,
|
|
189
|
+
sessionFile: params.sessionFile,
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.error("[FatHippo] Record turn error:", error);
|
|
214
195
|
}
|
|
215
196
|
}
|
|
216
197
|
detectToolsUsed(messages) {
|
|
@@ -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
|
|
238
|
-
|
|
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(
|
|
261
|
-
systemPromptAddition:
|
|
231
|
+
estimatedTokens: baseTokens + estimateTokens(runtimeAwareness),
|
|
232
|
+
systemPromptAddition: runtimeAwareness.trim() || undefined,
|
|
262
233
|
};
|
|
263
234
|
}
|
|
264
|
-
|
|
265
|
-
let
|
|
266
|
-
let syntheses = [];
|
|
267
|
-
let memoryHippoCue = null;
|
|
268
|
-
let hasRelevantCriticalMatch = false;
|
|
235
|
+
let hostedContext = "";
|
|
236
|
+
let cue = null;
|
|
269
237
|
try {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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 (
|
|
307
|
-
|
|
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
|
|
280
|
+
cue,
|
|
355
281
|
});
|
|
356
282
|
const fullContext = typeof params.tokenBudget === "number" && params.tokenBudget > 0
|
|
357
283
|
? this.fitContextToBudget({
|
|
358
284
|
sections: [
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
:
|
|
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(
|
|
389
|
-
systemPromptAddition:
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
|
617
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
937
|
-
|
|
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.
|
|
946
|
-
this.
|
|
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];
|