@defai.digital/ax-cli 3.8.6 → 3.8.8

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 (196) hide show
  1. package/README.md +28 -393
  2. package/config-defaults/models.yaml +0 -24
  3. package/config-defaults/settings.yaml +16 -16
  4. package/dist/agent/dependency-resolver.js +22 -1
  5. package/dist/agent/dependency-resolver.js.map +1 -1
  6. package/dist/agent/llm-agent.d.ts +23 -2
  7. package/dist/agent/llm-agent.js +126 -122
  8. package/dist/agent/llm-agent.js.map +1 -1
  9. package/dist/agent/loop-detector.d.ts +70 -0
  10. package/dist/agent/loop-detector.js +339 -0
  11. package/dist/agent/loop-detector.js.map +1 -0
  12. package/dist/agent/progress-tracker.d.ts +94 -0
  13. package/dist/agent/progress-tracker.js +222 -0
  14. package/dist/agent/progress-tracker.js.map +1 -0
  15. package/dist/agent/status-reporter.js +2 -2
  16. package/dist/agent/status-reporter.js.map +1 -1
  17. package/dist/agent/subagent.js +7 -3
  18. package/dist/agent/subagent.js.map +1 -1
  19. package/dist/analyzers/architecture/project-structure-scanner.js +6 -2
  20. package/dist/analyzers/architecture/project-structure-scanner.js.map +1 -1
  21. package/dist/analyzers/git/churn-calculator.js +2 -1
  22. package/dist/analyzers/git/churn-calculator.js.map +1 -1
  23. package/dist/checkpoint/manager.js +18 -4
  24. package/dist/checkpoint/manager.js.map +1 -1
  25. package/dist/checkpoint/storage.d.ts +6 -0
  26. package/dist/checkpoint/storage.js +96 -49
  27. package/dist/checkpoint/storage.js.map +1 -1
  28. package/dist/commands/cache.js +8 -6
  29. package/dist/commands/cache.js.map +1 -1
  30. package/dist/commands/doctor.js +19 -27
  31. package/dist/commands/doctor.js.map +1 -1
  32. package/dist/commands/mcp-migrate.js +6 -5
  33. package/dist/commands/mcp-migrate.js.map +1 -1
  34. package/dist/commands/mcp.js +14 -2
  35. package/dist/commands/mcp.js.map +1 -1
  36. package/dist/commands/models.js +8 -12
  37. package/dist/commands/models.js.map +1 -1
  38. package/dist/commands/plan.js +1 -10
  39. package/dist/commands/plan.js.map +1 -1
  40. package/dist/commands/setup.js +4 -3
  41. package/dist/commands/setup.js.map +1 -1
  42. package/dist/commands/status.js +40 -14
  43. package/dist/commands/status.js.map +1 -1
  44. package/dist/constants.d.ts +12 -0
  45. package/dist/constants.js +16 -4
  46. package/dist/constants.js.map +1 -1
  47. package/dist/hooks/hook-runner.d.ts +138 -0
  48. package/dist/hooks/hook-runner.js +429 -0
  49. package/dist/hooks/hook-runner.js.map +1 -0
  50. package/dist/hooks/index.d.ts +6 -0
  51. package/dist/hooks/index.js +7 -0
  52. package/dist/hooks/index.js.map +1 -0
  53. package/dist/index.js +3 -21
  54. package/dist/index.js.map +1 -1
  55. package/dist/llm/client.d.ts +9 -0
  56. package/dist/llm/client.js +306 -45
  57. package/dist/llm/client.js.map +1 -1
  58. package/dist/llm/tools.js +2 -39
  59. package/dist/llm/tools.js.map +1 -1
  60. package/dist/llm/types.d.ts +1 -47
  61. package/dist/llm/types.js +0 -18
  62. package/dist/llm/types.js.map +1 -1
  63. package/dist/mcp/automatosx-loader.js +2 -1
  64. package/dist/mcp/automatosx-loader.js.map +1 -1
  65. package/dist/mcp/client-v2.d.ts +3 -0
  66. package/dist/mcp/client-v2.js +85 -19
  67. package/dist/mcp/client-v2.js.map +1 -1
  68. package/dist/mcp/config-migrator.js +3 -2
  69. package/dist/mcp/config-migrator.js.map +1 -1
  70. package/dist/mcp/config-v2.d.ts +5 -0
  71. package/dist/mcp/config-v2.js +26 -0
  72. package/dist/mcp/config-v2.js.map +1 -1
  73. package/dist/mcp/error-formatter.js +4 -1
  74. package/dist/mcp/error-formatter.js.map +1 -1
  75. package/dist/mcp/health.js +1 -1
  76. package/dist/mcp/health.js.map +1 -1
  77. package/dist/mcp/reconnection.js +2 -1
  78. package/dist/mcp/reconnection.js.map +1 -1
  79. package/dist/mcp/registry.js +3 -2
  80. package/dist/mcp/registry.js.map +1 -1
  81. package/dist/mcp/resources.js +2 -1
  82. package/dist/mcp/resources.js.map +1 -1
  83. package/dist/mcp/validation.js +9 -0
  84. package/dist/mcp/validation.js.map +1 -1
  85. package/dist/memory/context-store.js +4 -6
  86. package/dist/memory/context-store.js.map +1 -1
  87. package/dist/memory/types.d.ts +2 -0
  88. package/dist/memory/types.js +4 -1
  89. package/dist/memory/types.js.map +1 -1
  90. package/dist/permissions/index.d.ts +6 -0
  91. package/dist/permissions/index.js +7 -0
  92. package/dist/permissions/index.js.map +1 -0
  93. package/dist/permissions/permission-manager.d.ts +145 -0
  94. package/dist/permissions/permission-manager.js +401 -0
  95. package/dist/permissions/permission-manager.js.map +1 -0
  96. package/dist/planner/plan-storage.js +3 -2
  97. package/dist/planner/plan-storage.js.map +1 -1
  98. package/dist/planner/task-planner.js +2 -1
  99. package/dist/planner/task-planner.js.map +1 -1
  100. package/dist/planner/types.d.ts +6 -6
  101. package/dist/schemas/settings-schemas.d.ts +0 -14
  102. package/dist/schemas/settings-schemas.js +0 -10
  103. package/dist/schemas/settings-schemas.js.map +1 -1
  104. package/dist/schemas/tool-schemas.d.ts +2 -2
  105. package/dist/schemas/yaml-schemas.d.ts +15 -0
  106. package/dist/schemas/yaml-schemas.js +3 -0
  107. package/dist/schemas/yaml-schemas.js.map +1 -1
  108. package/dist/tools/bash.js +35 -10
  109. package/dist/tools/bash.js.map +1 -1
  110. package/dist/tools/confirmation-tool.js +3 -2
  111. package/dist/tools/confirmation-tool.js.map +1 -1
  112. package/dist/tools/registry.d.ts +1 -1
  113. package/dist/tools/registry.js +2 -1
  114. package/dist/tools/registry.js.map +1 -1
  115. package/dist/tools/search.js +12 -13
  116. package/dist/tools/search.js.map +1 -1
  117. package/dist/tools/text-editor.d.ts +46 -0
  118. package/dist/tools/text-editor.js +455 -11
  119. package/dist/tools/text-editor.js.map +1 -1
  120. package/dist/tools/todo-tool.js +5 -4
  121. package/dist/tools/todo-tool.js.map +1 -1
  122. package/dist/ui/components/chat-input.js +10 -1
  123. package/dist/ui/components/chat-input.js.map +1 -1
  124. package/dist/ui/components/chat-interface.js +1 -0
  125. package/dist/ui/components/chat-interface.js.map +1 -1
  126. package/dist/ui/components/tool-group-display.js +0 -6
  127. package/dist/ui/components/tool-group-display.js.map +1 -1
  128. package/dist/ui/hooks/use-input-handler.js +7 -6
  129. package/dist/ui/hooks/use-input-handler.js.map +1 -1
  130. package/dist/ui/hooks/use-input-history.js +21 -13
  131. package/dist/ui/hooks/use-input-history.js.map +1 -1
  132. package/dist/ui/utils/tool-grouper.d.ts +1 -2
  133. package/dist/ui/utils/tool-grouper.js +4 -15
  134. package/dist/ui/utils/tool-grouper.js.map +1 -1
  135. package/dist/utils/api-error.d.ts +61 -0
  136. package/dist/utils/api-error.js +176 -0
  137. package/dist/utils/api-error.js.map +1 -0
  138. package/dist/utils/audit-logger.js +2 -1
  139. package/dist/utils/audit-logger.js.map +1 -1
  140. package/dist/utils/auto-accept-logger.js +3 -3
  141. package/dist/utils/auto-accept-logger.js.map +1 -1
  142. package/dist/utils/config-loader.d.ts +3 -0
  143. package/dist/utils/config-loader.js +27 -2
  144. package/dist/utils/config-loader.js.map +1 -1
  145. package/dist/utils/encryption.js +2 -1
  146. package/dist/utils/encryption.js.map +1 -1
  147. package/dist/utils/file-cache.js +4 -2
  148. package/dist/utils/file-cache.js.map +1 -1
  149. package/dist/utils/history-migration.js +5 -4
  150. package/dist/utils/history-migration.js.map +1 -1
  151. package/dist/utils/onboarding-manager.js +2 -1
  152. package/dist/utils/onboarding-manager.js.map +1 -1
  153. package/dist/utils/path-helpers.d.ts +8 -0
  154. package/dist/utils/path-helpers.js +35 -0
  155. package/dist/utils/path-helpers.js.map +1 -0
  156. package/dist/utils/path-security.js +3 -2
  157. package/dist/utils/path-security.js.map +1 -1
  158. package/dist/utils/retry-helper.d.ts +61 -0
  159. package/dist/utils/retry-helper.js +206 -0
  160. package/dist/utils/retry-helper.js.map +1 -0
  161. package/dist/utils/settings-manager.d.ts +1 -21
  162. package/dist/utils/settings-manager.js +2 -82
  163. package/dist/utils/settings-manager.js.map +1 -1
  164. package/dist/utils/streaming-analyzer.d.ts +2 -13
  165. package/dist/utils/streaming-analyzer.js +3 -25
  166. package/dist/utils/streaming-analyzer.js.map +1 -1
  167. package/dist/utils/token-counter.d.ts +13 -1
  168. package/dist/utils/token-counter.js +31 -6
  169. package/dist/utils/token-counter.js.map +1 -1
  170. package/package.json +3 -2
  171. package/packages/schemas/README.md +1 -1
  172. package/packages/schemas/package.json +1 -1
  173. package/dist/tools/web-search/cache.d.ts +0 -62
  174. package/dist/tools/web-search/cache.js +0 -105
  175. package/dist/tools/web-search/cache.js.map +0 -1
  176. package/dist/tools/web-search/engines/crates.d.ts +0 -19
  177. package/dist/tools/web-search/engines/crates.js +0 -87
  178. package/dist/tools/web-search/engines/crates.js.map +0 -1
  179. package/dist/tools/web-search/engines/npm.d.ts +0 -18
  180. package/dist/tools/web-search/engines/npm.js +0 -86
  181. package/dist/tools/web-search/engines/npm.js.map +0 -1
  182. package/dist/tools/web-search/engines/pypi.d.ts +0 -18
  183. package/dist/tools/web-search/engines/pypi.js +0 -75
  184. package/dist/tools/web-search/engines/pypi.js.map +0 -1
  185. package/dist/tools/web-search/index.d.ts +0 -11
  186. package/dist/tools/web-search/index.js +0 -11
  187. package/dist/tools/web-search/index.js.map +0 -1
  188. package/dist/tools/web-search/router.d.ts +0 -34
  189. package/dist/tools/web-search/router.js +0 -245
  190. package/dist/tools/web-search/router.js.map +0 -1
  191. package/dist/tools/web-search/types.d.ts +0 -45
  192. package/dist/tools/web-search/types.js +0 -6
  193. package/dist/tools/web-search/types.js.map +0 -1
  194. package/dist/tools/web-search/web-search-tool.d.ts +0 -51
  195. package/dist/tools/web-search/web-search-tool.js +0 -246
  196. package/dist/tools/web-search/web-search-tool.js.map +0 -1
