@ixo/common 1.1.2 → 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 (104) 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/nodes/index.d.ts +0 -1
  8. package/dist/ai/nodes/index.d.ts.map +1 -1
  9. package/dist/ai/nodes/index.js +0 -1
  10. package/dist/ai/nodes/index.js.map +1 -1
  11. package/dist/ai/semantic-router-factory/create-semantic-router.d.ts +1 -2
  12. package/dist/ai/semantic-router-factory/create-semantic-router.d.ts.map +1 -1
  13. package/dist/ai/semantic-router-factory/create-semantic-router.js +2 -3
  14. package/dist/ai/semantic-router-factory/create-semantic-router.js.map +1 -1
  15. package/dist/ai/tools/ask-ixo-guru/ask-ixo-guru.d.ts +1 -1
  16. package/dist/ai/tools/ask-ixo-guru/ask-ixo-guru.d.ts.map +1 -1
  17. package/dist/ai/tools/index.d.ts +0 -1
  18. package/dist/ai/tools/index.d.ts.map +1 -1
  19. package/dist/ai/tools/index.js +0 -1
  20. package/dist/ai/tools/index.js.map +1 -1
  21. package/dist/ai/tools/parser-action-tool.d.ts +1 -1
  22. package/dist/ai/tools/parser-action-tool.d.ts.map +1 -1
  23. package/dist/ai/tools/parser-action-tool.js +10 -3
  24. package/dist/ai/tools/parser-action-tool.js.map +1 -1
  25. package/dist/ai/tools/parser-browser-tool.d.ts +1 -1
  26. package/dist/ai/tools/parser-browser-tool.d.ts.map +1 -1
  27. package/dist/ai/tools/scrape-web-page.d.ts +1 -1
  28. package/dist/ai/tools/scrape-web-page.d.ts.map +1 -1
  29. package/dist/ai/tools/web-search-tool.d.ts +1 -1
  30. package/dist/ai/tools/web-search-tool.d.ts.map +1 -1
  31. package/dist/ai/utils/load-file.d.ts +1 -0
  32. package/dist/ai/utils/load-file.d.ts.map +1 -1
  33. package/dist/ai/utils/load-file.js +8 -0
  34. package/dist/ai/utils/load-file.js.map +1 -1
  35. package/dist/ai/utils/transformGraphStateMessageToListMessageResponse.d.ts +10 -0
  36. package/dist/ai/utils/transformGraphStateMessageToListMessageResponse.d.ts.map +1 -1
  37. package/dist/ai/utils/transformGraphStateMessageToListMessageResponse.js +4 -1
  38. package/dist/ai/utils/transformGraphStateMessageToListMessageResponse.js.map +1 -1
  39. package/dist/ai/utils/verify-matrix-openId-token.js +2 -2
  40. package/dist/ai/utils/verify-matrix-openId-token.js.map +1 -1
  41. package/dist/services/env/env.service.js.map +1 -1
  42. package/dist/services/memory-engine/memory-engine.service.d.ts +22 -16
  43. package/dist/services/memory-engine/memory-engine.service.d.ts.map +1 -1
  44. package/dist/services/memory-engine/memory-engine.service.js +136 -111
  45. package/dist/services/memory-engine/memory-engine.service.js.map +1 -1
  46. package/dist/services/memory-engine/types.d.ts +16 -0
  47. package/dist/services/memory-engine/types.d.ts.map +1 -1
  48. package/dist/services/memory-engine/types.js +4 -1
  49. package/dist/services/memory-engine/types.js.map +1 -1
  50. package/dist/services/session-manager/dto.d.ts +7 -0
  51. package/dist/services/session-manager/dto.d.ts.map +1 -1
  52. package/dist/services/session-manager/dto.js +42 -0
  53. package/dist/services/session-manager/dto.js.map +1 -1
  54. package/dist/services/session-manager/session-manager.service.d.ts +2 -2
  55. package/dist/services/session-manager/session-manager.service.d.ts.map +1 -1
  56. package/dist/services/session-manager/session-manager.service.js +81 -36
  57. package/dist/services/session-manager/session-manager.service.js.map +1 -1
  58. package/dist/utils/get-user-subscription.d.ts +2 -1
  59. package/dist/utils/get-user-subscription.d.ts.map +1 -1
  60. package/dist/utils/get-user-subscription.js +11 -7
  61. package/dist/utils/get-user-subscription.js.map +1 -1
  62. package/package.json +24 -25
  63. package/src/ai/models/openai.ts +31 -0
  64. package/src/ai/nodes/index.ts +0 -1
  65. package/src/ai/semantic-router-factory/create-semantic-router.test.ts +0 -3
  66. package/src/ai/semantic-router-factory/create-semantic-router.ts +2 -7
  67. package/src/ai/tools/index.ts +0 -1
  68. package/src/ai/tools/parser-action-tool.ts +19 -3
  69. package/src/ai/utils/load-file.ts +17 -0
  70. package/src/ai/utils/transformGraphStateMessageToListMessageResponse.ts +18 -1
  71. package/src/ai/utils/verify-matrix-openId-token.ts +2 -2
  72. package/src/services/env/env.service.ts +1 -1
  73. package/src/services/memory-engine/memory-engine.service.ts +228 -240
  74. package/src/services/memory-engine/types.ts +34 -0
  75. package/src/services/session-manager/dto.ts +30 -0
  76. package/src/services/session-manager/session-manager.service.ts +98 -38
  77. package/src/utils/get-user-subscription.ts +13 -6
  78. package/tsconfig.tsbuildinfo +1 -1
  79. package/dist/ai/nodes/find-docs/find-docs.prompt.d.ts +0 -3
  80. package/dist/ai/nodes/find-docs/find-docs.prompt.d.ts.map +0 -1
  81. package/dist/ai/nodes/find-docs/find-docs.prompt.js +0 -61
  82. package/dist/ai/nodes/find-docs/find-docs.prompt.js.map +0 -1
  83. package/dist/ai/nodes/find-docs/index.d.ts +0 -3
  84. package/dist/ai/nodes/find-docs/index.d.ts.map +0 -1
  85. package/dist/ai/nodes/find-docs/index.js +0 -3
  86. package/dist/ai/nodes/find-docs/index.js.map +0 -1
  87. package/dist/ai/nodes/find-docs/node.d.ts +0 -17
  88. package/dist/ai/nodes/find-docs/node.d.ts.map +0 -1
  89. package/dist/ai/nodes/find-docs/node.js +0 -46
  90. package/dist/ai/nodes/find-docs/node.js.map +0 -1
  91. package/dist/ai/tools/retriever-tool/index.d.ts +0 -2
  92. package/dist/ai/tools/retriever-tool/index.d.ts.map +0 -1
  93. package/dist/ai/tools/retriever-tool/index.js +0 -2
  94. package/dist/ai/tools/retriever-tool/index.js.map +0 -1
  95. package/dist/ai/tools/retriever-tool/retriever-tool.d.ts +0 -18
  96. package/dist/ai/tools/retriever-tool/retriever-tool.d.ts.map +0 -1
  97. package/dist/ai/tools/retriever-tool/retriever-tool.js +0 -62
  98. package/dist/ai/tools/retriever-tool/retriever-tool.js.map +0 -1
  99. package/src/ai/nodes/find-docs/find-docs.prompt.ts +0 -61
  100. package/src/ai/nodes/find-docs/index.ts +0 -2
  101. package/src/ai/nodes/find-docs/node.ts +0 -83
  102. package/src/ai/tools/retriever-tool/index.ts +0 -1
  103. package/src/ai/tools/retriever-tool/retriever-tool.test.ts +0 -163
  104. package/src/ai/tools/retriever-tool/retriever-tool.ts +0 -107
