@ixo/common 1.1.3 → 1.1.4

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.
Files changed (65) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/ai/models/openai.d.ts +16 -0
  3. package/dist/ai/models/openai.d.ts.map +1 -1
  4. package/dist/ai/models/openai.js +26 -0
  5. package/dist/ai/models/openai.js.map +1 -1
  6. package/dist/ai/nodes/generic-chat/generic-chat.node.d.ts +1 -1
  7. package/dist/ai/semantic-router-factory/create-semantic-router.d.ts +1 -2
  8. package/dist/ai/semantic-router-factory/create-semantic-router.d.ts.map +1 -1
  9. package/dist/ai/semantic-router-factory/create-semantic-router.js +2 -3
  10. package/dist/ai/semantic-router-factory/create-semantic-router.js.map +1 -1
  11. package/dist/ai/tools/ask-ixo-guru/ask-ixo-guru.d.ts +1 -1
  12. package/dist/ai/tools/ask-ixo-guru/ask-ixo-guru.d.ts.map +1 -1
  13. package/dist/ai/tools/parser-action-tool.d.ts +1 -1
  14. package/dist/ai/tools/parser-action-tool.d.ts.map +1 -1
  15. package/dist/ai/tools/parser-action-tool.js +10 -3
  16. package/dist/ai/tools/parser-action-tool.js.map +1 -1
  17. package/dist/ai/tools/parser-browser-tool.d.ts +1 -1
  18. package/dist/ai/tools/parser-browser-tool.d.ts.map +1 -1
  19. package/dist/ai/tools/scrape-web-page.d.ts +1 -1
  20. package/dist/ai/tools/scrape-web-page.d.ts.map +1 -1
  21. package/dist/ai/tools/web-search-tool.d.ts +1 -1
  22. package/dist/ai/tools/web-search-tool.d.ts.map +1 -1
  23. package/dist/ai/utils/load-file.d.ts +1 -0
  24. package/dist/ai/utils/load-file.d.ts.map +1 -1
  25. package/dist/ai/utils/load-file.js +8 -0
  26. package/dist/ai/utils/load-file.js.map +1 -1
  27. package/dist/ai/utils/transformGraphStateMessageToListMessageResponse.d.ts +10 -0
  28. package/dist/ai/utils/transformGraphStateMessageToListMessageResponse.d.ts.map +1 -1
  29. package/dist/ai/utils/transformGraphStateMessageToListMessageResponse.js +4 -1
  30. package/dist/ai/utils/transformGraphStateMessageToListMessageResponse.js.map +1 -1
  31. package/dist/services/env/env.service.js.map +1 -1
  32. package/dist/services/memory-engine/memory-engine.service.d.ts +13 -11
  33. package/dist/services/memory-engine/memory-engine.service.d.ts.map +1 -1
  34. package/dist/services/memory-engine/memory-engine.service.js +133 -106
  35. package/dist/services/memory-engine/memory-engine.service.js.map +1 -1
  36. package/dist/services/memory-engine/types.d.ts +16 -0
  37. package/dist/services/memory-engine/types.d.ts.map +1 -1
  38. package/dist/services/memory-engine/types.js +4 -1
  39. package/dist/services/memory-engine/types.js.map +1 -1
  40. package/dist/services/session-manager/dto.d.ts +3 -0
  41. package/dist/services/session-manager/dto.d.ts.map +1 -1
  42. package/dist/services/session-manager/dto.js +18 -0
  43. package/dist/services/session-manager/dto.js.map +1 -1
  44. package/dist/services/session-manager/session-manager.service.d.ts +2 -2
  45. package/dist/services/session-manager/session-manager.service.d.ts.map +1 -1
  46. package/dist/services/session-manager/session-manager.service.js +78 -38
  47. package/dist/services/session-manager/session-manager.service.js.map +1 -1
  48. package/dist/utils/get-user-subscription.d.ts +2 -1
  49. package/dist/utils/get-user-subscription.d.ts.map +1 -1
  50. package/dist/utils/get-user-subscription.js +9 -5
  51. package/dist/utils/get-user-subscription.js.map +1 -1
  52. package/package.json +24 -24
  53. package/src/ai/models/openai.ts +31 -0
  54. package/src/ai/semantic-router-factory/create-semantic-router.test.ts +0 -3
  55. package/src/ai/semantic-router-factory/create-semantic-router.ts +2 -7
  56. package/src/ai/tools/parser-action-tool.ts +19 -3
  57. package/src/ai/utils/load-file.ts +17 -0
  58. package/src/ai/utils/transformGraphStateMessageToListMessageResponse.ts +18 -1
  59. package/src/services/env/env.service.ts +1 -1
  60. package/src/services/memory-engine/memory-engine.service.ts +196 -222
  61. package/src/services/memory-engine/types.ts +34 -0
  62. package/src/services/session-manager/dto.ts +14 -0
  63. package/src/services/session-manager/session-manager.service.ts +93 -40
  64. package/src/utils/get-user-subscription.ts +11 -4
  65. package/tsconfig.tsbuildinfo +1 -1
