minovative-mind-cli 2.9.1 → 2.11.0

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 (34) hide show
  1. package/README.md +14 -6
  2. package/dist/services/agent/commandApproval.js +5 -2
  3. package/dist/services/agent/slashCommands.js +90 -53
  4. package/dist/services/agent-tools.d.ts +4 -3
  5. package/dist/services/agent-tools.js +32 -79
  6. package/dist/services/agent.d.ts +5 -6
  7. package/dist/services/agent.js +11 -15
  8. package/dist/services/ai.d.ts +21 -1
  9. package/dist/services/ai.js +236 -11
  10. package/dist/services/chatHistoryService.d.ts +95 -2
  11. package/dist/services/chatHistoryService.js +236 -9
  12. package/dist/services/contextAgent.js +196 -81
  13. package/dist/services/investigationComplexity.d.ts +1 -1
  14. package/dist/services/investigationComplexity.js +1 -1
  15. package/dist/services/orchestration/investigationAgent.js +101 -84
  16. package/dist/services/orchestration/investigationCache.d.ts +80 -5
  17. package/dist/services/orchestration/investigationCache.js +570 -41
  18. package/dist/services/orchestration/investigationOrchestrator.js +17 -4
  19. package/dist/services/orchestration/orchestrator.js +6 -3
  20. package/dist/services/orchestration/scopedTools.js +5 -0
  21. package/dist/services/orchestration/subAgent.d.ts +31 -1
  22. package/dist/services/orchestration/subAgent.js +153 -2
  23. package/dist/utils/analysisRunner.d.ts +29 -0
  24. package/dist/utils/analysisRunner.js +200 -5
  25. package/dist/utils/contextPrompts.d.ts +20 -4
  26. package/dist/utils/contextPrompts.js +158 -23
  27. package/dist/utils/historyPrompt.d.ts +92 -1
  28. package/dist/utils/historyPrompt.js +166 -2
  29. package/dist/utils/symbolExtractor.d.ts +12 -0
  30. package/dist/utils/symbolExtractor.js +946 -0
  31. package/dist/utils/systemPrompts.d.ts +5 -4
  32. package/dist/utils/systemPrompts.js +46 -14
  33. package/oclif.manifest.json +1 -1
  34. package/package.json +2 -2
@@ -9,7 +9,48 @@ import { debugLog } from '../utils/logger.js';
9
9
  import { buildDependencyGraph } from '../utils/dependencyTracer.js';
10
10
  import { runEphemeralScript } from '../utils/analysisRunner.js';
11
11
  import { getMetricCollector } from './metrics.js';
12
+ import { estimateTokenCount, pruneTextToTokenBudget } from '../utils/historyPrompt.js';
12
13
  const metrics = getMetricCollector();
