minovative-mind-cli 2.10.0 → 2.11.2

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 +1 -0
  2. package/dist/commands/chat.d.ts +1 -1
  3. package/dist/commands/chat.js +2 -1
  4. package/dist/services/agent/commandApproval.js +5 -2
  5. package/dist/services/agent/slashCommands.js +213 -51
  6. package/dist/services/agent-tools.d.ts +4 -3
  7. package/dist/services/agent-tools.js +32 -79
  8. package/dist/services/agent.d.ts +5 -6
  9. package/dist/services/agent.js +38 -15
  10. package/dist/services/ai.d.ts +25 -0
  11. package/dist/services/ai.js +253 -2
  12. package/dist/services/chatHistoryService.d.ts +95 -2
  13. package/dist/services/chatHistoryService.js +236 -9
  14. package/dist/services/contextAgent.js +184 -89
  15. package/dist/services/orchestration/investigationAgent.js +100 -84
  16. package/dist/services/orchestration/investigationOrchestrator.js +6 -2
  17. package/dist/services/orchestration/orchestrator.js +6 -3
  18. package/dist/services/orchestration/scopedTools.js +5 -0
  19. package/dist/services/orchestration/subAgent.d.ts +31 -1
  20. package/dist/services/orchestration/subAgent.js +153 -2
  21. package/dist/services/userProfileService.d.ts +97 -0
  22. package/dist/services/userProfileService.js +410 -0
  23. package/dist/utils/analysisRunner.d.ts +29 -0
  24. package/dist/utils/analysisRunner.js +200 -5
  25. package/dist/utils/contextPrompts.d.ts +19 -3
  26. package/dist/utils/contextPrompts.js +144 -26
  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 +6 -4
  32. package/dist/utils/systemPrompts.js +77 -9
  33. package/oclif.manifest.json +2 -2
  34. package/package.json +1 -1
@@ -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) {
@@ -544,11 +557,18 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
544
557
  parts: [{ text: userInput }],
545
558
  }, {
546
559
  role: 'model',
547
- parts: [{ text: handledByOrchestrator }],
560
+ parts: [{ text: `[ORCHESTRATOR_EXECUTION]\n${handledByOrchestrator}` }],
548
561
  });
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();
@@ -670,6 +690,14 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
670
690
  if (cleanFinalText) {
671
691
  console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(${chat.getModel()})`)}\n`);
672
692
  console.log(renderTerminalMarkdown(cleanFinalText));
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
+ }
673
701
  }
674
702
  }
