@defai.digital/ax-cli 3.7.2 → 3.8.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 (213) hide show
  1. package/README.md +148 -53
  2. package/dist/agent/context-manager.d.ts +15 -1
  3. package/dist/agent/context-manager.js +50 -19
  4. package/dist/agent/context-manager.js.map +1 -1
  5. package/dist/agent/dependency-resolver.js +13 -7
  6. package/dist/agent/dependency-resolver.js.map +1 -1
  7. package/dist/agent/llm-agent.d.ts +37 -0
  8. package/dist/agent/llm-agent.js +318 -98
  9. package/dist/agent/llm-agent.js.map +1 -1
  10. package/dist/agent/status-reporter.d.ts +114 -0
  11. package/dist/agent/status-reporter.js +335 -0
  12. package/dist/agent/status-reporter.js.map +1 -0
  13. package/dist/analyzers/best-practices/rules/typescript/no-magic-numbers.js +8 -2
  14. package/dist/analyzers/best-practices/rules/typescript/no-magic-numbers.js.map +1 -1
  15. package/dist/analyzers/best-practices/rules/typescript/no-unused-vars.js +3 -1
  16. package/dist/analyzers/best-practices/rules/typescript/no-unused-vars.js.map +1 -1
  17. package/dist/analyzers/best-practices/rules/typescript/prefer-const.js +3 -1
  18. package/dist/analyzers/best-practices/rules/typescript/prefer-const.js.map +1 -1
  19. package/dist/analyzers/best-practices/rules/typescript/prefer-readonly.js +3 -1
  20. package/dist/analyzers/best-practices/rules/typescript/prefer-readonly.js.map +1 -1
  21. package/dist/analyzers/code-smells/detectors/duplicate-code-detector.js +9 -3
  22. package/dist/analyzers/code-smells/detectors/duplicate-code-detector.js.map +1 -1
  23. package/dist/analyzers/git/churn-calculator.d.ts +2 -0
  24. package/dist/analyzers/git/churn-calculator.js +42 -8
  25. package/dist/analyzers/git/churn-calculator.js.map +1 -1
  26. package/dist/analyzers/git/hotspot-detector.js +2 -2
  27. package/dist/analyzers/git/hotspot-detector.js.map +1 -1
  28. package/dist/analyzers/metrics/metrics-analyzer.js +1 -1
  29. package/dist/analyzers/metrics/metrics-analyzer.js.map +1 -1
  30. package/dist/analyzers/security/security-analyzer.js +1 -1
  31. package/dist/analyzers/security/security-analyzer.js.map +1 -1
  32. package/dist/checkpoint/manager.d.ts +1 -0
  33. package/dist/checkpoint/manager.js +49 -9
  34. package/dist/checkpoint/manager.js.map +1 -1
  35. package/dist/checkpoint/storage.js +2 -2
  36. package/dist/checkpoint/storage.js.map +1 -1
  37. package/dist/commands/mcp-migrate.d.ts +9 -0
  38. package/dist/commands/mcp-migrate.js +172 -0
  39. package/dist/commands/mcp-migrate.js.map +1 -0
  40. package/dist/commands/status.d.ts +7 -0
  41. package/dist/commands/status.js +211 -0
  42. package/dist/commands/status.js.map +1 -0
  43. package/dist/commands/vscode.d.ts +7 -0
  44. package/dist/commands/vscode.js +363 -0
  45. package/dist/commands/vscode.js.map +1 -0
  46. package/dist/index.js +79 -30
  47. package/dist/index.js.map +1 -1
  48. package/dist/llm/client.js +33 -4
  49. package/dist/llm/client.js.map +1 -1
  50. package/dist/mcp/automatosx-loader.d.ts +84 -0
  51. package/dist/mcp/automatosx-loader.js +238 -0
  52. package/dist/mcp/automatosx-loader.js.map +1 -0
  53. package/dist/mcp/client-mutex-patch.d.ts +36 -0
  54. package/dist/mcp/client-mutex-patch.js +75 -0
  55. package/dist/mcp/client-mutex-patch.js.map +1 -0
  56. package/dist/mcp/client-v2.d.ts +229 -0
  57. package/dist/mcp/client-v2.js +740 -0
  58. package/dist/mcp/client-v2.js.map +1 -0
  59. package/dist/mcp/client.d.ts +111 -13
  60. package/dist/mcp/client.js +168 -253
  61. package/dist/mcp/client.js.map +1 -1
  62. package/dist/mcp/config-detector-v2.d.ts +83 -0
  63. package/dist/mcp/config-detector-v2.js +328 -0
  64. package/dist/mcp/config-detector-v2.js.map +1 -0
  65. package/dist/mcp/config-detector.d.ts +90 -0
  66. package/dist/mcp/config-detector.js +242 -0
  67. package/dist/mcp/config-detector.js.map +1 -0
  68. package/dist/mcp/config-migrator-v2.d.ts +89 -0
  69. package/dist/mcp/config-migrator-v2.js +288 -0
  70. package/dist/mcp/config-migrator-v2.js.map +1 -0
  71. package/dist/mcp/config-migrator.d.ts +63 -0
  72. package/dist/mcp/config-migrator.js +269 -0
  73. package/dist/mcp/config-migrator.js.map +1 -0
  74. package/dist/mcp/config-v2.d.ts +106 -0
  75. package/dist/mcp/config-v2.js +417 -0
  76. package/dist/mcp/config-v2.js.map +1 -0
  77. package/dist/mcp/config.d.ts +12 -1
  78. package/dist/mcp/config.js +95 -10
  79. package/dist/mcp/config.js.map +1 -1
  80. package/dist/mcp/error-formatter.d.ts +46 -0
  81. package/dist/mcp/error-formatter.js +244 -0
  82. package/dist/mcp/error-formatter.js.map +1 -0
  83. package/dist/mcp/health.d.ts +5 -0
  84. package/dist/mcp/health.js +22 -2
  85. package/dist/mcp/health.js.map +1 -1
  86. package/dist/mcp/invariants.d.ts +141 -0
  87. package/dist/mcp/invariants.js +243 -0
  88. package/dist/mcp/invariants.js.map +1 -0
  89. package/dist/mcp/mutex-safe.d.ts +153 -0
  90. package/dist/mcp/mutex-safe.js +260 -0
  91. package/dist/mcp/mutex-safe.js.map +1 -0
  92. package/dist/mcp/mutex.d.ts +73 -0
  93. package/dist/mcp/mutex.js +137 -0
  94. package/dist/mcp/mutex.js.map +1 -0
  95. package/dist/mcp/reconnection.d.ts +4 -0
  96. package/dist/mcp/reconnection.js +25 -1
  97. package/dist/mcp/reconnection.js.map +1 -1
  98. package/dist/mcp/transports-v2.d.ts +152 -0
  99. package/dist/mcp/transports-v2.js +481 -0
  100. package/dist/mcp/transports-v2.js.map +1 -0
  101. package/dist/mcp/type-safety.d.ts +231 -0
  102. package/dist/mcp/type-safety.js +273 -0
  103. package/dist/mcp/type-safety.js.map +1 -0
  104. package/dist/planner/task-planner.js +13 -0
  105. package/dist/planner/task-planner.js.map +1 -1
  106. package/dist/planner/types.d.ts +6 -6
  107. package/dist/schemas/confirmation-schemas.d.ts +2 -2
  108. package/dist/schemas/settings-schemas.d.ts +196 -0
  109. package/dist/schemas/settings-schemas.js +146 -5
  110. package/dist/schemas/settings-schemas.js.map +1 -1
  111. package/dist/sdk/index.d.ts +118 -2
  112. package/dist/sdk/index.js +146 -4
  113. package/dist/sdk/index.js.map +1 -1
  114. package/dist/sdk/testing.d.ts +182 -0
  115. package/dist/sdk/testing.js +231 -0
  116. package/dist/sdk/testing.js.map +1 -1
  117. package/dist/sdk/version.d.ts +114 -15
  118. package/dist/sdk/version.js +137 -15
  119. package/dist/sdk/version.js.map +1 -1
  120. package/dist/tools/bash.js +54 -9
  121. package/dist/tools/bash.js.map +1 -1
  122. package/dist/tools/registry.d.ts +146 -0
  123. package/dist/tools/registry.js +170 -0
  124. package/dist/tools/registry.js.map +1 -0
  125. package/dist/tools/search.js +12 -2
  126. package/dist/tools/search.js.map +1 -1
  127. package/dist/tools/text-editor.js +84 -26
  128. package/dist/tools/text-editor.js.map +1 -1
  129. package/dist/ui/components/chat-history.js +6 -1
  130. package/dist/ui/components/chat-history.js.map +1 -1
  131. package/dist/ui/components/chat-input.d.ts +2 -1
  132. package/dist/ui/components/chat-input.js +5 -2
  133. package/dist/ui/components/chat-input.js.map +1 -1
  134. package/dist/ui/components/chat-interface.js +187 -5
  135. package/dist/ui/components/chat-interface.js.map +1 -1
  136. package/dist/ui/components/context-breakdown.d.ts +23 -0
  137. package/dist/ui/components/context-breakdown.js +124 -0
  138. package/dist/ui/components/context-breakdown.js.map +1 -0
  139. package/dist/ui/components/keyboard-help.d.ts +17 -0
  140. package/dist/ui/components/keyboard-help.js +116 -0
  141. package/dist/ui/components/keyboard-help.js.map +1 -0
  142. package/dist/ui/components/keyboard-hints.js +2 -2
  143. package/dist/ui/components/keyboard-hints.js.map +1 -1
  144. package/dist/ui/components/quick-actions.js +43 -7
  145. package/dist/ui/components/quick-actions.js.map +1 -1
  146. package/dist/ui/components/status-bar.d.ts +3 -0
  147. package/dist/ui/components/status-bar.js +25 -16
  148. package/dist/ui/components/status-bar.js.map +1 -1
  149. package/dist/ui/components/toast-notification.d.ts +42 -0
  150. package/dist/ui/components/toast-notification.js +30 -2
  151. package/dist/ui/components/toast-notification.js.map +1 -1
  152. package/dist/ui/components/tool-group-display.js +34 -4
  153. package/dist/ui/components/tool-group-display.js.map +1 -1
  154. package/dist/ui/components/welcome-panel.js +2 -2
  155. package/dist/ui/components/welcome-panel.js.map +1 -1
  156. package/dist/ui/hooks/use-enhanced-input.d.ts +9 -1
  157. package/dist/ui/hooks/use-enhanced-input.js +901 -90
  158. package/dist/ui/hooks/use-enhanced-input.js.map +1 -1
  159. package/dist/ui/hooks/use-input-handler.d.ts +11 -1
  160. package/dist/ui/hooks/use-input-handler.js +67 -3
  161. package/dist/ui/hooks/use-input-handler.js.map +1 -1
  162. package/dist/ui/hooks/use-input-history.d.ts +1 -1
  163. package/dist/ui/hooks/use-input-history.js +50 -14
  164. package/dist/ui/hooks/use-input-history.js.map +1 -1
  165. package/dist/ui/utils/bracketed-paste-handler.d.ts +97 -0
  166. package/dist/ui/utils/bracketed-paste-handler.js +322 -0
  167. package/dist/ui/utils/bracketed-paste-handler.js.map +1 -0
  168. package/dist/ui/utils/change-summarizer.js +16 -6
  169. package/dist/ui/utils/change-summarizer.js.map +1 -1
  170. package/dist/ui/utils/tool-grouper.d.ts +10 -1
  171. package/dist/ui/utils/tool-grouper.js +143 -30
  172. package/dist/ui/utils/tool-grouper.js.map +1 -1
  173. package/dist/utils/auto-accept-logger.d.ts +173 -0
  174. package/dist/utils/auto-accept-logger.js +420 -0
  175. package/dist/utils/auto-accept-logger.js.map +1 -0
  176. package/dist/utils/background-task-manager.d.ts +11 -0
  177. package/dist/utils/background-task-manager.js +124 -38
  178. package/dist/utils/background-task-manager.js.map +1 -1
  179. package/dist/utils/confirmation-service.d.ts +1 -0
  180. package/dist/utils/confirmation-service.js +6 -1
  181. package/dist/utils/confirmation-service.js.map +1 -1
  182. package/dist/utils/encryption.d.ts +8 -0
  183. package/dist/utils/encryption.js +44 -27
  184. package/dist/utils/encryption.js.map +1 -1
  185. package/dist/utils/enhanced-error-messages.d.ts +33 -0
  186. package/dist/utils/enhanced-error-messages.js +420 -0
  187. package/dist/utils/enhanced-error-messages.js.map +1 -0
  188. package/dist/utils/error-handler.d.ts +13 -3
  189. package/dist/utils/error-handler.js +16 -4
  190. package/dist/utils/error-handler.js.map +1 -1
  191. package/dist/utils/external-editor.d.ts +47 -0
  192. package/dist/utils/external-editor.js +179 -0
  193. package/dist/utils/external-editor.js.map +1 -0
  194. package/dist/utils/history-migration.d.ts +9 -0
  195. package/dist/utils/history-migration.js +36 -0
  196. package/dist/utils/history-migration.js.map +1 -0
  197. package/dist/utils/paste-utils.js +12 -11
  198. package/dist/utils/paste-utils.js.map +1 -1
  199. package/dist/utils/rate-limiter.js +20 -1
  200. package/dist/utils/rate-limiter.js.map +1 -1
  201. package/dist/utils/safety-rules.d.ts +64 -0
  202. package/dist/utils/safety-rules.js +225 -0
  203. package/dist/utils/safety-rules.js.map +1 -0
  204. package/dist/utils/settings-manager.d.ts +89 -1
  205. package/dist/utils/settings-manager.js +359 -3
  206. package/dist/utils/settings-manager.js.map +1 -1
  207. package/dist/utils/token-counter.d.ts +2 -0
  208. package/dist/utils/token-counter.js +32 -9
  209. package/dist/utils/token-counter.js.map +1 -1
  210. package/dist/utils/version.d.ts +11 -2
  211. package/dist/utils/version.js +54 -21
  212. package/dist/utils/version.js.map +1 -1
  213. package/package.json +2 -1