@@ -1,32 +1,52 @@
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
 
11
+ interface MemoryEngineAuthHeaders {
12
+ oracleToken: string;
13
+ userToken: string;
14
+ oracleHomeServer: string;
15
+ userHomeServer: string;
16
+ /** When set, uses UCAN auth instead of Matrix tokens */
17
+ ucanInvocation?: string;
18
+ }
19
+
8
20
  export class MemoryEngineService {
9
- 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;
10
24
 
11
- constructor(
12
- private readonly memoryEngineUrl: string,
13
- private readonly memoryServiceApiKey: string,
14
- ) {}
25
+ constructor(private readonly memoryEngineUrl: string) {}
15
26
 
16
27
  /**
17
- * Wraps a promise with a timeout, returning fallback value if timeout is exceeded
28
+ * Build HTTP headers for memory engine requests (UCAN or Matrix)
18
29
  */
19
- private async withTimeout<T>(
20
- promise: Promise<T>,
21
- timeoutMs: number,
22
- fallback: T,
23
- ): Promise<T> {
24
- return Promise.race([
25
- promise,
26
- new Promise<T>((resolve) =>
27
- setTimeout(() => resolve(fallback), timeoutMs),
28
- ),
29
- ]);
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
+ };
30
50
  }
31
51
 
32
52
  /**
@@ -34,101 +54,175 @@ export class MemoryEngineService {
34
54
  */