@@ -40,6 +40,16 @@ export interface StreamingChunk {
40
40
  /** Tool execution duration in milliseconds (for tool_result type) */
41
41
  executionDurationMs?: number;
42
42
  }
43
+ /**
44
+ * Accumulated message from streaming response
45
+ * Contains the full message content and any tool calls
46
+ */
47
+ export interface AccumulatedMessage {
48
+ role?: string;
49
+ content?: string;
50
+ tool_calls?: LLMToolCall[];
51
+ [key: string]: unknown;
52
+ }
43
53
  export declare class LLMAgent extends EventEmitter {
44
54
  private llmClient;
45
55
  private textEditor;
@@ -47,7 +57,6 @@ export declare class LLMAgent extends EventEmitter {
47
57
  private bashOutput;
48
58
  private todoTool;
49
59
  private search;
50
- private webSearch;
51
60
  private _architectureTool?;
52
61
  private _validationTool?;
53
62
  private chatHistory;
@@ -68,6 +77,8 @@ export declare class LLMAgent extends EventEmitter {
68
77
  private samplingConfig;
69
78
  /** Thinking/reasoning mode configuration */
70
79
  private thinkingConfig;
80
+ /** Stored reference to context overflow listener for proper cleanup */
81
+ private contextOverflowListener;
71
82
  /** Track if agent has been disposed */
72
83
  private disposed;
73
84
  /** Tool approval system for VSCode integration */
@@ -160,13 +171,23 @@ export declare class LLMAgent extends EventEmitter {
160
171
  private get validationTool();
161
172
  /**
162
173
  * Detect if a tool call is repetitive (likely causing a loop)
163
- * Returns true if the same tool with similar arguments was called multiple times recently
174
+ * Uses the intelligent LoopDetector which provides:
175
+ * - Tool-specific thresholds (file ops get higher limits)
176
+ * - Progress-based detection (tracks success/failure)
177
+ * - Cycle pattern detection (A→B→A→B loops)
164
178
  */
165
179
  private isRepetitiveToolCall;
180
+ /** Last loop detection result for error messages */
181
+ private lastLoopResult?;
166
182
  /**
167
183
  * Reset the tool call tracking (called at start of new user message)
168
184
  */
169
185
  private resetToolCallTracking;
186
+ /**
187
+ * Generate a helpful warning message when a loop is detected
188
+ * Uses the lastLoopResult for context-aware suggestions
189
+ */
190
+ private getLoopWarningMessage;
170
191
  /**
171
192
  * Check if a request should trigger multi-phase planning
172
193
  */
@@ -3,11 +3,10 @@ import { getAllGrokTools, getMCPManager, initializeMCPServers, } from "../llm/to
3
3
  import { loadMCPConfig } from "../mcp/config.js";
4
4
  import { TextEditorTool, BashTool, TodoTool, SearchTool, } from "../tools/index.js";
5
5
  import { BashOutputTool } from "../tools/bash-output.js";
6
- import { WebSearchTool } from "../tools/web-search/index.js";
7
6
  import { ArchitectureTool } from "../tools/analysis-tools/architecture-tool.js";
8
7
  import { ValidationTool } from "../tools/analysis-tools/validation-tool.js";
9
8
  import { EventEmitter } from "events";
10
- import { AGENT_CONFIG } from "../constants.js";
9
+ import { AGENT_CONFIG, CACHE_CONFIG } from "../constants.js";
11
10
  import { getTokenCounter } from "../utils/token-counter.js";
12
11
  import { loadCustomInstructions } from "../utils/custom-instructions.js";
13
12
  import { getSettingsManager } from "../utils/settings-manager.js";
@@ -22,6 +21,7 @@ import { PLANNER_CONFIG } from "../constants.js";
22
21
  import { resolveMCPReferences, extractMCPReferences } from "../mcp/resources.js";
23
22
  import { SDKError, SDKErrorCode } from "../sdk/errors.js";
24
23
  import { getStatusReporter } from "./status-reporter.js";
24
+ import { getLoopDetector, resetLoopDetector } from "./loop-detector.js";
25
25
  export class LLMAgent extends EventEmitter {
26
26
  llmClient;
27
27
  textEditor;
@@ -29,7 +29,6 @@ export class LLMAgent extends EventEmitter {
29
29
  bashOutput;
30
30
  todoTool;
31
31
  search;
32
- webSearch;
33
32
  // Lazy-loaded tools (rarely used)
34
33
  _architectureTool;
35
34
  _validationTool;
@@ -51,6 +50,8 @@ export class LLMAgent extends EventEmitter {
51
50
  samplingConfig;
52
51
  /** Thinking/reasoning mode configuration */
53
52
  thinkingConfig;
53
+ /** Stored reference to context overflow listener for proper cleanup */
54
+ contextOverflowListener;
54
55
  /** Track if agent has been disposed */
55
56
  disposed = false;
56
57
  /** Tool approval system for VSCode integration */
@@ -73,7 +74,6 @@ export class LLMAgent extends EventEmitter {
73
74
  this.bashOutput = new BashOutputTool();
74
75
  this.todoTool = new TodoTool();
75
76
  this.search = new SearchTool();
76
- this.webSearch = new WebSearchTool();
77
77
  // architectureTool and validationTool are lazy-loaded (see getters below)
78
78
  this.tokenCounter = getTokenCounter(modelToUse);
79
79
  this.contextManager = new ContextManager({ model: modelToUse });
@@ -88,6 +88,9 @@ export class LLMAgent extends EventEmitter {
88
88
  this.textEditor.setCheckpointCallback(async (files, description) => {
89
89
  // Create immutable snapshot of chat history at callback time
90
90
  // This prevents inconsistencies if messages are added during checkpoint creation
91
+ // BUG FIX: Check if agent is disposed before creating checkpoint
92
+ if (this.disposed)
93
+ return;
91
94
  const chatHistorySnapshot = JSON.parse(JSON.stringify(this.chatHistory));
92
95
  await this.checkpointManager.createCheckpoint({
93
96
  files,
@@ -125,14 +128,19 @@ export class LLMAgent extends EventEmitter {
125
128
  // NEW: Listen for context pruning to generate summaries
126
129
  // CRITICAL FIX: Wrap async callback to prevent uncaught promise rejections
127
130
  // Event listeners don't handle async errors automatically, so we must catch them
128
- this.contextManager.on('before_prune', (data) => {
131
+ // Store listener reference for proper cleanup in dispose()
132
+ this.contextOverflowListener = (data) => {
133
+ // Skip if agent is disposed to prevent operations on disposed resources
134
+ if (this.disposed)
135
+ return;
129
136
  this.handleContextOverflow(data).catch((error) => {
130
137
  const errorMsg = extractErrorMessage(error);
131
138
  console.error('Error handling context overflow:', errorMsg);
132
139
  // Emit error event for monitoring
133
140
  this.emit('error', error);
134
141
  });
135
- });
142
+ };
143
+ this.contextManager.on('before_prune', this.contextOverflowListener);
136
144
  }
137
145
  initializeCheckpointManager() {
138
146
  // Initialize checkpoint manager in the background
@@ -254,6 +262,10 @@ export class LLMAgent extends EventEmitter {
254
262
  * @returns Promise<boolean> - true if approved, false if rejected or timeout
255
263
  */
256
264
  waitForToolApproval(toolCall) {
265
+ // If agent is already disposed, immediately reject approval wait to avoid dangling promises
266
+ if (this.disposed) {
267
+ return Promise.resolve(false);
268
+ }
257
269
  return new Promise((resolve) => {
258
270
  // Emit event so external integrations can show diff preview
259
271
  this.emit('tool:approval_required', toolCall);
@@ -331,12 +343,11 @@ export class LLMAgent extends EventEmitter {
331
343
  }
332
344
  // CRITICAL FIX: Add hard limit for messages array as safety backstop
333
345
  // In case contextManager.shouldPrune() always returns false
334
- const MAX_MESSAGES = 500;
335
- if (this.messages.length > MAX_MESSAGES) {
346
+ if (this.messages.length > AGENT_CONFIG.MAX_MESSAGES) {
336
347
  // Keep system message (if exists) + last N messages
337
348
  const systemMessages = this.messages.filter(m => m.role === 'system');
338
349
  const nonSystemMessages = this.messages.filter(m => m.role !== 'system');
339
- const keepMessages = Math.min(nonSystemMessages.length, MAX_MESSAGES - systemMessages.length);
350
+ const keepMessages = Math.min(nonSystemMessages.length, AGENT_CONFIG.MAX_MESSAGES - systemMessages.length);
340
351
  this.messages = [
341
352
  ...systemMessages,
342
353
  ...nonSystemMessages.slice(-keepMessages)
@@ -363,16 +374,14 @@ export class LLMAgent extends EventEmitter {
363
374
  const args = JSON.parse(toolCall.function.arguments || '{}');
364
375
  this.toolCallArgsCache.set(toolCall.id, args);
365
376
  // CRITICAL FIX: Prevent unbounded memory growth with proper cache eviction
366
- // When cache exceeds limit, reduce to 80% capacity (not just remove 100 entries)
367
- if (this.toolCallArgsCache.size > 500) {
368
- const targetSize = 400; // 80% of max capacity
377
+ // When cache exceeds limit, reduce to 80% capacity (not just remove fixed entries)
378
+ if (this.toolCallArgsCache.size > CACHE_CONFIG.TOOL_ARGS_CACHE_MAX_SIZE) {
379
+ const targetSize = Math.floor(CACHE_CONFIG.TOOL_ARGS_CACHE_MAX_SIZE * 0.8);
369
380
  const toRemove = this.toolCallArgsCache.size - targetSize;
370
- let deleted = 0;
371
- for (const key of this.toolCallArgsCache.keys()) {
381
+ // BUG FIX: Don't modify Map while iterating - create array of keys first
382
+ const keysToDelete = Array.from(this.toolCallArgsCache.keys()).slice(0, toRemove);
383
+ for (const key of keysToDelete) {
372
384
  this.toolCallArgsCache.delete(key);
373
- deleted++;
374
- if (deleted >= toRemove)
375
- break;
376
385
  }
377
386
  }
378
387
  return args;
@@ -404,102 +413,87 @@ export class LLMAgent extends EventEmitter {
404
413
  }
405
414
  /**
406
415
  * Detect if a tool call is repetitive (likely causing a loop)
407
- * Returns true if the same tool with similar arguments was called multiple times recently
416
+ * Uses the intelligent LoopDetector which provides:
417
+ * - Tool-specific thresholds (file ops get higher limits)
418
+ * - Progress-based detection (tracks success/failure)
419
+ * - Cycle pattern detection (A→B→A→B loops)
408
420
  */
409
421
  isRepetitiveToolCall(toolCall) {
410
422
  // Check if loop detection is disabled globally
411
423
  if (!AGENT_CONFIG.ENABLE_LOOP_DETECTION) {
412
424
  return false;
413
425
  }
414
- // Check if threshold is 0 (disabled via threshold)
415
- if (AGENT_CONFIG.LOOP_DETECTION_THRESHOLD <= 0) {
416
- return false;
417
- }
418
- try {
419
- const args = this.parseToolArgumentsCached(toolCall);
420
- // Create a detailed signature that includes key arguments
421
- // This allows multiple different commands but catches true repetitions
422
- let signature = toolCall.function.name;
423
- if (toolCall.function.name === 'bash' && args.command && typeof args.command === 'string') {
424
- // Normalize command: trim whitespace, collapse multiple spaces
425
- const normalizedCommand = args.command.trim().replace(/\s+/g, ' ');
426
- // Use full command for exact matching (catches true duplicates)
427
- signature = `bash:${normalizedCommand}`;
428
- }
429
- else if (toolCall.function.name === 'search' && args.query && typeof args.query === 'string') {
430
- // For search, include the normalized query
431
- const normalizedQuery = args.query.trim().toLowerCase().replace(/\s+/g, ' ');
432
- signature = `search:${normalizedQuery}`;
433
- }
434
- else if (toolCall.function.name === 'view_file' && args.path && typeof args.path === 'string') {
435
- // For file reads, include the path
436
- signature = `view:${args.path}`;
437
- }
438
- else if (toolCall.function.name === 'create_file' && args.path && typeof args.path === 'string') {
439
- // For file writes, include the path
440
- signature = `create:${args.path}`;
441
- }
442
- else if (toolCall.function.name === 'str_replace_editor' && args.path && typeof args.path === 'string') {
443
- // For text editor, include the path
444
- signature = `edit:${args.path}`;
445
- }
446
- // Track by detailed signature
447
- const count = this.recentToolCalls.get(signature) || 0;
448
- // Debug logging
449
- if (process.env.DEBUG_LOOP_DETECTION === '1') {
450
- console.error(`[LOOP DETECTION] Tool: ${toolCall.function.name}`);
451
- console.error(`[LOOP DETECTION] Signature: ${signature}`);
452
- console.error(`[LOOP DETECTION] Count: ${count}`);
453
- console.error(`[LOOP DETECTION] Threshold: ${AGENT_CONFIG.LOOP_DETECTION_THRESHOLD}`);
454
- console.error(`[LOOP DETECTION] Map size: ${this.recentToolCalls.size}`);
455
- }
456
- // Increment the count first
457
- const newCount = count + 1;
458
- this.recentToolCalls.set(signature, newCount);
459
- // Check if we've exceeded the configured threshold
460
- // newCount > threshold means we've seen it threshold+1 times
461
- if (newCount > AGENT_CONFIG.LOOP_DETECTION_THRESHOLD) {
462
- if (process.env.DEBUG_LOOP_DETECTION === '1') {
463
- console.error(`[LOOP DETECTION] ⚠️ LOOP DETECTED! Signature: ${signature} (count: ${newCount}, threshold: ${AGENT_CONFIG.LOOP_DETECTION_THRESHOLD})`);
464
- }
465
- return true;
466
- }
467
- if (process.env.DEBUG_LOOP_DETECTION === '1') {
468
- console.error(`[LOOP DETECTION] ✅ Allowed, count now: ${newCount}`);
469
- console.error(`[LOOP DETECTION] Current map:`, Array.from(this.recentToolCalls.entries()));
470
- }
471
- // Clean up old entries (keep only last N unique calls)
472
- // Batch cleanup when exceeding threshold to prevent unbounded growth
473
- if (this.recentToolCalls.size > AGENT_CONFIG.MAX_RECENT_TOOL_CALLS) {
474
- const excessCount = this.recentToolCalls.size - AGENT_CONFIG.MAX_RECENT_TOOL_CALLS + 10;
475
- let removed = 0;
476
- for (const key of this.recentToolCalls.keys()) {
477
- if (removed >= excessCount)
478
- break;
479
- this.recentToolCalls.delete(key);
480
- removed++;
481
- }
426
+ // Use the new intelligent loop detector
427
+ const detector = getLoopDetector();
428
+ const result = detector.checkForLoop(toolCall);
429
+ // Debug logging
430
+ if (process.env.DEBUG_LOOP_DETECTION === '1') {
431
+ console.error(`[LOOP DETECTION] Tool: ${toolCall.function.name}`);
432
+ console.error(`[LOOP DETECTION] Count: ${result.count}`);
433
+ console.error(`[LOOP DETECTION] Threshold: ${result.threshold}`);
434
+ console.error(`[LOOP DETECTION] Is Loop: ${result.isLoop}`);
435
+ if (result.reason) {
436
+ console.error(`[LOOP DETECTION] Reason: ${result.reason}`);
482
437
  }
483
- return false;
438
+ const stats = detector.getStats();
439
+ console.error(`[LOOP DETECTION] Stats: ${JSON.stringify(stats)}`);
484
440
  }
485
- catch (error) {
486
- // If we can't parse, assume it's not repetitive
441
+ if (result.isLoop) {
442
+ // Store the result for generating better error message
443
+ this.lastLoopResult = result;
487
444
  if (process.env.DEBUG_LOOP_DETECTION === '1') {
488
- console.error(`[LOOP DETECTION] Parse error:`, error);
445
+ console.error(`[LOOP DETECTION] ⚠️ LOOP DETECTED!`);
446
+ console.error(`[LOOP DETECTION] Reason: ${result.reason}`);
447
+ console.error(`[LOOP DETECTION] Suggestion: ${result.suggestion}`);
489
448
  }
490
- return false;
449
+ return true;
450
+ }
451
+ // Record the tool call (will be marked success/failure after execution)
452
+ // For now, pre-record with success=true, we update if it fails
453
+ detector.recordToolCall(toolCall, true);
454
+ if (process.env.DEBUG_LOOP_DETECTION === '1') {
455
+ console.error(`[LOOP DETECTION] ✅ Allowed, count: ${result.count}/${result.threshold}`);
491
456
  }
457
+ return false;
492
458
  }
459
+ /** Last loop detection result for error messages */
460
+ lastLoopResult;
493
461
  /**
494
462
  * Reset the tool call tracking (called at start of new user message)
495
463
  */
496
464
  resetToolCallTracking() {
497
465
  if (process.env.DEBUG_LOOP_DETECTION === '1') {
498
- console.error(`[LOOP TRACKING] 🔄 Resetting tool call tracking (map had ${this.recentToolCalls.size} entries)`);
466
+ const detector = getLoopDetector();
467
+ const stats = detector.getStats();
468
+ console.error(`[LOOP TRACKING] 🔄 Resetting tool call tracking (had ${stats.uniqueSignatures} signatures)`);
499
469
  }
470
+ // Reset the new intelligent loop detector
471
+ resetLoopDetector();
472
+ // Also reset the legacy tracking (keep for backward compatibility during transition)
500
473
  this.recentToolCalls.clear();
501
474
  // Also clear the args cache to prevent memory leak
502
475
  this.toolCallArgsCache.clear();
476
+ // Clear last loop result
477
+ this.lastLoopResult = undefined;
478
+ }
479
+ /**
480
+ * Generate a helpful warning message when a loop is detected
481
+ * Uses the lastLoopResult for context-aware suggestions
482
+ */
483
+ getLoopWarningMessage() {
484
+ const base = "\n\n⚠️ Detected repetitive operations. Stopping to prevent infinite loop.";
485
+ if (this.lastLoopResult) {
486
+ const parts = [base];
487
+ if (this.lastLoopResult.reason) {
488
+ parts.push(`\n\nReason: ${this.lastLoopResult.reason}`);
489
+ }
490
+ if (this.lastLoopResult.suggestion) {
491
+ parts.push(`\n\n💡 Suggestion: ${this.lastLoopResult.suggestion}`);
492
+ }
493
+ parts.push("\n\nI'll stop here and provide what I've accomplished so far. You can ask me to continue with a different approach.");
494
+ return parts.join('');
495
+ }
496
+ return base + "\n\nI apologize, but I seem to be stuck in a loop. Let me provide what I can without further tool use.";
503
497
  }
504
498
  // ============================================================================
505
499
  // Multi-Phase Planning Integration
@@ -949,14 +943,15 @@ export class LLMAgent extends EventEmitter {
949
943
  // Add assistant message to history
950
944
  this.addAssistantMessage(streamResult.accumulated);
951
945
  // Handle tool calls if present
952
- if (streamResult.accumulated.tool_calls?.length > 0) {
946
+ if (streamResult.accumulated.tool_calls && streamResult.accumulated.tool_calls.length > 0) {
953
947
  toolRounds++;
954
948
  // Check for repetitive tool calls (loop detection)
955
949
  const hasRepetitiveCall = streamResult.accumulated.tool_calls.some((tc) => this.isRepetitiveToolCall(tc));
956
950
  if (hasRepetitiveCall) {
951
+ const loopMsg = this.getLoopWarningMessage();
957
952
  yield {
958
953
  type: "content",
959
- content: "\n\n⚠️ Detected repetitive tool calls. Stopping to prevent infinite loop.\n",
954
+ content: loopMsg,
960
955
  };
961
956
  break;
962
957
  }
@@ -1074,9 +1069,10 @@ export class LLMAgent extends EventEmitter {
1074
1069
  if (process.env.DEBUG_LOOP_DETECTION === '1') {
1075
1070
  console.error(`[LOOP CHECK] 🛑 Breaking loop!`);
1076
1071
  }
1072
+ const loopMsg = this.getLoopWarningMessage();
1077
1073
  const warningEntry = {
1078
1074
  type: "assistant",
1079
- content: "⚠️ Detected repetitive tool calls. Stopping to prevent infinite loop.\n\nI apologize, but I seem to be stuck in a loop trying to answer your question. Let me provide what I can without further tool use.",
1075
+ content: loopMsg,
1080
1076
  timestamp: new Date(),
1081
1077
  };
1082
1078
  this.chatHistory.push(warningEntry);
@@ -1116,7 +1112,11 @@ export class LLMAgent extends EventEmitter {
1116
1112
  const result = await this.executeTool(toolCall);
1117
1113
  // Update the existing tool_call entry with the result (O(1) lookup)
1118
1114
  const entryIndex = this.toolCallIndexMap.get(toolCall.id);
1119
- if (entryIndex !== undefined) {
1115
+ // Validate entryIndex is still valid after potential context pruning
1116
+ // The index could become stale if chatHistory was modified between store and access
1117
+ if (entryIndex !== undefined &&
1118
+ entryIndex < this.chatHistory.length &&
1119
+ this.chatHistory[entryIndex]?.toolCall?.id === toolCall.id) {
1120
1120
  const updatedEntry = {
1121
1121
  ...this.chatHistory[entryIndex],
1122
1122
  type: "tool_result",
@@ -1406,15 +1406,17 @@ export class LLMAgent extends EventEmitter {
1406
1406
  finally {
1407
1407
  // CRITICAL FIX: Properly close the async iterator to release HTTP connections and buffers
1408
1408
  // This prevents socket leaks when streams are cancelled or errors occur
1409
- if (typeof stream.return === 'function') {
1410
- try {
1411
- await stream.return();
1412
- }
1413
- catch (cleanupError) {
1414
- // Log but don't throw - cleanup errors shouldn't break the flow
1415
- console.warn('Stream cleanup warning:', cleanupError);
1409
+ try {
1410
+ // Use a type assertion to safely access the return method
1411
+ const streamWithReturn = stream;
1412
+ if (typeof streamWithReturn.return === 'function') {
1413
+ await streamWithReturn.return();
1416
1414
  }
1417
1415
  }
1416
+ catch (cleanupError) {
1417
+ // Log but don't throw - cleanup errors shouldn't break the flow
1418
+ console.warn('Stream cleanup warning:', cleanupError);
1419
+ }
1418
1420
  }
1419
1421
  }
1420
1422
  /**
@@ -1564,14 +1566,15 @@ export class LLMAgent extends EventEmitter {
1564
1566
  // Add assistant message to history
1565
1567
  this.addAssistantMessage(streamResult.accumulated);
1566
1568
  // Handle tool calls if present
1567
- if (streamResult.accumulated.tool_calls?.length > 0) {
1569
+ if (streamResult.accumulated.tool_calls && streamResult.accumulated.tool_calls.length > 0) {
1568
1570
  toolRounds++;
1569
1571
  // Check for repetitive tool calls (loop detection)
1570
1572
  const hasRepetitiveCall = streamResult.accumulated.tool_calls.some((tc) => this.isRepetitiveToolCall(tc));
1571
1573
  if (hasRepetitiveCall) {
1574
+ const loopMsg = this.getLoopWarningMessage();
1572
1575
  yield {
1573
1576
  type: "content",
1574
- content: "\n\n⚠️ Detected repetitive tool calls. Stopping to prevent infinite loop.\n\nI apologize, but I seem to be stuck in a loop trying to answer your question. Let me provide what I can without further tool use.",
1577
+ content: loopMsg,
1575
1578
  };
1576
1579
  break;
1577
1580
  }
@@ -1741,18 +1744,6 @@ export class LLMAgent extends EventEmitter {
1741
1744
  fileTypes: Array.isArray(args.file_types) ? args.file_types : undefined,
1742
1745
  includeHidden: getBoolean('include_hidden'),
1743
1746
  });
1744
- case "web_search": {
1745
- const freshnessValue = args.freshness;
1746
- const validFreshness = (freshnessValue === 'day' || freshnessValue === 'week' || freshnessValue === 'month' || freshnessValue === 'year') ? freshnessValue : undefined;
1747
- const searchDepthValue = args.searchDepth;
1748
- const validSearchDepth = (searchDepthValue === 'basic' || searchDepthValue === 'advanced') ? searchDepthValue : 'basic';
1749
- return await this.webSearch.search(getString('query'), {
1750
- maxResults: getNumber('maxResults'),
1751
- includeAnswer: getBoolean('includeAnswer'),
1752
- searchDepth: validSearchDepth,
1753
- freshness: validFreshness,
1754
- });
1755
- }
1756
1747
  case "analyze_architecture": {
1757
1748
  const projectPath = typeof args.projectPath === 'string' ? args.projectPath : undefined;
1758
1749
  const depth = typeof args.depth === 'string' ? args.depth : undefined;
@@ -1796,10 +1787,11 @@ export class LLMAgent extends EventEmitter {
1796
1787
  // Extract error message from MCP result content
1797
1788
  // Safely check content structure before accessing
1798
1789
  let errorMsg = "MCP tool error";
1799
- if (result.content && result.content.length > 0) {
1790
+ if (result.content && Array.isArray(result.content) && result.content.length > 0) {
1800
1791
  const firstContent = result.content[0];
1801
1792
  if (typeof firstContent === 'object' && firstContent !== null && 'text' in firstContent) {
1802
- errorMsg = String(firstContent.text) || errorMsg;
1793
+ const textValue = firstContent.text;
1794
+ errorMsg = typeof textValue === 'string' ? textValue : String(textValue || errorMsg);
1803
1795
  }
1804
1796
  }
1805
1797
  return {
@@ -2135,9 +2127,12 @@ export class LLMAgent extends EventEmitter {
2135
2127
  this.disposed = true;
2136
2128
  // Remove all event listeners to prevent memory leaks
2137
2129
  this.removeAllListeners();
2138
- // CRITICAL FIX: Remove event listeners from contextManager to prevent memory leak
2139
- // The 'before_prune' listener was registered in constructor (line 188) but never removed
2140
- this.contextManager.removeAllListeners('before_prune');
2130
+ // CRITICAL FIX: Remove event listener from contextManager to prevent memory leak
2131
+ // Only remove the specific listener we registered, not all listeners for this event
2132
+ if (this.contextOverflowListener) {
2133
+ this.contextManager.removeListener('before_prune', this.contextOverflowListener);
2134
+ this.contextOverflowListener = undefined;
2135
+ }
2141
2136
  // Dispose tools that have cleanup methods
2142
2137
  this.bash.dispose();
2143
2138
  // Clear in-memory caches
@@ -2150,6 +2145,15 @@ export class LLMAgent extends EventEmitter {
2150
2145
  clearTimeout(timeout);
2151
2146
  }
2152
2147
  this.toolApprovalTimeouts.clear();
2148
+ // Resolve any pending approval callbacks so awaiting promises don't hang forever
2149
+ for (const [, callback] of this.toolApprovalCallbacks) {
2150
+ try {
2151
+ callback(false);
2152
+ }
2153
+ catch {
2154
+ // Ignore callback errors during teardown
2155
+ }
2156
+ }
2153
2157
  this.toolApprovalCallbacks.clear();
2154
2158
  // Clear conversation history to free memory
2155
2159
  this.chatHistory = [];