minovative-mind-cli 2.6.3 → 2.6.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.
@@ -1413,12 +1413,23 @@ const currentTasksByAgent = new Map();
1413
1413
  * @returns A promise resolving to a {@link ToolResult}.
1414
1414
  */
1415
1415
  export async function executeTool(workspaceRoot, toolName, args, abortSignal) {
1416
+ // Normalize common LLM tool alias hallucinations to registered tool names
1417
+ let normalizedToolName = toolName;
1418
+ if (normalizedToolName === 'search' || normalizedToolName === 'web_search') {
1419
+ normalizedToolName = (args.query || args.searchQuery || args.url) ? 'perform_web_search' : 'grep_search';
1420
+ }
1421
+ else if (normalizedToolName === 'grep' || normalizedToolName === 'search_codebase') {
1422
+ normalizedToolName = 'grep_search';
1423
+ }
1424
+ else if (normalizedToolName === 'finish_investigation') {
1425
+ normalizedToolName = 'finish_task';
1426
+ }
1416
1427
  // ─── Multi-Workspace Path Resolution ─────────────────────────────
1417
1428
  // Intercept @alias/ prefixed paths and swap workspaceRoot + relative path
1418
1429
  // before dispatching to the underlying tool functions (which remain unchanged).
1419
- const { effectiveRoot, resolvedArgs } = resolveWorkspaceArgs(workspaceRoot, toolName, args);
1430
+ const { effectiveRoot, resolvedArgs } = resolveWorkspaceArgs(workspaceRoot, normalizedToolName, args);
1420
1431
  let result;
1421
- switch (toolName) {
1432
+ switch (normalizedToolName) {
1422
1433
  case 'perform_web_search': {
1423
1434
  const { createWebSearchAgentSession } = await import('./ai.js');
1424
1435
  const webSession = createWebSearchAgentSession();
@@ -112,24 +112,14 @@ export declare function executeSingleTurn(workspaceRoot: string, userInput: stri
112
112
  } | void>;
113
113
  /**
114
114
  * Raw chat history entry count threshold to trigger chat history summarization.
115
- * 12 entries = 6 conversational user/model turns.
116
- */
117
- export declare const HISTORY_SUMMARIZATION_THRESHOLD = 12;
118
- /**
119
- * Checks and summarizes older chat history entries if the active session's history
120
- * exceeds HISTORY_SUMMARIZATION_THRESHOLD entries.
121
- *
122
- * Preserves the most recent 4 entries (2 turns) intact for conversational continuity,
123
- * replacing all preceding turns with a single summarized pair in the chat history.
124
- *
125
- * @param chat - The active ProxyChatSession instance.
126
- * @returns True if history was summarized and updated; false otherwise.
115
+ * 50 entries = 25 conversational user/model turns.
127
116
  */
117
+ export declare const HISTORY_SUMMARIZATION_THRESHOLD = 50;
128
118
  /**
129
119
  * Summarizes older chat history entries if the active session's history
130
120
  * exceeds {@link HISTORY_SUMMARIZATION_THRESHOLD} entries.
131
121
  *
132
- * Preserves the most recent 4 entries (2 turns) intact for conversational continuity,
122
+ * Preserves the most recent 20 entries (10 turns) intact for conversational continuity,
133
123
  * replacing all preceding turns with a single summarized pair in the chat history.
134
124
  *
135
125
  * @param chat - The active ProxyChatSession instance.
@@ -152,7 +152,7 @@ export async function startAgentLoop(workspaceRoot, version) {
152
152
  process.stdout.write('\n\n\n\x1b[3A');
153
153
  console.log(pc.dim('\nType your coding request below. Type "exit" or "quit" to leave.\n'));
154
154
  while (true) {
155
- if (chat.getRawHistory().length === 0 && chatSessionState.title !== '') {
155
+ if (chat.getFullHistory().length === 0 && chatSessionState.title !== '') {
156
156
  chatSessionState.id = crypto.randomUUID();
157
157
  chatSessionState.title = '';
158
158
  chatSessionState.totalTokens = 0;
@@ -466,19 +466,47 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
466
466
  const orchestrator = new Orchestrator(workspaceRoot, chatSessionState.id, inputHandler);
467
467
  const handledByOrchestrator = await orchestrator.runOrchestration(finalInput, dynamicSystemInstruction, ac.signal);
468
468
  if (typeof handledByOrchestrator === 'string') {
469
- // The orchestrator successfully decomposed and ran the task.
470
- // Inject the task context into the main proxy chat session so the agent remembers what happened
471
- // and so the history state gets saved properly on disk.
472
- chat.getRawHistory().push({
473
- role: 'user',
474
- parts: [{ text: userInput }],
475
- });
476
- chat.getRawHistory().push({
477
- role: 'model',
478
- parts: [{ text: handledByOrchestrator }],
479
- });
480
- // Print the summary text just like single-agent mode
481
- if (handledByOrchestrator !== '[Generation stopped.]') {
469
+ const isStopped = handledByOrchestrator.includes('Generation stopped') ||
470
+ handledByOrchestrator.includes('[Generation stopped');
471
+ if (isStopped) {
472
+ const currentChanges = changeLogger.getCurrentChangeSet()?.changes || [];
473
+ const modifiedFiles = currentChanges
474
+ .filter((c) => c.action === 'modify' || c.action === 'create' || c.action === 'delete')
475
+ .map((c) => `${c.action}: ${path.relative(workspaceRoot, c.filePath)}`);
476
+ let summaryMsg = '';
477
+ if (modifiedFiles.length > 0) {
478
+ summaryMsg = `Generation was stopped by the user before completion. Partial code changes made before stopping:\n- ${modifiedFiles.join('\n- ')}`;
479
+ p.log.warn(pc.yellow('\n⚠️ Generation stopped by user before completion.'));
480
+ p.log.info(pc.cyan('Partial code changes created/modified before stopping:'));
481
+ modifiedFiles.forEach((file) => p.log.message(` ${pc.dim('•')} ${file}`));
482
+ }
483
+ else {
484
+ summaryMsg = 'Generation was stopped by the user before completion. No code files were modified before stopping.';
485
+ p.log.warn(pc.yellow('\n⚠️ Generation stopped by user before completion (no files were modified).'));
486
+ }
487
+ chat.addTurn({
488
+ role: 'user',
489
+ parts: [{ text: `[USER INTERRUPT] User issued a "stop" command during execution: "${userInput}"` }],
490
+ }, {
491
+ role: 'model',
492
+ parts: [
493
+ {
494
+ text: `[GENERATION INTERRUPTED BY USER] I acknowledge that generation was manually stopped by the user before task completion.\n${summaryMsg}`,
495
+ },
496
+ ],
497
+ });
498
+ }
499
+ else {
500
+ // Inject the task context into the main proxy chat session so the agent remembers what happened
501
+ // and so the full history state gets saved properly on disk.
502
+ chat.addTurn({
503
+ role: 'user',
504
+ parts: [{ text: userInput }],
505
+ }, {
506
+ role: 'model',
507
+ parts: [{ text: handledByOrchestrator }],
508
+ });
509
+ // Print the summary text just like single-agent mode
482
510
  console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(Orchestrator)`)}\n`);
483
511
  const cleanText = handledByOrchestrator.replace(/\n([ \t]*\n){2,}/g, '\n\n');
484
512
  console.log(marked.parse(cleanText));
@@ -519,7 +547,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
519
547
  changeLogger.markComplete();
520
548
  changeLogger.commitChangeSet();
521
549
  // Auto-save chat history for the orchestrator turn
522
- const history = chat.getRawHistory();
550
+ const history = chat.getFullHistory();
523
551
  if (history.length > 0) {
524
552
  if (!chatSessionState.title) {
525
553
  chatSessionState.title = 'Generating title...';
@@ -628,8 +656,38 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
628
656
  const turnEndTime = Date.now();
629
657
  const turnDuration = ((turnEndTime - turnStartTime) / 1000).toFixed(1);
630
658
  p.log.info(`${pc.dim('Generated in')} ${pc.cyan(turnDuration + 's')}`);
631
- // If we reach this point without throwing or breaking early, and the generation wasn't stopped, the turn finished completely.
632
- if (finalText !== '[Generation stopped.]') {
659
+ // Check if generation was stopped by user
660
+ const isStopped = finalText.includes('Generation stopped') ||
661
+ finalText.includes('[Generation stopped');
662
+ if (isStopped) {
663
+ const currentChanges = changeLogger.getCurrentChangeSet()?.changes || [];
664
+ const modifiedFiles = currentChanges
665
+ .filter((c) => c.action === 'modify' || c.action === 'create' || c.action === 'delete')
666
+ .map((c) => `${c.action}: ${path.relative(workspaceRoot, c.filePath)}`);
667
+ let summaryMsg = '';
668
+ if (modifiedFiles.length > 0) {
669
+ summaryMsg = `Generation was stopped by the user before completion. Partial code changes made before stopping:\n- ${modifiedFiles.join('\n- ')}`;
670
+ p.log.warn(pc.yellow('\n⚠️ Generation stopped by user before completion.'));
671
+ p.log.info(pc.cyan('Partial code changes created/modified before stopping:'));
672
+ modifiedFiles.forEach((file) => p.log.message(` ${pc.dim('•')} ${file}`));
673
+ }
674
+ else {
675
+ summaryMsg = 'Generation was stopped by the user before completion. No code files were modified before stopping.';
676
+ p.log.warn(pc.yellow('\n⚠️ Generation stopped by user before completion (no files were modified).'));
677
+ }
678
+ chat.addTurn({
679
+ role: 'user',
680
+ parts: [{ text: `[USER INTERRUPT] User issued a "stop" command during execution: "${userInput}"` }],
681
+ }, {
682
+ role: 'model',
683
+ parts: [
684
+ {
685
+ text: `[GENERATION INTERRUPTED BY USER] I acknowledge that generation was manually stopped by the user before task completion.\n${summaryMsg}`,
686
+ },
687
+ ],
688
+ });
689
+ }
690
+ else {
633
691
  changeLogger.markComplete();
634
692
  }
635
693
  if (usage) {
@@ -647,7 +705,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
647
705
  chatSessionState.totalCreditsUsed += usage.creditsUsed || 0;
648
706
  }
649
707
  // Auto-save chat history
650
- const history = chat.getRawHistory();
708
+ const history = chat.getFullHistory();
651
709
  if (history.length > 0) {
652
710
  if (!chatSessionState.title) {
653
711
  chatSessionState.title = 'Generating title...';
@@ -884,24 +942,14 @@ async function compressContextFiles(workspaceRoot, contextResult) {
884
942
  }
885
943
  /**
886
944
  * Raw chat history entry count threshold to trigger chat history summarization.
887
- * 12 entries = 6 conversational user/model turns.
888
- */
889
- export const HISTORY_SUMMARIZATION_THRESHOLD = 12;
890
- /**
891
- * Checks and summarizes older chat history entries if the active session's history
892
- * exceeds HISTORY_SUMMARIZATION_THRESHOLD entries.
893
- *
894
- * Preserves the most recent 4 entries (2 turns) intact for conversational continuity,
895
- * replacing all preceding turns with a single summarized pair in the chat history.
896
- *
897
- * @param chat - The active ProxyChatSession instance.
898
- * @returns True if history was summarized and updated; false otherwise.
945
+ * 50 entries = 25 conversational user/model turns.
899
946
  */
947
+ export const HISTORY_SUMMARIZATION_THRESHOLD = 50;
900
948
  /**
901
949
  * Summarizes older chat history entries if the active session's history
902
950
  * exceeds {@link HISTORY_SUMMARIZATION_THRESHOLD} entries.
903
951
  *
904
- * Preserves the most recent 4 entries (2 turns) intact for conversational continuity,
952
+ * Preserves the most recent 20 entries (10 turns) intact for conversational continuity,
905
953
  * replacing all preceding turns with a single summarized pair in the chat history.
906
954
  *
907
955
  * @param chat - The active ProxyChatSession instance.
@@ -913,8 +961,8 @@ export async function summarizeHistoryIfNeeded(chat) {
913
961
  return false;
914
962
  }
915
963
  try {
916
- // Keep the most recent 4 entries (2 full user/model turns) intact
917
- const recentCount = 4;
964
+ // Keep the most recent 20 entries (10 full user/model turns) intact
965
+ const recentCount = 20;
918
966
  const olderHistory = history.slice(0, history.length - recentCount);
919
967
  const recentHistory = history.slice(history.length - recentCount);
920
968
  debugLog(`Summarizing ${olderHistory.length} older chat history entries...`);
@@ -930,7 +978,7 @@ export async function summarizeHistoryIfNeeded(chat) {
930
978
  parts: [{ text: 'Understood. I have reviewed and absorbed the summary of our preceding conversation.' }],
931
979
  },
932
980
  ];
933
- chat.loadRawHistory([...summaryContent, ...recentHistory]);
981
+ chat.loadCompressedHistory([...summaryContent, ...recentHistory]);
934
982
  debugLog(`Chat history compressed successfully from ${history.length} to ${summaryContent.length + recentHistory.length} entries.`);
935
983
  return true;
936
984
  }
@@ -5,6 +5,7 @@ export declare function setGlobalActiveModel(model: string): void;
5
5
  export declare function getGlobalActiveModel(): string;
6
6
  export declare class ProxyChatSession {
7
7
  private history;
8
+ private fullHistory;
8
9
  private modelName;
9
10
  private systemInstruction;
10
11
  private tools;
@@ -17,7 +18,10 @@ export declare class ProxyChatSession {
17
18
  getModel(): string;
18
19
  clearHistory(): void;
19
20
  getRawHistory(): Content[];
21
+ getFullHistory(): Content[];
20
22
  loadRawHistory(history: Content[]): void;
23
+ loadCompressedHistory(history: Content[]): void;
24
+ addTurn(userContent: Content, modelContent: Content): void;
21
25
  /**
22
26
  * Retrieves the most recent conversation history as a formatted string.
23
27
  * Useful for passing conversation context to stateless background agents.
@@ -69,6 +69,7 @@ function truncatePartText(text) {
69
69
  }
70
70
  export class ProxyChatSession {
71
71
  history = [];
72
+ fullHistory = [];
72
73
  modelName;
73
74
  systemInstruction;
74
75
  tools;
@@ -95,25 +96,37 @@ export class ProxyChatSession {
95
96
  }
96
97
  clearHistory() {
97
98
  this.history = [];
99
+ this.fullHistory = [];
98
100
  }
99
101
  getRawHistory() {
100
102
  return this.history;
101
103
  }
104
+ getFullHistory() {
105
+ return this.fullHistory;
106
+ }
102
107
  loadRawHistory(history) {
108
+ this.history = [...history];
109
+ this.fullHistory = JSON.parse(JSON.stringify(history));
110
+ }
111
+ loadCompressedHistory(history) {
103
112
  this.history = history;
104
113
  }
114
+ addTurn(userContent, modelContent) {
115
+ this.history.push(userContent);
116
+ this.history.push(modelContent);
117
+ this.fullHistory.push(JSON.parse(JSON.stringify(userContent)));
118
+ this.fullHistory.push(JSON.parse(JSON.stringify(modelContent)));
119
+ }
105
120
  /**
106
121
  * Retrieves the most recent conversation history as a formatted string.
107
122
  * Useful for passing conversation context to stateless background agents.
108
123
  * @param turns Number of back-and-forth turns (user + model pair = 1 turn) to retrieve.
109
124
  */
110
125
  getRecentHistory(turns = 2) {
111
- if (this.history.length === 0)
126
+ if (this.fullHistory.length === 0 && this.history.length === 0)
112
127
  return '';
113
- // History is an array of Content { role: 'user' | 'model', parts: Part[] }
114
- // A single turn is generally 2 items (user, then model). Sometimes tools are interspersed.
115
- // We'll just grab the last N * 2 items.
116
- const recentItems = this.history.slice(-(turns * 2));
128
+ const sourceHistory = this.fullHistory.length > 0 ? this.fullHistory : this.history;
129
+ const recentItems = sourceHistory.slice(-(turns * 2));
117
130
  let formattedHistory = '';
118
131
  for (const item of recentItems) {
119
132
  const role = item.role === 'user' ? 'User' : 'Assistant';
@@ -134,6 +147,10 @@ export class ProxyChatSession {
134
147
  this.history.pop();
135
148
  this.history.pop();
136
149
  }
150
+ if (this.fullHistory.length >= 2) {
151
+ this.fullHistory.pop();
152
+ this.fullHistory.pop();
153
+ }
137
154
  }
138
155
  sessionId;
139
156
  workspaceRoot;
@@ -148,12 +165,15 @@ export class ProxyChatSession {
148
165
  */
149
166
  async pruneHistory() {
150
167
  if (this.history.length > MAX_HISTORY_ENTRIES) {
151
- // Always keep pairs aligned (user/model), so trim from the front
152
168
  const excess = this.history.length - MAX_HISTORY_ENTRIES;
153
- // Round up to the nearest even number to keep user/model pairs intact
154
169
  const trimCount = excess % 2 === 0 ? excess : excess + 1;
155
- const pruned = this.history.slice(0, trimCount);
156
170
  this.history = this.history.slice(trimCount);
171
+ }
172
+ if (this.fullHistory.length > MAX_HISTORY_ENTRIES) {
173
+ const excess = this.fullHistory.length - MAX_HISTORY_ENTRIES;
174
+ const trimCount = excess % 2 === 0 ? excess : excess + 1;
175
+ const pruned = this.fullHistory.slice(0, trimCount);
176
+ this.fullHistory = this.fullHistory.slice(trimCount);
157
177
  if (this.sessionId && this.workspaceRoot) {
158
178
  const archiveFile = `archives/archived_history_${this.sessionId}.json`;
159
179
  const existingArchive = readCache(this.workspaceRoot, archiveFile) || [];
@@ -199,10 +219,12 @@ export class ProxyChatSession {
199
219
  if (additionalText) {
200
220
  newParts.push({ text: truncatePartText(additionalText) });
201
221
  }
202
- this.history.push({
222
+ const userEntry = {
203
223
  role: 'user',
204
224
  parts: newParts,
205
- });
225
+ };
226
+ this.history.push(userEntry);
227
+ this.fullHistory.push(JSON.parse(JSON.stringify(userEntry)));
206
228
  // Prune old history before sending to keep payload bounded
207
229
  await this.pruneHistory();
208
230
  const effectiveGenerationConfig = { ...this.generationConfig };
@@ -286,10 +308,12 @@ export class ProxyChatSession {
286
308
  });
287
309
  }
288
310
  if (modelParts.length > 0) {
289
- this.history.push({
311
+ const modelEntry = {
290
312
  role: 'model',
291
313
  parts: modelParts,
292
- });
314
+ };
315
+ this.history.push(modelEntry);
316
+ this.fullHistory.push(JSON.parse(JSON.stringify(modelEntry)));
293
317
  // We must prune again if we just pushed new history
294
318
  await this.pruneHistory();
295
319
  }
@@ -65,5 +65,5 @@
65
65
  ]
66
66
  }
67
67
  },
68
- "version": "2.6.3"
68
+ "version": "2.6.4"
69
69
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "minovative-mind-cli",
3
3
  "description": "An automated AI agent powered by Vertex AI that helps you write software",
4
- "version": "2.6.3",
4
+ "version": "2.6.4",
5
5
  "author": "Daniel Ward",
6
6
  "bin": {
7
7
  "minovative-mind-cli": "bin/run.js"