35
55
  async gatherUserContext(params: {
36
56
  oracleDid: string;
37
- userDid: string;
38
57
  roomId: string;
58
+ oracleToken: string;
59
+ userToken: string;
60
+ oracleHomeServer: string;
61
+ userHomeServer: string;
62
+ /** When set, uses UCAN auth instead of Matrix tokens */
63
+ ucanInvocation?: string;
39
64
  }): Promise<UserContextData> {
40
- const { oracleDid, userDid, roomId } = params;
65
+ const {
66
+ oracleDid,
67
+ roomId,
68
+ oracleToken,
69
+ userToken,
70
+ oracleHomeServer,
71
+ userHomeServer,
72
+ ucanInvocation,
73
+ } = params;
41
74
 
42
75
  Logger.info(
43
76
  `[MemoryEngineService] Gathering user context for oracle: ${oracleDid}, room: ${roomId}`,
44
77
  );
45
78
 
46
- try {
47
- // Execute all 6 queries in parallel with timeouts using Promise.allSettled
48
- const results = await Promise.allSettled([
49
- this.withTimeout(
50
- this.queryIdentity(oracleDid, userDid, roomId),
51
- this.QUERY_TIMEOUT_MS,
52
- undefined,
53
- ),
54
- this.withTimeout(
55
- this.queryWork(oracleDid, userDid, roomId),
56
- this.QUERY_TIMEOUT_MS,
57
- undefined,
58
- ),
59
- this.withTimeout(
60
- this.queryGoals(oracleDid, userDid, roomId),
61
- this.QUERY_TIMEOUT_MS,
62
- undefined,
63
- ),
64
- this.withTimeout(
65
- this.queryInterests(oracleDid, userDid, roomId),
66
- this.QUERY_TIMEOUT_MS,
67
- undefined,
68
- ),
69
- this.withTimeout(
70
- this.queryRelationships(oracleDid, userDid, roomId),
71
- this.QUERY_TIMEOUT_MS,
72
- undefined,
73
- ),
74
- this.withTimeout(
75
- this.queryRecent(oracleDid, userDid, roomId),
76
- this.QUERY_TIMEOUT_MS,
77
- undefined,
78
- ),
79
- ]);
80
-
81
- // Extract results from Promise.allSettled outcomes
82
- const identity =
83
- results[0].status === 'fulfilled' ? results[0].value : undefined;
84
- const work =
85
- results[1].status === 'fulfilled' ? results[1].value : undefined;
86
- const goals =
87
- results[2].status === 'fulfilled' ? results[2].value : undefined;
88
- const interests =
89
- results[3].status === 'fulfilled' ? results[3].value : undefined;
90
- const relationships =
91
- results[4].status === 'fulfilled' ? results[4].value : undefined;
92
- const recent =
93
- results[5].status === 'fulfilled' ? results[5].value : undefined;
94
-
95
- // Log any failures
96
- results.forEach((result, index) => {
97
- if (result.status === 'rejected') {
79
+ const authHeaders: MemoryEngineAuthHeaders = {
80
+ oracleToken,
81
+ userToken,
82
+ oracleHomeServer,
83
+ userHomeServer,
84
+ ucanInvocation,
85
+ };
86
+
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)) {
98
122
  Logger.warn(
99
- `[MemoryEngineService] Query ${index} failed:`,
100
- result.reason,
123
+ `[MemoryEngineService] Batch slot "${label}" failed (${slot.error.status_code}): ${slot.error.detail}`,
101
124
  );
125
+ return undefined;
102
126
  }
103
- });
127
+ return slot;
128
+ },
129
+ );
104
130
 