@@ -1,8 +1,11 @@
1
1
  import { Logger } from '@ixo/logger';
2
- import type {
3
- SearchEnhancedRequest,
4
- SearchEnhancedResponse,
5
- UserContextData,
2
+ import {
3
+ isBatchErrorSlot,
4
+ type SearchEnhancedBatchRequest,
5
+ type SearchEnhancedBatchResponse,
6
+ type SearchEnhancedRequest,
7
+ type SearchEnhancedResponse,
8
+ type UserContextData,
6
9
  } from './types.js';
7
10
 
8
11
  interface MemoryEngineAuthHeaders {
@@ -10,27 +13,40 @@ interface MemoryEngineAuthHeaders {
10
13
  userToken: string;
11
14
  oracleHomeServer: string;
12
15
  userHomeServer: string;
16
+ /** When set, uses UCAN auth instead of Matrix tokens */
17
+ ucanInvocation?: string;
13
18
  }
14
19
 
15
20
  export class MemoryEngineService {
16
- private readonly QUERY_TIMEOUT_MS = 2500; // 2.5 seconds per query
21
+ // Batch covers 6 queries running in parallel server-side. Bound by the
22
+ // slowest query, not 6× — but we leave headroom for cold caches.
23
+ private readonly BATCH_TIMEOUT_MS = 15000;
17
24
 
18
25
  constructor(private readonly memoryEngineUrl: string) {}
19
26
 
20
27
  /**
21
- * Wraps a promise with a timeout, returning fallback value if timeout is exceeded
28
+ * Build HTTP headers for memory engine requests (UCAN or Matrix)
22
29
  */
23
- private async withTimeout<T>(
24
- promise: Promise<T>,
25
- timeoutMs: number,
26
- fallback: T,
27
- ): Promise<T> {
28
- return Promise.race([
29
- promise,
30
- new Promise<T>((resolve) =>
31
- setTimeout(() => resolve(fallback), timeoutMs),
32
- ),
33
- ]);
30
+ private buildHeaders(
31
+ auth: MemoryEngineAuthHeaders,
32
+ roomId: string,
33
+ ): Record<string, string> {
34
+ if (auth.ucanInvocation) {
35
+ return {
36
+ Authorization: `Bearer ${auth.ucanInvocation}`,
37
+ 'X-Auth-Type': 'ucan',
38
+ 'x-room-id': roomId,
39
+ 'Content-Type': 'application/json',
40
+ };
41
+ }
42
+ return {
43
+ 'x-oracle-token': auth.oracleToken,
44
+ 'x-user-token': auth.userToken,
45
+ 'x-oracle-matrix-homeserver': auth.oracleHomeServer,
46
+ 'x-user-matrix-homeserver': auth.userHomeServer,
47
+ 'x-room-id': roomId,
48
+ 'Content-Type': 'application/json',
49
+ };
34
50
  }
35
51
 
36
52
  /**
@@ -43,6 +59,8 @@ export class MemoryEngineService {
43
59
  userToken: string;
44
60
  oracleHomeServer: string;
45
61
  userHomeServer: string;
62
+ /** When set, uses UCAN auth instead of Matrix tokens */
63
+ ucanInvocation?: string;
46
64
  }): Promise<UserContextData> {
47
65
  const {
48
66
  oracleDid,
@@ -51,105 +69,160 @@ export class MemoryEngineService {
51
69
  userToken,
52
70
  oracleHomeServer,
53
71
  userHomeServer,
72
+ ucanInvocation,
54
73
  } = params;
55
74
 
56
75
  Logger.info(
57
76
  `[MemoryEngineService] Gathering user context for oracle: ${oracleDid}, room: ${roomId}`,
58
77
  );
59
78
 
60
- try {
61
- // Execute all 6 queries in parallel with timeouts using Promise.allSettled
62
- const authHeaders = {
63
- oracleToken,
64
- userToken,
65
- oracleHomeServer,
66
- userHomeServer,
67
- };
79
+ const authHeaders: MemoryEngineAuthHeaders = {
80
+ oracleToken,
81
+ userToken,
82
+ oracleHomeServer,
83
+ userHomeServer,
84
+ ucanInvocation,
85
+ };
68
86
 
69
- const results = await Promise.allSettled([
70
- this.withTimeout(
71
- this.queryIdentity(oracleDid, roomId, authHeaders),
72
- this.QUERY_TIMEOUT_MS,
73
- undefined,
74
- ),
75
- this.withTimeout(
76
- this.queryWork(oracleDid, roomId, authHeaders),
77
- this.QUERY_TIMEOUT_MS,
78
- undefined,
79
- ),
80
- this.withTimeout(
81
- this.queryGoals(oracleDid, roomId, authHeaders),
82
- this.QUERY_TIMEOUT_MS,
83
- undefined,
84
- ),
85
- this.withTimeout(
86
- this.queryInterests(oracleDid, roomId, authHeaders),
87
- this.QUERY_TIMEOUT_MS,
88
- undefined,
89
- ),
90
- this.withTimeout(
91
- this.queryRelationships(oracleDid, roomId, authHeaders),
92
- this.QUERY_TIMEOUT_MS,
93
- undefined,
94
- ),
95
- this.withTimeout(
96
- this.queryRecent(oracleDid, roomId, authHeaders),
97
- this.QUERY_TIMEOUT_MS,
98
- undefined,
99
- ),
100
- ]);
101
-
102
- // Extract results from Promise.allSettled outcomes
103
- const identity =
104
- results[0].status === 'fulfilled' ? results[0].value : undefined;
105
- const work =
106
- results[1].status === 'fulfilled' ? results[1].value : undefined;
107
- const goals =
108
- results[2].status === 'fulfilled' ? results[2].value : undefined;
109
- const interests =
110
- results[3].status === 'fulfilled' ? results[3].value : undefined;
111
- const relationships =
112
- results[4].status === 'fulfilled' ? results[4].value : undefined;
113
- const recent =
114
- results[5].status === 'fulfilled' ? results[5].value : undefined;
115
-
116
- // Log any failures
117
- results.forEach((result, index) => {
118
- if (result.status === 'rejected') {
87
+ // The 6 queries that make up userContext. Order matters: it determines
88
+ // how we map batch result slots back to UserContextData fields.
89
+ const labels = [
90
+ 'identity',
91
+ 'work',
92
+ 'goals',
93
+ 'interests',
94
+ 'relationships',
95
+ 'recent',
96
+ ] as const;
97
+ const requests: SearchEnhancedRequest[] = [
98
+ this.buildIdentityRequest(oracleDid),
99
+ this.buildWorkRequest(oracleDid),
100
+ this.buildGoalsRequest(oracleDid),
101
+ this.buildInterestsRequest(oracleDid),
102
+ this.buildRelationshipsRequest(oracleDid),
103
+ this.buildRecentRequest(oracleDid),
104
+ ];
105
+
106
+ const gatherStart = Date.now();
107
+ const batch = await this.executeBatch(requests, roomId, authHeaders);
108
+ const gatherElapsed = Date.now() - gatherStart;
109
+
110
+ if (!batch) {
111
+ Logger.error(
112
+ `[MemoryEngineService] gatherUserContext failed after ${gatherElapsed}ms — returning empty context`,
113
+ );
114
+ return {};
115
+ }
116
+
117
+ // Map each slot back to the labelled field. Error slots become undefined.
118
+ const fields: (SearchEnhancedResponse | undefined)[] = batch.results.map(
119
+ (slot, index) => {
120
+ const label = labels[index];
121
+ if (isBatchErrorSlot(slot)) {
119
122
  Logger.warn(
120
- `[MemoryEngineService] Query ${index} failed:`,
121
- result.reason,
123
+ `[MemoryEngineService] Batch slot "${label}" failed (${slot.error.status_code}): ${slot.error.detail}`,
122
124
  );
125
+ return undefined;
123
126
  }
124
- });
127
+ return slot;
128
+ },
129
+ );
125
130
 
126
- return {
127
- identity,
128
- work,
129
- goals,
130
- interests,
131
- relationships,
132
- recent,
133
- };
134
- } catch (error) {
135
- Logger.error(
136
- '[MemoryEngineService] Failed to gather user context:',
137
- error,
131
+ if (batch.results.length !== labels.length) {
132
+ Logger.warn(
133
+ `[MemoryEngineService] Batch length mismatch: expected ${labels.length}, got ${batch.results.length}`,
138
134
  );
139
- // Return empty context on error
140
- return {};
141
135
  }
136
+
137
+ const summary = labels.map((label, index) => {
138
+ const value = fields[index];
139
+ if (value === undefined) return `${label}=missing`;
140
+ return `${label}=ok(f${value.total_results.facts}/e${value.total_results.entities})`;
141
+ });
142
+ Logger.info(
143
+ `[MemoryEngineService] gatherUserContext completed in ${gatherElapsed}ms (batch) — ${summary.join(', ')}`,
144
+ );
145
+
146
+ return {
147
+ identity: fields[0],
148
+ work: fields[1],
149
+ goals: fields[2],
150
+ interests: fields[3],
151
+ relationships: fields[4],
152
+ recent: fields[5],
153
+ };
142
154
  }
143
155
 
144
156
  /**
145
- * Query 1: User Identity & Attributes
157
+ * POST /search-enhanced-batch single round-trip for N parallel queries.
158
+ * Returns undefined on transport/HTTP failure; a partially-failed batch
159
+ * still resolves with per-slot error markers (handled by caller via
160
+ * `isBatchErrorSlot`).
146
161
  */
147
- private async queryIdentity(
148
- oracleDid: string,
162
+ private async executeBatch(
163
+ queries: SearchEnhancedRequest[],
149
164
  roomId: string,
150
165
  auth: MemoryEngineAuthHeaders,
151
- ): Promise<SearchEnhancedResponse | undefined> {
152
- const request: SearchEnhancedRequest = {
166
+ ): Promise<SearchEnhancedBatchResponse | undefined> {
167
+ if (!roomId) {
168
+ Logger.warn(
169
+ `[MemoryEngineService] No room id provided, skipping batch search`,
170
+ );
171
+ return undefined;
172
+ }
173
+ if (!auth.ucanInvocation && (!auth.oracleToken || !auth.userToken)) {
174
+ Logger.warn(
175
+ `[MemoryEngineService] Missing auth (no UCAN and no Matrix tokens), skipping batch search`,
176
+ );
177
+ return undefined;
178
+ }
179
+
180
+ const body: SearchEnhancedBatchRequest = { queries };
181
+
182
+ const controller = new AbortController();
183
+ const timer = setTimeout(() => controller.abort(), this.BATCH_TIMEOUT_MS);
184
+
185
+ try {
186
+ const response = await fetch(
187
+ `${this.memoryEngineUrl}/search-enhanced-batch`,
188
+ {
189
+ method: 'POST',
190
+ headers: this.buildHeaders(auth, roomId),
191
+ body: JSON.stringify(body),
192
+ signal: controller.signal,
193
+ },
194
+ );
195
+
196
+ if (!response.ok) {
197
+ const errorText = await response.text();
198
+ Logger.warn(
199
+ `[MemoryEngineService] Batch search failed (${response.status}): ${errorText}`,
200
+ );
201
+ return undefined;
202
+ }
203
+
204
+ return (await response.json()) as SearchEnhancedBatchResponse;
205
+ } catch (error) {
206
+ if ((error as Error).name === 'AbortError') {
207
+ Logger.warn(
208
+ `[MemoryEngineService] Batch search aborted after ${this.BATCH_TIMEOUT_MS}ms`,
209
+ );
210
+ } else {
211
+ Logger.error(`[MemoryEngineService] Batch search threw:`, error);
212
+ }
213
+ return undefined;
214
+ } finally {
215
+ clearTimeout(timer);
216
+ }
217
+ }
218
+
219
+ // ── Per-query request builders ────────────────────────────────────────────
220
+ // These produce SearchEnhancedRequest payloads consumed by gatherUserContext
221
+ // via the batch endpoint. Order matches the labels array in
222
+ // gatherUserContext — keep the two in sync.
223
+
224
+ private buildIdentityRequest(oracleDid: string): SearchEnhancedRequest {
225
+ return {
153
226
  oracle_dids: [oracleDid],
154
227
  query:
155
228
  'username and nickname and age user identity traits values personality characteristics communication style beliefs preferences',
@@ -173,19 +246,10 @@ export class MemoryEngineService {
173
246
  invalid_at: [[{ date: null, comparison_operator: 'IS NULL' }]],
174
247
  },
175
248
  };
176
-
177
- return this.executeQuery(request, roomId, auth);
178
249
  }
179
250
 
180
- /**
181
- * Query 2: Work Context
182
- */
183
- private async queryWork(
184
- oracleDid: string,
185
- roomId: string,
186
- auth: MemoryEngineAuthHeaders,
187
- ): Promise<SearchEnhancedResponse | undefined> {
188
- const request: SearchEnhancedRequest = {
251
+ private buildWorkRequest(oracleDid: string): SearchEnhancedRequest {
252
+ return {
189
253
  oracle_dids: [oracleDid],
190
254
  query:
191
255
  'work job career projects skills organization employment role responsibilities expertise',
@@ -216,19 +280,10 @@ export class MemoryEngineService {
216
280
  invalid_at: [[{ date: null, comparison_operator: 'IS NULL' }]],
217
281
  },
218
282
  };
219
-
220
- return this.executeQuery(request, roomId, auth);
221
283
  }
222
284
 
223
- /**
224
- * Query 3: Goals & Habits
225
- */
226
- private async queryGoals(
227
- oracleDid: string,
228
- roomId: string,
229
- auth: MemoryEngineAuthHeaders,
230
- ): Promise<SearchEnhancedResponse | undefined> {
231
- const request: SearchEnhancedRequest = {
285
+ private buildGoalsRequest(oracleDid: string): SearchEnhancedRequest {
286
+ return {
232
287
  oracle_dids: [oracleDid],
233
288
  query:
234
289
  'goals aspirations objectives milestones habits routines patterns achievements progress',
@@ -251,19 +306,10 @@ export class MemoryEngineService {
251
306
  invalid_at: [[{ date: null, comparison_operator: 'IS NULL' }]],
252
307
  },
253
308
  };
254
-
255
- return this.executeQuery(request, roomId, auth);
256
309
  }
257
310
 
258
- /**
259
- * Query 4: Interests & Preferences
260
- */
261
- private async queryInterests(
262
- oracleDid: string,
263
- roomId: string,
264
- auth: MemoryEngineAuthHeaders,
265
- ): Promise<SearchEnhancedResponse | undefined> {
266
- const request: SearchEnhancedRequest = {
311
+ private buildInterestsRequest(oracleDid: string): SearchEnhancedRequest {
312
+ return {
267
313
  oracle_dids: [oracleDid],
268
314
  query:
269
315
  'interests hobbies passions preferences likes dislikes expertise topics content',
@@ -293,19 +339,10 @@ export class MemoryEngineService {
293
339
  invalid_at: [[{ date: null, comparison_operator: 'IS NULL' }]],
294
340
  },
295
341
  };
296
-
297
- return this.executeQuery(request, roomId, auth);
298
342
  }
299
343
 
300
- /**
301
- * Query 5: Relationships
302
- */
303
- private async queryRelationships(
304
- oracleDid: string,
305
- roomId: string,
306
- auth: MemoryEngineAuthHeaders,
307
- ): Promise<SearchEnhancedResponse | undefined> {
308
- const request: SearchEnhancedRequest = {
344
+ private buildRelationshipsRequest(oracleDid: string): SearchEnhancedRequest {
345
+ return {
309
346
  oracle_dids: [oracleDid],
310
347
  query:
311
348
  'relationships people connections social network colleagues friends family contacts',
@@ -328,24 +365,17 @@ export class MemoryEngineService {
328
365
  invalid_at: [[{ date: null, comparison_operator: 'IS NULL' }]],
329
366
  },
330
367
  };
331
-
332
- return this.executeQuery(request, roomId, auth);
333
368
  }
334
369
 
335
- /**
336
- * Query 6: Recent Context
337
- */
338
- private async queryRecent(
339
- oracleDid: string,
340
- roomId: string,
341
- auth: MemoryEngineAuthHeaders,
342
- ): Promise<SearchEnhancedResponse | undefined> {
343
- // Calculate date 90 days ago for recent context
370
+ private buildRecentRequest(oracleDid: string): SearchEnhancedRequest {
371
+ // Server-side `recent_memory` strategy auto-injects a created_at >= now-90d
372
+ // filter. We still pass it explicitly as defense-in-depth — the server's
373
+ // merge logic respects an existing lower bound and won't double-apply.
344
374
  const ninetyDaysAgo = new Date();
345
375
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
346
376
  const dateString = ninetyDaysAgo.toISOString();
347
377
 
348
- const request: SearchEnhancedRequest = {
378
+ return {
349
379
  oracle_dids: [oracleDid],
350
380
  query:
351
381
  'recent conversations messages discussions activities updates interactions',
@@ -359,8 +389,6 @@ export class MemoryEngineService {
359
389
  created_at: [[{ date: dateString, comparison_operator: '>=' }]],
360
390
  },
361
391
  };
362
-
363
- return this.executeQuery(request, roomId, auth);
364
392
  }
365
393
 
366
394
  /**
@@ -373,6 +401,7 @@ export class MemoryEngineService {
373
401
  userToken,
374
402
  oracleHomeServer,
375
403
  userHomeServer,
404
+ ucanInvocation,
376
405
  }: {
377
406
  messages: Array<{
378
407
  content: string;
@@ -386,6 +415,8 @@ export class MemoryEngineService {
386
415
  userToken: string;
387
416
  oracleHomeServer: string;
388
417
  userHomeServer: string;
418
+ /** When set, uses UCAN auth instead of Matrix tokens */
419
+ ucanInvocation?: string;
389
420
  }): Promise<{ success: boolean }> {
390
421
  if (!roomId) {
391
422
  Logger.warn(
@@ -393,9 +424,9 @@ export class MemoryEngineService {
393
424
  );
394
425
  return { success: false };
395
426
  }
396
- if (!oracleToken || !userToken) {
427
+ if (!ucanInvocation && (!oracleToken || !userToken)) {
397
428
  Logger.warn(
398
- `[MemoryEngineService] Missing oracle or user token, skipping conversation processing`,
429
+ `[MemoryEngineService] Missing auth (no UCAN and no Matrix tokens), skipping conversation processing`,
399
430
  );
400
431
  return { success: false };
401
432
  }
@@ -407,16 +438,16 @@ export class MemoryEngineService {
407
438
  }
408
439
 
409
440
  try {
441
+ const auth: MemoryEngineAuthHeaders = {
442
+ oracleToken,
443
+ userToken,
444
+ oracleHomeServer,
445
+ userHomeServer,
446
+ ucanInvocation,
447
+ };
410
448
  const response = await fetch(`${this.memoryEngineUrl}/messages`, {
411
449
  method: 'POST',
412
- headers: {
413
- 'x-oracle-token': oracleToken,
414
- 'x-user-token': userToken,
415
- 'x-oracle-matrix-homeserver': oracleHomeServer,
416
- 'x-user-matrix-homeserver': userHomeServer,
417
- 'x-room-id': roomId,
418
- 'Content-Type': 'application/json',
419
- },
450
+ headers: this.buildHeaders(auth, roomId),
420
451
  body: JSON.stringify({ messages }),
421
452
  });
422
453
 
@@ -440,61 +471,4 @@ export class MemoryEngineService {
440
471
  return { success: false };
441
472
  }
442
473
  }
443
-
444
- /**
445
- * Execute a search query against the Memory Engine API
446
- */
447
- private async executeQuery(
448
- request: SearchEnhancedRequest,
449
- roomId: string,
450
- auth: MemoryEngineAuthHeaders,
451
- ): Promise<SearchEnhancedResponse | undefined> {
452
- if (!roomId) {
453
- Logger.warn(
454
- `[MemoryEngineService] No room id provided, skipping query "${request.query}"`,
455
- );
456
- return undefined;
457
- }
458
- if (!auth.oracleToken || !auth.userToken) {
459
- Logger.warn(
460
- `[MemoryEngineService] Missing oracle or user token, skipping query "${request.query}"`,
461
- );
462
- return undefined;
463
- }
464
-
465
- try {
466
- const response = await fetch(`${this.memoryEngineUrl}/search-enhanced`, {
467
- method: 'POST',
468
- headers: {
469
- 'x-oracle-token': auth.oracleToken,
470
- 'x-user-token': auth.userToken,
471
- 'x-oracle-matrix-homeserver': auth.oracleHomeServer,
472
- 'x-user-matrix-homeserver': auth.userHomeServer,
473
- 'x-room-id': roomId,
474
- 'Content-Type': 'application/json',
475
- },
476
- body: JSON.stringify(request),
477
- });
478
-
479
- if (!response.ok) {
480
- const errorText = await response.text();
481
- Logger.warn(
482
- `[MemoryEngineService] Memory Engine query failed (${response.status}): ${errorText}`,
483
- );
484
- return undefined;
485
- }
486
-
487
- const result = (await response.json()) as SearchEnhancedResponse;
488
- Logger.info(
489
- `[MemoryEngineService] Query "${request.query}" returned ${result.total_results.facts} facts, ${result.total_results.entities} entities`,
490
- );
491
- return result;
492
- } catch (error) {
493
- Logger.error(
494
- `[MemoryEngineService] Failed to execute query "${request.query}":`,
495
- error,
496
- );
497
- return undefined;
498
- }
499
- }
500
474
  }
@@ -206,3 +206,37 @@ export interface UserContextData {
206
206
  relationships?: SearchEnhancedResponse;
207
207
  recent?: SearchEnhancedResponse;
208
208
  }
209
+
210
+ // Batch search types — backend endpoint POST /search-enhanced-batch
211
+ export interface SearchEnhancedBatchRequest {
212
+ queries: SearchEnhancedRequest[];
213
+ }
214
+
215
+ // A failed slot in the batch response. The server returns this in place of
216
+ // SearchEnhancedResponse when a single query fails — the rest of the batch
217
+ // still completes.
218
+ export interface SearchEnhancedBatchErrorSlot {
219
+ error: {
220
+ status_code: number;
221
+ detail: string;
222
+ };
223
+ query: string;
224
+ strategy_used: string;
225
+ }
226
+
227
+ export type SearchEnhancedBatchSlot =
228
+ | SearchEnhancedResponse
229
+ | SearchEnhancedBatchErrorSlot;
230
+
231
+ export interface SearchEnhancedBatchResponse {
232
+ results: SearchEnhancedBatchSlot[];
233
+ }
234
+
235
+ export function isBatchErrorSlot(
236
+ slot: SearchEnhancedBatchSlot,
237
+ ): slot is SearchEnhancedBatchErrorSlot {
238
+ return (
239
+ typeof (slot as SearchEnhancedBatchErrorSlot).error === 'object' &&
240
+ (slot as SearchEnhancedBatchErrorSlot).error !== null
241
+ );
242
+ }
@@ -32,6 +32,11 @@ export class ListChatSessionsDto extends UserAuthDto {
32
32
  @IsOptional()
33
33
  @Min(0)
34
34
  offset?: number;
35
+
36
+ /** Filter sessions by roomId. When set, only sessions in this room are returned. */
37
+ @IsString()
38
+ @IsOptional()
39
+ roomId?: string;
35
40
  }
36
41
 
37
42
  export class CreateChatSessionDto extends UserAuthDto {
@@ -66,6 +71,15 @@ export class CreateChatSessionDto extends UserAuthDto {
66
71
  @IsString()
67
72
  @IsOptional()
68
73
  userHomeServer?: string;
74
+
75
+ @IsString()
76
+ @IsOptional()
77
+ ucanInvocation?: string;
78
+
79
+ /** Override the roomId stored on the session (e.g. task-specific room). */
80
+ @IsString()
81
+ @IsOptional()
82
+ roomId?: string;
69
83
  }
70
84
 
71
85
  export class DeleteChatSessionDto extends UserAuthDto {