minovative-mind-cli 2.14.0 → 2.14.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 (35) hide show
  1. package/README.md +51 -68
  2. package/dist/services/agent/slashCommands.js +74 -31
  3. package/dist/services/agent/toolLoop.js +3 -1
  4. package/dist/services/agent-tools.d.ts +18 -0
  5. package/dist/services/agent-tools.js +220 -43
  6. package/dist/services/agent.js +6 -3
  7. package/dist/services/ai.d.ts +3 -0
  8. package/dist/services/ai.js +65 -20
  9. package/dist/services/contextAgent.d.ts +10 -4
  10. package/dist/services/contextAgent.js +33 -9
  11. package/dist/services/orchestration/investigationAgent.js +34 -9
  12. package/dist/services/orchestration/investigationCache.js +19 -9
  13. package/dist/services/orchestration/readCache.d.ts +1 -0
  14. package/dist/services/orchestration/readCache.js +5 -2
  15. package/dist/services/orchestration/scopedTools.js +37 -10
  16. package/dist/services/orchestration/subAgent.js +16 -2
  17. package/dist/services/proxyClient.d.ts +6 -0
  18. package/dist/services/proxyClient.js +24 -10
  19. package/dist/services/userProfileService.d.ts +14 -0
  20. package/dist/services/userProfileService.js +105 -3
  21. package/dist/utils/analysisRunner.d.ts +120 -8
  22. package/dist/utils/analysisRunner.js +946 -125
  23. package/dist/utils/contextPrompts.d.ts +39 -0
  24. package/dist/utils/contextPrompts.js +81 -9
  25. package/dist/utils/contextRanker.d.ts +216 -0
  26. package/dist/utils/contextRanker.js +603 -0
  27. package/dist/utils/dependencyTracer/modules/graph.d.ts +4 -1
  28. package/dist/utils/dependencyTracer/modules/graph.js +11 -0
  29. package/dist/utils/dependencyTracer/modules/types.d.ts +10 -0
  30. package/dist/utils/dependencyTracer.d.ts +25 -0
  31. package/dist/utils/dependencyTracer.js +34 -0
  32. package/dist/utils/systemPrompts.d.ts +1 -1
  33. package/dist/utils/systemPrompts.js +13 -2
  34. package/oclif.manifest.json +1 -1
  35. package/package.json +1 -1
@@ -557,8 +557,8 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
557
557
  else if (call.name === 'find_recent_changes') {
558
558
  logMsg = ` [Context Agent] Looking for recently modified files`;
559
559
  }
560
- else if (call.name === 'run_analysis_script') {
561
- logMsg = ` [Context Agent] Running analysis script (${args.language})`;
560
+ else if (call.name === 'run_analysis_script' || call.name === 'run_debug_script') {
561
+ logMsg = ` [Context Agent] Running analysis script (${args.language || 'auto'})`;
562
562
  }
563
563
  if (onProgress) {
564
564
  onProgress(logMsg.trim());
@@ -745,18 +745,42 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
745
745
  },
746
746
  };
747
747
  }