105
- return {
106
- identity,
107
- work,
108
- goals,
109
- interests,
110
- relationships,
111
- recent,
112
- };
113
- } catch (error) {
114
- Logger.error(
115
- '[MemoryEngineService] Failed to gather user context:',
116
- error,
131
+ if (batch.results.length !== labels.length) {
132
+ Logger.warn(
133
+ `[MemoryEngineService] Batch length mismatch: expected ${labels.length}, got ${batch.results.length}`,
117
134
  );
118
- // Return empty context on error
119
- return {};
120
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
+ };
121
154
  }
122
155
 
123
156
  /**
124
- * 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`).
125
161
  */
126
- private async queryIdentity(
127
- oracleDid: string,
128
- userDid: string,
162
+ private async executeBatch(
163
+ queries: SearchEnhancedRequest[],
129
164
  roomId: string,
130
- ): Promise<SearchEnhancedResponse | undefined> {
131
- const request: SearchEnhancedRequest = {
165
+ auth: MemoryEngineAuthHeaders,
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 {
132
226
  oracle_dids: [oracleDid],
133
227
  query:
134
228
  'username and nickname and age user identity traits values personality characteristics communication style beliefs preferences',
@@ -152,19 +246,10 @@ export class MemoryEngineService {
152
246
  invalid_at: [[{ date: null, comparison_operator: 'IS NULL' }]],
153
247
  },
154
248
  };
155
-
156
- return this.executeQuery(request, userDid, oracleDid, roomId);
157
249
  }
158
250
 
159
- /**
160
- * Query 2: Work Context
161
- */
162
- private async queryWork(
163
- oracleDid: string,
164
- userDid: string,
165
- roomId: string,
166
- ): Promise<SearchEnhancedResponse | undefined> {
167
- const request: SearchEnhancedRequest = {
251
+ private buildWorkRequest(oracleDid: string): SearchEnhancedRequest {
252
+ return {
168
253
  oracle_dids: [oracleDid],
169
254
  query:
170
255
  'work job career projects skills organization employment role responsibilities expertise',
@@ -195,19 +280,10 @@ export class MemoryEngineService {
195
280
  invalid_at: [[{ date: null, comparison_operator: 'IS NULL' }]],
196
281
  },
197
282
  };
198
-
199
- return this.executeQuery(request, userDid, oracleDid, roomId);
200
283
  }
201
284
 
202
- /**
203
- * Query 3: Goals & Habits
204
- */
205
- private async queryGoals(
206
- oracleDid: string,
207
- userDid: string,
208
- roomId: string,
209
- ): Promise<SearchEnhancedResponse | undefined> {
210
- const request: SearchEnhancedRequest = {
285
+ private buildGoalsRequest(oracleDid: string): SearchEnhancedRequest {
286
+ return {
211
287
  oracle_dids: [oracleDid],
212
288
  query:
213
289
  'goals aspirations objectives milestones habits routines patterns achievements progress',
@@ -230,19 +306,10 @@ export class MemoryEngineService {
230
306
  invalid_at: [[{ date: null, comparison_operator: 'IS NULL' }]],
231
307
  },
232
308
  };
233
-
234
- return this.executeQuery(request, userDid, oracleDid, roomId);
235
309
  }
236
310
 
237
- /**
238
- * Query 4: Interests & Preferences
239
- */
240
- private async queryInterests(
241
- oracleDid: string,
242
- userDid: string,
243
- roomId: string,
244
- ): Promise<SearchEnhancedResponse | undefined> {
245
- const request: SearchEnhancedRequest = {
311
+ private buildInterestsRequest(oracleDid: string): SearchEnhancedRequest {
312
+ return {
246
313
  oracle_dids: [oracleDid],
247
314
  query:
248
315
  'interests hobbies passions preferences likes dislikes expertise topics content',
@@ -272,19 +339,10 @@ export class MemoryEngineService {
272
339
  invalid_at: [[{ date: null, comparison_operator: 'IS NULL' }]],
273
340
  },
274
341
  };
275
-
276
- return this.executeQuery(request, userDid, oracleDid, roomId);
277
342
  }
278
343
 
279
- /**
280
- * Query 5: Relationships
281
- */
282
- private async queryRelationships(
283
- oracleDid: string,
284
- userDid: string,
285
- roomId: string,
286
- ): Promise<SearchEnhancedResponse | undefined> {
287
- const request: SearchEnhancedRequest = {
344
+ private buildRelationshipsRequest(oracleDid: string): SearchEnhancedRequest {
345
+ return {
288
346
  oracle_dids: [oracleDid],
289
347
  query:
290
348
  'relationships people connections social network colleagues friends family contacts',
@@ -307,24 +365,17 @@ export class MemoryEngineService {
307
365
  invalid_at: [[{ date: null, comparison_operator: 'IS NULL' }]],
308
366
  },
309
367
  };
310
-
311
- return this.executeQuery(request, userDid, oracleDid, roomId);
312
368
  }
313
369
 
314
- /**
315
- * Query 6: Recent Context
316
- */
317
- private async queryRecent(
318
- oracleDid: string,
319
- userDid: string,
320
- roomId: string,
321
- ): Promise<SearchEnhancedResponse | undefined> {
322
- // 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.
323
374
  const ninetyDaysAgo = new Date();
324
375
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
325
376
  const dateString = ninetyDaysAgo.toISOString();
326
377
 
327
- const request: SearchEnhancedRequest = {
378
+ return {
328
379
  oracle_dids: [oracleDid],
329
380
  query:
330
381
  'recent conversations messages discussions activities updates interactions',
@@ -338,8 +389,6 @@ export class MemoryEngineService {
338
389
  created_at: [[{ date: dateString, comparison_operator: '>=' }]],
339
390
  },
340
391
  };
341
-
342
- return this.executeQuery(request, userDid, oracleDid, roomId);
343
392
  }
344
393
 
345
394
  /**
@@ -347,9 +396,12 @@ export class MemoryEngineService {
347
396
  */
348
397
  async processConversationHistory({
349
398
  messages,
350
- userDid,
351
- oracleDid,
352
399
  roomId,
400
+ oracleToken,
401
+ userToken,
402
+ oracleHomeServer,
403
+ userHomeServer,
404
+ ucanInvocation,
353
405
  }: {
354
406
  messages: Array<{
355
407
  content: string;
@@ -358,25 +410,23 @@ export class MemoryEngineService {
358
410
  name?: string;
359
411
  source_description?: string;
360
412
  }>;
361
- userDid: string;
362
- oracleDid: string;
363
413
  roomId: string;
414
+ oracleToken: string;
415
+ userToken: string;
416
+ oracleHomeServer: string;
417
+ userHomeServer: string;
418
+ /** When set, uses UCAN auth instead of Matrix tokens */
419
+ ucanInvocation?: string;
364
420
  }): Promise<{ success: boolean }> {
365
- if (!userDid) {
366
- Logger.warn(
367
- `[MemoryEngineService] No user DID provided, skipping conversation processing`,
368
- );
369
- return { success: false };
370
- }
371
- if (!oracleDid) {
421
+ if (!roomId) {
372
422
  Logger.warn(
373
- `[MemoryEngineService] No oracle did provided, skipping conversation processing`,
423
+ `[MemoryEngineService] No room id provided, skipping conversation processing`,
374
424
  );
375
425
  return { success: false };
376
426
  }
377
- if (!roomId) {
427
+ if (!ucanInvocation && (!oracleToken || !userToken)) {
378
428
  Logger.warn(
379
- `[MemoryEngineService] No room id provided, skipping conversation processing`,
429
+ `[MemoryEngineService] Missing auth (no UCAN and no Matrix tokens), skipping conversation processing`,
380
430
  );
381
431
  return { success: false };
382
432
  }
@@ -388,15 +438,16 @@ export class MemoryEngineService {
388
438
  }
389
439
 
390
440
  try {
441
+ const auth: MemoryEngineAuthHeaders = {
442
+ oracleToken,
443
+ userToken,
444
+ oracleHomeServer,
445
+ userHomeServer,
446
+ ucanInvocation,
447
+ };
391
448
  const response = await fetch(`${this.memoryEngineUrl}/messages`, {
392
449
  method: 'POST',
393
- headers: {
394
- 'x-user-did': userDid,
395
- 'x-oracle-did': oracleDid,
396
- 'x-room-id': roomId,
397
- 'x-service-api-key': this.memoryServiceApiKey,
398
- 'Content-Type': 'application/json',
399
- },
450
+ headers: this.buildHeaders(auth, roomId),
400
451
  body: JSON.stringify({ messages }),
401
452
  });
402
453
 
@@ -420,67 +471,4 @@ export class MemoryEngineService {
420
471
  return { success: false };
421
472
  }
422
473
  }
423
-
424
- /**
425
- * Execute a search query against the Memory Engine API
426
- */
427
- private async executeQuery(
428
- request: SearchEnhancedRequest,
429
- userDid: string,
430
- oracleDid: string,
431
- roomId: string,
432
- ): Promise<SearchEnhancedResponse | undefined> {
433
- if (!userDid) {
434
- Logger.warn(
435
- `[MemoryEngineService] No user DID provided, skipping query "${request.query}"`,
436
- );
437
- return undefined;
438
- }
439
- if (!oracleDid) {
440
- Logger.warn(
441
- `[MemoryEngineService] No oracle did provided, skipping query "${request.query}"`,
442
- );
443
- return undefined;
444
- }
445
- if (!roomId) {
446
- Logger.warn(
447
- `[MemoryEngineService] No room id provided, skipping query "${request.query}"`,
448
- );
449
- return undefined;
450
- }
451
-
452
- try {
453
- const response = await fetch(`${this.memoryEngineUrl}/search-enhanced`, {
454
- method: 'POST',
455
- headers: {
456
- 'x-user-did': userDid,
457
- 'x-oracle-did': oracleDid,
458
- 'x-room-id': roomId,
459
- 'x-service-api-key': this.memoryServiceApiKey,
460
- 'Content-Type': 'application/json',
461
- },
462
- body: JSON.stringify(request),
463
- });
464
-
465
- if (!response.ok) {
466
- const errorText = await response.text();
467
- Logger.warn(
468
- `[MemoryEngineService] Memory Engine query failed (${response.status}): ${errorText}`,
469
- );
470
- return undefined;
471
- }
472
-
473
- const result = (await response.json()) as SearchEnhancedResponse;
474
- Logger.info(
475
- `[MemoryEngineService] Query "${request.query}" returned ${result.total_results.facts} facts, ${result.total_results.entities} entities`,
476
- );
477
- return result;
478
- } catch (error) {
479
- Logger.error(
480
- `[MemoryEngineService] Failed to execute query "${request.query}":`,
481
- error,
482
- );
483
- return undefined;
484
- }
485
- }
486
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 {
@@ -50,6 +55,31 @@ export class CreateChatSessionDto extends UserAuthDto {
50
55
  @IsString()
51
56
  @IsOptional()
52
57
  slackThreadTs?: string;
58
+
59
+ @IsString()
60
+ @IsOptional()
61
+ oracleToken?: string;
62
+
63
+ @IsString()
64
+ @IsOptional()
65
+ userToken?: string;
66
+
67
+ @IsString()
68
+ @IsOptional()
69
+ oracleHomeServer?: string;
70
+
71
+ @IsString()
72
+ @IsOptional()
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;
53
83
  }
54
84
 
55
85
  export class DeleteChatSessionDto extends UserAuthDto {