@@ -20,6 +20,8 @@ import { SubagentOrchestrator } from "./subagent-orchestrator.js";
20
20
  import { getTaskPlanner, isComplexRequest, } from "../planner/index.js";
21
21
  import { PLANNER_CONFIG } from "../constants.js";
22
22
  import { resolveMCPReferences, extractMCPReferences } from "../mcp/resources.js";
23
+ import { SDKError, SDKErrorCode } from "../sdk/errors.js";
24
+ import { getStatusReporter } from "./status-reporter.js";
23
25
  export class LLMAgent extends EventEmitter {
24
26
  llmClient;
25
27
  textEditor;
@@ -51,6 +53,11 @@ export class LLMAgent extends EventEmitter {
51
53
  thinkingConfig;
52
54
  /** Track if agent has been disposed */
53
55
  disposed = false;
56
+ /** Tool approval system for VSCode integration */
57
+ requireToolApproval = false;
58
+ toolApprovalCallbacks = new Map();
59
+ /** BUG FIX: Track approval timeouts for cleanup to prevent memory leaks */
60
+ toolApprovalTimeouts = new Map();
54
61
  constructor(apiKey, baseURL, model, maxToolRounds) {
55
62
  super();
56
63
  const manager = getSettingsManager();
@@ -76,10 +83,15 @@ export class LLMAgent extends EventEmitter {
76
83
  // Load sampling configuration from settings (supports env vars, project, and user settings)
77
84
  this.samplingConfig = manager.getSamplingSettings();
78
85
  // Wire up checkpoint callback for automatic checkpoint creation
86
+ // CRITICAL FIX: Deep clone chatHistory to prevent race conditions
87
+ // The checkpoint creation is async and chatHistory can be modified during the operation
79
88
  this.textEditor.setCheckpointCallback(async (files, description) => {
89
+ // Create immutable snapshot of chat history at callback time
90
+ // This prevents inconsistencies if messages are added during checkpoint creation
91
+ const chatHistorySnapshot = JSON.parse(JSON.stringify(this.chatHistory));
80
92
  await this.checkpointManager.createCheckpoint({
81
93
  files,
82
- conversationState: this.chatHistory,
94
+ conversationState: chatHistorySnapshot,
83
95
  description,
84
96
  metadata: {
85
97
  model: this.llmClient.getCurrentModel(),
@@ -110,6 +122,17 @@ export class LLMAgent extends EventEmitter {
110
122
  role: "system",
111
123
  content: `Current working directory: ${process.cwd()}\nTimestamp: ${new Date().toISOString().split('T')[0]}`,
112
124
  });
125
+ // NEW: Listen for context pruning to generate summaries
126
+ // CRITICAL FIX: Wrap async callback to prevent uncaught promise rejections
127
+ // Event listeners don't handle async errors automatically, so we must catch them
128
+ this.contextManager.on('before_prune', (data) => {
129
+ this.handleContextOverflow(data).catch((error) => {
130
+ const errorMsg = extractErrorMessage(error);
131
+ console.error('Error handling context overflow:', errorMsg);
132
+ // Emit error event for monitoring
133
+ this.emit('error', error);
134
+ });
135
+ });
113
136
  }
114
137
  initializeCheckpointManager() {
115
138
  // Initialize checkpoint manager in the background
@@ -130,7 +153,6 @@ export class LLMAgent extends EventEmitter {
130
153
  }
131
154
  async initializeMCP() {
132
155
  // Initialize MCP in the background without blocking
133
- // Single error handler - no redundant catch needed since inner try-catch handles all errors
134
156
  Promise.resolve().then(async () => {
135
157
  try {
136
158
  const config = loadMCPConfig();
@@ -144,6 +166,9 @@ export class LLMAgent extends EventEmitter {
144
166
  console.warn("MCP initialization failed:", errorMsg);
145
167
  this.emit('system', `MCP initialization failed: ${errorMsg}`);
146
168
  }
169
+ }).catch((error) => {
170
+ // Catch any errors from emit() or other unexpected failures
171
+ console.error("Unexpected MCP initialization error:", error);
147
172
  });
148
173
  }
149
174
  /**
@@ -188,32 +213,134 @@ export class LLMAgent extends EventEmitter {
188
213
  getSamplingConfig() {
189
214
  return this.samplingConfig;
190
215
  }
216
+ /**
217
+ * Enable or disable tool approval requirement
218
+ * When enabled, text_editor operations will emit 'tool:approval_required' events
219
+ * and wait for approval before executing
220
+ *
221
+ * This is used by VSCode extension to show diff previews
222
+ *
223
+ * @param enabled - Whether to require approval for text_editor operations
224
+ */
225
+ setRequireToolApproval(enabled) {
226
+ this.requireToolApproval = enabled;
227
+ }
228
+ /**
229
+ * Approve or reject a pending tool call
230
+ * Called by external integrations (e.g., VSCode extension) in response to
231
+ * 'tool:approval_required' events
232
+ *
233
+ * @param toolCallId - The ID of the tool call to approve/reject
234
+ * @param approved - true to execute the tool, false to reject it
235
+ */
236
+ approveToolCall(toolCallId, approved) {
237
+ const callback = this.toolApprovalCallbacks.get(toolCallId);
238
+ if (callback) {
239
+ // BUG FIX: Clear the timeout when approval is received (prevents memory leak)
240
+ const timeout = this.toolApprovalTimeouts.get(toolCallId);
241
+ if (timeout) {
242
+ clearTimeout(timeout);
243
+ this.toolApprovalTimeouts.delete(toolCallId);
244
+ }
245
+ callback(approved);
246
+ this.toolApprovalCallbacks.delete(toolCallId);
247
+ }
248
+ }
249
+ /**
250
+ * Wait for external approval of a tool call
251
+ * Emits 'tool:approval_required' event and waits for approveToolCall() to be called
252
+ *
253
+ * @param toolCall - The tool call awaiting approval
254
+ * @returns Promise<boolean> - true if approved, false if rejected or timeout
255
+ */
256
+ waitForToolApproval(toolCall) {
257
+ return new Promise((resolve) => {
258
+ // Emit event so external integrations can show diff preview
259
+ this.emit('tool:approval_required', toolCall);
260
+ // Store callback
261
+ this.toolApprovalCallbacks.set(toolCall.id, resolve);
262
+ // BUG FIX: Track the timeout so it can be cleared on approval/disposal
263
+ // This prevents memory leaks from dangling timers
264
+ const timeoutId = setTimeout(() => {
265
+ // Clean up both the callback and timeout tracking
266
+ this.toolApprovalTimeouts.delete(toolCall.id);
267
+ if (this.toolApprovalCallbacks.has(toolCall.id)) {
268
+ this.toolApprovalCallbacks.delete(toolCall.id);
269
+ resolve(false); // Auto-reject on timeout
270
+ }
271
+ }, 5 * 60 * 1000);
272
+ // Track the timeout for cleanup
273
+ this.toolApprovalTimeouts.set(toolCall.id, timeoutId);
274
+ });
275
+ }
276
+ /**
277
+ * Handle context overflow by generating a summary
278
+ * Called when context manager is about to prune messages
279
+ */
280
+ async handleContextOverflow(data) {
281
+ try {
282
+ const reporter = getStatusReporter();
283
+ const summary = await reporter.generateContextSummary(data.messages, this.chatHistory, 'context_overflow', data.tokenCount);
284
+ // Log for debugging
285
+ if (process.env.DEBUG) {
286
+ console.log(`[Context Overflow] Summary generated: ${summary.path}`);
287
+ }
288
+ // Add a chat entry to inform user (non-blocking)
289
+ const summaryEntry = {
290
+ type: 'assistant',
291
+ content: `⚠️ Context window approaching limit (${data.tokenCount.toLocaleString()} tokens). Summary saved to:\n\`${summary.path}\``,
292
+ timestamp: new Date(),
293
+ };
294
+ this.chatHistory.push(summaryEntry);
295
+ // Emit event for UI/logging
296
+ this.emit('context:summary', summary);
297
+ }
298
+ catch (error) {
299
+ // Summary generation failure should not block execution
300
+ const errorMsg = extractErrorMessage(error);
301
+ console.warn('Failed to generate context summary:', errorMsg);
302
+ }
303
+ }
191
304
  /**
192
305
  * Apply context pruning to both messages and chatHistory
193
306
  * BUGFIX: Prevents chatHistory from growing unbounded
194
307
  */
195
308
  applyContextPruning() {
309
+ // Prune LLM messages if needed
196
310
  if (this.contextManager.shouldPrune(this.messages, this.tokenCounter)) {
197
- // Prune LLM messages
198
311
  this.messages = this.contextManager.pruneMessages(this.messages, this.tokenCounter);
199
- // Also prune chatHistory to prevent unlimited growth
200
- // Keep last 200 entries which is more than enough for UI display
201
- const MAX_CHAT_HISTORY_ENTRIES = 200;
202
- if (this.chatHistory.length > MAX_CHAT_HISTORY_ENTRIES) {
203
- const entriesToRemove = this.chatHistory.length - MAX_CHAT_HISTORY_ENTRIES;
204
- this.chatHistory = this.chatHistory.slice(entriesToRemove);
205
- // Update tool call index map after pruning
206
- // Clear and rebuild only for remaining entries
207
- this.toolCallIndexMap.clear();
208
- this.chatHistory.forEach((entry, index) => {
209
- if (entry.type === "tool_call" && entry.toolCall?.id) {
210
- this.toolCallIndexMap.set(entry.toolCall.id, index);
211
- }
212
- else if (entry.type === "tool_result" && entry.toolCall?.id) {
213
- this.toolCallIndexMap.set(entry.toolCall.id, index);
214
- }
215
- });
216
- }
312
+ }
313
+ // CRITICAL FIX: Always check and prune chatHistory to prevent unbounded growth
314
+ // This must happen UNCONDITIONALLY, even if context pruning is disabled
315
+ // Keep last 200 entries which is more than enough for UI display
316
+ const MAX_CHAT_HISTORY_ENTRIES = 200;
317
+ if (this.chatHistory.length > MAX_CHAT_HISTORY_ENTRIES) {
318
+ const entriesToRemove = this.chatHistory.length - MAX_CHAT_HISTORY_ENTRIES;
319
+ this.chatHistory = this.chatHistory.slice(entriesToRemove);
320
+ // Update tool call index map after pruning
321
+ // Clear and rebuild only for remaining entries
322
+ this.toolCallIndexMap.clear();
323
+ this.chatHistory.forEach((entry, index) => {
324
+ if (entry.type === "tool_call" && entry.toolCall?.id) {
325
+ this.toolCallIndexMap.set(entry.toolCall.id, index);
326
+ }
327
+ else if (entry.type === "tool_result" && entry.toolCall?.id) {
328
+ this.toolCallIndexMap.set(entry.toolCall.id, index);
329
+ }
330
+ });
331
+ }
332
+ // CRITICAL FIX: Add hard limit for messages array as safety backstop
333
+ // In case contextManager.shouldPrune() always returns false
334
+ const MAX_MESSAGES = 500;
335
+ if (this.messages.length > MAX_MESSAGES) {
336
+ // Keep system message (if exists) + last N messages
337
+ const systemMessages = this.messages.filter(m => m.role === 'system');
338
+ const nonSystemMessages = this.messages.filter(m => m.role !== 'system');
339
+ const keepMessages = Math.min(nonSystemMessages.length, MAX_MESSAGES - systemMessages.length);
340
+ this.messages = [
341
+ ...systemMessages,
342
+ ...nonSystemMessages.slice(-keepMessages)
343
+ ];
217
344
  }
218
345
  }
219
346
  /**
@@ -235,13 +362,16 @@ export class LLMAgent extends EventEmitter {
235
362
  try {
236
363
  const args = JSON.parse(toolCall.function.arguments || '{}');
237
364
  this.toolCallArgsCache.set(toolCall.id, args);
238
- // Prevent unbounded memory growth - limit cache size
365
+ // CRITICAL FIX: Prevent unbounded memory growth with proper cache eviction
366
+ // When cache exceeds limit, reduce to 80% capacity (not just remove 100 entries)
239
367
  if (this.toolCallArgsCache.size > 500) {
368
+ const targetSize = 400; // 80% of max capacity
369
+ const toRemove = this.toolCallArgsCache.size - targetSize;
240
370
  let deleted = 0;
241
371
  for (const key of this.toolCallArgsCache.keys()) {
242
372
  this.toolCallArgsCache.delete(key);
243
373
  deleted++;
244
- if (deleted >= 100)
374
+ if (deleted >= toRemove)
245
375
  break;
246
376
  }
247
377
  }
@@ -727,6 +857,29 @@ export class LLMAgent extends EventEmitter {
727
857
  }
728
858
  // Emit plan completed event
729
859
  this.emit("plan:completed", { plan, result: planResult });
860
+ // Generate status report on plan completion
861
+ try {
862
+ const reporter = getStatusReporter();
863
+ const tokenCount = this.tokenCounter.countMessageTokens(this.messages);
864
+ const statusReport = await reporter.generateStatusReport({
865
+ messages: this.messages,
866
+ chatHistory: this.chatHistory,
867
+ tokenCount,
868
+ plan,
869
+ });
870
+ // Notify user of status report
871
+ yield {
872
+ type: "content",
873
+ content: `\n📊 Status report saved to: \`${statusReport.path}\`\n`,
874
+ };
875
+ // Emit event for UI/logging
876
+ this.emit("plan:report", statusReport);
877
+ }
878
+ catch (error) {
879
+ // Status report generation failure should not block execution
880
+ const errorMsg = extractErrorMessage(error);
881
+ console.warn("Failed to generate status report:", errorMsg);
882
+ }
730
883
  this.currentPlan = null;
731
884
  }
732
885
  catch (error) {
@@ -1167,85 +1320,102 @@ export class LLMAgent extends EventEmitter {
1167
1320
  let accumulatedContent = "";
1168
1321
  let toolCallsYielded = false;
1169
1322
  let usageData = null;
1170
- for await (const chunk of stream) {
1171
- // Check for cancellation in the streaming loop
1172
- if (this.isCancelled()) {
1173
- yield* this.yieldCancellation();
1174
- // Return empty state after cancellation to avoid processing partial results
1175
- return { accumulated: {}, content: "", yielded: false };
1176
- }
1177
- if (!chunk.choices?.[0])
1178
- continue;
1179
- // Capture usage data from chunks (usually in the final chunk)
1180
- if (chunk.usage) {
1181
- usageData = chunk.usage;
1182
- }
1183
- // Accumulate the message using reducer
1184
- accumulatedMessage = this.messageReducer(accumulatedMessage, chunk);
1185
- // Check for tool calls - yield when we have complete tool calls with function names
1186
- const toolCalls = accumulatedMessage.tool_calls;
1187
- if (!toolCallsYielded && toolCalls && Array.isArray(toolCalls) && toolCalls.length > 0) {
1188
- const hasCompleteTool = toolCalls.some((tc) => tc.function?.name);
1189
- if (hasCompleteTool) {
1323
+ // CRITICAL FIX: Ensure stream is properly closed on cancellation or error
1324
+ // Without this, HTTP connections and buffers remain in memory
1325
+ try {
1326
+ for await (const chunk of stream) {
1327
+ // Check for cancellation in the streaming loop
1328
+ if (this.isCancelled()) {
1329
+ yield* this.yieldCancellation();
1330
+ // Return empty state after cancellation to avoid processing partial results
1331
+ return { accumulated: {}, content: "", yielded: false };
1332
+ }
1333
+ if (!chunk.choices?.[0])
1334
+ continue;
1335
+ // Capture usage data from chunks (usually in the final chunk)
1336
+ if (chunk.usage) {
1337
+ usageData = chunk.usage;
1338
+ }
1339
+ // Accumulate the message using reducer
1340
+ accumulatedMessage = this.messageReducer(accumulatedMessage, chunk);
1341
+ // Check for tool calls - yield when we have complete tool calls with function names
1342
+ const toolCalls = accumulatedMessage.tool_calls;
1343
+ if (!toolCallsYielded && toolCalls && Array.isArray(toolCalls) && toolCalls.length > 0) {
1344
+ const hasCompleteTool = toolCalls.some((tc) => tc.function?.name);
1345
+ if (hasCompleteTool) {
1346
+ yield {
1347
+ type: "tool_calls",
1348
+ toolCalls: toolCalls,
1349
+ };
1350
+ toolCallsYielded = true;
1351
+ }
1352
+ }
1353
+ // Stream reasoning content (GLM-4.6 thinking mode)
1354
+ // Safety check: ensure choices[0] exists before accessing
1355
+ if (chunk.choices[0]?.delta?.reasoning_content) {
1190
1356
  yield {
1191
- type: "tool_calls",
1192
- toolCalls: toolCalls,
1357
+ type: "reasoning",
1358
+ reasoningContent: chunk.choices[0].delta.reasoning_content,
1193
1359
  };
1194
- toolCallsYielded = true;
1360
+ }
1361
+ // Stream content as it comes
1362
+ if (chunk.choices[0]?.delta?.content) {
1363
+ accumulatedContent += chunk.choices[0].delta.content;
1364
+ yield {
1365
+ type: "content",
1366
+ content: chunk.choices[0].delta.content,
1367
+ };
1368
+ // Emit token count update (throttled and optimized)
1369
+ const now = Date.now();
1370
+ if (now - lastTokenUpdate.value > 1000) { // Increased throttle to 1s for better performance
1371
+ lastTokenUpdate.value = now;
1372
+ // Use fast estimation during streaming (4 chars ≈ 1 token)
1373
+ // This is ~70% faster than tiktoken encoding
1374
+ const estimatedOutputTokens = Math.floor(accumulatedContent.length / 4) +
1375
+ (accumulatedMessage.tool_calls
1376
+ ? Math.floor(JSON.stringify(accumulatedMessage.tool_calls).length / 4)
1377
+ : 0);
1378
+ totalOutputTokens.value = estimatedOutputTokens;
1379
+ yield {
1380
+ type: "token_count",
1381
+ tokenCount: inputTokens + estimatedOutputTokens,
1382
+ };
1383
+ }
1195
1384
  }
1196
1385
  }
1197
- // Stream reasoning content (GLM-4.6 thinking mode)
1198
- // Safety check: ensure choices[0] exists before accessing
1199
- if (chunk.choices[0]?.delta?.reasoning_content) {
1200
- yield {
1201
- type: "reasoning",
1202
- reasoningContent: chunk.choices[0].delta.reasoning_content,
1203
- };
1204
- }
1205
- // Stream content as it comes
1206
- if (chunk.choices[0]?.delta?.content) {
1207
- accumulatedContent += chunk.choices[0].delta.content;
1208
- yield {
1209
- type: "content",
1210
- content: chunk.choices[0].delta.content,
1211
- };
1212
- // Emit token count update (throttled and optimized)
1213
- const now = Date.now();
1214
- if (now - lastTokenUpdate.value > 1000) { // Increased throttle to 1s for better performance
1215
- lastTokenUpdate.value = now;
1216
- // Use fast estimation during streaming (4 chars ≈ 1 token)
1217
- // This is ~70% faster than tiktoken encoding
1218
- const estimatedOutputTokens = Math.floor(accumulatedContent.length / 4) +
1219
- (accumulatedMessage.tool_calls
1220
- ? Math.floor(JSON.stringify(accumulatedMessage.tool_calls).length / 4)
1221
- : 0);
1222
- totalOutputTokens.value = estimatedOutputTokens;
1386
+ // Track usage if available and emit accurate final token count
1387
+ if (usageData) {
1388
+ const tracker = getUsageTracker();
1389
+ tracker.trackUsage(this.llmClient.getCurrentModel(), usageData);
1390
+ // Emit accurate token count from API usage data (replaces estimation)
1391
+ const totalTokens = usageData.total_tokens;
1392
+ const completionTokens = usageData.completion_tokens;
1393
+ if (totalTokens) {
1394
+ totalOutputTokens.value = completionTokens || 0;
1223
1395
  yield {
1224
1396
  type: "token_count",
1225
- tokenCount: inputTokens + estimatedOutputTokens,
1397
+ tokenCount: totalTokens,
1226
1398
  };
1227
1399
  }
1228
1400
  }
1401
+ // CRITICAL: Yield the accumulated result so the main loop can access it!
1402
+ const result = { accumulated: accumulatedMessage, content: accumulatedContent, yielded: toolCallsYielded };
1403
+ yield result;
1404
+ return result;
1229
1405
  }
1230
- // Track usage if available and emit accurate final token count
1231
- if (usageData) {
1232
- const tracker = getUsageTracker();
1233
- tracker.trackUsage(this.llmClient.getCurrentModel(), usageData);
1234
- // Emit accurate token count from API usage data (replaces estimation)
1235
- const totalTokens = usageData.total_tokens;
1236
- const completionTokens = usageData.completion_tokens;
1237
- if (totalTokens) {
1238
- totalOutputTokens.value = completionTokens || 0;
1239
- yield {
1240
- type: "token_count",
1241
- tokenCount: totalTokens,
1242
- };
1406
+ finally {
1407
+ // CRITICAL FIX: Properly close the async iterator to release HTTP connections and buffers
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);
1416
+ }
1243
1417
  }
1244
1418
  }
1245
- // CRITICAL: Yield the accumulated result so the main loop can access it!
1246
- const result = { accumulated: accumulatedMessage, content: accumulatedContent, yielded: toolCallsYielded };
1247
- yield result;
1248
- return result;
1249
1419
  }
1250
1420
  /**
1251
1421
  * Add assistant message to history and conversation
@@ -1485,6 +1655,27 @@ export class LLMAgent extends EventEmitter {
1485
1655
  return { success: false, error: parseResult.error };
1486
1656
  }
1487
1657
  const args = parseResult.args;
1658
+ // Check if tool approval is required (for VSCode integration)
1659
+ if (this.requireToolApproval) {
1660
+ // Only require approval for file modification operations
1661
+ const needsApproval = toolCall.function.name === "create_file" ||
1662
+ toolCall.function.name === "str_replace_editor" ||
1663
+ toolCall.function.name === "insert_text";
1664
+ if (needsApproval) {
1665
+ // Emit event and wait for approval
1666
+ const approved = await this.waitForToolApproval(toolCall);
1667
+ if (!approved) {
1668
+ // User rejected the change
1669
+ this.emit('tool:rejected', toolCall);
1670
+ return {
1671
+ success: false,
1672
+ error: 'Change rejected by user'
1673
+ };
1674
+ }
1675
+ // User approved
1676
+ this.emit('tool:approved', toolCall);
1677
+ }
1678
+ }
1488
1679
  // Helper to safely get string argument with validation
1489
1680
  const getString = (key, required = true) => {
1490
1681
  const value = args[key];
@@ -1724,6 +1915,9 @@ export class LLMAgent extends EventEmitter {
1724
1915
  // Safely preserve system message if it exists
1725
1916
  const systemMessage = this.messages.length > 0 ? this.messages[0] : null;
1726
1917
  this.messages = systemMessage ? [systemMessage] : [];
1918
+ // CRITICAL FIX: Track tool calls to validate tool results
1919
+ // Prevents API errors from orphaned tool results without corresponding tool calls
1920
+ const toolCallIds = new Set();
1727
1921
  for (const entry of conversationState) {
1728
1922
  if (entry.type === 'user') {
1729
1923
  this.messages.push({
@@ -1732,6 +1926,14 @@ export class LLMAgent extends EventEmitter {
1732
1926
  });
1733
1927
  }
1734
1928
  else if (entry.type === 'assistant') {
1929
+ // Track tool call IDs from assistant messages
1930
+ if (entry.toolCalls && Array.isArray(entry.toolCalls)) {
1931
+ for (const toolCall of entry.toolCalls) {
1932
+ if (toolCall?.id) {
1933
+ toolCallIds.add(toolCall.id);
1934
+ }
1935
+ }
1936
+ }
1735
1937
  this.messages.push({
1736
1938
  role: 'assistant',
1737
1939
  content: entry.content,
@@ -1739,11 +1941,18 @@ export class LLMAgent extends EventEmitter {
1739
1941
  });
1740
1942
  }
1741
1943
  else if (entry.type === 'tool_result' && entry.toolCall) {
1742
- this.messages.push({
1743
- role: 'tool',
1744
- content: entry.content,
1745
- tool_call_id: entry.toolCall.id,
1746
- });
1944
+ // CRITICAL FIX: Only add tool result if corresponding tool call exists
1945
+ // This prevents "tool message without corresponding tool call" API errors
1946
+ if (toolCallIds.has(entry.toolCall.id)) {
1947
+ this.messages.push({
1948
+ role: 'tool',
1949
+ content: entry.content,
1950
+ tool_call_id: entry.toolCall.id,
1951
+ });
1952
+ }
1953
+ else {
1954
+ console.warn(`Skipping orphaned tool result for tool_call_id: ${entry.toolCall.id}`);
1955
+ }
1747
1956
  }
1748
1957
  }
1749
1958
  this.emit('system', `Conversation rewound to checkpoint ${checkpointId}`);
@@ -1890,7 +2099,6 @@ export class LLMAgent extends EventEmitter {
1890
2099
  */
1891
2100
  checkDisposed() {
1892
2101
  if (this.disposed) {
1893
- const { SDKError, SDKErrorCode } = require('../sdk/errors.js');
1894
2102
  throw new SDKError(SDKErrorCode.AGENT_DISPOSED, 'Agent has been disposed and cannot be used. Create a new agent instance.');
1895
2103
  }
1896
2104
  }
@@ -1927,17 +2135,29 @@ export class LLMAgent extends EventEmitter {
1927
2135
  this.disposed = true;
1928
2136
  // Remove all event listeners to prevent memory leaks
1929
2137
  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');
1930
2141
  // Dispose tools that have cleanup methods
1931
2142
  this.bash.dispose();
1932
2143
  // Clear in-memory caches
1933
2144
  this.recentToolCalls.clear();
1934
2145
  this.toolCallIndexMap.clear();
1935
2146
  this.toolCallArgsCache.clear();
2147
+ // BUG FIX: Clear all pending tool approval timeouts to prevent memory leaks
2148
+ // These timers would otherwise keep running for up to 5 minutes after dispose
2149
+ for (const timeout of this.toolApprovalTimeouts.values()) {
2150
+ clearTimeout(timeout);
2151
+ }
2152
+ this.toolApprovalTimeouts.clear();
2153
+ this.toolApprovalCallbacks.clear();
1936
2154
  // Clear conversation history to free memory
1937
2155
  this.chatHistory = [];
1938
2156
  this.messages = [];
1939
- // Dispose token counter and context manager
1940
- this.tokenCounter.dispose();
2157
+ // Dispose context manager (tokenCounter is a singleton, don't dispose)
2158
+ // CRITICAL FIX: tokenCounter is obtained via getTokenCounter() which returns
2159
+ // a shared singleton instance. Disposing it would break other agent instances
2160
+ // using the same model. The singleton manages its own lifecycle.
1941
2161
  this.contextManager.dispose();
1942
2162
  // Abort any in-flight requests
1943
2163
  if (this.abortController) {