minovative-mind-cli 2.11.0 → 2.11.3

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.
package/README.md CHANGED
@@ -122,6 +122,7 @@ Background tasks automatically route to dedicated auxiliary models with native `
122
122
  | Command | What it does |
123
123
  | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
124
124
  | `/config-key` | Configure custom Google AI Studio API key (BYOK mode) |
125
+ | `/profile` | View, inspect, delete, or reset global adaptive persona memory and AI side-notes |
125
126
  | `/models` | Hot-swap the active model |
126
127
  | `/plan` | Toggle plan mode to review implementation strategies |
127
128
  | `/paste` | Multi-line input mode (cancel with Ctrl+C) |
@@ -9,7 +9,7 @@ import { Command } from '@oclif/core';
9
9
  export default class DefaultCommand extends Command {
10
10
  /**
11
11
  * The description displayed in the CLI help output.
12
- * Details all supported slash commands (/config-key, /paste, /plan, /clear, /models,
12
+ * Details all supported slash commands (/config-key, /profile, /paste, /plan, /clear, /models,
13
13
  * /debug, /auto-approve, /sub-agents, /stats, /revert, /chats, /workspaces, /commit)
14
14
  * along with interactive chat controls.
15
15
  */
@@ -17,7 +17,7 @@ import { updateWorkspaceStatus } from '../services/workspace.js';
17
17
  export default class DefaultCommand extends Command {
18
18
  /**
19
19
  * The description displayed in the CLI help output.
20
- * Details all supported slash commands (/config-key, /paste, /plan, /clear, /models,
20
+ * Details all supported slash commands (/config-key, /profile, /paste, /plan, /clear, /models,
21
21
  * /debug, /auto-approve, /sub-agents, /stats, /revert, /chats, /workspaces, /commit)
22
22
  * along with interactive chat controls.
23
23
  */
@@ -25,6 +25,7 @@ export default class DefaultCommand extends Command {
25
25
 
26
26
  Inside the chat session, you can use the following commands in the slash menu:
27
27
  /config-key - BYOK — Configure, clear, or view status of custom Google AI Studio API key
28
+ /profile - View, inspect, delete, or reset global adaptive persona memory and AI side-notes
28
29
  /paste - Enter multi-line paste mode for long code snippets and prompts
29
30
  /plan - Toggle plan mode to review step-by-step implementation strategies
30
31
  /clear - Clear conversation history, reset terminal screen, and display logo
@@ -17,12 +17,14 @@ import { checkByokSubscription } from '../auth.js';
17
17
  import { GEMINI_MODELS, isByokEnabled } from '../../utils/config.js';
18
18
  import { loadCredentials, updateCredentialField } from '../../utils/credentialStore.js';
19
19
  import { getGlobalActiveModel, setGlobalActiveModel, ProxyChatSession } from '../ai.js';
20
+ import { loadUserProfile, deleteSideNote, clearUserProfile, getUserProfilePath, } from '../userProfileService.js';
20
21
  /**
21
22
  * @file slashCommands.ts
22
23
  * @description Interactive slash command handler module for Minovative Mind CLI.
23
24
  *
24
25
  * Supported slash commands:
25
26
  * - `/config-key` : Configure, validate, toggle, or clear Bring Your Own Key (BYOK) Google AI Studio API credentials.
27
+ * - `/profile` : View, inspect, delete, or reset global adaptive persona memory and AI side-notes.
26
28
  * - `/paste` : Enter multi-line paste mode using EOF tracking (`Ctrl+D` submission).
27
29
  * - `/plan` : Toggle AI step-by-step implementation planning mode.
28
30
  * - `/clear` : Clear conversation history, wipe terminal screen, and reset CLI header.
@@ -1146,5 +1148,138 @@ Strict Formatting Rules:
1146
1148
  }
1147
1149
  return { shouldContinue: true };
1148
1150
  }
1151
+ /**
1152
+ * `/profile` - Displays and manages global adaptive user profile traits, persona memory, and AI side-notes.
1153
+ */
1154
+ if (lowerCommand === '/profile') {
1155
+ const profile = await loadUserProfile();
1156
+ const action = await p.select({
1157
+ message: 'Adaptive User Profile & AI Memory Bank:',
1158
+ options: [
1159
+ {
1160
+ value: 'view',
1161
+ label: 'View Profile & Observations',
1162
+ hint: `${profile.agentNotes.length} side-note(s) recorded`,
1163
+ },
1164
+ {
1165
+ value: 'delete_note',
1166
+ label: 'Delete Specific Side-Note',
1167
+ hint: 'Select and remove an individual note',
1168
+ },
1169
+ {
1170
+ value: 'clear_all',
1171
+ label: 'Reset Profile & Clear Memory',
1172
+ hint: 'Wipe all persona traits and side-notes',
1173
+ },
1174
+ { value: 'cancel', label: 'Back to Chat', hint: 'Return to active session' },
1175
+ ],
1176
+ });
1177
+ if (p.isCancel(action) || action === 'cancel') {
1178
+ return { shouldContinue: true };
1179
+ }
1180
+ if (action === 'view') {
1181
+ const hasStyle = profile.communicationStyle?.tonePreference ||
1182
+ profile.communicationStyle?.verbosity ||
1183
+ profile.communicationStyle?.formulationStyle;
1184
+ const hasCognitive = profile.cognitiveTraits?.architecturalStyle ||
1185
+ profile.cognitiveTraits?.decisionPreference ||
1186
+ profile.cognitiveTraits?.riskTolerance ||
1187
+ profile.cognitiveTraits?.delegationDepth ||
1188
+ profile.cognitiveTraits?.debuggingStyle ||
1189
+ profile.cognitiveTraits?.explanationFormat;
1190
+ const hasStrengths = profile.technicalPreferences?.strengths && profile.technicalPreferences.strengths.length > 0;
1191
+ const hasConventions = profile.technicalPreferences?.conventions && profile.technicalPreferences.conventions.length > 0;
1192
+ const hasNotes = profile.agentNotes && profile.agentNotes.length > 0;
1193
+ console.log(`\n${pc.bold(pc.cyan('🧠 Global Adaptive User Profile & Persona Memory'))}`);
1194
+ console.log(`${pc.dim('Storage:')} ${pc.dim(getUserProfilePath())}\n`);
1195
+ if (!hasStyle && !hasCognitive && !hasStrengths && !hasConventions && !hasNotes) {
1196
+ p.log.info(pc.yellow('No personalized observations recorded yet. Mino will learn your communication style and preferences organically as you chat.'));
1197
+ }
1198
+ else {
1199
+ if (hasStyle) {
1200
+ console.log(pc.bold('Communication Style:'));
1201
+ if (profile.communicationStyle?.tonePreference) {
1202
+ console.log(` ${pc.dim('•')} Tone & Demeanor: ${pc.green(profile.communicationStyle.tonePreference)}`);
1203
+ }
1204
+ if (profile.communicationStyle?.verbosity) {
1205
+ console.log(` ${pc.dim('•')} Output Formatting: ${pc.green(profile.communicationStyle.verbosity)}`);
1206
+ }
1207
+ if (profile.communicationStyle?.formulationStyle) {
1208
+ console.log(` ${pc.dim('•')} Thought Formulation: ${pc.green(profile.communicationStyle.formulationStyle)}`);
1209
+ }
1210
+ console.log('');
1211
+ }
1212
+ if (hasCognitive) {
1213
+ console.log(pc.bold('Decision-Making & Cognitive Traits:'));
1214
+ if (profile.cognitiveTraits?.architecturalStyle) {
1215
+ console.log(` ${pc.dim('•')} Architectural Orientation: ${pc.magenta(profile.cognitiveTraits.architecturalStyle)}`);
1216
+ }
1217
+ if (profile.cognitiveTraits?.decisionPreference) {
1218
+ console.log(` ${pc.dim('•')} Decision Autonomy: ${pc.magenta(profile.cognitiveTraits.decisionPreference)}`);
1219
+ }
1220
+ if (profile.cognitiveTraits?.riskTolerance) {
1221
+ console.log(` ${pc.dim('•')} Risk & Velocity: ${pc.magenta(profile.cognitiveTraits.riskTolerance)}`);
1222
+ }
1223
+ if (profile.cognitiveTraits?.delegationDepth) {
1224
+ console.log(` ${pc.dim('•')} Delegation Depth: ${pc.magenta(profile.cognitiveTraits.delegationDepth)}`);
1225
+ }
1226
+ if (profile.cognitiveTraits?.debuggingStyle) {
1227
+ console.log(` ${pc.dim('•')} Debugging Preference: ${pc.magenta(profile.cognitiveTraits.debuggingStyle)}`);
1228
+ }
1229
+ if (profile.cognitiveTraits?.explanationFormat) {
1230
+ console.log(` ${pc.dim('•')} Explanation Preference: ${pc.magenta(profile.cognitiveTraits.explanationFormat)}`);
1231
+ }
1232
+ console.log('');
1233
+ }
1234
+ if (hasStrengths || hasConventions) {
1235
+ console.log(pc.bold('Technical Preferences:'));
1236
+ if (hasStrengths) {
1237
+ console.log(` ${pc.dim('•')} Strengths: ${pc.cyan(profile.technicalPreferences.strengths.join(', '))}`);
1238
+ }
1239
+ if (hasConventions) {
1240
+ console.log(` ${pc.dim('•')} Conventions: ${pc.cyan(profile.technicalPreferences.conventions.join(', '))}`);
1241
+ }
1242
+ console.log('');
1243
+ }
1244
+ if (hasNotes) {
1245
+ console.log(pc.bold(`AI Observations & Side-Notes (${profile.agentNotes.length}):`));
1246
+ profile.agentNotes.forEach((note, idx) => {
1247
+ console.log(` ${pc.yellow(`[${idx + 1}]`)} ${note}`);
1248
+ });
1249
+ console.log('');
1250
+ }
1251
+ }
1252
+ }
1253
+ else if (action === 'delete_note') {
1254
+ if (profile.agentNotes.length === 0) {
1255
+ p.log.warn('No side-notes recorded yet to delete.');
1256
+ }
1257
+ else {
1258
+ const noteChoices = profile.agentNotes.map((note, idx) => ({
1259
+ value: idx,
1260
+ label: `${idx + 1}. ${note.length > 70 ? note.slice(0, 67) + '...' : note}`,
1261
+ }));
1262
+ const selectedIndex = await p.select({
1263
+ message: 'Select a side-note to delete:',
1264
+ options: [...noteChoices, { value: -1, label: 'Cancel' }],
1265
+ });
1266
+ if (!p.isCancel(selectedIndex) && typeof selectedIndex === 'number' && selectedIndex >= 0) {
1267
+ await deleteSideNote(selectedIndex);
1268
+ p.log.success(`Deleted side-note [${selectedIndex + 1}].`);
1269
+ }
1270
+ }
1271
+ }
1272
+ else if (action === 'clear_all') {
1273
+ const confirmClear = await p.confirm({
1274
+ message: 'Are you sure you want to clear your global profile and all AI side-notes?',
1275
+ initialValue: false,
1276
+ });
1277
+ if (confirmClear && !p.isCancel(confirmClear)) {
1278
+ await clearUserProfile();
1279
+ p.log.success('User profile memory and side-notes have been reset.');
1280
+ }
1281
+ }
1282
+ return { shouldContinue: true };
1283
+ }
1149
1284
  return { shouldContinue: true };
1150
1285
  }
@@ -35,6 +35,7 @@ import { verifyChangedFiles } from './verificationService.js';
35
35
  import { buildContextInjection } from '../utils/contextPrompts.js';
36
36
  import { registerContextFiles } from '../utils/fileReadGuard.js';
37
37
  import { historyText } from '../utils/historyPrompt.js';
38
+ import { loadUserProfile, formatUserProfileForContext, extractAndSaveUserInsights, } from './userProfileService.js';
38
39
  // Submodule Imports
39
40
  import { AsyncInputHandler } from './agent/inputHandler.js';
40
41
  import { processResponse } from './agent/toolLoop.js';
@@ -207,6 +208,7 @@ export async function startAgentLoop(workspaceRoot, version) {
207
208
  hint: 'Manage external workspaces for cross-project development',
208
209
  },
209
210
  { value: '/config-key', label: '/config-key', hint: 'BYOK — Use your own Google AI Studio API key' },
211
+ { value: '/profile', label: '/profile', hint: 'View or manage AI observations and persona side-notes' },
210
212
  { value: '/stats', label: '/stats', hint: 'View current session statistics and configuration' },
211
213
  { value: '/commit', label: '/commit', hint: 'Auto-commit changes with AI message' },
212
214
  { value: '/revert', label: '/revert', hint: 'Undo last change' },
@@ -451,6 +453,17 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
451
453
  collector.recordCompressedContextSize(contextInjection.length);
452
454
  dynamicSystemInstruction += '\n\n' + contextInjection;
453
455
  }
456
+ // Inject global adaptive user persona and learned preferences
457
+ try {
458
+ const userProfile = await loadUserProfile();
459
+ const profileInjection = formatUserProfileForContext(userProfile);
460
+ if (profileInjection) {
461
+ dynamicSystemInstruction += '\n\n' + profileInjection;
462
+ }
463
+ }
464
+ catch (e) {
465
+ debugLog(`Failed to inject user profile context: ${e}`);
466
+ }
454
467
  // Apply the dynamic prompt updates and tool registrations to the active chat session
455
468
  chat.setAgentConfig(dynamicSystemInstruction, config.tools);
456
469
  if (getGlobalActiveModel() === GEMINI_MODELS.AUTO) {
@@ -549,6 +562,13 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
549
562
  // Print the summary text just like single-agent mode
550
563
  console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(Orchestrator)`)}\n`);
551
564
  console.log(renderTerminalMarkdown(handledByOrchestrator));
565
+ // Observe user interaction traits and update global user profile before next prompt
566
+ try {
567
+ await extractAndSaveUserInsights(userInput, handledByOrchestrator, ac.signal);
568
+ }
569
+ catch (err) {
570
+ debugLog(`Background user profile insight extraction failed: ${err}`);
571
+ }
552
572
  }
553
573
  // Print Usage Stats
554
574
  const usage = getAndResetTurnUsage();
@@ -671,6 +691,13 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
671
691
  console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(${chat.getModel()})`)}\n`);
672
692
  console.log(renderTerminalMarkdown(cleanFinalText));
673
693
  chat.appendFinalSummary(cleanFinalText);
694
+ // Observe user interaction traits and update global user profile before next prompt
695
+ try {
696
+ await extractAndSaveUserInsights(userInput, cleanFinalText, ac.signal);
697
+ }
698
+ catch (err) {
699
+ debugLog(`Background user profile insight extraction failed: ${err}`);
700
+ }
674
701
  }
675
702
  }
676
703
  if (usage) {
@@ -114,9 +114,21 @@ export declare function createInvestigationSemanticSession(): any;
114
114
  export declare function createWebSearchAgentSession(): any;
115
115
  export declare function createHistorySummarizerSession(): any;
116
116
  /**
117
- * Summarizes an array of Content history entries using Gemini Flash Lite.
117
+ * Sanitizes and formats raw conversation history entries into a structured text transcript
118
+ * for safe consumption by the History Summarizer without requiring tool declarations or
119
+ * risking multi-turn schema rejections from the API.
120
+ */
121
+ export declare function formatHistoryForSummarization(history: Content[]): string;
122
+ /**
123
+ * Summarizes an array of Content history entries using Gemini Flash.
118
124
  */
119
125
  export declare function summarizeChatHistory(history: Content[], abortSignal?: AbortSignal): Promise<string>;
126
+ export declare function createUserProfileExtractorSession(): any;
127
+ /**
128
+ * Creates a Flash-Lite session with structured output to classify whether a user message
129
+ * is trivial/low-signal (greetings, confirmations, terminal commands) or contains substantive signal.
130
+ */
131
+ export declare function createTrivialMessageClassifierSession(): any;
120
132
  /**
121
133
  * Generates a concise title for a chat session based on the user's first message.
122
134
  */
@@ -5,7 +5,7 @@ import { getMetricCollector } from './metrics.js';
5
5
  import { getAuthorizedIdToken, checkByokSubscription } from './auth.js';
6
6
  import { debugLog } from '../utils/logger.js';
7
7
  import { readCache, writeCache } from '../utils/projectStorage.js';
8
- import { GENERAL_CHAT_INSTRUCTION, PLAN_EXECUTION_INSTRUCTION, PLAN_MODE_INSTRUCTION, CONTEXT_SYSTEM_INSTRUCTION, INTENT_ROUTER_SYSTEM_INSTRUCTION, WEB_SEARCH_SYSTEM_INSTRUCTION, EXECUTION_COMPLEXITY_SYSTEM_INSTRUCTION, INVESTIGATION_COMPLEXITY_SYSTEM_INSTRUCTION, INVESTIGATION_SEMANTIC_SYSTEM_INSTRUCTION, HISTORY_SUMMARIZER_SYSTEM_INSTRUCTION, } from '../utils/systemPrompts.js';
8
+ import { GENERAL_CHAT_INSTRUCTION, PLAN_EXECUTION_INSTRUCTION, PLAN_MODE_INSTRUCTION, CONTEXT_SYSTEM_INSTRUCTION, INTENT_ROUTER_SYSTEM_INSTRUCTION, WEB_SEARCH_SYSTEM_INSTRUCTION, EXECUTION_COMPLEXITY_SYSTEM_INSTRUCTION, INVESTIGATION_COMPLEXITY_SYSTEM_INSTRUCTION, INVESTIGATION_SEMANTIC_SYSTEM_INSTRUCTION, HISTORY_SUMMARIZER_SYSTEM_INSTRUCTION, USER_PROFILE_EXTRACTOR_SYSTEM_INSTRUCTION, TRIVIAL_MESSAGE_CLASSIFIER_SYSTEM_INSTRUCTION, } from '../utils/systemPrompts.js';
9
9
  import { workspaceRegistry } from './workspaceRegistry.js';
10
10
  import { loadCredentials } from '../utils/credentialStore.js';
11
11
  import { ProxyClient } from './proxyClient.js';
@@ -836,7 +836,65 @@ export function createHistorySummarizerSession() {
836
836
  });
837
837
  }
838
838
  /**
839
- * Summarizes an array of Content history entries using Gemini Flash Lite.
839
+ * Sanitizes and formats raw conversation history entries into a structured text transcript
840
+ * for safe consumption by the History Summarizer without requiring tool declarations or
841
+ * risking multi-turn schema rejections from the API.
842
+ */
843
+ export function formatHistoryForSummarization(history) {
844
+ if (!history || history.length === 0)
845
+ return '';
846
+ const lines = [];
847
+ for (const entry of history) {
848
+ if (!entry || !Array.isArray(entry.parts))
849
+ continue;
850
+ const roleLabel = entry.role === 'model' ? 'Assistant' : 'User';
851
+ const textPieces = [];
852
+ for (const part of entry.parts) {
853
+ if (!part)
854
+ continue;
855
+ if (typeof part.text === 'string' && part.text.trim()) {
856
+ textPieces.push(part.text.trim());
857
+ }
858
+ else if (part.functionCall) {
859
+ const name = part.functionCall.name || 'unknown_tool';
860
+ const argsStr = part.functionCall.args ? JSON.stringify(part.functionCall.args) : '';
861
+ const compactArgs = argsStr.length > 500 ? argsStr.substring(0, 500) + '...' : argsStr;
862
+ textPieces.push(`[Tool Invoked: ${name}(${compactArgs})]`);
863
+ }
864
+ else if (part.functionResponse) {
865
+ const name = part.functionResponse.name || 'unknown_tool';
866
+ const respObj = part.functionResponse.response;
867
+ let respStr = '';
868
+ if (respObj) {
869
+ if (typeof respObj.output === 'string') {
870
+ respStr = respObj.output;
871
+ }
872
+ else if (typeof respObj.result === 'string') {
873
+ respStr = respObj.result;
874
+ }
875
+ else if (typeof respObj.error === 'string') {
876
+ respStr = `Error: ${respObj.error}`;
877
+ }
878
+ else {
879
+ respStr = JSON.stringify(respObj);
880
+ }
881
+ }
882
+ const compactResp = respStr.length > 1000 ? respStr.substring(0, 1000) + '...' : respStr;
883
+ textPieces.push(`[Tool Output (${name})]: ${compactResp}`);
884
+ }
885
+ else if (part.inlineData) {
886
+ textPieces.push(`[Inline Data Attachment: ${part.inlineData.mimeType || 'unknown'}]`);
887
+ }
888
+ }
889
+ const turnText = textPieces.join('\n').trim();
890
+ if (turnText) {
891
+ lines.push(`[${roleLabel}]:\n${turnText}`);
892
+ }
893
+ }
894
+ return lines.join('\n\n');
895
+ }
896
+ /**
897
+ * Summarizes an array of Content history entries using Gemini Flash.
840
898
  */
841
899
  export async function summarizeChatHistory(history, abortSignal) {
842
900
  if (!history || history.length === 0) {
@@ -848,9 +906,13 @@ export async function summarizeChatHistory(history, abortSignal) {
848
906
  throw err;
849
907
  }
850
908
  try {
909
+ const formattedTranscript = formatHistoryForSummarization(history);
910
+ if (!formattedTranscript || !formattedTranscript.trim()) {
911
+ return '';
912
+ }
851
913
  const session = createHistorySummarizerSession();
852
- session.loadRawHistory(JSON.parse(JSON.stringify(history)));
853
- const result = await session.sendMessage('Summarize the preceding conversation history following your compression rules.', undefined, abortSignal);
914
+ const prompt = `Summarize the preceding conversation history following your compression rules:\n\n<conversation_history>\n${formattedTranscript}\n</conversation_history>`;
915
+ const result = await session.sendMessage(prompt, undefined, abortSignal);
854
916
  return result.response.text() || '';
855
917
  }
856
918
  catch (error) {
@@ -863,6 +925,150 @@ export async function summarizeChatHistory(history, abortSignal) {
863
925
  return '';
864
926
  }
865
927
  }
928
+ // ─── User Profile Extractor Service ────────────────────────────────────
929
+ export function createUserProfileExtractorSession() {
930
+ // Configured exclusively with FLASH_LITE for lightweight zero-latency profiling
931
+ const model = GEMINI_MODELS.FLASH_LITE;
932
+ return new ProxyChatSession(model, USER_PROFILE_EXTRACTOR_SYSTEM_INSTRUCTION, [], // no tools
933
+ {
934
+ temperature: 0,
935
+ responseMimeType: 'application/json',
936
+ responseSchema: {
937
+ type: SchemaType.OBJECT,
938
+ properties: {
939
+ tonePreference: {
940
+ type: SchemaType.STRING,
941
+ description: 'Observed tone and demeanor traits of the user',
942
+ },
943
+ verbosity: {
944
+ type: SchemaType.STRING,
945
+ description: 'Observed verbosity and output formatting preference',
946
+ },
947
+ formulationStyle: {
948
+ type: SchemaType.STRING,
949
+ description: 'Observed thought formulation and prompting style',
950
+ },
951
+ cognitiveTraits: {
952
+ type: SchemaType.OBJECT,
953
+ properties: {
954
+ architecturalStyle: {
955
+ type: SchemaType.STRING,
956
+ description: 'e.g., "top-down-design", "bottom-up-code", "balanced"',
957
+ },
958
+ decisionPreference: {
959
+ type: SchemaType.STRING,
960
+ description: 'e.g., "direct-recommendation", "present-options"',
961
+ },
962
+ riskTolerance: {
963
+ type: SchemaType.STRING,
964
+ description: 'e.g., "defensive-rigor", "pragmatic-speed"',
965
+ },
966
+ delegationDepth: {
967
+ type: SchemaType.STRING,
968
+ description: 'e.g., "autonomous-delegation", "hands-on-stepwise"',
969
+ },
970
+ debuggingStyle: {
971
+ type: SchemaType.STRING,
972
+ description: 'e.g., "minimal-diff-fix", "root-cause-deep-dive"',
973
+ },
974
+ explanationFormat: {
975
+ type: SchemaType.STRING,
976
+ description: 'e.g., "code-first", "bullet-summaries", "conceptual-analogies"',
977
+ },
978
+ },
979
+ description: 'Observed cognitive decision-making, collaboration, and problem-solving traits',
980
+ },
981
+ strengths: {
982
+ type: SchemaType.ARRAY,
983
+ items: {
984
+ type: SchemaType.STRING,
985
+ },
986
+ description: 'Observed technical strengths or technologies the user is actively familiar with',
987
+ },
988
+ removeStrengths: {
989
+ type: SchemaType.ARRAY,
990
+ items: {
991
+ type: SchemaType.STRING,
992
+ },
993
+ description: 'Outdated or abandoned technologies to remove when a pivot/contradiction occurs',
994
+ },
995
+ conventions: {
996
+ type: SchemaType.ARRAY,
997
+ items: {
998
+ type: SchemaType.STRING,
999
+ },
1000
+ description: 'Observed architectural habits or coding conventions',
1001
+ },
1002
+ removeConventions: {
1003
+ type: SchemaType.ARRAY,
1004
+ items: {
1005
+ type: SchemaType.STRING,
1006
+ },
1007
+ description: 'Outdated coding conventions to remove when a pivot/contradiction occurs',
1008
+ },
1009
+ addNotes: {
1010
+ type: SchemaType.ARRAY,
1011
+ items: {
1012
+ type: SchemaType.STRING,
1013
+ },
1014
+ description: 'New non-conflicting side-notes to add (empty if none)',
1015
+ },
1016
+ updateNotes: {
1017
+ type: SchemaType.ARRAY,
1018
+ items: {
1019
+ type: SchemaType.OBJECT,
1020
+ properties: {
1021
+ index: {
1022
+ type: SchemaType.INTEGER,
1023
+ description: '0-based index of the existing note to update',
1024
+ },
1025
+ updatedText: {
1026
+ type: SchemaType.STRING,
1027
+ description: 'Revised note text that supersedes the outdated observation',
1028
+ },
1029
+ },
1030
+ required: ['index', 'updatedText'],
1031
+ },
1032
+ description: 'Targeted updates to existing notes that evolved',
1033
+ },
1034
+ deleteNoteIndices: {
1035
+ type: SchemaType.ARRAY,
1036
+ items: {
1037
+ type: SchemaType.INTEGER,
1038
+ },
1039
+ description: '0-based indices of contradicted or obsolete notes to delete',
1040
+ },
1041
+ },
1042
+ required: ['addNotes', 'updateNotes', 'deleteNoteIndices'],
1043
+ },
1044
+ });
1045
+ }
1046
+ /**
1047
+ * Creates a Flash-Lite session with structured output to classify whether a user message
1048
+ * is trivial/low-signal (greetings, confirmations, terminal commands) or contains substantive signal.
1049
+ */
1050
+ export function createTrivialMessageClassifierSession() {
1051
+ const model = GEMINI_MODELS.FLASH_LITE;
1052
+ return new ProxyChatSession(model, TRIVIAL_MESSAGE_CLASSIFIER_SYSTEM_INSTRUCTION, [], // no tools
1053
+ {
1054
+ temperature: 0,
1055
+ responseMimeType: 'application/json',
1056
+ responseSchema: {
1057
+ type: SchemaType.OBJECT,
1058
+ properties: {
1059
+ isTrivial: {
1060
+ type: SchemaType.BOOLEAN,
1061
+ description: 'True if the message is a low-signal greeting, confirmation, affirmation, or simple control command with zero technical/personal traits',
1062
+ },
1063
+ reason: {
1064
+ type: SchemaType.STRING,
1065
+ description: 'Brief reason for the classification decision',
1066
+ },
1067
+ },
1068
+ required: ['isTrivial'],
1069
+ },
1070
+ });
1071
+ }
866
1072
  /**
867
1073
  * Generates a concise title for a chat session based on the user's first message.
868
1074
  */
@@ -322,24 +322,26 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
322
322
  onProgress(`⚡ Memory Bank HIT — loaded ${cacheHit.entry.relevantFiles.length} files from cache`);
323
323
  const cachedFiles = new Map();
324
324
  let cumulativeFileTokens = 0;
325
- for (const filePath of cacheHit.entry.relevantFiles) {
326
- if (abortSignal.aborted) {
327
- const err = new Error('Operation aborted');
328
- err.name = 'AbortError';
329
- throw err;
330
- }
331
- if (cumulativeFileTokens >= MAX_TOTAL_FILE_TOKENS)
332
- break;
325
+ const readResults = await Promise.all(cacheHit.entry.relevantFiles.map(async (filePath) => {
326
+ if (abortSignal.aborted)
327
+ return { filePath, error: 'aborted' };
333
328
  const readResult = await executeTool(workspaceRoot, 'read_file', { filePath });
334
- if (!readResult.error) {
335
- const boundedText = boundFileContent(filePath, readResult.output);
336
- cachedFiles.set(filePath, { text: boundedText, inlineData: readResult.inlineData });
337
- cumulativeFileTokens += estimateTokenCount(boundedText);
329
+ return { filePath, readResult };
330
+ }));
331
+ for (const item of readResults) {
332
+ if (item.readResult && !item.readResult.error) {
333
+ const boundedText = boundFileContent(item.filePath, item.readResult.output);
334
+ const tokens = estimateTokenCount(boundedText);
335
+ if (cumulativeFileTokens + tokens <= MAX_TOTAL_FILE_TOKENS) {
336
+ cachedFiles.set(item.filePath, { text: boundedText, inlineData: item.readResult.inlineData });
337
+ cumulativeFileTokens += tokens;
338
+ }
338
339
  }
339
340
  }
340
341
  const MAX_TOTAL_FILES = 30;
341
342
  try {
342
343
  const { resolveAndValidateMultiWorkspacePath } = await import('../utils/pathSecurity.js');
344
+ const depsToRead = [];
343
345
  const autoDiscovered = new Set();
344
346
  for (const filePath of cacheHit.entry.relevantFiles) {
345
347
  if (abortSignal.aborted)
@@ -349,25 +351,34 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
349
351
  const graph = await buildDependencyGraph(resolved.workspaceRoot);
350
352
  const reverseDeps = graph.getImportedBy(resolved.relativePath);
351
353
  for (const dep of reverseDeps) {
352
- if (abortSignal.aborted)
353
- break;
354
354
  if (cachedFiles.has(dep) || autoDiscovered.has(dep))
355
355
  continue;
356
356
  if (cachedFiles.size + autoDiscovered.size >= MAX_TOTAL_FILES)
357
357
  break;
358
- if (cumulativeFileTokens >= MAX_TOTAL_FILE_TOKENS)
359
- break;
360
358
  autoDiscovered.add(dep);
361
- const depReadRes = await executeTool(resolved.workspaceRoot, 'read_file', { filePath: dep });
362
- if (!depReadRes.error) {
363
- const boundedText = boundFileContent(dep, depReadRes.output);
364
- cachedFiles.set(dep, { text: boundedText, inlineData: depReadRes.inlineData });
365
- cumulativeFileTokens += estimateTokenCount(boundedText);
366
- }
359
+ depsToRead.push({ dep, resolvedWorkspace: resolved.workspaceRoot });
367
360
  }
368
361
  }
369
362
  catch (e) { }
370
363
  }
364
+ if (depsToRead.length > 0) {
365
+ const depResults = await Promise.all(depsToRead.map(async ({ dep, resolvedWorkspace }) => {
366
+ if (abortSignal.aborted)
367
+ return { dep, error: 'aborted' };
368
+ const depReadRes = await executeTool(resolvedWorkspace, 'read_file', { filePath: dep });
369
+ return { dep, depReadRes };
370
+ }));
371
+ for (const item of depResults) {
372
+ if (item.depReadRes && !item.depReadRes.error) {
373
+ const boundedText = boundFileContent(item.dep, item.depReadRes.output);
374
+ const tokens = estimateTokenCount(boundedText);
375
+ if (cumulativeFileTokens + tokens <= MAX_TOTAL_FILE_TOKENS) {
376
+ cachedFiles.set(item.dep, { text: boundedText, inlineData: item.depReadRes.inlineData });
377
+ cumulativeFileTokens += tokens;
378
+ }
379
+ }
380
+ }
381
+ }
371
382
  }
372
383
  catch (e) { }
373
384
  const { accumulateUsage } = await import('./metrics.js');
@@ -135,7 +135,6 @@ export class InvestigationAgentRunner {
135
135
  success = true;
136
136
  break;
137
137
  }
138
- let isFinished = false;
139
138
  if (onTool) {
140
139
  for (const call of functionCalls) {
141
140
  onTool(formatToolCall(call.name, (call.args || {})));
@@ -359,11 +359,12 @@ export class ProxyClient {
359
359
  // ignore parsing error
360
360
  }
361
361
  const errorMsg = errorData.error?.message || response.statusText;
362
- if (response.status === 400 ||
363
- response.status === 401 ||
362
+ if (response.status === 401 ||
364
363
  response.status === 403 ||
365
364
  errorMsg.includes('API_KEY_INVALID') ||
365
+ errorMsg.includes('API_KEY_EXPIRED') ||
366
366
  errorMsg.includes('quota') ||
367
+ errorMsg.includes('RESOURCE_EXHAUSTED') ||
367
368
  errorMsg.includes('PERMISSION_DENIED')) {
368
369
  throw new Error('AI_BYOK_ERROR: Your API key or quota is invalid. Please run /config-key to update your settings.');
369
370
  }