675
703
  if (usage) {
@@ -989,15 +1017,14 @@ async function compressContextFiles(workspaceRoot, contextResult) {
989
1017
  }
990
1018
  /**
991
1019
  * Raw chat history entry count threshold to trigger chat history summarization.
992
- * 50 entries = 25 conversational user/model turns.
1020
+ * 2 entries = 1 complete conversational user/model turn.
993
1021
  */
994
- export const HISTORY_SUMMARIZATION_THRESHOLD = 50;
1022
+ export const HISTORY_SUMMARIZATION_THRESHOLD = 2;
995
1023
  /**
996
- * Summarizes older chat history entries if the active session's history
997
- * exceeds {@link HISTORY_SUMMARIZATION_THRESHOLD} entries.
1024
+ * Summarizes chat history entries starting from the first full user/model turn.
998
1025
  *
999
- * Preserves the most recent 20 entries (10 turns) intact for conversational continuity,
1000
- * replacing all preceding turns with a single summarized pair in the chat history.
1026
+ * Compresses preceding conversation history into a structured high-density summary pair
1027
+ * in the active session while permanently preserving uncompressed history in fullHistory.
1001
1028
  *
1002
1029
  * @param chat - The active ProxyChatSession instance.
1003
1030
  * @returns A promise resolving to true if history was summarized and updated; false otherwise.
@@ -1008,12 +1035,8 @@ export async function summarizeHistoryIfNeeded(chat, abortSignal) {
1008
1035
  return false;
1009
1036
  }
1010
1037
  try {
1011
- // Keep the most recent 20 entries (10 full user/model turns) intact
1012
- const recentCount = 20;
1013
- const olderHistory = history.slice(0, history.length - recentCount);
1014
- const recentHistory = history.slice(history.length - recentCount);
1015
- debugLog(`Summarizing ${olderHistory.length} older chat history entries...`);
1016
- const summaryText = await summarizeChatHistory(olderHistory, abortSignal);
1038
+ debugLog(`Summarizing ${history.length} chat history entries (Flash-Lite Summarizer triggered after first turn)...`);
1039
+ const summaryText = await summarizeChatHistory(history, abortSignal);
1017
1040
  if (summaryText && summaryText.trim().length > 0) {
1018
1041
  const summaryContent = [
1019
1042
  {
@@ -1025,8 +1048,8 @@ export async function summarizeHistoryIfNeeded(chat, abortSignal) {
1025
1048
  parts: [{ text: 'Understood. I have reviewed and absorbed the summary of our preceding conversation.' }],
1026
1049
  },
1027
1050
  ];
1028
- chat.loadCompressedHistory([...summaryContent, ...recentHistory]);
1029
- debugLog(`Chat history compressed successfully from ${history.length} to ${summaryContent.length + recentHistory.length} entries.`);
1051
+ chat.loadCompressedHistory(summaryContent);
1052
+ debugLog(`Chat history compressed successfully from ${history.length} to ${summaryContent.length} entries.`);
1030
1053
  return true;
1031
1054
  }
1032
1055
  }
@@ -22,6 +22,12 @@ export declare class ProxyChatSession {
22
22
  loadRawHistory(history: Content[]): void;
23
23
  loadCompressedHistory(history: Content[]): void;
24
24
  addTurn(userContent: Content, modelContent: Content): void;
25
+ /**
26
+ * Appends or merges the final conversational summary text into the active session history.
27
+ * Ensures that the final response produced by finish_task, PM reconciliation, or final turns
28
+ * is permanently preserved as a model text response in both working history and full transcript.
29
+ */
30
+ appendFinalSummary(text: string): void;
25
31
  /**
26
32
  * Retrieves the most recent conversation history as a formatted string.
27
33
  * Useful for passing conversation context to stateless background agents.
@@ -42,6 +48,19 @@ export declare class ProxyChatSession {
42
48
  * conversational context while preventing OOM crashes.
43
49
  */
44
50
  private pruneHistory;
51
+ /**
52
+ * Collapses oversized functionResponse outputs in historical turns (>1 turn old)
53
+ * while strictly maintaining Gemini functionCall and functionResponse pairing.
54
+ *
55
+ * Gemini requires every functionCall in a model turn to have a matching functionResponse
56
+ * in the immediately following user turn. Deleting parts or entries breaks this invariant and
57
+ * causes 400 Bad Request errors. This method mutates oversized response outputs in-place
58
+ * on older turns to drastically conserve context window tokens.
59
+ *
60
+ * @param threshold Maximum characters allowed for a historical tool response before collapsing (default: 1500).
61
+ * @param turnsToKeep Number of recent turns to preserve unpruned (default: 1).
62
+ */
63
+ pruneToolOutputHistory(threshold?: number, turnsToKeep?: number): void;
45
64
  sendMessage(message: string | Array<{
46
65
  functionResponse: {
47
66
  name: string;
@@ -98,6 +117,12 @@ export declare function createHistorySummarizerSession(): any;
98
117
  * Summarizes an array of Content history entries using Gemini Flash Lite.
99
118
  */
100
119
  export declare function summarizeChatHistory(history: Content[], abortSignal?: AbortSignal): Promise<string>;
120
+ export declare function createUserProfileExtractorSession(): any;
121
+ /**
122
+ * Creates a Flash-Lite session with structured output to classify whether a user message
123
+ * is trivial/low-signal (greetings, confirmations, terminal commands) or contains substantive signal.
124
+ */
125
+ export declare function createTrivialMessageClassifierSession(): any;
101
126
  /**
102
127
  * Generates a concise title for a chat session based on the user's first message.
103
128
  */
@@ -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';
@@ -62,6 +62,14 @@ const MAX_HISTORY_ENTRIES = 500;
62
62
  * payload stays within sane memory bounds.
63
63
  */
64
64
  const MAX_PART_TEXT_LENGTH = 60_000;
65
+ const HISTORICAL_TOOL_OUTPUT_THRESHOLD = 1500;
66
+ const COLLAPSED_TOOL_OUTPUT_MARKER = '\n... [Historical tool output collapsed to save context]';
67
+ function collapseHistoricalOutput(val, threshold = HISTORICAL_TOOL_OUTPUT_THRESHOLD) {
68
+ if (val.length <= threshold || val.includes('[Historical tool output collapsed')) {
69
+ return val;
70
+ }
71
+ return `${val.substring(0, threshold)}${COLLAPSED_TOOL_OUTPUT_MARKER}`;
72
+ }
65
73
  function truncatePartText(text) {
66
74
  if (text.length <= MAX_PART_TEXT_LENGTH)
67
75
  return text;
@@ -117,6 +125,64 @@ export class ProxyChatSession {
117
125
  this.fullHistory.push(JSON.parse(JSON.stringify(userContent)));
118
126
  this.fullHistory.push(JSON.parse(JSON.stringify(modelContent)));
119
127
  }
128
+ /**
129
+ * Appends or merges the final conversational summary text into the active session history.
130
+ * Ensures that the final response produced by finish_task, PM reconciliation, or final turns
131
+ * is permanently preserved as a model text response in both working history and full transcript.
132
+ */
133
+ appendFinalSummary(text) {
134
+ if (!text || !text.trim())
135
+ return;
136
+ const clean = text.replace(/\[TASK_FINISHED\]/g, '').trim();
137
+ if (!clean)
138
+ return;
139
+ // 1. Ensure fullHistory has the model text response
140
+ if (this.fullHistory.length > 0) {
141
+ const lastFull = this.fullHistory[this.fullHistory.length - 1];
142
+ if (lastFull.role === 'model') {
143
+ const hasText = lastFull.parts?.some((p) => p.text && p.text.trim() === clean);
144
+ if (!hasText) {
145
+ lastFull.parts = lastFull.parts || [];
146
+ lastFull.parts.push({ text: clean });
147
+ }
148
+ }
149
+ else {
150
+ this.fullHistory.push({
151
+ role: 'model',
152
+ parts: [{ text: clean }],
153
+ });
154
+ }
155
+ }
156
+ else {
157
+ this.fullHistory.push({
158
+ role: 'model',
159
+ parts: [{ text: clean }],
160
+ });
161
+ }
162
+ // 2. Ensure working history has the model text response
163
+ if (this.history.length > 0) {
164
+ const lastHist = this.history[this.history.length - 1];
165
+ if (lastHist.role === 'model') {
166
+ const hasText = lastHist.parts?.some((p) => p.text && p.text.trim() === clean);
167
+ if (!hasText) {
168
+ lastHist.parts = lastHist.parts || [];
169
+ lastHist.parts.push({ text: clean });
170
+ }
171
+ }
172
+ else {
173
+ this.history.push({
174
+ role: 'model',
175
+ parts: [{ text: clean }],
176
+ });
177
+ }
178
+ }
179
+ else {
180
+ this.history.push({
181
+ role: 'model',
182
+ parts: [{ text: clean }],
183
+ });
184
+ }
185
+ }
120
186
  /**
121
187
  * Retrieves the most recent conversation history as a formatted string.
122
188
  * Useful for passing conversation context to stateless background agents.
@@ -164,6 +230,7 @@ export class ProxyChatSession {
164
230
  * conversational context while preventing OOM crashes.
165
231
  */
166
232
  async pruneHistory() {
233
+ this.pruneToolOutputHistory();
167
234
  if (this.history.length > MAX_HISTORY_ENTRIES) {
168
235
  const excess = this.history.length - MAX_HISTORY_ENTRIES;
169
236
  const trimCount = excess % 2 === 0 ? excess : excess + 1;
@@ -181,6 +248,46 @@ export class ProxyChatSession {
181
248
  }
182
249
  }
183
250
  }
251
+ /**
252
+ * Collapses oversized functionResponse outputs in historical turns (>1 turn old)
253
+ * while strictly maintaining Gemini functionCall and functionResponse pairing.
254
+ *
255
+ * Gemini requires every functionCall in a model turn to have a matching functionResponse
256
+ * in the immediately following user turn. Deleting parts or entries breaks this invariant and
257
+ * causes 400 Bad Request errors. This method mutates oversized response outputs in-place
258
+ * on older turns to drastically conserve context window tokens.
259
+ *
260
+ * @param threshold Maximum characters allowed for a historical tool response before collapsing (default: 1500).
261
+ * @param turnsToKeep Number of recent turns to preserve unpruned (default: 1).
262
+ */
263
+ pruneToolOutputHistory(threshold = HISTORICAL_TOOL_OUTPUT_THRESHOLD, turnsToKeep = 1) {
264
+ // 1 turn = 1 user Content + 1 model Content pair (2 entries)
265
+ const cutoffIndex = Math.max(0, this.history.length - turnsToKeep * 2);
266
+ if (cutoffIndex <= 0)
267
+ return;
268
+ for (let i = 0; i < cutoffIndex; i++) {
269
+ const entry = this.history[i];
270
+ if (entry && entry.role === 'user' && Array.isArray(entry.parts)) {
271
+ for (const part of entry.parts) {
272
+ if (part && typeof part === 'object' && 'functionResponse' in part && part.functionResponse) {
273
+ const funcResp = part.functionResponse;
274
+ if (funcResp.response && typeof funcResp.response === 'object') {
275
+ const respObj = funcResp.response;
276
+ if (typeof respObj.output === 'string') {
277
+ respObj.output = collapseHistoricalOutput(respObj.output, threshold);
278
+ }
279
+ if (typeof respObj.error === 'string') {
280
+ respObj.error = collapseHistoricalOutput(respObj.error, threshold);
281
+ }
282
+ if (typeof respObj.result === 'string') {
283
+ respObj.result = collapseHistoricalOutput(respObj.result, threshold);
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+ }
184
291
  async sendMessage(message, additionalText, abortSignal, onChunk) {
185
292
  const idToken = await getAuthorizedIdToken();
186
293
  if (!idToken) {
@@ -756,6 +863,150 @@ export async function summarizeChatHistory(history, abortSignal) {
756
863
  return '';
757
864
  }
758
865
  }
866
+ // ─── User Profile Extractor Service ────────────────────────────────────
867
+ export function createUserProfileExtractorSession() {
868
+ // Configured exclusively with FLASH_LITE for lightweight zero-latency profiling
869
+ const model = GEMINI_MODELS.FLASH_LITE;
870
+ return new ProxyChatSession(model, USER_PROFILE_EXTRACTOR_SYSTEM_INSTRUCTION, [], // no tools
871
+ {
872
+ temperature: 0,
873
+ responseMimeType: 'application/json',
874
+ responseSchema: {
875
+ type: SchemaType.OBJECT,
876
+ properties: {
877
+ tonePreference: {
878
+ type: SchemaType.STRING,
879
+ description: 'Observed tone and demeanor traits of the user',
880
+ },
881
+ verbosity: {
882
+ type: SchemaType.STRING,
883
+ description: 'Observed verbosity and output formatting preference',
884
+ },
885
+ formulationStyle: {
886
+ type: SchemaType.STRING,
887
+ description: 'Observed thought formulation and prompting style',
888
+ },
889
+ cognitiveTraits: {
890
+ type: SchemaType.OBJECT,
891
+ properties: {
892
+ architecturalStyle: {
893
+ type: SchemaType.STRING,
894
+ description: 'e.g., "top-down-design", "bottom-up-code", "balanced"',
895
+ },
896
+ decisionPreference: {
897
+ type: SchemaType.STRING,
898
+ description: 'e.g., "direct-recommendation", "present-options"',
899
+ },
900
+ riskTolerance: {
901
+ type: SchemaType.STRING,
902
+ description: 'e.g., "defensive-rigor", "pragmatic-speed"',
903
+ },
904
+ delegationDepth: {
905
+ type: SchemaType.STRING,
906
+ description: 'e.g., "autonomous-delegation", "hands-on-stepwise"',
907
+ },
908
+ debuggingStyle: {
909
+ type: SchemaType.STRING,
910
+ description: 'e.g., "minimal-diff-fix", "root-cause-deep-dive"',
911
+ },
912
+ explanationFormat: {
913
+ type: SchemaType.STRING,
914
+ description: 'e.g., "code-first", "bullet-summaries", "conceptual-analogies"',
915
+ },
916
+ },
917
+ description: 'Observed cognitive decision-making, collaboration, and problem-solving traits',
918
+ },
919
+ strengths: {
920
+ type: SchemaType.ARRAY,
921
+ items: {
922
+ type: SchemaType.STRING,
923
+ },
924
+ description: 'Observed technical strengths or technologies the user is actively familiar with',
925
+ },
926
+ removeStrengths: {
927
+ type: SchemaType.ARRAY,
928
+ items: {
929
+ type: SchemaType.STRING,
930
+ },
931
+ description: 'Outdated or abandoned technologies to remove when a pivot/contradiction occurs',
932
+ },
933
+ conventions: {
934
+ type: SchemaType.ARRAY,
935
+ items: {
936
+ type: SchemaType.STRING,
937
+ },
938
+ description: 'Observed architectural habits or coding conventions',
939
+ },
940
+ removeConventions: {
941
+ type: SchemaType.ARRAY,
942
+ items: {
943
+ type: SchemaType.STRING,
944
+ },
945
+ description: 'Outdated coding conventions to remove when a pivot/contradiction occurs',
946
+ },
947
+ addNotes: {
948
+ type: SchemaType.ARRAY,
949
+ items: {
950
+ type: SchemaType.STRING,
951
+ },
952
+ description: 'New non-conflicting side-notes to add (empty if none)',
953
+ },
954
+ updateNotes: {
955
+ type: SchemaType.ARRAY,
956
+ items: {
957
+ type: SchemaType.OBJECT,
958
+ properties: {
959
+ index: {
960
+ type: SchemaType.INTEGER,
961
+ description: '0-based index of the existing note to update',
962
+ },
963
+ updatedText: {
964
+ type: SchemaType.STRING,
965
+ description: 'Revised note text that supersedes the outdated observation',
966
+ },
967
+ },
968
+ required: ['index', 'updatedText'],
969
+ },
970
+ description: 'Targeted updates to existing notes that evolved',
971
+ },
972
+ deleteNoteIndices: {
973
+ type: SchemaType.ARRAY,
974
+ items: {
975
+ type: SchemaType.INTEGER,
976
+ },
977
+ description: '0-based indices of contradicted or obsolete notes to delete',
978
+ },
979
+ },
980
+ required: ['addNotes', 'updateNotes', 'deleteNoteIndices'],
981
+ },
982
+ });
983
+ }
984
+ /**
985
+ * Creates a Flash-Lite session with structured output to classify whether a user message
986
+ * is trivial/low-signal (greetings, confirmations, terminal commands) or contains substantive signal.
987
+ */
988
+ export function createTrivialMessageClassifierSession() {
989
+ const model = GEMINI_MODELS.FLASH_LITE;
990
+ return new ProxyChatSession(model, TRIVIAL_MESSAGE_CLASSIFIER_SYSTEM_INSTRUCTION, [], // no tools
991
+ {
992
+ temperature: 0,
993
+ responseMimeType: 'application/json',
994
+ responseSchema: {
995
+ type: SchemaType.OBJECT,
996
+ properties: {
997
+ isTrivial: {
998
+ type: SchemaType.BOOLEAN,
999
+ description: 'True if the message is a low-signal greeting, confirmation, affirmation, or simple control command with zero technical/personal traits',
1000
+ },
1001
+ reason: {
1002
+ type: SchemaType.STRING,
1003
+ description: 'Brief reason for the classification decision',
1004
+ },
1005
+ },
1006
+ required: ['isTrivial'],
1007
+ },
1008
+ });
1009
+ }
759
1010
  /**
760
1011
  * Generates a concise title for a chat session based on the user's first message.
761
1012
  */
@@ -773,7 +1024,7 @@ export async function generateChatTitle(firstMessage, abortSignal) {
773
1024
  const contents = [{ role: 'user', parts: [{ text: firstMessage.substring(0, 500) }] }];
774
1025
  let model = getGlobalActiveModel();
775
1026
  if (model === 'auto' || model.includes('claude'))
776
- model = GEMINI_MODELS.FLASH;
1027
+ model = GEMINI_MODELS.FLASH_LITE;
777
1028
  const byokEnabled = await isByokEnabled();
778
1029
  let result;
779
1030
  if (byokEnabled) {
@@ -1,4 +1,17 @@
1
1
  import type { Content } from '@google/generative-ai';
2
+ /**
3
+ * Options for configuring dynamic history pruning.
4
+ */
5
+ export interface HistoryPruneOptions {
6
+ /** Maximum number of history entries (user + model contents) to keep in active memory. Defaults to 50. */
7
+ maxEntries?: number;
8
+ /** Maximum estimated tokens allowed for the active conversation history. Defaults to 64,000. */
9
+ maxTokens?: number;
10
+ /** Minimum number of recent entries to retain regardless of token limits. Defaults to 10 (5 full turns). */
11
+ minRecentEntries?: number;
12
+ /** Whether to automatically archive pruned entries to disk. Defaults to true. */
13
+ archivePruned?: boolean;
14
+ }
2
15
  /**
3
16
  * Represents the structure of a saved chat session.
4
17
  * This data is persisted in the local workspace cache to allow users to resume
@@ -41,7 +54,16 @@ export interface ChatSessionData {
41
54
  modelUsageCounts?: Record<string, number>;
42
55
  }
43
56
  /**
44
- * Service responsible for managing the persistence, retrieval, and deletion of chat session histories.
57
+ * Estimates the token count for a Gemini Content object.
58
+ *
59
+ * @param content The Content object containing parts and role.
60
+ * @returns Estimated number of tokens.
61
+ */
62
+ export declare function estimateContentTokens(content: Content): number;
63
+ /**
64
+ * Service responsible for managing the persistence, retrieval, history pruning,
65
+ * and deletion of chat session histories.
66
+ *
45
67
  * It stores session metadata and message history in a local JSON cache (`chat_sessions.json`)
46
68
  * within the project's storage directory and enforces a maximum limit of 250 sessions to prevent
47
69
  * unbounded storage growth.
@@ -51,6 +73,10 @@ declare class ChatHistoryService {
51
73
  private workspaceRoot;
52
74
  /** The maximum number of chat sessions allowed in the cache. Oldest sessions are discarded when this limit is exceeded. */
53
75
  private readonly MAX_SESSIONS;
76
+ /** Default maximum entries allowed in active history before automatic archiving. */
77
+ private readonly DEFAULT_MAX_HISTORY_ENTRIES;
78
+ /** Default maximum estimated tokens before pruning. */
79
+ private readonly DEFAULT_MAX_TOKENS;
54
80
  /**
55
81
  * Initializes the chat history service with the workspace root path.
56
82
  * This must be called before attempting to read, save, or delete sessions.
@@ -65,15 +91,82 @@ declare class ChatHistoryService {
65
91
  * @returns An array of saved `ChatSessionData` objects, ordered as stored in the cache.
66
92
  */
67
93
  getSessions(): ChatSessionData[];
94
+ /**
95
+ * Retrieves a single chat session by ID.
96
+ *
97
+ * @param id - The unique session identifier.
98
+ * @returns The session data, or null if not found.
99
+ */
100
+ getSession(id: string): ChatSessionData | null;
101
+ /**
102
+ * Estimates the total token count of all messages within a given chat session.
103
+ *
104
+ * @param session The chat session to evaluate.
105
+ * @returns Total estimated tokens.
106
+ */
107
+ estimateSessionTokens(session: ChatSessionData): number;
108
+ /**
109
+ * Validates and cleans conversation turns by removing empty parts or invalid structures.
110
+ *
111
+ * @param history Array of Content objects.
112
+ * @returns Sanitized array of Content objects.
113
+ */
114
+ sanitizeSessionHistory(history: Content[]): Content[];
115
+ /**
116
+ * Appends pruned history items to the session's archive JSON file on disk.
117
+ *
118
+ * @param id - The session identifier.
119
+ * @param prunedEntries - Array of pruned Content objects.
120
+ */
121
+ archiveSessionHistory(id: string, prunedEntries: Content[]): Promise<void>;
122
+ /**
123
+ * Retrieves previously archived history entries for a given session.
124
+ *
125
+ * @param id - The session identifier.
126
+ * @returns Array of archived Content objects.
127
+ */
128
+ getArchivedSessionHistory(id: string): Promise<Content[]>;
129
+ /**
130
+ * Prunes a session's history to satisfy entry and token bounds while preserving
131
+ * recent conversational context and alternating turn integrity.
132
+ *
133
+ * @param session The session to prune.
134
+ * @param options History pruning configuration options.
135
+ * @returns An object containing the pruned session and count of archived entries.
136
+ */
137
+ pruneSessionHistory(session: ChatSessionData, options?: HistoryPruneOptions): Promise<{
138
+ prunedSession: ChatSessionData;
139
+ archivedCount: number;
140
+ }>;
68
141
  /**
69
142
  * Saves or updates a chat session in the local workspace cache.
70
143
  * If a session with the same ID already exists, it is updated in place. Otherwise, it is appended.
144
+ * Automatically executes history pruning and archiving if the session exceeds token/turn thresholds.
71
145
  * Enforces the maximum session limit of 250 by removing the oldest session if the limit is exceeded.
72
146
  *
73
147
  * @param session - The chat session data to be saved.
148
+ * @param pruneOptions - Optional pruning configuration overrides.
74
149
  * @returns A promise that resolves when the session has been successfully written to the cache.
75
150
  */
76
- saveSession(session: ChatSessionData): Promise<void>;
151
+ saveSession(session: ChatSessionData, pruneOptions?: HistoryPruneOptions): Promise<void>;
152
+ /**
153
+ * Retrieves a chat session with its history constrained to a specific token budget.
154
+ *
155
+ * @param id - The unique session identifier.
156
+ * @param maxTokens - Maximum token budget for the returned history.
157
+ * @returns The session with budget-constrained history, or null if session not found.
158
+ */
159
+ getSessionWithTokenBudget(id: string, maxTokens: number): Promise<ChatSessionData | null>;
160
+ /**
161
+ * Prunes stale sessions based on age (in days) or total count limit.
162
+ *
163
+ * @param options Pruning options specifying maxAgeDays and maxTotalSessions.
164
+ * @returns Number of sessions pruned.
165
+ */
166
+ pruneOldSessions(options?: {
167
+ maxAgeDays?: number;
168
+ maxTotalSessions?: number;
169
+ }): Promise<number>;
77
170
  /**
78
171
  * Updates the user-friendly title of an existing chat session in the local workspace cache.
79
172
  * If the session with the matching ID exists, its title is updated and persisted to `chat_sessions.json`.