748
- else if (call.name === 'run_analysis_script') {
749
- const analysisResult = await runEphemeralScript(workspaceRoot, args.language, args.code, {
748
+ else if (call.name === 'run_analysis_script' || call.name === 'run_debug_script') {
749
+ const analysisResult = await runEphemeralScript(workspaceRoot, args.language || 'auto', args.code, {
750
750
  abortSignal,
751
751
  });
752
- const rawOutput = analysisResult.exitCode === 0
753
- ? analysisResult.stdout || '(script produced no output)'
754
- : `Script failed (exit ${analysisResult.exitCode}):\n${analysisResult.stderr}`;
755
- const output = boundToolOutput(rawOutput);
752
+ let rawOutput = '';
753
+ if (analysisResult.exitCode !== 0) {
754
+ rawOutput += `Script failed with exit code ${analysisResult.exitCode}.\n`;
755
+ }
756
+ if (analysisResult.structuredResult !== undefined) {
757
+ const formattedStructured = typeof analysisResult.structuredResult === 'string'
758
+ ? analysisResult.structuredResult
759
+ : JSON.stringify(analysisResult.structuredResult, null, 2);
760
+ rawOutput += `[STRUCTURED RESULT]\n${formattedStructured}\n`;
761
+ }
762
+ if (analysisResult.stdout) {
763
+ rawOutput += `[STDOUT]\n${analysisResult.stdout}\n`;
764
+ }
765
+ if (analysisResult.stderr) {
766
+ rawOutput += `[STDERR]\n${analysisResult.stderr}\n`;
767
+ }
768
+ if (!rawOutput.trim()) {
769
+ rawOutput = '(script produced no output)';
770
+ }
771
+ const output = boundToolOutput(rawOutput.trim());
756
772
  return {
757
773
  functionResponse: {
758
774
  name: call.name,
759
- response: { output },
775
+ response: {
776
+ output,
777
+ ...(analysisResult.structuredResult !== undefined
778
+ ? { structuredResult: analysisResult.structuredResult }
779
+ : {}),
780
+ ...(analysisResult.exitCode !== 0
781
+ ? { error: `Script failed (exit ${analysisResult.exitCode}):\n${analysisResult.stderr}` }
782
+ : {}),
783
+ },
760
784
  },
761
785
  };
762
786
  }
@@ -11,7 +11,7 @@
11
11
  * components" + "Auth UI styling") and produces an `InvestigationResult` containing
12
12
  * the files it found and a summary of its findings.
13
13
  */
14
- import { ProxyChatSession, getContextToolDeclarations, getGlobalActiveModel, getMultiWorkspaceBlock } from '../ai.js';
14
+ import { ProxyChatSession, getContextToolDeclarations, getGlobalActiveModel, getModelThinkingLevel, getMultiWorkspaceBlock } from '../ai.js';
15
15
  import { GEMINI_MODELS, MAX_OUTPUT_TOKENS } from '../../utils/config.js';
16
16
  import { CONTEXT_SYSTEM_INSTRUCTION } from '../../utils/systemPrompts.js';
17
17
  import { debugLog } from '../../utils/logger.js';
@@ -54,13 +54,14 @@ export class InvestigationAgentRunner {
54
54
  let model = getGlobalActiveModel();
55
55
  if (model === GEMINI_MODELS.AUTO || model.includes('claude'))
56
56
  model = GEMINI_MODELS.FLASH;
57
+ const thinkingLevel = getModelThinkingLevel(model);
57
58
  this.chat = new ProxyChatSession(model, this.buildSystemInstruction(), getContextToolDeclarations(), {
58
59
  maxOutputTokens: MAX_OUTPUT_TOKENS,
59
60
  temperature: 1,
60
61
  topP: 0.95,
61
62
  topK: 40,
62
63
  thinkingConfig: {
63
- thinkingLevel: 'MEDIUM',
64
+ thinkingLevel,
64
65
  },
65
66
  }, {
66
67
  functionCallingConfig: {
@@ -328,19 +329,43 @@ export class InvestigationAgentRunner {
328
329
  },
329
330
  };
330
331
  }
331
- else if (call.name === 'run_analysis_script') {
332
+ else if (call.name === 'run_analysis_script' || call.name === 'run_debug_script') {
332
333
  if (onProgress)
333
- onProgress(`${logPrefix} Running analysis script`);
334
- const analysisResult = await runEphemeralScript(this.workspaceRoot, args.language, args.code, {
334
+ onProgress(`${logPrefix} Running analysis script (${args.language || 'auto'})`);
335
+ const analysisResult = await runEphemeralScript(this.workspaceRoot, args.language || 'auto', args.code, {
335
336
  abortSignal,
336
337
  });
337
- const output = analysisResult.exitCode === 0
338
- ? analysisResult.stdout || '(script produced no output)'
339
- : `Script failed (exit ${analysisResult.exitCode}):\n${analysisResult.stderr}`;
338
+ let rawOutput = '';
339
+ if (analysisResult.exitCode !== 0) {
340
+ rawOutput += `Script failed with exit code ${analysisResult.exitCode}.\n`;
341
+ }
342
+ if (analysisResult.structuredResult !== undefined) {
343
+ const formattedStructured = typeof analysisResult.structuredResult === 'string'
344
+ ? analysisResult.structuredResult
345
+ : JSON.stringify(analysisResult.structuredResult, null, 2);
346
+ rawOutput += `[STRUCTURED RESULT]\n${formattedStructured}\n`;
347
+ }
348
+ if (analysisResult.stdout) {
349
+ rawOutput += `[STDOUT]\n${analysisResult.stdout}\n`;
350
+ }
351
+ if (analysisResult.stderr) {
352
+ rawOutput += `[STDERR]\n${analysisResult.stderr}\n`;
353
+ }
354
+ if (!rawOutput.trim()) {
355
+ rawOutput = '(script produced no output)';
356
+ }
340
357
  return {
341
358
  functionResponse: {
342
359
  name: call.name,
343
- response: { output },
360
+ response: {
361
+ output: rawOutput.trim(),
362
+ ...(analysisResult.structuredResult !== undefined
363
+ ? { structuredResult: analysisResult.structuredResult }
364
+ : {}),
365
+ ...(analysisResult.exitCode !== 0
366
+ ? { error: `Script failed (exit ${analysisResult.exitCode}):\n${analysisResult.stderr}` }
367
+ : {}),
368
+ },
344
369
  },
345
370
  };
346
371
  }
@@ -332,20 +332,21 @@ function tokenJaccardSimilarity(a, b) {
332
332
  export async function generateWorkspaceFingerprint(workspaceRoot, relevantFiles) {
333
333
  const fileStats = [];
334
334
  for (const file of relevantFiles) {
335
+ const normFile = file.replace(/\\/g, '/').toLowerCase();
335
336
  try {
336
- const fullPath = path.join(workspaceRoot, file);
337
+ const fullPath = path.isAbsolute(file) ? file : path.join(workspaceRoot, file);
337
338
  const stat = await fs.stat(fullPath);
338
- fileStats.push(`${file}:${stat.mtimeMs}:${stat.size}`);
339
+ fileStats.push(`${normFile}:${stat.mtimeMs}:${stat.size}`);
339
340
  }
340
341
  catch (e) {
341
342
  if (e.code === 'ENOENT') {
342
- fileStats.push(`${file}:deleted`);
343
+ fileStats.push(`${normFile}:deleted`);
343
344
  }
344
345
  else if (e.code === 'EACCES' || e.code === 'EPERM') {
345
- fileStats.push(`${file}:inaccessible`);
346
+ fileStats.push(`${normFile}:inaccessible`);
346
347
  }
347
348
  else {
348
- fileStats.push(`${file}:error`);
349
+ fileStats.push(`${normFile}:error`);
349
350
  }
350
351
  }
351
352
  }
@@ -356,15 +357,17 @@ export async function generateWorkspaceFingerprint(workspaceRoot, relevantFiles)
356
357
  * Enforces LRU eviction to keep cache store below MAX_CACHE_SIZE_BYTES (5MB).
357
358
  */
358
359
  export function pruneCacheStore(store) {
360
+ if (!store || !store.entries)
361
+ return;
359
362
  let storeJson = JSON.stringify(store);
360
363
  while (Buffer.byteLength(storeJson, 'utf8') > MAX_CACHE_SIZE_BYTES) {
361
364
  const keys = Object.keys(store.entries);
362
365
  if (keys.length === 0)
363
366
  break;
364
367
  let oldestKey = keys[0];
365
- let oldestTime = store.entries[oldestKey].lastAccessedAt || store.entries[oldestKey].createdAt;
368
+ let oldestTime = store.entries[oldestKey].lastAccessedAt || store.entries[oldestKey].createdAt || 0;
366
369
  for (let i = 1; i < keys.length; i++) {
367
- const entryTime = store.entries[keys[i]].lastAccessedAt || store.entries[keys[i]].createdAt;
370
+ const entryTime = store.entries[keys[i]].lastAccessedAt || store.entries[keys[i]].createdAt || 0;
368
371
  if (entryTime < oldestTime) {
369
372
  oldestKey = keys[i];
370
373
  oldestTime = entryTime;
@@ -612,7 +615,14 @@ export async function invalidateFilesFromInvestigationCache(workspaceRoot, chang
612
615
  const entry = store.entries[hash];
613
616
  const dependsOnChange = entry.relevantFiles.some((f) => {
614
617
  const normF = f.replace(/\\/g, '/').toLowerCase();
615
- return normalizedChanged.some((c) => normF === c || normF.endsWith('/' + c) || c.endsWith('/' + normF));
618
+ const baseF = path.basename(normF);
619
+ return normalizedChanged.some((c) => {
620
+ const baseC = path.basename(c);
621
+ return (normF === c ||
622
+ normF.endsWith('/' + c) ||
623
+ c.endsWith('/' + normF) ||
624
+ (baseF === baseC && (normF.includes(c) || c.includes(normF))));
625
+ });
616
626
  });
617
627
  if (dependsOnChange) {
618
628
  delete store.entries[hash];
@@ -620,7 +630,7 @@ export async function invalidateFilesFromInvestigationCache(workspaceRoot, chang
620
630
  }
621
631
  }
622
632
  if (invalidated > 0) {
623
- writeCache(workspaceRoot, CACHE_FILE, store);
633
+ await writeCache(workspaceRoot, CACHE_FILE, store);
624
634
  debugLog(`[DEBUG] Investigation cache: invalidated ${invalidated} entries referencing changed files`);
625
635
  }
626
636
  }
@@ -73,6 +73,7 @@ export declare class ReadCache {
73
73
  /**
74
74
  * Invalidates and evicts all cache entries associated with a file path,
75
75
  * including full-file reads and any ranged or targeted sub-reads.
76
+ * Uses case-insensitive and normalized path comparison.
76
77
  *
77
78
  * @param filePath - The file path that was modified or deleted.
78
79
  */
@@ -115,16 +115,19 @@ export class ReadCache {
115
115
  /**
116
116
  * Invalidates and evicts all cache entries associated with a file path,
117
117
  * including full-file reads and any ranged or targeted sub-reads.
118
+ * Uses case-insensitive and normalized path comparison.
118
119
  *
119
120
  * @param filePath - The file path that was modified or deleted.
120
121
  */
121
122
  invalidate(filePath) {
122
123
  if (!filePath)
123
124
  return;
124
- const prefix = `${filePath}::`;
125
+ const normalizedTarget = filePath.replace(/\\/g, '/').toLowerCase();
126
+ const prefix = `${normalizedTarget}::`;
125
127
  const keysToRemove = [];
126
128
  for (const key of this.cache.keys()) {
127
- if (key === filePath || key.startsWith(prefix)) {
129
+ const normKey = key.replace(/\\/g, '/').toLowerCase();
130
+ if (normKey === normalizedTarget || normKey.startsWith(prefix)) {
128
131
  keysToRemove.push(key);
129
132
  }
130
133
  }
@@ -215,18 +215,37 @@ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus,
215
215
  inlineData: result.inlineData,
216
216
  });
217
217
  }
218
- // If a file was modified/written/deleted/renamed, invalidate readCache entries for affected file(s)
219
- if (readCache) {
220
- if (name === 'write_file' || name === 'modify_file' || name === 'delete_file') {
221
- if (args?.filePath) {
218
+ // If a file was modified/written/deleted/renamed, invalidate readCache and investigationCache entries for affected file(s)
219
+ if (name === 'write_file' || name === 'modify_file' || name === 'delete_file') {
220
+ if (args?.filePath) {
221
+ if (readCache) {
222
222
  readCache.invalidate(args.filePath);
223
223
  }
224
+ import('./investigationCache.js')
225
+ .then(({ invalidateFilesFromInvestigationCache }) => {
226
+ invalidateFilesFromInvestigationCache(workspaceRoot, [args.filePath]).catch(() => { });
227
+ })
228
+ .catch(() => { });
224
229
  }
225
- else if (name === 'rename_file') {
226
- if (args?.sourcePath)
230
+ }
231
+ else if (name === 'rename_file') {
232
+ const affected = [];
233
+ if (args?.sourcePath) {
234
+ if (readCache)
227
235
  readCache.invalidate(args.sourcePath);
228
- if (args?.targetPath)
236
+ affected.push(args.sourcePath);
237
+ }
238
+ if (args?.targetPath) {
239
+ if (readCache)
229
240
  readCache.invalidate(args.targetPath);
241
+ affected.push(args.targetPath);
242
+ }
243
+ if (affected.length > 0) {
244
+ import('./investigationCache.js')
245
+ .then(({ invalidateFilesFromInvestigationCache }) => {
246
+ invalidateFilesFromInvestigationCache(workspaceRoot, affected).catch(() => { });
247
+ })
248
+ .catch(() => { });
230
249
  }
231
250
  }
232
251
  // If we got a file lock and had context from a previous writer, we should
@@ -307,10 +326,18 @@ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus,
307
326
  targetDesc = String(args.filePath);
308
327
  actionDesc = 'Deleted';
309
328
  }
310
- else if (name === 'run_debug_script') {
329
+ else if (name === 'run_debug_script' || name === 'run_analysis_script' || name === 'debug_script' || name === 'analysis_script') {
311
330
  targetDesc = String(args.language ?? 'node');
312
- actionDesc = 'Ran debug script';
313
- resultSummary = typeof result === 'object' && result?.error ? result.error : 'Completed debug script';
331
+ actionDesc = name.includes('analysis') ? 'Ran analysis script' : 'Ran debug script';
332
+ if (typeof result === 'object' && result?.error) {
333
+ resultSummary = result.error;
334
+ }
335
+ else if (typeof result === 'object' && result?.structuredResult !== undefined) {
336
+ resultSummary = 'Completed script (structured result)';
337
+ }
338
+ else {
339
+ resultSummary = 'Completed script';
340
+ }
314
341
  }
315
342
  else if (name === 'run_fuzz_probe') {
316
343
  targetDesc = String(args.language ?? 'node');
@@ -4,7 +4,7 @@
4
4
  * Implements the lifecycle, health monitoring, and tool-loop execution for a single
5
5
  * parallelized sub-agent.
6
6
  */
7
- import { ProxyChatSession, getGlobalActiveModel, getModelThinkingLevel } from '../ai.js';
7
+ import { ProxyChatSession, getGlobalActiveModel, getModelThinkingLevel, HISTORICAL_TOOL_OUTPUT_THRESHOLD } from '../ai.js';
8
8
  import { GEMINI_MODELS, MAX_OUTPUT_TOKENS } from '../../utils/config.js';
9
9
  import { ReadCache } from './readCache.js';
10
10
  import { executeScopedTool, getScopedToolDeclarations } from './scopedTools.js';
@@ -99,6 +99,15 @@ export function createCompactToolReference(toolName, output, args) {
99
99
  const file = args?.filePath ? `${args.filePath}` : 'file';
100
100
  return `[Wrote ${file} - tool execution completed]`;
101
101
  }
102
+ if (toolName === 'delete_file') {
103
+ const file = args?.filePath ? `${args.filePath}` : 'file';
104
+ return `[Deleted ${file} - tool execution completed]`;
105
+ }
106
+ if (toolName === 'rename_file') {
107
+ const src = args?.sourcePath || 'source';
108
+ const tgt = args?.targetPath || 'target';
109
+ return `[Renamed ${src} to ${tgt} - tool execution completed]`;
110
+ }
102
111
  if (toolName === 'find_dependencies') {
103
112
  const file = args?.filePath ? `for ${args.filePath}` : '';
104
113
  return `[Dependency trace ${file} completed]`.replace(/\s+/g, ' ');
@@ -237,7 +246,7 @@ export class SubAgentRunner {
237
246
  }
238
247
  }
239
248
  // Also trigger proxy chat session pruning to collapse any remaining long fields
240
- this.chat.pruneToolOutputHistory();
249
+ this.chat.pruneToolOutputHistory(HISTORICAL_TOOL_OUTPUT_THRESHOLD, turnsToKeep);
241
250
  }
242
251
  /**
243
252
  * Executes the sub-agent with a health monitor harness.
@@ -389,6 +398,11 @@ export class SubAgentRunner {
389
398
  `DO NOT IMPLEMENT THIS FULL REQUEST. THIS IS JUST FOR CONTEXT.\n\n` +
390
399
  `${scopedContext}\n` +
391
400
  `</reference_context>\n\n` +
401
+ `<documentation_and_quality_standards>\n` +
402
+ `- Comprehensive Documentation: Write documentation for senior engineers: explain the 'why', document edge-cases/private states, use precise types, and avoid restating the code.\n` +
403
+ `- Provide JSDoc/TSDoc/DocStrings (as appropriate for the language) for all newly added or modified APIs, functions, classes, interfaces, and types (documenting parameters, return values, and behavior).\n` +
404
+ `- Use clean inline comments to explain complex or non-obvious logic, keeping implementation code highly organized and maintainable.\n` +
405
+ `</documentation_and_quality_standards>\n\n` +
392
406
  `<critical_guidelines>\n` +
393
407
  `1. You are ONE worker in a team. Focus EXCLUSIVELY on your specific objective: "${this.intent}".\n` +
394
408
  `2. DO NOT attempt to fulfill the entire original user request in the reference context. Other agents handle other tasks.\n` +
@@ -76,6 +76,12 @@ export declare function accumulateTurnUsage(usage: {
76
76
  candidatesTokenCount?: number;
77
77
  cachedContentTokenCount?: number;
78
78
  }, modelName: string): void;
79
+ /**
80
+ * Detects whether an error message relates to an expired or mismatched Gemini context cache.
81
+ * When models transition (e.g. Flash to Flash-Lite) or a server cache expires,
82
+ * this triggers a fast retry to refresh/recreate the cache under the active model.
83
+ */
84
+ export declare function isCacheMismatchOrExpiredError(errMsg: string | undefined): boolean;
79
85
  /**
80
86
  * Client service interacting directly with the serverless Gemini proxy endpoint.
81
87
  * Ensures authorization via Firebase token passing and parses streamed content.
@@ -82,6 +82,22 @@ export function accumulateTurnUsage(usage, modelName) {
82
82
  globalSessionAccumulatedUsage.totalTokenCount += pTokens + cachedTokens + cTokens;
83
83
  globalSessionAccumulatedUsage.modelsUsed[modelName] = (globalSessionAccumulatedUsage.modelsUsed[modelName] || 0) + 1;
84
84
  }
85
+ /**
86
+ * Detects whether an error message relates to an expired or mismatched Gemini context cache.
87
+ * When models transition (e.g. Flash to Flash-Lite) or a server cache expires,
88
+ * this triggers a fast retry to refresh/recreate the cache under the active model.
89
+ */
90
+ export function isCacheMismatchOrExpiredError(errMsg) {
91
+ if (!errMsg)
92
+ return false;
93
+ const lower = errMsg.toLowerCase();
94
+ return (lower.includes('is expired') ||
95
+ lower.includes('cache content') ||
96
+ lower.includes('cached content') ||
97
+ lower.includes('cached_content') ||
98
+ lower.includes('does not match the model in the cached content') ||
99
+ lower.includes('model in the inference request'));
100
+ }
85
101
  /**
86
102
  * Client service interacting directly with the serverless Gemini proxy endpoint.
87
103
  * Ensures authorization via Firebase token passing and parses streamed content.
@@ -194,9 +210,9 @@ export class ProxyClient {
194
210
  if (!response.ok) {
195
211
  const errorData = await response.json().catch(() => ({}));
196
212
  const errorMsg = JSON.stringify(errorData) || response.statusText;
197
- if (errorMsg.includes('is expired') || errorMsg.includes('Cache content')) {
213
+ if (isCacheMismatchOrExpiredError(errorMsg)) {
198
214
  if (attempt < MAX_RETRIES) {
199
- debugLog(`Gemini Context Cache expired on server. Retrying request to refresh cache (Attempt ${attempt + 1}/${MAX_RETRIES})...`);
215
+ debugLog(`Gemini Context Cache expired or mismatched on server. Retrying request to refresh cache (Attempt ${attempt + 1}/${MAX_RETRIES})...`);
200
216
  await delay(500, abortSignal);
201
217
  attempt++;
202
218
  continue retryLoop;
@@ -293,8 +309,7 @@ export class ProxyClient {
293
309
  err.name = 'AbortError';
294
310
  throw err;
295
311
  }
296
- const isCacheExpired = streamError.message?.includes('is expired') ||
297
- streamError.message?.includes('Cache content');
312
+ const isCacheExpired = isCacheMismatchOrExpiredError(streamError.message);
298
313
  if (streamError.message?.includes('429') ||
299
314
  streamError.message?.includes('502') ||
300
315
  streamError.message?.includes('503') ||
@@ -312,7 +327,7 @@ export class ProxyClient {
312
327
  if (isDebugOn()) {
313
328
  process.stdout.write('\n');
314
329
  console.warn(isCacheExpired
315
- ? `Gemini Context Cache expired on server. Refreshing and retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`
330
+ ? `Gemini Context Cache expired or mismatched on server. Refreshing and retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`
316
331
  : `Server error or rate limit hit during stream. Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
317
332
  }
318
333
  await delay(delayTime, abortSignal);
@@ -435,9 +450,9 @@ export class ProxyClient {
435
450
  // ignore parsing error
436
451
  }
437
452
  const errorMsg = errorData.error?.message || JSON.stringify(errorData) || response.statusText;
438
- if (errorMsg.includes('is expired') || errorMsg.includes('Cache content')) {
453
+ if (isCacheMismatchOrExpiredError(errorMsg)) {
439
454
  if (attempt < MAX_RETRIES) {
440
- debugLog(`BYOK Gemini Context Cache expired on server. Retrying request to refresh cache (Attempt ${attempt + 1}/${MAX_RETRIES})...`);
455
+ debugLog(`BYOK Gemini Context Cache expired or mismatched on server. Retrying request to refresh cache (Attempt ${attempt + 1}/${MAX_RETRIES})...`);
441
456
  await delay(500, abortSignal);
442
457
  attempt++;
443
458
  continue retryLoop;
@@ -526,8 +541,7 @@ export class ProxyClient {
526
541
  err.name = 'AbortError';
527
542
  throw err;
528
543
  }
529
- const isCacheExpired = streamError.message?.includes('is expired') ||
530
- streamError.message?.includes('Cache content');
544
+ const isCacheExpired = isCacheMismatchOrExpiredError(streamError.message);
531
545
  if (streamError.message?.includes('429') ||
532
546
  streamError.message?.includes('502') ||
533
547
  streamError.message?.includes('503') ||
@@ -545,7 +559,7 @@ export class ProxyClient {
545
559
  if (isDebugOn()) {
546
560
  process.stdout.write('\n');
547
561
  console.warn(isCacheExpired
548
- ? `BYOK Gemini Context Cache expired on server. Refreshing and retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`
562
+ ? `BYOK Gemini Context Cache expired or mismatched on server. Refreshing and retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`
549
563
  : `Server error or rate limit hit during BYOK stream. Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
550
564
  }
551
565
  await delay(delayTime, abortSignal);
@@ -30,6 +30,19 @@ export interface UserProfileCognitiveTraits {
30
30
  debuggingStyle?: string;
31
31
  explanationFormat?: string;
32
32
  }
33
+ /**
34
+ * Deep interpersonal, psychological, and conversational personality traits observed by the agent.
35
+ * Captures how the user speaks to the AI, relationship posture, banter, apology tolerance, and quirks.
36
+ */
37
+ export interface UserProfileConversationalPersona {
38
+ relationshipModel?: string;
39
+ banterAffinity?: string;
40
+ formalityLevel?: string;
41
+ apologyTolerance?: string;
42
+ stressCadence?: string;
43
+ promptingHabit?: string;
44
+ conversationalQuirks?: string[];
45
+ }
33
46
  /**
34
47
  * Technical habits and technology strengths observed by the agent.
35
48
  */
@@ -43,6 +56,7 @@ export interface UserProfileTechnicalPreferences {
43
56
  export interface UserProfile {
44
57
  communicationStyle?: UserProfileCommunicationStyle;
45
58
  cognitiveTraits?: UserProfileCognitiveTraits;
59
+ conversationalPersona?: UserProfileConversationalPersona;
46
60
  technicalPreferences?: UserProfileTechnicalPreferences;
47
61
  agentNotes: string[];
48
62
  lastUpdated?: number;
@@ -50,6 +50,7 @@ export async function loadUserProfile() {
50
50
  return {
51
51
  communicationStyle: {},
52
52
  cognitiveTraits: {},
53
+ conversationalPersona: { conversationalQuirks: [] },
53
54
  technicalPreferences: { strengths: [], conventions: [] },
54
55
  agentNotes: [],
55
56
  lastUpdated: Date.now(),
@@ -61,6 +62,17 @@ export async function loadUserProfile() {
61
62
  return {
62
63
  communicationStyle: parsed.communicationStyle || {},
63
64
  cognitiveTraits: parsed.cognitiveTraits || {},
65
+ conversationalPersona: {
66
+ relationshipModel: parsed.conversationalPersona?.relationshipModel,
67
+ banterAffinity: parsed.conversationalPersona?.banterAffinity,
68
+ formalityLevel: parsed.conversationalPersona?.formalityLevel,
69
+ apologyTolerance: parsed.conversationalPersona?.apologyTolerance,
70
+ stressCadence: parsed.conversationalPersona?.stressCadence,
71
+ promptingHabit: parsed.conversationalPersona?.promptingHabit,
72
+ conversationalQuirks: Array.isArray(parsed.conversationalPersona?.conversationalQuirks)
73
+ ? parsed.conversationalPersona.conversationalQuirks
74
+ : [],
75
+ },
64
76
  technicalPreferences: {
65
77
  strengths: Array.isArray(parsed.technicalPreferences?.strengths) ? parsed.technicalPreferences.strengths : [],
66
78
  conventions: Array.isArray(parsed.technicalPreferences?.conventions)
@@ -76,6 +88,7 @@ export async function loadUserProfile() {
76
88
  return {
77
89
  communicationStyle: {},
78
90
  cognitiveTraits: {},
91
+ conversationalPersona: { conversationalQuirks: [] },
79
92
  technicalPreferences: { strengths: [], conventions: [] },
80
93
  agentNotes: [],
81
94
  lastUpdated: Date.now(),
@@ -133,6 +146,8 @@ export async function deleteSideNote(index) {
133
146
  export async function clearUserProfile() {
134
147
  const emptyProfile = {
135
148
  communicationStyle: {},
149
+ cognitiveTraits: {},
150
+ conversationalPersona: { conversationalQuirks: [] },
136
151
  technicalPreferences: { strengths: [], conventions: [] },
137
152
  agentNotes: [],
138
153
  lastUpdated: Date.now(),
@@ -153,15 +168,43 @@ export function formatUserProfileForContext(profile) {
153
168
  profile.cognitiveTraits?.delegationDepth ||
154
169
  profile.cognitiveTraits?.debuggingStyle ||
155
170
  profile.cognitiveTraits?.explanationFormat;
171
+ const hasPersona = profile.conversationalPersona?.relationshipModel ||
172
+ profile.conversationalPersona?.banterAffinity ||
173
+ profile.conversationalPersona?.formalityLevel ||
174
+ profile.conversationalPersona?.apologyTolerance ||
175
+ profile.conversationalPersona?.stressCadence ||
176
+ profile.conversationalPersona?.promptingHabit ||
177
+ (profile.conversationalPersona?.conversationalQuirks && profile.conversationalPersona.conversationalQuirks.length > 0);
156
178
  const hasStrengths = profile.technicalPreferences?.strengths && profile.technicalPreferences.strengths.length > 0;
157
179
  const hasConventions = profile.technicalPreferences?.conventions && profile.technicalPreferences.conventions.length > 0;
158
180
  const hasNotes = profile.agentNotes && profile.agentNotes.length > 0;
159
- if (!hasStyle && !hasCognitive && !hasStrengths && !hasConventions && !hasNotes) {
181
+ if (!hasStyle && !hasCognitive && !hasPersona && !hasStrengths && !hasConventions && !hasNotes) {
160
182
  return '';
161
183
  }
162
184
  let block = '<user_profile>\n';
163
185
  block +=
164
- 'The following are continuous, adaptive insights and preferences learned from past interactions with this user:\n';
186
+ 'The following are continuous, adaptive insights, conversational personality traits, and technical preferences learned from past interactions with this user:\n';
187
+ if (profile.conversationalPersona?.relationshipModel) {
188
+ block += `- Teammate & Pairing Dynamic: ${profile.conversationalPersona.relationshipModel}\n`;
189
+ }
190
+ if (profile.conversationalPersona?.banterAffinity) {
191
+ block += `- Banter & Humor Affinity: ${profile.conversationalPersona.banterAffinity}\n`;
192
+ }
193
+ if (profile.conversationalPersona?.formalityLevel) {
194
+ block += `- Speech Formality: ${profile.conversationalPersona.formalityLevel}\n`;
195
+ }
196
+ if (profile.conversationalPersona?.apologyTolerance) {
197
+ block += `- Apology & Error Reaction: ${profile.conversationalPersona.apologyTolerance}\n`;
198
+ }
199
+ if (profile.conversationalPersona?.stressCadence) {
200
+ block += `- Stress & Urgency Cadence: ${profile.conversationalPersona.stressCadence}\n`;
201
+ }
202
+ if (profile.conversationalPersona?.promptingHabit) {
203
+ block += `- Prompting & Input Style: ${profile.conversationalPersona.promptingHabit}\n`;
204
+ }
205
+ if (profile.conversationalPersona?.conversationalQuirks && profile.conversationalPersona.conversationalQuirks.length > 0) {
206
+ block += `- Conversational Quirks & Catchphrases: ${profile.conversationalPersona.conversationalQuirks.join('; ')}\n`;
207
+ }
165
208
  if (profile.communicationStyle?.tonePreference) {
166
209
  block += `- Tone & Demeanor: ${profile.communicationStyle.tonePreference}\n`;
167
210
  }
@@ -202,7 +245,7 @@ export function formatUserProfileForContext(profile) {
202
245
  }
203
246
  }
204
247
  block +=
205
- 'Adopt these communication, cognitive, and technical preferences naturally without ever explicitly mentioning this profile block.\n';
248
+ 'Adopt these conversational personality traits, tone, and technical preferences naturally as an intuitive pair programming partner without ever explicitly mentioning this profile block.\n';
206
249
  block += '</user_profile>';
207
250
  return block;
208
251
  }
@@ -314,6 +357,65 @@ AI Response Summary:
314
357
  }
315
358
  }
316
359
  }
360
+ // Process conversational persona & teammate dynamics
361
+ if ((parsed.conversationalPersona && typeof parsed.conversationalPersona === 'object') ||
362
+ parsed.relationshipModel ||
363
+ parsed.banterAffinity ||
364
+ parsed.formalityLevel ||
365
+ parsed.apologyTolerance ||
366
+ parsed.stressCadence ||
367
+ parsed.promptingHabit ||
368
+ parsed.conversationalQuirks) {
369
+ currentProfile.conversationalPersona = currentProfile.conversationalPersona || { conversationalQuirks: [] };
370
+ const sourceObj = (parsed.conversationalPersona && typeof parsed.conversationalPersona === 'object')
371
+ ? parsed.conversationalPersona
372
+ : {};
373
+ const personaKeys = [
374
+ 'relationshipModel',
375
+ 'banterAffinity',
376
+ 'formalityLevel',
377
+ 'apologyTolerance',
378
+ 'stressCadence',
379
+ 'promptingHabit',
380
+ ];
381
+ for (const k of personaKeys) {
382
+ const val = sourceObj[k] || parsed[k];
383
+ if (typeof val === 'string' && val.trim().length > 0) {
384
+ currentProfile.conversationalPersona[k] = val.trim();
385
+ changed = true;
386
+ }
387
+ }
388
+ const quirksToAdd = Array.isArray(sourceObj.conversationalQuirks)
389
+ ? sourceObj.conversationalQuirks
390
+ : Array.isArray(parsed.conversationalQuirks)
391
+ ? parsed.conversationalQuirks
392
+ : [];
393
+ if (quirksToAdd.length > 0) {
394
+ const existingQuirks = new Set(currentProfile.conversationalPersona.conversationalQuirks || []);
395
+ for (const q of quirksToAdd) {
396
+ if (typeof q === 'string' && q.trim().length > 0) {
397
+ existingQuirks.add(q.trim());
398
+ changed = true;
399
+ }
400
+ }
401
+ currentProfile.conversationalPersona.conversationalQuirks = Array.from(existingQuirks).slice(0, 10);
402
+ }
403
+ const quirksToRemove = Array.isArray(sourceObj.removeQuirks)
404
+ ? sourceObj.removeQuirks
405
+ : Array.isArray(parsed.removeQuirks)
406
+ ? parsed.removeQuirks
407
+ : [];
408
+ if (quirksToRemove.length > 0) {
409
+ if (currentProfile.conversationalPersona.conversationalQuirks) {
410
+ const toRemove = new Set(quirksToRemove.map((q) => q.toLowerCase().trim()));
411
+ const filtered = currentProfile.conversationalPersona.conversationalQuirks.filter((q) => !toRemove.has(q.toLowerCase().trim()));
412
+ if (filtered.length !== currentProfile.conversationalPersona.conversationalQuirks.length) {
413
+ currentProfile.conversationalPersona.conversationalQuirks = filtered;
414
+ changed = true;
415
+ }
416
+ }
417
+ }
418
+ }
317
419
  if (Array.isArray(parsed.strengths) && parsed.strengths.length > 0) {
318
420
  currentProfile.technicalPreferences = currentProfile.technicalPreferences || { strengths: [], conventions: [] };
319
421
  const existing = new Set(currentProfile.technicalPreferences.strengths || []);