@aj-archipelago/cortex 1.4.6 → 1.4.7

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 (38) hide show
  1. package/helper-apps/cortex-file-handler/package-lock.json +2 -2
  2. package/helper-apps/cortex-file-handler/package.json +1 -1
  3. package/helper-apps/cortex-file-handler/src/index.js +27 -4
  4. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +74 -10
  5. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +23 -2
  6. package/helper-apps/cortex-file-handler/src/start.js +2 -0
  7. package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +287 -0
  8. package/helper-apps/cortex-file-handler/tests/start.test.js +1 -1
  9. package/lib/entityConstants.js +1 -1
  10. package/lib/fileUtils.js +1481 -0
  11. package/lib/pathwayTools.js +7 -1
  12. package/lib/util.js +2 -313
  13. package/package.json +4 -3
  14. package/pathways/image_qwen.js +1 -1
  15. package/pathways/system/entity/memory/sys_read_memory.js +17 -3
  16. package/pathways/system/entity/memory/sys_save_memory.js +22 -6
  17. package/pathways/system/entity/sys_entity_agent.js +21 -4
  18. package/pathways/system/entity/tools/sys_tool_analyzefile.js +171 -0
  19. package/pathways/system/entity/tools/sys_tool_codingagent.js +38 -4
  20. package/pathways/system/entity/tools/sys_tool_editfile.js +403 -0
  21. package/pathways/system/entity/tools/sys_tool_file_collection.js +433 -0
  22. package/pathways/system/entity/tools/sys_tool_image.js +172 -10
  23. package/pathways/system/entity/tools/sys_tool_image_gemini.js +123 -10
  24. package/pathways/system/entity/tools/sys_tool_readfile.js +217 -124
  25. package/pathways/system/entity/tools/sys_tool_validate_url.js +137 -0
  26. package/pathways/system/entity/tools/sys_tool_writefile.js +211 -0
  27. package/pathways/system/workspaces/run_workspace_prompt.js +4 -3
  28. package/pathways/transcribe_gemini.js +2 -1
  29. package/server/executeWorkspace.js +1 -1
  30. package/server/plugins/neuralSpacePlugin.js +2 -6
  31. package/server/plugins/openAiWhisperPlugin.js +2 -1
  32. package/server/plugins/replicateApiPlugin.js +4 -14
  33. package/server/typeDef.js +10 -1
  34. package/tests/integration/features/tools/fileCollection.test.js +858 -0
  35. package/tests/integration/features/tools/fileOperations.test.js +851 -0
  36. package/tests/integration/features/tools/writefile.test.js +350 -0
  37. package/tests/unit/core/fileCollection.test.js +259 -0
  38. package/tests/unit/core/util.test.js +320 -1
@@ -63,7 +63,13 @@ const callTool = async (toolName, args, toolDefinitions, pathwayResolver) => {
63
63
  throw new Error(`Tool ${toolName} not found in available tools`);
64
64
  }
65
65
 
66
- logger.debug(`callTool: Starting execution of ${toolName}`);
66
+ // Create a sanitized copy of args for logging (exclude large objects like chatHistory)
67
+ const logArgs = { ...args };
68
+ if (logArgs.chatHistory) {
69
+ logArgs.chatHistory = `[${Array.isArray(logArgs.chatHistory) ? logArgs.chatHistory.length : 'N/A'} messages]`;
70
+ }
71
+
72
+ logger.debug(`callTool: Starting execution of ${toolName} ${JSON.stringify(logArgs)}`);
67
73
 