14
+ /** Token budget constants for context optimization */
15
+ const MAX_TREE_TOKENS = 12000;
16
+ const MAX_CHAT_HISTORY_ROUTER_TOKENS = 3000;
17
+ const MAX_CHAT_HISTORY_INVESTIGATION_TOKENS = 4500;
18
+ const MAX_SINGLE_FILE_TOKENS = 25000;
19
+ const MAX_TOTAL_FILE_TOKENS = 150000;
20
+ const MAX_TOTAL_FILES = 30;
21
+ const MAX_TOOL_OUTPUT_TOKENS = 10000;
22
+ /**
23
+ * Bounds individual file content to ensure it does not exceed the single-file token budget.
24
+ *
25
+ * @param filePath The file path.
26
+ * @param content Raw file text.
27
+ * @returns Budget-constrained file text.
28
+ */
29
+ function boundFileContent(filePath, content) {
30
+ const tokens = estimateTokenCount(content);
31
+ if (tokens <= MAX_SINGLE_FILE_TOKENS) {
32
+ return content;
33
+ }
34
+ debugLog(`Truncating large file context for ${filePath}: ${tokens} tokens -> capped at ${MAX_SINGLE_FILE_TOKENS}`);
35
+ return pruneTextToTokenBudget(content, MAX_SINGLE_FILE_TOKENS, {
36
+ truncationMarker: `\n\n... [File content truncated by Context Agent: ${tokens} estimated tokens exceeded the ${MAX_SINGLE_FILE_TOKENS} token limit. Use read_file with line ranges if specific sections are needed.] ...\n\n`,
37
+ });
38
+ }
39
+ /**
40
+ * Bounds tool output text to prevent massive outputs from blowing the context window during investigation turns.
41
+ *
42
+ * @param output Raw tool output string.
43
+ * @returns Budget-constrained tool output.
44
+ */
45
+ function boundToolOutput(output) {
46
+ const tokens = estimateTokenCount(output);
47
+ if (tokens <= MAX_TOOL_OUTPUT_TOKENS) {
48
+ return output;
49
+ }
50
+ return pruneTextToTokenBudget(output, MAX_TOOL_OUTPUT_TOKENS, {
51
+ truncationMarker: `\n\n... [Tool output truncated: exceeded ${MAX_TOOL_OUTPUT_TOKENS} tokens limit] ...\n`,
52
+ });
53
+ }
13
54
  async function detectProjectType(workspaceRoot) {
14
55
  const types = [];
15
56
  const fileExists = async (fileName) => {
@@ -149,7 +190,8 @@ export async function routeIntent(userRequest, chatHistory = '', abortSignal) {
149
190
  const session = createIntentRouterSession();
150
191
  let prompt = `User Request: "${userRequest}"`;
151
192
  if (chatHistory) {
152
- prompt = `Previous Conversation Context:\n${chatHistory}\n\n${prompt}`;
193
+ const prunedHistory = pruneTextToTokenBudget(chatHistory, MAX_CHAT_HISTORY_ROUTER_TOKENS, { fromStart: true });
194
+ prompt = `Previous Conversation Context:\n${prunedHistory}\n\n${prompt}`;
153
195
  }
154
196
  const result = await session.sendMessage(prompt, undefined, abortSignal);
155
197
  const text = result.response.text()?.trim() || '{}';
@@ -183,7 +225,8 @@ export async function evaluateExecutionComplexity(userRequest, investigationSumm
183
225
  Investigation Summary: ${investigationSummary || 'None (0 files needed)'}
184
226
  Number of Relevant Files: ${numRelevantFiles}`;
185
227
  if (chatHistory) {
186
- prompt = `Previous Conversation Context:\n${chatHistory}\n\n${prompt}`;
228
+ const prunedHistory = pruneTextToTokenBudget(chatHistory, MAX_CHAT_HISTORY_ROUTER_TOKENS, { fromStart: true });
229
+ prompt = `Previous Conversation Context:\n${prunedHistory}\n\n${prompt}`;
187
230
  }
188
231
  const result = await session.sendMessage(prompt, undefined, abortSignal);
189
232
  const text = result.response.text()?.trim() || '{}';
@@ -235,8 +278,11 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
235
278
  const label = alias ? `@${alias} (${root})` : `Primary Workspace (${root})`;
236
279
  const treeResult = await executeTool(root, 'list_directory', { dirPath: '.', maxDepth: 10 });
237
280
  let tree = treeResult.output;
238
- if (tree.length > 30000) {
239
- tree = tree.substring(0, 30000) + '\n... (Project tree truncated due to size)';
281
+ const treeTokens = estimateTokenCount(tree);
282
+ if (treeTokens > MAX_TREE_TOKENS) {
283
+ tree = pruneTextToTokenBudget(tree, MAX_TREE_TOKENS, {
284
+ truncationMarker: '\n... (Project tree truncated to fit token budget)\n',
285
+ });
240
286
  }
241
287
  const type = await detectProjectType(root);
242
288
  if (!alias) {
@@ -246,20 +292,49 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
246
292
  }
247
293
  const projectType = primaryProjectType;
248
294
  const { lookupInvestigation, saveInvestigation } = await import('./orchestration/investigationCache.js');
249
- const cacheHit = await lookupInvestigation(workspaceRoot, userRequest);
295
+ const collector = getMetricCollector();
296
+ const lookupStart = Date.now();
297
+ let cacheHit = null;
298
+ try {
299
+ cacheHit = await lookupInvestigation(workspaceRoot, userRequest, {
300
+ abortSignal,
301
+ onProgress,
302
+ allowSemanticFallback: true,
303
+ });
304
+ if (collector) {
305
+ collector.recordCachePerformance('investigation', Date.now() - lookupStart);
306
+ if (cacheHit) {
307
+ collector.recordCacheHit('investigation');
308
+ }
309
+ else {
310
+ collector.recordCacheMiss('investigation');
311
+ }
312
+ }
313
+ }
314
+ catch (err) {
315
+ if (err?.name === 'AbortError' || abortSignal?.aborted) {
316
+ throw err;
317
+ }
318
+ debugLog(`Investigation cache lookup error: ${err?.message || err}`);
319
+ }
250
320
  if (cacheHit) {
251
321
  if (onProgress)
252
322
  onProgress(`⚡ Memory Bank HIT — loaded ${cacheHit.entry.relevantFiles.length} files from cache`);
253
323
  const cachedFiles = new Map();
324
+ let cumulativeFileTokens = 0;
254
325
  for (const filePath of cacheHit.entry.relevantFiles) {
255
326
  if (abortSignal.aborted) {
256
327
  const err = new Error('Operation aborted');
257
328
  err.name = 'AbortError';
258
329
  throw err;
259
330
  }
331
+ if (cumulativeFileTokens >= MAX_TOTAL_FILE_TOKENS)
332
+ break;
260
333
  const readResult = await executeTool(workspaceRoot, 'read_file', { filePath });
261
334
  if (!readResult.error) {
262
- cachedFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
335
+ const boundedText = boundFileContent(filePath, readResult.output);
336
+ cachedFiles.set(filePath, { text: boundedText, inlineData: readResult.inlineData });
337
+ cumulativeFileTokens += estimateTokenCount(boundedText);
263
338
  }
264
339
  }
265
340
  const MAX_TOTAL_FILES = 30;
@@ -280,10 +355,14 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
280
355
  continue;
281
356
  if (cachedFiles.size + autoDiscovered.size >= MAX_TOTAL_FILES)
282
357
  break;
358
+ if (cumulativeFileTokens >= MAX_TOTAL_FILE_TOKENS)
359
+ break;
283
360
  autoDiscovered.add(dep);
284
361
  const depReadRes = await executeTool(resolved.workspaceRoot, 'read_file', { filePath: dep });
285
362
  if (!depReadRes.error) {
286
- cachedFiles.set(dep, { text: depReadRes.output, inlineData: depReadRes.inlineData });
363
+ const boundedText = boundFileContent(dep, depReadRes.output);
364
+ cachedFiles.set(dep, { text: boundedText, inlineData: depReadRes.inlineData });
365
+ cumulativeFileTokens += estimateTokenCount(boundedText);
287
366
  }
288
367
  }
289
368
  }
@@ -328,10 +407,12 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
328
407
  let isInvestigationFinished = false;
329
408
  let chainedMessages = [];
330
409
  let webSearchSummary = '';
331
- // Initial prompt
410
+ let cumulativeFileTokens = 0;
411
+ // Initial prompt with history budget management
332
412
  let currentMessage = `User Request: "${userRequest}"\n\nProject Type: ${projectType}\n\nProject Structure:\n${projectTree}`;
333
413
  if (chatHistory) {
334
- currentMessage = `Previous Conversation Context:\n${chatHistory}\n\n` + currentMessage;
414
+ const prunedHistory = pruneTextToTokenBudget(chatHistory, MAX_CHAT_HISTORY_INVESTIGATION_TOKENS, { fromStart: true });
415
+ currentMessage = `Previous Conversation Context:\n${prunedHistory}\n\n` + currentMessage;
335
416
  }
336
417
  currentMessage += `\n\nStart investigating to find relevant files.`;
337
418
  const MAX_TURNS = Infinity;
@@ -379,11 +460,11 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
379
460
  break;
380
461
  }
381
462
  let isFinished = false;
382
- const functionResponses = [];
463
+ // Log progress for all requested tool calls in this turn
383
464
  for (const call of functionCalls) {
384
465
  if (abortSignal.aborted)
385
466
  break;
386
- const args = call.args;
467
+ const args = (call.args || {});
387
468
  let logMsg = ` [Context Agent] Executing ${call.name}`;
388
469
  if (call.name === 'finish_investigation') {
389
470
  const filesToRead = args.relevantFiles || [];
@@ -410,69 +491,84 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
410
491
  logMsg = ` [Context Agent] Looking for recently modified files`;
411
492
  }
412
493
  else if (call.name === 'run_analysis_script') {
413
- const target = args.targetFile ? ` for: ${args.targetFile}` : '';
414
- logMsg = ` [Context Agent] Running analysis script${target}`;
415
- }
416
- const cleanMsg = logMsg.trim().replace(/^\[Context Agent\] /, '');
417
- if (onToolCall) {
418
- onToolCall(cleanMsg, 'Context Agent');
494
+ logMsg = ` [Context Agent] Running analysis script (${args.language})`;
419
495
  }
420
- else if (onProgress) {
421
- onProgress(cleanMsg);
496
+ if (onProgress) {
497
+ onProgress(logMsg.trim());
422
498
  }
423
499
  else {
424
500
  console.log(pc.dim(logMsg));
425
501
  }
502
+ }
503
+ // Execute tool calls concurrently while preserving 1:1 Gemini response positional ordering
504
+ const functionResponses = await Promise.all(functionCalls.map(async (call) => {
505
+ if (abortSignal.aborted) {
506
+ return {
507
+ functionResponse: {
508
+ name: call.name,
509
+ response: { error: 'Operation aborted.' },
510
+ },
511
+ };
512
+ }
513
+ const args = (call.args || {});
426
514
  if (call.name === 'finish_investigation') {
515
+ isInvestigationFinished = true;
516
+ isFinished = true;
427
517
  summary = args.summary || '';
428
518
  const filesToRead = args.relevantFiles || [];
429
- debugLog(`Context Agent finished. Selected files: ${JSON.stringify(filesToRead)}`);
430
- for (const filePath of filesToRead) {
431
- if (!relevantFiles.has(filePath)) {
432
- const readResult = await executeTool(workspaceRoot, 'read_file', { filePath });
433
- if (!readResult.error) {
434
- relevantFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
435
- }
519
+ const uniqueFilesToRead = filesToRead.filter((fp) => !relevantFiles.has(fp));
520
+ const readResults = await Promise.all(uniqueFilesToRead.map(async (filePath) => {
521
+ const readResult = await executeTool(workspaceRoot, 'read_file', { filePath });
522
+ return { filePath, readResult };
523
+ }));
524
+ for (const { filePath, readResult } of readResults) {
525
+ if (cumulativeFileTokens >= MAX_TOTAL_FILE_TOKENS)
526
+ break;
527
+ if (!readResult.error) {
528
+ const boundedText = boundFileContent(filePath, readResult.output);
529
+ relevantFiles.set(filePath, { text: boundedText, inlineData: readResult.inlineData });
530
+ cumulativeFileTokens += estimateTokenCount(boundedText);
436
531
  }
437
532
  }
438
- isFinished = true;
439
- isInvestigationFinished = true;
440
- // ── Auto-trace reverse dependencies ──
441
- // When the Context Agent finalizes its investigation, we automatically
442
- // discover files that DEPEND ON the selected files. This ensures the
443
- // Execution Agent won't break imports when modifying/deleting/renaming.
444
- const MAX_TOTAL_FILES = 30;
533
+ // Auto-trace reverse dependencies for all selected files to include active importers
445
534
  try {
535
+ const { buildDependencyGraph } = await import('../utils/dependencyTracer.js');
446
536
  const { resolveAndValidateMultiWorkspacePath } = await import('../utils/pathSecurity.js');
447
537
  const autoDiscovered = new Set();
448
- for (const filePath of filesToRead) {
538
+ await Promise.all(filesToRead.map(async (filePath) => {
449
539
  try {
450
540
  const resolved = resolveAndValidateMultiWorkspacePath(workspaceRoot, filePath);
451
541
  const graph = await buildDependencyGraph(resolved.workspaceRoot);
452
542
  const reverseDeps = graph.getImportedBy(resolved.relativePath);
453
543
  for (const dep of reverseDeps) {
454
544
  const aliasedDep = resolved.alias ? `@${resolved.alias}/${dep}` : dep;
455
- if (!filesToRead.includes(aliasedDep) && !autoDiscovered.has(aliasedDep)) {
545
+ if (!filesToRead.includes(aliasedDep)) {
456
546
  autoDiscovered.add(aliasedDep);
457
547
  }
458
548
  }
459
549
  }
460
- catch (e) {
550
+ catch {
461
551
  // Ignore path resolution errors for trace dependencies
462
552
  }
463
- }
464
- // Merge auto-discovered dependents, respecting the file cap
553
+ }));
554
+ // Merge auto-discovered dependents, respecting both file count and token budget caps
465
555
  const remaining = MAX_TOTAL_FILES - relevantFiles.size;
556
+ const depsToRead = Array.from(autoDiscovered)
557
+ .filter((dep) => !relevantFiles.has(dep))
558
+ .slice(0, Math.max(0, remaining));
559
+ const depReadResults = await Promise.all(depsToRead.map(async (dep) => {
560
+ const readResult = await executeTool(workspaceRoot, 'read_file', { filePath: dep });
561
+ return { dep, readResult };
562
+ }));
466
563
  let added = 0;
467
- for (const dep of autoDiscovered) {
468
- if (added >= remaining)
564
+ for (const { dep, readResult } of depReadResults) {
565
+ if (relevantFiles.size >= MAX_TOTAL_FILES || cumulativeFileTokens >= MAX_TOTAL_FILE_TOKENS)
469
566
  break;
470
- if (!relevantFiles.has(dep)) {
471
- const readResult = await executeTool(workspaceRoot, 'read_file', { filePath: dep });
472
- if (!readResult.error) {
473
- relevantFiles.set(dep, { text: readResult.output, inlineData: readResult.inlineData });
474
- added++;
475
- }
567
+ if (!readResult.error) {
568
+ const boundedText = boundFileContent(dep, readResult.output);
569
+ relevantFiles.set(dep, { text: boundedText, inlineData: readResult.inlineData });
570
+ cumulativeFileTokens += estimateTokenCount(boundedText);
571
+ added++;
476
572
  }
477
573
  }
478
574
  if (added > 0) {
@@ -486,46 +582,50 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
486
582
  catch {
487
583
  // Dependency tracing is best-effort — don't block investigation
488
584
  }
489
- functionResponses.push({
585
+ return {
490
586
  functionResponse: {
491
587
  name: call.name,
492
588
  response: { output: 'Investigation finished.' },
493
589
  },
494
- });
495
- break;
590
+ };
496
591
  }
497
592
  else if (call.name === 'list_directory') {
498
593
  const listRes = await executeTool(workspaceRoot, 'list_directory', args);
499
- functionResponses.push({
594
+ const output = boundToolOutput(listRes.output);
595
+ return {
500
596
  functionResponse: {
501
597
  name: call.name,
502
598
  response: {
503
- output: listRes.output,
599
+ output,
504
600
  ...(listRes.error ? { error: listRes.error } : {}),
505
601
  },
506
602
  },
507
- });
603
+ };
508
604
  }
509
605
  else if (call.name === 'search_codebase') {
510
606
  const grepRes = await executeTool(workspaceRoot, 'grep_search', { ...args, workspace: 'all' });
511
- functionResponses.push({
607
+ const output = boundToolOutput(grepRes.error ? grepRes.error : grepRes.output);
608
+ return {
512
609
  functionResponse: {
513
610
  name: call.name,
514
- response: { output: grepRes.error ? grepRes.error : grepRes.output },
611
+ response: { output },
515
612
  },
516
- });
613
+ };
517
614
  }
518
615
  else if (call.name === 'read_file') {
519
616
  const readRes = await executeTool(workspaceRoot, 'read_file', args);
520
617
  if (!readRes.error) {
521
- relevantFiles.set(args.filePath, { text: readRes.output, inlineData: readRes.inlineData });
618
+ const boundedText = boundFileContent(args.filePath, readRes.output);
619
+ relevantFiles.set(args.filePath, { text: boundedText, inlineData: readRes.inlineData });
620
+ cumulativeFileTokens += estimateTokenCount(boundedText);
522
621
  }
523
- functionResponses.push({
622
+ const output = boundToolOutput(readRes.error ? readRes.error : readRes.output);
623
+ return {
524
624
  functionResponse: {
525
625
  name: call.name,
526
- response: { output: readRes.error ? readRes.error : readRes.output },
626
+ response: { output },
527
627
  },
528
- });
628
+ };
529
629
  }
530
630
  else if (call.name === 'perform_web_search') {
531
631
  try {
@@ -540,63 +640,70 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
540
640
  console.log(pc.dim(` [Web Search] ${webMsg}`));
541
641
  }
542
642
  const searchSummary = webResult.response.text()?.trim() || 'No relevant information found.';
543
- webSearchSummary += `\nQuery: ${args.query}\nFindings:\n${searchSummary}\n`;
544
- functionResponses.push({
643
+ const boundedSummary = boundToolOutput(searchSummary);
644
+ webSearchSummary += `\nQuery: ${args.query}\nFindings:\n${boundedSummary}\n`;
645
+ return {
545
646
  functionResponse: {
546
647
  name: call.name,
547
- response: { output: searchSummary },
648
+ response: { output: boundedSummary },
548
649
  },
549
- });
650
+ };
550
651
  }
551
652
  catch (e) {
552
- functionResponses.push({
653
+ return {
553
654
  functionResponse: {
554
655
  name: call.name,
555
656
  response: { error: e.message || 'Failed to search the web' },
556
657
  },
557
- });
658
+ };
558
659
  }
559
660
  }
560
661
  else if (call.name === 'find_dependencies') {
561
662
  const depResult = await executeTool(workspaceRoot, 'find_dependencies', args);
562
- functionResponses.push({
663
+ const output = boundToolOutput(depResult.error ? depResult.error : depResult.output);
664
+ return {
563
665
  functionResponse: {
564
666
  name: call.name,
565
- response: { output: depResult.error ? depResult.error : depResult.output },
667
+ response: { output },
566
668
  },
567
- });
669
+ };
568
670
  }
569
671
  else if (call.name === 'find_recent_changes') {
570
672
  const recentRes = await executeTool(workspaceRoot, 'find_recent_changes', args);
571
- functionResponses.push({
673
+ const output = boundToolOutput(recentRes.error ? recentRes.error : recentRes.output);
674
+ return {
572
675
  functionResponse: {
573
676
  name: call.name,
574
- response: { output: recentRes.error ? recentRes.error : recentRes.output },
677
+ response: { output },
575
678
  },
576
- });
679
+ };
577
680
  }
578
681
  else if (call.name === 'run_analysis_script') {
579
- const analysisResult = await runEphemeralScript(workspaceRoot, args.language, args.code, {
580
- abortSignal,
581
- });
582
- const output = analysisResult.exitCode === 0
682
+ const analysisResult = await runEphemeralScript(workspaceRoot, args.language, args.code, { abortSignal });
683
+ const rawOutput = analysisResult.exitCode === 0
583
684
  ? analysisResult.stdout || '(script produced no output)'
584
685
  : `Script failed (exit ${analysisResult.exitCode}):\n${analysisResult.stderr}`;
585
- functionResponses.push({
686
+ const output = boundToolOutput(rawOutput);
687
+ return {
586
688
  functionResponse: {
587
689
  name: call.name,
588
690
  response: { output },
589
691
  },
590
- });
692
+ };
591
693
  }
592
- }
694
+ return {
695
+ functionResponse: {
696
+ name: call.name,
697
+ response: { output: `Unknown tool: ${call.name}` },
698
+ },
699
+ };
700
+ }));
593
701
  if (isFinished) {
594
702
  break;
595
703
  }
596
704
  // Prepare next turn
597
705
  currentMessage = functionResponses;
598
706
  }
599
- const collector = getMetricCollector();
600
707
  if (collector) {
601
708
  collector.recordContextSelectedFiles(Array.from(relevantFiles.keys()));
602
709
  if (!isInvestigationFinished || relevantFiles.size === 0) {
@@ -604,8 +711,16 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
604
711
  }
605
712
  }
606
713
  if (isInvestigationFinished && relevantFiles.size > 0) {
607
- const { saveInvestigation } = await import('./orchestration/investigationCache.js');
608
- await saveInvestigation(workspaceRoot, userRequest, Array.from(relevantFiles.keys()), summary);
714
+ try {
715
+ const { saveInvestigation } = await import('./orchestration/investigationCache.js');
716
+ await saveInvestigation(workspaceRoot, userRequest, Array.from(relevantFiles.keys()), summary, undefined, abortSignal);
717
+ }
718
+ catch (err) {
719
+ if (err?.name === 'AbortError' || abortSignal?.aborted) {
720
+ throw err;
721
+ }
722
+ debugLog(`Failed to save investigation to cache: ${err?.message || err}`);
723
+ }
609
724
  }
610
725
  return {
611
726
  contextResult: { projectTree, projectType, relevantFiles, summary, webSearchSummary, isParallel: false },
@@ -31,7 +31,7 @@ export interface InvestigationComplexityResult {
31
31
  /**
32
32
  * Evaluates whether the investigation phase should be parallelized.
33
33
  *
34
- * Uses `gemini-3.5-flash-lite` (under auto mode) at temperature 0 to classify the prompt's
34
+ * Uses `gemini-3.7-flash` (under auto mode) at temperature 0 to classify the prompt's
35
35
  * investigation complexity. The PM dynamically identifies domains and groups
36
36
  * them into agent assignments. Agent count = `agentAssignments.length`, which
37
37
  * may be fewer than `domains.length` when related domains are batched together.
@@ -16,7 +16,7 @@ import { debugLog } from '../utils/logger.js';
16
16
  /**
17
17
  * Evaluates whether the investigation phase should be parallelized.
18
18
  *
19
- * Uses `gemini-3.5-flash-lite` (under auto mode) at temperature 0 to classify the prompt's
19
+ * Uses `gemini-3.7-flash` (under auto mode) at temperature 0 to classify the prompt's
20
20
  * investigation complexity. The PM dynamically identifies domains and groups
21
21
  * them into agent assignments. Agent count = `agentAssignments.length`, which
22
22
  * may be fewer than `domains.length` when related domains are batched together.