68
74
  try {
69
75
  const pathwayName = toolDef.pathwayName;
package/lib/util.js CHANGED
@@ -1,20 +1,8 @@
1
1
  import logger from "./logger.js";
2
- import stream from 'stream';
3
2
  import subvibe from '@aj-archipelago/subvibe';
4
- import os from 'os';
5
- import http from 'http';
6
- import https from 'https';
7
3
  import { URL } from 'url';
8
4
  import { v4 as uuidv4 } from 'uuid';
9
- import { promisify } from 'util';
10
- import { axios } from './requestExecutor.js';
11
- import { config } from '../config.js';
12
- import fs from 'fs';
13
- import path from 'path';
14
- import FormData from 'form-data';
15
5
 
16
- const pipeline = promisify(stream.pipeline);
17
- const MEDIA_API_URL = config.get('whisperMediaApiUrl');
18
6
 
19
7
  function getUniqueId(){
20
8
  return uuidv4();
@@ -107,70 +95,6 @@ function chatArgsHasImageUrl(args){
107
95
  return chatArgsHasType(args, 'image_url');
108
96
  }
109
97
 
110
-
111
- async function deleteTempPath(path) {
112
- try {
113
- if (!path) {
114
- logger.warn('Temporary path is not defined.');
115
- return;
116
- }
117
- if (!fs.existsSync(path)) {
118
- logger.warn(`Temporary path ${path} does not exist.`);
119
- return;
120
- }
121
- const stats = fs.statSync(path);
122
- if (stats.isFile()) {
123
- fs.unlinkSync(path);
124
- logger.info(`Temporary file ${path} deleted successfully.`);
125
- } else if (stats.isDirectory()) {
126
- fs.rmSync(path, { recursive: true });
127
- logger.info(`Temporary folder ${path} and its contents deleted successfully.`);
128
- }
129
- } catch (err) {
130
- logger.error(`Error occurred while deleting the temporary path: ${err}`);
131
- }
132
- }
133
-
134
- function generateUniqueFilename(extension) {
135
- return `${uuidv4()}.${extension}`;
136
- }
137
-
138
- const downloadFile = async (fileUrl) => {
139
- const urlObj = new URL(fileUrl);
140
- const pathname = urlObj.pathname;
141
- const fileExtension = pathname.substring(pathname.lastIndexOf('.') + 1);
142
- const uniqueFilename = generateUniqueFilename(fileExtension);
143
- const tempDir = os.tmpdir();
144
- const localFilePath = `${tempDir}/${uniqueFilename}`;
145
-
146
- // eslint-disable-next-line no-async-promise-executor
147
- return new Promise(async (resolve, reject) => {
148
- try {
149
- const parsedUrl = new URL(fileUrl);
150
- const protocol = parsedUrl.protocol === 'https:' ? https : http;
151
-
152
- const response = await new Promise((resolve, reject) => {
153
- protocol.get(parsedUrl, (res) => {
154
- if (res.statusCode === 200) {
155
- resolve(res);
156
- } else {
157
- reject(new Error(`HTTP request failed with status code ${res.statusCode}`));
158
- }
159
- }).on('error', reject);
160
- });
161
-
162
- await pipeline(response, fs.createWriteStream(localFilePath));
163
- logger.info(`Downloaded file to ${localFilePath}`);
164
- resolve(localFilePath);
165
- } catch (error) {
166
- fs.unlink(localFilePath, () => {
167
- reject(error);
168
- });
169
- //throw error;
170
- }
171
- });
172
- };
173
-
174
98
  // convert srt format to text
175
99
  function convertSrtToText(str) {
176
100
  return str
@@ -223,36 +147,6 @@ function alignSubtitles(subtitles, format, offsets) {
223
147
  }
224
148
  }
225
149
 
226
-
227
- async function getMediaChunks(file, requestId) {
228
- try {
229
- if (MEDIA_API_URL) {
230
- //call helper api and get list of file uris
231
- const res = await axios.get(MEDIA_API_URL, { params: { uri: file, requestId } });
232
- return res.data;
233
- } else {
234
- logger.info(`No API_URL set, returning file as chunk`);
235
- return [file];
236
- }
237
- } catch (err) {
238
- logger.error(`Error getting media chunks list from api: ${err}`);
239
- throw err;
240
- }
241
- }
242
-
243
- async function markCompletedForCleanUp(requestId) {
244
- try {
245
- if (MEDIA_API_URL) {
246
- //call helper api to mark processing as completed
247
- const res = await axios.delete(MEDIA_API_URL, { params: { requestId } });
248
- logger.info(`Marked request ${requestId} as completed: ${JSON.stringify(res.data)}`);
249
- return res.data;
250
- }
251
- } catch (err) {
252
- logger.error(`Error marking request ${requestId} as completed: ${err}`);
253
- }
254
- }
255
-
256
150
  function removeOldImageAndFileContent(chatHistory) {
257
151
  if (!chatHistory || !Array.isArray(chatHistory) || chatHistory.length === 0) {
258
152
  return chatHistory;
@@ -387,204 +281,6 @@ function removeImageAndFileFromMessage(message) {
387
281
  return modifiedMessage;
388
282
  }
389
283
 
390
- // Helper function to extract file URLs from a content object
391
- function extractFileUrlsFromContent(contentObj) {
392
- const urls = [];
393
- if (contentObj.type === 'image_url' && contentObj.image_url?.url) {
394
- urls.push(contentObj.image_url.url);
395
- } else if (contentObj.type === 'file' && contentObj.file) {
396
- urls.push(contentObj.file);
397
- }
398
- return urls;
399
- }
400
-
401
- function getAvailableFiles(chatHistory) {
402
- const availableFiles = [];
403
-
404
- if (!chatHistory || !Array.isArray(chatHistory)) {
405
- return availableFiles;
406
- }
407
-
408
- for (const message of chatHistory) {
409
- if (!message || !message.content) {
410
- continue;
411
- }
412
-
413
- // Handle array content
414
- if (Array.isArray(message.content)) {
415
- for (const content of message.content) {
416
- try {
417
- const contentObj = typeof content === 'string' ? JSON.parse(content) : content;
418
- availableFiles.push(...extractFileUrlsFromContent(contentObj));
419
- } catch (e) {
420
- // Not JSON or couldn't be parsed, continue
421
- continue;
422
- }
423
- }
424
- }
425
- // Handle string content
426
- else if (typeof message.content === 'string') {
427
- try {
428
- const contentObj = JSON.parse(message.content);
429
- availableFiles.push(...extractFileUrlsFromContent(contentObj));
430
- } catch (e) {
431
- // Not JSON or couldn't be parsed, continue
432
- continue;
433
- }
434
- }
435
- // Handle object content
436
- else if (typeof message.content === 'object') {
437
- availableFiles.push(...extractFileUrlsFromContent(message.content));
438
- }
439
- }
440
-
441
- return availableFiles;
442
- }
443
-
444
- // Helper function to upload base64 image data to cloud storage
445
- const uploadImageToCloud = async (base64Data, mimeType, pathwayResolver = null) => {
446
- let tempFilePath = null;
447
- let tempDir = null;
448
-
449
- try {
450
- // Convert base64 to buffer
451
- const imageBuffer = Buffer.from(base64Data, 'base64');
452
-
453
- // Determine file extension from mime type
454
- const extension = mimeType.split('/')[1] || 'png';
455
- const filename = `generated_image_${Date.now()}.${extension}`;
456
-
457
- // Create temporary file
458
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'image-upload-'));
459
- tempFilePath = path.join(tempDir, filename);
460
-
461
- // Write buffer to temp file
462
- fs.writeFileSync(tempFilePath, imageBuffer);
463
-
464
- // Upload to file handler service
465
- const fileHandlerUrl = MEDIA_API_URL;
466
- if (!fileHandlerUrl) {
467
- throw new Error('WHISPER_MEDIA_API_URL is not set');
468
- }
469
- const requestId = uuidv4();
470
-
471
- // Create form data for upload
472
- const formData = new FormData();
473
- formData.append('file', fs.createReadStream(tempFilePath), {
474
- filename: filename,
475
- contentType: mimeType
476
- });
477
-
478
- // Append requestId parameter (preserving existing query parameters like subscription-key)
479
- const separator = fileHandlerUrl.includes('?') ? '&' : '?';
480
- const uploadUrl = `${fileHandlerUrl}${separator}requestId=${requestId}`;
481
-
482
- // Upload file
483
- const uploadResponse = await axios.post(uploadUrl, formData, {
484
- headers: {
485
- ...formData.getHeaders()
486
- },
487
- timeout: 30000
488
- });
489
-
490
- if (uploadResponse.data && uploadResponse.data.url) {
491
- return uploadResponse.data.url;
492
- } else {
493
- throw new Error('No URL returned from file handler');
494
- }
495
-
496
- } catch (error) {
497
- const errorMessage = `Failed to upload image: ${error.message}`;
498
- if (pathwayResolver && pathwayResolver.logError) {
499
- pathwayResolver.logError(errorMessage);
500
- } else {
501
- logger.error(errorMessage);
502
- }
503
- throw error;
504
- } finally {
505
- // Clean up temp files
506
- if (tempFilePath && fs.existsSync(tempFilePath)) {
507
- try {
508
- fs.unlinkSync(tempFilePath);
509
- } catch (cleanupError) {
510
- const warningMessage = `Failed to clean up temp file: ${cleanupError.message}`;
511
- if (pathwayResolver && pathwayResolver.logWarning) {
512
- pathwayResolver.logWarning(warningMessage);
513
- } else {
514
- logger.warn(warningMessage);
515
- }
516
- }
517
- }
518
- if (tempDir && fs.existsSync(tempDir)) {
519
- try {
520
- fs.rmdirSync(tempDir);
521
- } catch (cleanupError) {
522
- const warningMessage = `Failed to clean up temp directory: ${cleanupError.message}`;
523
- if (pathwayResolver && pathwayResolver.logWarning) {
524
- pathwayResolver.logWarning(warningMessage);
525
- } else {
526
- logger.warn(warningMessage);
527
- }
528
- }
529
- }
530
- }
531
- };
532
-
533
- /**
534
- * Convert file hashes to content format suitable for LLM processing
535
- * @param {Array<string>} fileHashes - Array of file hashes to resolve
536
- * @param {Object} config - Configuration object with file service endpoints
537
- * @returns {Promise<Array<string>>} Array of stringified file content objects
538
- */
539
- async function resolveFileHashesToContent(fileHashes, config) {
540
- if (!fileHashes || fileHashes.length === 0) return [];
541
-
542
- const fileContentPromises = fileHashes.map(async (hash) => {
543
- try {
544
- // Use the existing file handler (cortex-file-handler) to resolve file hashes
545
- const fileHandlerUrl = config?.get?.('whisperMediaApiUrl');
546
-
547
- if (fileHandlerUrl && fileHandlerUrl !== 'null') {
548
- // Make request to file handler to get file content by hash
549
- const response = await axios.get(fileHandlerUrl, {
550
- params: { hash: hash, checkHash: true }
551
- });
552
- if (response.status === 200) {
553
- const fileData = response.data;
554
- const fileUrl = fileData.shortLivedUrl || fileData.url;
555
- const convertedUrl = fileData.converted?.url;
556
- const convertedGcsUrl = fileData.converted?.gcs;
557
-
558
- return JSON.stringify({
559
- type: "image_url",
560
- url: convertedUrl,
561
- image_url: { url: convertedUrl },
562
- gcs: convertedGcsUrl || fileData.gcs, // Add GCS URL for Gemini models
563
- originalFilename: fileData.filename,
564
- hash: hash
565
- });
566
- }
567
- }
568
-
569
- // Fallback: create a placeholder that indicates file resolution is needed
570
- return JSON.stringify({
571
- type: "file_hash",
572
- hash: hash,
573
- _cortex_needs_resolution: true
574
- });
575
- } catch (error) {
576
- // Return error indicator
577
- return JSON.stringify({
578
- type: "file_error",
579
- hash: hash,
580
- error: error.message
581
- });
582
- }
583
- });
584
-
585
- return Promise.all(fileContentPromises);
586
- }
587
-
588
284
  export {
589
285
  getUniqueId,
590
286
  getSearchResultId,
@@ -592,14 +288,7 @@ export {
592
288
  convertToSingleContentChatHistory,
593
289
  chatArgsHasImageUrl,
594
290
  chatArgsHasType,
595
- deleteTempPath,
596
- downloadFile,
597
- uploadImageToCloud,
598
291
  convertSrtToText,
599
292
  alignSubtitles,
600
- getMediaChunks,
601
- markCompletedForCleanUp,
602
- removeOldImageAndFileContent,
603
- getAvailableFiles,
604
- resolveFileHashesToContent
605
- };
293
+ removeOldImageAndFileContent
294
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
4
4
  "description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
5
5
  "private": false,
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "type": "module",
34
34
  "homepage": "https://github.com/aj-archipelago/cortex#readme",
35
35
  "dependencies": {
36
- "@aj-archipelago/merval": "^1.0.3",
36
+ "@aj-archipelago/merval": "^1.0.4",
37
37
  "@aj-archipelago/subvibe": "^1.0.12",
38
38
  "@apollo/server": "^4.7.3",
39
39
  "@apollo/server-plugin-response-cache": "^4.1.2",
@@ -67,7 +67,8 @@
67
67
  "mime-types": "^2.1.35",
68
68
  "uuid": "^9.0.0",
69
69
  "winston": "^3.11.0",
70
- "ws": "^8.12.0"
70
+ "ws": "^8.12.0",
71
+ "xxhash-wasm": "^1.1.0"
71
72
  },
72
73
  "devDependencies": {
73
74
  "@faker-js/faker": "^8.4.1",
@@ -7,7 +7,7 @@ export default {
7
7
  negativePrompt: "",
8
8
  width: 1024,
9
9
  height: 1024,
10
- aspectRatio: "16:9", // Options: "1:1", "16:9", "9:16", "4:3", "3:4", "match_input_image" (use "match_input_image" for qwen-image-edit-plus)
10
+ aspectRatio: "match_input_image", // Options: "1:1", "16:9", "9:16", "4:3", "3:4", "match_input_image" (use "match_input_image" for qwen-image-edit-plus)
11
11
  numberResults: 1,
12
12
  output_format: "webp",
13
13
  output_quality: 80, // Use 95 for qwen-image-edit-plus
@@ -99,19 +99,33 @@ export default {
99
99
  return savedContext.memoryContext || "";
100
100
  }
101
101
 
102
- const validSections = ['memorySelf', 'memoryDirectives', 'memoryTopics', 'memoryUser', 'memoryContext', 'memoryVersion'];
102
+ const validSections = ['memorySelf', 'memoryDirectives', 'memoryTopics', 'memoryUser', 'memoryContext', 'memoryVersion', 'memoryFiles'];
103
+ // memoryFiles can only be accessed explicitly, not as part of memoryAll
104
+ const allSections = ['memorySelf', 'memoryDirectives', 'memoryTopics', 'memoryUser', 'memoryContext', 'memoryVersion'];
103
105
 
104
106
  if (section !== 'memoryAll') {
105
107
  if (validSections.includes(section)) {
106
108
  const content = (getvWithDoubleDecryption && (await getvWithDoubleDecryption(`${contextId}-${section}`, contextKey))) || "";
109
+ // memoryFiles is JSON, skip processing but ensure it's a string
110
+ if (section === 'memoryFiles') {
111
+ if (!content) {
112
+ return "[]";
113
+ }
114
+ // If content is already an object (from getvWithDoubleDecryption parsing), stringify it
115
+ if (typeof content === 'object') {
116
+ return JSON.stringify(content);
117
+ }
118
+ // Otherwise it's already a string, return as-is
119
+ return content;
120
+ }
107
121
  return processMemoryContent(content, options);
108
122
  }
109
123
  return "";
110
124
  }
111
125
 
112
- // otherwise, read all sections and return them as a JSON object
126
+ // otherwise, read all sections (excluding memoryFiles) and return them as a JSON object
113
127
  const memoryContents = {};
114
- for (const section of validSections) {
128
+ for (const section of allSections) {
115
129
  if (section === 'memoryContext') continue;
116
130
 
117
131
  const content = (getvWithDoubleDecryption && (await getvWithDoubleDecryption(`${contextId}-${section}`, contextKey))) || "";
@@ -29,34 +29,50 @@ export default {
29
29
  return aiMemory;
30
30
  }
31
31
 
32
- const validSections = ['memorySelf', 'memoryDirectives', 'memoryTopics', 'memoryUser', 'memoryVersion'];
32
+ const validSections = ['memorySelf', 'memoryDirectives', 'memoryTopics', 'memoryUser', 'memoryVersion', 'memoryFiles'];
33
+ // memoryFiles can only be accessed explicitly, not as part of memoryAll
34
+ const allSections = ['memorySelf', 'memoryDirectives', 'memoryTopics', 'memoryUser', 'memoryVersion'];
33
35
 
34
36
  // Handle single section save
35
37
  if (section !== 'memoryAll') {
36
38
  if (validSections.includes(section)) {
39
+ // memoryFiles should be JSON array, validate if provided
40
+ if (section === 'memoryFiles' && aiMemory && aiMemory.trim() !== '') {
41
+ try {
42
+ // Validate it's valid JSON (but keep as string for storage)
43
+ JSON.parse(aiMemory);
44
+ } catch (e) {
45
+ // If not valid JSON, return error
46
+ return JSON.stringify({ error: 'memoryFiles must be a valid JSON array' });
47
+ }
48
+ }
37
49
  await setvWithDoubleEncryption(`${contextId}-${section}`, aiMemory, contextKey);
38
50
  }
39
51
  return aiMemory;
40
52
  }
41
53
 
42
- // if the aiMemory is an empty string, set all sections to empty strings
54
+ // if the aiMemory is an empty string, set all sections (excluding memoryFiles) to empty strings
43
55
  if (aiMemory.trim() === "") {
44
- for (const section of validSections) {
56
+ for (const section of allSections) {
45
57
  await setvWithDoubleEncryption(`${contextId}-${section}`, "", contextKey);
46
58
  }
47
59
  return "";
48
60
  }
49
61
 
50
- // Handle multi-section save
62
+ // Handle multi-section save (excluding memoryFiles)
51
63
  try {
52
64
  const memoryObject = JSON.parse(aiMemory);
53
- for (const section of validSections) {
65
+ for (const section of allSections) {
54
66
  if (section in memoryObject) {
55
67
  await setvWithDoubleEncryption(`${contextId}-${section}`, memoryObject[section], contextKey);
56
68
  }
57
69
  }
70
+ // Explicitly ignore memoryFiles if present in the object
71
+ if ('memoryFiles' in memoryObject) {
72
+ // Silently ignore - memoryFiles can only be saved explicitly
73
+ }
58
74
  } catch {
59
- for (const section of validSections) {
75
+ for (const section of allSections) {
60
76
  await setvWithDoubleEncryption(`${contextId}-${section}`, "", contextKey);
61
77
  }
62
78
  await setvWithDoubleEncryption(`${contextId}-memoryUser`, aiMemory, contextKey);
@@ -5,7 +5,8 @@ const MAX_TOOL_CALLS = 50;
5
5
  import { callPathway, callTool, say } from '../../../lib/pathwayTools.js';
6
6
  import logger from '../../../lib/logger.js';
7
7
  import { config } from '../../../config.js';
8
- import { chatArgsHasImageUrl, removeOldImageAndFileContent, getAvailableFiles } from '../../../lib/util.js';
8
+ import { chatArgsHasImageUrl, removeOldImageAndFileContent } from '../../../lib/util.js';
9
+ import { getAvailableFiles } from '../../../lib/fileUtils.js';
9
10
  import { Prompt } from '../../../server/prompt.js';
10
11
  import { getToolsForEntity, loadEntityConfig } from './tools/shared/sys_entity_tools.js';
11
12
  import CortexResponse from '../../../lib/cortexResponse.js';
@@ -329,7 +330,11 @@ export default {
329
330
  memoryLookupRequiredPromise = Promise.race([
330
331
  callPathway('sys_memory_lookup_required', { ...args, chatHistory: chatHistoryLastTurn, stream: false }),
331
332
  timeoutPromise
332
- ]);
333
+ ]).catch(error => {
334
+ // Handle timeout or other errors gracefully - return null so the await doesn't throw
335
+ logger.warn(`Memory lookup promise rejected: ${error.message}`);
336
+ return null;
337
+ });
333
338
  }
334
339
  }
335
340
 
@@ -388,7 +393,8 @@ export default {
388
393
  args.chatHistory = args.chatHistory.slice(-20);
389
394
  }
390
395
 
391
- const availableFiles = getAvailableFiles(args.chatHistory);
396
+ // Get available files from collection (async, syncs files from chat history)
397
+ const availableFiles = await getAvailableFiles(args.chatHistory, args.contextId, args.contextKey);
392
398
 
393
399
  // remove old image and file content
394
400
  const visionContentPresent = chatArgsHasImageUrl(args);
@@ -407,7 +413,18 @@ export default {
407
413
 
408
414
  try {
409
415
  if (memoryLookupRequiredPromise) {
410
- memoryLookupRequired = JSON.parse(await memoryLookupRequiredPromise)?.memoryRequired;
416
+ const result = await memoryLookupRequiredPromise;
417
+ // If result is null (timeout) or empty, default to false
418
+ if (result && typeof result === 'string') {
419
+ try {
420
+ memoryLookupRequired = JSON.parse(result)?.memoryRequired || false;
421
+ } catch (parseError) {
422
+ logger.warn(`Failed to parse memory lookup result: ${parseError.message}`);
423
+ memoryLookupRequired = false;
424
+ }
425
+ } else {
426
+ memoryLookupRequired = false;
427
+ }
411
428
  } else {
412
429
  memoryLookupRequired = false;
413
430
  }