cowork-os 0.3.21 → 0.3.25

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 (252) hide show
  1. package/README.md +372 -10
  2. package/connectors/README.md +20 -0
  3. package/connectors/asana-mcp/README.md +24 -0
  4. package/connectors/asana-mcp/dist/index.js +427 -0
  5. package/connectors/asana-mcp/package.json +15 -0
  6. package/connectors/asana-mcp/src/index.ts +553 -0
  7. package/connectors/asana-mcp/tsconfig.json +13 -0
  8. package/connectors/hubspot-mcp/README.md +35 -0
  9. package/connectors/hubspot-mcp/dist/index.js +454 -0
  10. package/connectors/hubspot-mcp/package.json +15 -0
  11. package/connectors/hubspot-mcp/src/index.ts +562 -0
  12. package/connectors/hubspot-mcp/tsconfig.json +13 -0
  13. package/connectors/jira-mcp/README.md +49 -0
  14. package/connectors/jira-mcp/dist/index.js +588 -0
  15. package/connectors/jira-mcp/package.json +15 -0
  16. package/connectors/jira-mcp/src/index.ts +711 -0
  17. package/connectors/jira-mcp/tsconfig.json +13 -0
  18. package/connectors/linear-mcp/README.md +22 -0
  19. package/connectors/linear-mcp/dist/index.js +402 -0
  20. package/connectors/linear-mcp/package.json +15 -0
  21. package/connectors/linear-mcp/src/index.ts +522 -0
  22. package/connectors/linear-mcp/tsconfig.json +13 -0
  23. package/connectors/okta-mcp/README.md +24 -0
  24. package/connectors/okta-mcp/dist/index.js +411 -0
  25. package/connectors/okta-mcp/package.json +15 -0
  26. package/connectors/okta-mcp/src/index.ts +520 -0
  27. package/connectors/okta-mcp/tsconfig.json +13 -0
  28. package/connectors/salesforce-mcp/README.md +47 -0
  29. package/connectors/salesforce-mcp/dist/index.js +584 -0
  30. package/connectors/salesforce-mcp/package.json +15 -0
  31. package/connectors/salesforce-mcp/src/index.ts +722 -0
  32. package/connectors/salesforce-mcp/tsconfig.json +13 -0
  33. package/connectors/servicenow-mcp/README.md +26 -0
  34. package/connectors/servicenow-mcp/dist/index.js +400 -0
  35. package/connectors/servicenow-mcp/package.json +15 -0
  36. package/connectors/servicenow-mcp/src/index.ts +500 -0
  37. package/connectors/servicenow-mcp/tsconfig.json +13 -0
  38. package/connectors/templates/mcp-connector/README.md +31 -0
  39. package/connectors/templates/mcp-connector/package.json +15 -0
  40. package/connectors/templates/mcp-connector/src/index.ts +330 -0
  41. package/connectors/templates/mcp-connector/tsconfig.json +13 -0
  42. package/connectors/zendesk-mcp/README.md +40 -0
  43. package/connectors/zendesk-mcp/dist/index.js +431 -0
  44. package/connectors/zendesk-mcp/package.json +15 -0
  45. package/connectors/zendesk-mcp/src/index.ts +543 -0
  46. package/connectors/zendesk-mcp/tsconfig.json +13 -0
  47. package/dist/electron/electron/agent/custom-skill-loader.js +31 -1
  48. package/dist/electron/electron/agent/daemon.js +189 -13
  49. package/dist/electron/electron/agent/executor.js +895 -78
  50. package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
  51. package/dist/electron/electron/agent/llm/azure-openai-provider.js +328 -0
  52. package/dist/electron/electron/agent/llm/bedrock-provider.js +49 -9
  53. package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
  54. package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
  55. package/dist/electron/electron/agent/llm/index.js +13 -1
  56. package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
  57. package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
  58. package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
  59. package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
  60. package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
  61. package/dist/electron/electron/agent/llm/provider-factory.js +350 -4
  62. package/dist/electron/electron/agent/llm/types.js +66 -1
  63. package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
  64. package/dist/electron/electron/agent/search/provider-factory.js +38 -2
  65. package/dist/electron/electron/agent/tools/box-tools.js +231 -0
  66. package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
  67. package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
  68. package/dist/electron/electron/agent/tools/file-tools.js +66 -3
  69. package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
  70. package/dist/electron/electron/agent/tools/grep-tools.js +90 -10
  71. package/dist/electron/electron/agent/tools/image-tools.js +11 -1
  72. package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
  73. package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
  74. package/dist/electron/electron/agent/tools/registry.js +548 -10
  75. package/dist/electron/electron/agent/tools/search-tools.js +28 -10
  76. package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
  77. package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
  78. package/dist/electron/electron/agent/tools/x-tools.js +1 -1
  79. package/dist/electron/electron/agents/agent-dispatch.js +63 -0
  80. package/dist/electron/electron/database/repositories.js +19 -5
  81. package/dist/electron/electron/database/schema.js +8 -0
  82. package/dist/electron/electron/gateway/channels/whatsapp.js +55 -0
  83. package/dist/electron/electron/gateway/index.js +75 -1
  84. package/dist/electron/electron/gateway/router.js +209 -154
  85. package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
  86. package/dist/electron/electron/ipc/handlers.js +763 -267
  87. package/dist/electron/electron/main.js +63 -0
  88. package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
  89. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
  90. package/dist/electron/electron/memory/MemoryService.js +2 -1
  91. package/dist/electron/electron/preload.js +78 -1
  92. package/dist/electron/electron/settings/appearance-manager.js +18 -1
  93. package/dist/electron/electron/settings/box-manager.js +54 -0
  94. package/dist/electron/electron/settings/dropbox-manager.js +54 -0
  95. package/dist/electron/electron/settings/google-drive-manager.js +54 -0
  96. package/dist/electron/electron/settings/notion-manager.js +56 -0
  97. package/dist/electron/electron/settings/onedrive-manager.js +54 -0
  98. package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
  99. package/dist/electron/electron/utils/box-api.js +153 -0
  100. package/dist/electron/electron/utils/dropbox-api.js +144 -0
  101. package/dist/electron/electron/utils/env-migration.js +19 -0
  102. package/dist/electron/electron/utils/google-drive-api.js +152 -0
  103. package/dist/electron/electron/utils/notion-api.js +103 -0
  104. package/dist/electron/electron/utils/onedrive-api.js +113 -0
  105. package/dist/electron/electron/utils/sharepoint-api.js +109 -0
  106. package/dist/electron/electron/utils/validation.js +98 -3
  107. package/dist/electron/electron/utils/x-cli.js +1 -1
  108. package/dist/electron/shared/channelMessages.js +284 -3
  109. package/dist/electron/shared/llm-provider-catalog.js +198 -0
  110. package/dist/electron/shared/types.js +90 -1
  111. package/package.json +14 -3
  112. package/resources/skills/nano-banana-pro.json +4 -4
  113. package/resources/skills/openai-image-gen.json +3 -3
  114. package/resources/skills/scripts/gen.py +163 -0
  115. package/resources/skills/scripts/generate_image.py +91 -0
  116. package/src/electron/agent/custom-skill-loader.ts +34 -1
  117. package/src/electron/agent/daemon.ts +210 -14
  118. package/src/electron/agent/executor.ts +1124 -85
  119. package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
  120. package/src/electron/agent/llm/azure-openai-provider.ts +388 -0
  121. package/src/electron/agent/llm/bedrock-provider.ts +62 -9
  122. package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
  123. package/src/electron/agent/llm/groq-provider.ts +39 -0
  124. package/src/electron/agent/llm/index.ts +6 -0
  125. package/src/electron/agent/llm/kimi-provider.ts +39 -0
  126. package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
  127. package/src/electron/agent/llm/openai-compatible.ts +133 -0
  128. package/src/electron/agent/llm/openai-oauth.ts +2 -1
  129. package/src/electron/agent/llm/openrouter-provider.ts +2 -1
  130. package/src/electron/agent/llm/provider-factory.ts +459 -6
  131. package/src/electron/agent/llm/types.ts +95 -1
  132. package/src/electron/agent/llm/xai-provider.ts +39 -0
  133. package/src/electron/agent/search/provider-factory.ts +43 -2
  134. package/src/electron/agent/tools/box-tools.ts +239 -0
  135. package/src/electron/agent/tools/builtin-settings.ts +36 -0
  136. package/src/electron/agent/tools/dropbox-tools.ts +237 -0
  137. package/src/electron/agent/tools/file-tools.ts +66 -3
  138. package/src/electron/agent/tools/gmail-tools.ts +240 -0
  139. package/src/electron/agent/tools/google-calendar-tools.ts +258 -0
  140. package/src/electron/agent/tools/google-drive-tools.ts +228 -0
  141. package/src/electron/agent/tools/grep-tools.ts +97 -12
  142. package/src/electron/agent/tools/image-tools.ts +11 -1
  143. package/src/electron/agent/tools/notion-tools.ts +330 -0
  144. package/src/electron/agent/tools/onedrive-tools.ts +217 -0
  145. package/src/electron/agent/tools/registry.ts +794 -10
  146. package/src/electron/agent/tools/search-tools.ts +29 -11
  147. package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
  148. package/src/electron/agent/tools/shell-tools.ts +11 -3
  149. package/src/electron/agent/tools/x-tools.ts +1 -1
  150. package/src/electron/agents/agent-dispatch.ts +79 -0
  151. package/src/electron/database/SecureSettingsRepository.ts +7 -1
  152. package/src/electron/database/repositories.ts +58 -6
  153. package/src/electron/database/schema.ts +8 -0
  154. package/src/electron/gateway/channels/discord.ts +4 -0
  155. package/src/electron/gateway/channels/google-chat.ts +3 -0
  156. package/src/electron/gateway/channels/line.ts +3 -0
  157. package/src/electron/gateway/channels/matrix-client.ts +15 -0
  158. package/src/electron/gateway/channels/matrix.ts +31 -0
  159. package/src/electron/gateway/channels/mattermost.ts +3 -0
  160. package/src/electron/gateway/channels/signal.ts +3 -0
  161. package/src/electron/gateway/channels/slack.ts +9 -4
  162. package/src/electron/gateway/channels/teams.ts +4 -0
  163. package/src/electron/gateway/channels/telegram.ts +2 -0
  164. package/src/electron/gateway/channels/twitch.ts +2 -0
  165. package/src/electron/gateway/channels/types.ts +8 -0
  166. package/src/electron/gateway/channels/whatsapp.ts +66 -0
  167. package/src/electron/gateway/index.ts +95 -2
  168. package/src/electron/gateway/router.ts +231 -161
  169. package/src/electron/gateway/security.ts +21 -9
  170. package/src/electron/ipc/canvas-handlers.ts +10 -0
  171. package/src/electron/ipc/handlers.ts +848 -292
  172. package/src/electron/main.ts +35 -0
  173. package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
  174. package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
  175. package/src/electron/memory/MemoryService.ts +7 -1
  176. package/src/electron/preload.ts +200 -5
  177. package/src/electron/settings/appearance-manager.ts +20 -2
  178. package/src/electron/settings/box-manager.ts +58 -0
  179. package/src/electron/settings/dropbox-manager.ts +58 -0
  180. package/src/electron/settings/google-workspace-manager.ts +59 -0
  181. package/src/electron/settings/notion-manager.ts +60 -0
  182. package/src/electron/settings/onedrive-manager.ts +58 -0
  183. package/src/electron/settings/sharepoint-manager.ts +58 -0
  184. package/src/electron/utils/box-api.ts +184 -0
  185. package/src/electron/utils/dropbox-api.ts +171 -0
  186. package/src/electron/utils/env-migration.ts +22 -0
  187. package/src/electron/utils/gmail-api.ts +121 -0
  188. package/src/electron/utils/google-calendar-api.ts +115 -0
  189. package/src/electron/utils/google-workspace-api.ts +228 -0
  190. package/src/electron/utils/google-workspace-auth.ts +109 -0
  191. package/src/electron/utils/google-workspace-oauth.ts +232 -0
  192. package/src/electron/utils/notion-api.ts +126 -0
  193. package/src/electron/utils/onedrive-api.ts +137 -0
  194. package/src/electron/utils/sharepoint-api.ts +132 -0
  195. package/src/electron/utils/validation.ts +128 -1
  196. package/src/electron/utils/x-cli.ts +1 -1
  197. package/src/renderer/App.tsx +119 -8
  198. package/src/renderer/components/ActivityFeedItem.tsx +34 -17
  199. package/src/renderer/components/AgentWorkingStatePanel.tsx +7 -5
  200. package/src/renderer/components/AppearanceSettings.tsx +37 -2
  201. package/src/renderer/components/BlueBubblesSettings.tsx +18 -7
  202. package/src/renderer/components/BoxSettings.tsx +203 -0
  203. package/src/renderer/components/BrowserView.tsx +101 -0
  204. package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
  205. package/src/renderer/components/CanvasPreview.tsx +68 -1
  206. package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
  207. package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
  208. package/src/renderer/components/ConnectorsSettings.tsx +397 -0
  209. package/src/renderer/components/ControlPlaneSettings.tsx +2 -0
  210. package/src/renderer/components/DiscordSettings.tsx +18 -7
  211. package/src/renderer/components/DropboxSettings.tsx +202 -0
  212. package/src/renderer/components/EmailSettings.tsx +18 -7
  213. package/src/renderer/components/FileViewer.tsx +21 -13
  214. package/src/renderer/components/GoogleChatSettings.tsx +17 -7
  215. package/src/renderer/components/GoogleWorkspaceSettings.tsx +332 -0
  216. package/src/renderer/components/ImessageSettings.tsx +22 -11
  217. package/src/renderer/components/LineIcons.tsx +376 -0
  218. package/src/renderer/components/LineSettings.tsx +18 -7
  219. package/src/renderer/components/MCPSettings.tsx +56 -0
  220. package/src/renderer/components/MainContent.tsx +740 -76
  221. package/src/renderer/components/MatrixSettings.tsx +18 -7
  222. package/src/renderer/components/MattermostSettings.tsx +18 -7
  223. package/src/renderer/components/NodesSettings.tsx +58 -99
  224. package/src/renderer/components/NotificationPanel.tsx +25 -11
  225. package/src/renderer/components/NotionSettings.tsx +231 -0
  226. package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
  227. package/src/renderer/components/OnboardingModal.tsx +70 -1
  228. package/src/renderer/components/OneDriveSettings.tsx +212 -0
  229. package/src/renderer/components/RightPanel.tsx +141 -28
  230. package/src/renderer/components/ScheduledTasksSettings.tsx +10 -62
  231. package/src/renderer/components/SearchSettings.tsx +118 -114
  232. package/src/renderer/components/Settings.tsx +1425 -651
  233. package/src/renderer/components/SharePointSettings.tsx +224 -0
  234. package/src/renderer/components/Sidebar.tsx +94 -19
  235. package/src/renderer/components/SignalSettings.tsx +18 -7
  236. package/src/renderer/components/SkillHubBrowser.tsx +144 -185
  237. package/src/renderer/components/SlackSettings.tsx +18 -7
  238. package/src/renderer/components/TaskQuickActions.tsx +11 -6
  239. package/src/renderer/components/TaskTimeline.tsx +58 -26
  240. package/src/renderer/components/TeamsSettings.tsx +18 -7
  241. package/src/renderer/components/TelegramSettings.tsx +18 -7
  242. package/src/renderer/components/ThemeIcon.tsx +16 -0
  243. package/src/renderer/components/TwitchSettings.tsx +18 -7
  244. package/src/renderer/components/VoiceSettings.tsx +30 -74
  245. package/src/renderer/components/WhatsAppSettings.tsx +48 -37
  246. package/src/renderer/components/WorkingStateHistory.tsx +7 -5
  247. package/src/renderer/components/WorkspaceSelector.tsx +42 -13
  248. package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
  249. package/src/renderer/styles/index.css +2333 -209
  250. package/src/shared/channelMessages.ts +367 -4
  251. package/src/shared/llm-provider-catalog.ts +217 -0
  252. package/src/shared/types.ts +251 -2
@@ -1,4 +1,4 @@
1
- import { Task, Workspace, Plan, PlanStep, TaskEvent, SuccessCriteria } from '../../shared/types';
1
+ import { Task, Workspace, Plan, PlanStep, TaskEvent, SuccessCriteria, TEMP_WORKSPACE_ID } from '../../shared/types';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { AgentDaemon } from './daemon';
@@ -9,6 +9,7 @@ import {
9
9
  LLMProviderFactory,
10
10
  LLMMessage,
11
11
  LLMToolResult,
12
+ LLMToolUse,
12
13
  } from './llm';
13
14
  import {
14
15
  ContextManager,
@@ -21,6 +22,7 @@ import { calculateCost, formatCost } from './llm/pricing';
21
22
  import { getCustomSkillLoader } from './custom-skill-loader';
22
23
  import { MemoryService } from '../memory/MemoryService';
23
24
  import { InputSanitizer, OutputFilter } from './security';
25
+ import { BuiltinToolsSettingsManager } from './tools/builtin-settings';
24
26
 
25
27
  class AwaitingUserInputError extends Error {
26
28
  constructor(message: string) {
@@ -35,7 +37,7 @@ const LLM_TIMEOUT_MS = 2 * 60 * 1000;
35
37
  // Per-step timeout (5 minutes max per step)
36
38
  const STEP_TIMEOUT_MS = 5 * 60 * 1000;
37
39
 
38
- // Per-tool execution timeout (45 seconds - balance responsiveness with heavier tools)
40
+ // Default per-tool execution timeout (overrideable per tool)
39
41
  const TOOL_TIMEOUT_MS = 30 * 1000;
40
42
 
41
43
  // Maximum consecutive failures for the same tool before giving up
@@ -88,6 +90,25 @@ const INPUT_DEPENDENT_ERROR_PATTERNS = [
88
90
  /user denied/i, // User denied an approval request
89
91
  ];
90
92
 
93
+ // Keywords that imply a step wants image verification.
94
+ const IMAGE_VERIFICATION_KEYWORDS = [
95
+ 'image',
96
+ 'photo',
97
+ 'photograph',
98
+ 'picture',
99
+ 'render',
100
+ 'illustration',
101
+ 'png',
102
+ 'jpg',
103
+ 'jpeg',
104
+ 'webp',
105
+ ];
106
+
107
+ const IMAGE_FILE_EXTENSION_REGEX = /\.(png|jpe?g|webp|gif|bmp)$/i;
108
+
109
+ // Allow a small buffer for file timestamp granularity/clock skew.
110
+ const IMAGE_VERIFICATION_TIME_SKEW_MS = 1000;
111
+
91
112
  /**
92
113
  * Check if an error is non-retryable (quota/rate limit related)
93
114
  * These errors indicate a systemic problem with the tool/API
@@ -149,30 +170,6 @@ function isAskingQuestion(text: string): boolean {
149
170
  const trimmed = text.trim();
150
171
  if (!trimmed) return false;
151
172
 
152
- // Keep this lightweight and conservative: only pause on questions that
153
- // clearly request input/decisions needed to proceed.
154
- const blockingQuestionPatterns = [
155
- // Direct requests for info or confirmation
156
- /(?:^|\n)\s*(?:please\s+)?(?:provide|share|send|upload|enter|paste|specify|clarify|confirm|choose|pick|select)\b/i,
157
- /(?:can|could|would)\s+you\s+(?:please\s+)?(?:provide|share|send|upload|enter|paste|specify|clarify|confirm|choose|pick|select)\b/i,
158
-
159
- // Decision/approval questions
160
- /would\s+you\s+like\s+me\s+to\b/i,
161
- /would\s+you\s+prefer\b/i,
162
- /should\s+i\b/i,
163
- /do\s+you\s+want\s+me\s+to\b/i,
164
- /do\s+you\s+prefer\b/i,
165
- /is\s+it\s+(?:ok|okay|alright)\s+if\s+i\b/i,
166
-
167
- // Clarifying questions about specifics
168
- /\bwhat\s+(?:is|are|was|were|should|would|can|could|do|does|did)\s+(?:the|your|this|that)\b/i,
169
- /\bwhat\s+should\s+i\b/i,
170
- /\bwhich\s+(?:one|option|approach|method|file|version|environment|format|branch|repo|path)\b/i,
171
- /\bwhere\s+(?:is|are|should|can|could)\b/i,
172
- /\bwhen\s+(?:is|are|should|can|could)\b/i,
173
- /\bhow\s+should\s+i\b/i,
174
- ];
175
-
176
173
  const nonBlockingQuestionPatterns = [
177
174
  // Conversational/offboarding prompts that shouldn't pause execution
178
175
  /\bwhat\s+(?:else\s+)?can\s+i\s+help\b/i,
@@ -185,23 +182,73 @@ function isAskingQuestion(text: string): boolean {
185
182
  /\bdoes\s+that\s+(?:help|make\s+sense)\b/i,
186
183
  ];
187
184
 
188
- const isShort = trimmed.length < 1000;
189
- if (!isShort) return false;
185
+ const maxLengthForAnalysis = 4000;
186
+ const sample = trimmed.slice(0, maxLengthForAnalysis);
190
187
 
191
- // If we see explicit blocking cues, pause.
192
- if (blockingQuestionPatterns.some(pattern => pattern.test(trimmed))) {
193
- return true;
194
- }
188
+ const blockingCuePatterns = [
189
+ /(?:need|required)\s+(?:your|a|the)\b/i,
190
+ /before\s+i\s+can\s+(?:proceed|continue)\b/i,
191
+ /to\s+(?:proceed|continue|move\s+forward)\b/i,
192
+ /i\s+can(?:not|'t)\s+(?:proceed|continue)\b/i,
193
+ /\bawaiting\s+your\b/i,
194
+ ];
195
+
196
+ const explicitProceedPatterns = [
197
+ /\bi\s+(?:will|\'ll)\s+(?:proceed|continue|go\s+ahead|move\s+forward)\b/i,
198
+ /\bi\s+can\s+(?:proceed|continue|move\s+forward)\b/i,
199
+ /\bi\s+(?:will|\'ll)\s+assume\b/i,
200
+ /\bif\s+you\s+do\s+not\s+(?:respond|answer|reply)\b/i,
201
+ /\bif\s+you\s+don\'t\s+(?:respond|answer|reply)\b/i,
202
+ ];
203
+
204
+ const questionWordPatterns = [
205
+ /^(?:who|what|where|when|why|how|which)\b/i,
206
+ ];
195
207
 
196
- // If it's a non-blocking conversational prompt, don't pause.
197
- const lastLine = trimmed.split('\n').filter(Boolean).pop() ?? trimmed;
208
+ const imperativePatterns = [
209
+ /^(?:please\s+)?(?:provide|share|send|upload|enter|paste|specify|clarify|confirm|choose|pick|select|list|tell|give)\b/i,
210
+ ];
211
+
212
+ const decisionPatterns = [
213
+ /^(?:do\s+you\s+want|do\s+you\s+prefer|would\s+you\s+like|would\s+you\s+prefer|should\s+i|is\s+it\s+(?:ok|okay|alright)\s+if\s+i)\b/i,
214
+ ];
215
+
216
+ const hasBlockingCue = blockingCuePatterns.some(pattern => pattern.test(sample));
217
+ const hasExplicitProceed = explicitProceedPatterns.some(pattern => pattern.test(sample));
218
+ if (hasBlockingCue) return true;
219
+ const lines = sample.split('\n').map(l => l.trim()).filter(Boolean);
220
+ if (lines.length === 0) return false;
221
+
222
+ const lastLine = lines[lines.length - 1] ?? sample;
198
223
  const sentenceMatch = lastLine.match(/[^.!?]+[.!?]*$/);
199
224
  const lastSentence = sentenceMatch ? sentenceMatch[0].trim() : lastLine;
200
- if (nonBlockingQuestionPatterns.some(pattern => pattern.test(lastSentence))) {
201
- return false;
225
+ const hasNonBlockingTail = nonBlockingQuestionPatterns.some(pattern => pattern.test(lastSentence));
226
+
227
+ const tailLines = lines.slice(-2);
228
+ let tailQuestion = false;
229
+ let tailImperative = false;
230
+
231
+ for (const line of tailLines) {
232
+ const normalized = line.replace(/^[\-\*]?\s*\d*[\).]?\s*/, '').trim();
233
+ if (!normalized) continue;
234
+ if (nonBlockingQuestionPatterns.some(pattern => pattern.test(normalized))) {
235
+ continue;
236
+ }
237
+ if (imperativePatterns.some(pattern => pattern.test(normalized)) || decisionPatterns.some(pattern => pattern.test(normalized))) {
238
+ tailImperative = true;
239
+ }
240
+ if (normalized.endsWith('?') || questionWordPatterns.some(pattern => pattern.test(normalized))) {
241
+ tailQuestion = true;
242
+ }
243
+ }
244
+
245
+ if (tailImperative) return true;
246
+ if (tailQuestion) {
247
+ if (hasNonBlockingTail) return false;
248
+ if (hasExplicitProceed) return false;
249
+ return true;
202
250
  }
203
251
 
204
- // Default to not pausing on generic questions.
205
252
  return false;
206
253
  }
207
254
 
@@ -1014,6 +1061,14 @@ export class TaskExecutor {
1014
1061
  private modelKey: string;
1015
1062
  private conversationHistory: LLMMessage[] = [];
1016
1063
  private systemPrompt: string = '';
1064
+ private lastUserMessage: string;
1065
+ private toolResultMemory: Array<{ tool: string; summary: string; timestamp: number }> = [];
1066
+ private lastAssistantOutput: string | null = null;
1067
+ private lastNonVerificationOutput: string | null = null;
1068
+ private readonly toolResultMemoryLimit = 8;
1069
+ private readonly shouldPauseForQuestions: boolean;
1070
+ private dispatchedMentionedAgents = false;
1071
+ private lastAssistantText: string | null = null;
1017
1072
 
1018
1073
  // Plan revision tracking to prevent infinite revision loops
1019
1074
  private planRevisionCount: number = 0;
@@ -1040,7 +1095,10 @@ export class TaskExecutor {
1040
1095
  private workspace: Workspace,
1041
1096
  private daemon: AgentDaemon
1042
1097
  ) {
1098
+ this.lastUserMessage = task.prompt;
1043
1099
  this.requiresTestRun = this.detectTestRequirement(`${task.title}\n${task.prompt}`);
1100
+ // Only main tasks should pause for user input. Sub/parallel tasks should complete and report back.
1101
+ this.shouldPauseForQuestions = !task.parentTaskId && (task.agentType ?? 'main') === 'main';
1044
1102
  // Get base settings
1045
1103
  const settings = LLMProviderFactory.loadSettings();
1046
1104
 
@@ -1056,13 +1114,19 @@ export class TaskExecutor {
1056
1114
  const effectiveModelKey = taskModelKey || settings.modelKey;
1057
1115
 
1058
1116
  // Get the model ID
1117
+ const azureDeployment = settings.azure?.deployment || settings.azure?.deployments?.[0];
1059
1118
  this.modelId = LLMProviderFactory.getModelId(
1060
1119
  effectiveModelKey,
1061
1120
  settings.providerType,
1062
1121
  settings.ollama?.model,
1063
1122
  settings.gemini?.model,
1064
1123
  settings.openrouter?.model,
1065
- settings.openai?.model
1124
+ settings.openai?.model,
1125
+ azureDeployment,
1126
+ settings.groq?.model,
1127
+ settings.xai?.model,
1128
+ settings.kimi?.model,
1129
+ settings.customProviders
1066
1130
  );
1067
1131
  this.modelKey = effectiveModelKey;
1068
1132
 
@@ -1151,6 +1215,9 @@ export class TaskExecutor {
1151
1215
  error.message?.includes('rate limit') ||
1152
1216
  error.message?.includes('ECONNRESET') ||
1153
1217
  error.message?.includes('ETIMEDOUT') ||
1218
+ error.message?.includes('ENOTFOUND') ||
1219
+ error.message?.includes('EAI_AGAIN') ||
1220
+ error.message?.includes('ECONNREFUSED') ||
1154
1221
  error.message?.includes('network') ||
1155
1222
  error.status === 429 ||
1156
1223
  error.status === 503 ||
@@ -1220,6 +1287,23 @@ export class TaskExecutor {
1220
1287
  this.globalTurnCount++; // Track global turns across all steps
1221
1288
  }
1222
1289
 
1290
+ private getToolTimeoutMs(toolName: string, input: unknown): number {
1291
+ const settingsTimeout = BuiltinToolsSettingsManager.getToolTimeoutMs(toolName);
1292
+ const normalizedSettingsTimeout = settingsTimeout && settingsTimeout > 0 ? settingsTimeout : null;
1293
+
1294
+ if (toolName === 'run_command') {
1295
+ const inputTimeout = typeof (input as { timeout?: unknown })?.timeout === 'number'
1296
+ ? (input as { timeout?: number }).timeout
1297
+ : undefined;
1298
+ if (typeof inputTimeout === 'number' && Number.isFinite(inputTimeout) && inputTimeout > 0) {
1299
+ return Math.round(inputTimeout);
1300
+ }
1301
+ return normalizedSettingsTimeout ?? TOOL_TIMEOUT_MS;
1302
+ }
1303
+
1304
+ return normalizedSettingsTimeout ?? TOOL_TIMEOUT_MS;
1305
+ }
1306
+
1223
1307
  /**
1224
1308
  * Check if a file operation should be blocked (redundant read or duplicate creation)
1225
1309
  * @returns Object with blocked flag, reason, and suggestion if blocked, plus optional cached result
@@ -1372,11 +1456,108 @@ export class TaskExecutor {
1372
1456
  }
1373
1457
  }
1374
1458
 
1459
+ private stepRequiresImageVerification(step: PlanStep): boolean {
1460
+ const description = (step.description || '').toLowerCase();
1461
+ if (!description.includes('verify')) return false;
1462
+ return IMAGE_VERIFICATION_KEYWORDS.some((keyword) => description.includes(keyword));
1463
+ }
1464
+
1465
+ private hasNewImageFromGlobResult(result: any, since: number): boolean {
1466
+ const matches = result?.matches;
1467
+ if (!Array.isArray(matches)) return false;
1468
+
1469
+ const threshold = Math.max(0, since - IMAGE_VERIFICATION_TIME_SKEW_MS);
1470
+
1471
+ for (const match of matches) {
1472
+ const path = typeof match === 'string' ? match : match?.path;
1473
+ if (!path || !IMAGE_FILE_EXTENSION_REGEX.test(path)) continue;
1474
+
1475
+ const modified = typeof match === 'object' ? match?.modified : undefined;
1476
+ if (!modified) continue;
1477
+
1478
+ const modifiedTime = Date.parse(modified);
1479
+ if (!Number.isNaN(modifiedTime) && modifiedTime >= threshold) {
1480
+ return true;
1481
+ }
1482
+ }
1483
+
1484
+ return false;
1485
+ }
1486
+
1375
1487
  /**
1376
1488
  * Infer missing parameters for tool calls (helps weaker models)
1377
1489
  * This auto-fills parameters when the LLM fails to provide them but context is available
1378
1490
  */
1379
1491
  private inferMissingParameters(toolName: string, input: any): { input: any; modified: boolean; inference?: string } {
1492
+ if (toolName === 'create_document') {
1493
+ let modified = false;
1494
+ let inference = '';
1495
+ input = input || {};
1496
+
1497
+ if (!input.filename) {
1498
+ if (input.path) {
1499
+ input.filename = path.basename(String(input.path));
1500
+ modified = true;
1501
+ inference = 'Normalized path -> filename';
1502
+ } else if (input.name) {
1503
+ input.filename = String(input.name);
1504
+ modified = true;
1505
+ inference = 'Normalized name -> filename';
1506
+ }
1507
+ }
1508
+
1509
+ if (!input.format) {
1510
+ const ext = input.filename ? path.extname(String(input.filename)).toLowerCase() : '';
1511
+ if (ext === '.pdf') {
1512
+ input.format = 'pdf';
1513
+ modified = true;
1514
+ inference = `${inference ? `${inference}; ` : ''}Inferred format="pdf" from filename`;
1515
+ } else if (ext === '.docx') {
1516
+ input.format = 'docx';
1517
+ modified = true;
1518
+ inference = `${inference ? `${inference}; ` : ''}Inferred format="docx" from filename`;
1519
+ } else {
1520
+ input.format = 'docx';
1521
+ modified = true;
1522
+ inference = `${inference ? `${inference}; ` : ''}Defaulted format="docx"`;
1523
+ }
1524
+ }
1525
+
1526
+ if (!input.content) {
1527
+ const fallback = this.getContentFallback();
1528
+ if (fallback) {
1529
+ input.content = fallback;
1530
+ modified = true;
1531
+ inference = `${inference ? `${inference}; ` : ''}Inferred content from latest assistant output`;
1532
+ }
1533
+ }
1534
+
1535
+ return { input, modified, inference: modified ? inference : undefined };
1536
+ }
1537
+
1538
+ if (toolName === 'write_file') {
1539
+ let modified = false;
1540
+ let inference = '';
1541
+ input = input || {};
1542
+
1543
+ if (!input.path && input.filename) {
1544
+ input.path = String(input.filename);
1545
+ modified = true;
1546
+ inference = 'Normalized filename -> path';
1547
+ }
1548
+
1549
+ if (!input.content) {
1550
+ const fallback = this.getContentFallback();
1551
+ if (fallback) {
1552
+ input.content = fallback;
1553
+ modified = true;
1554
+ inference = `${inference ? `${inference}; ` : ''}Inferred content from latest assistant output`;
1555
+ }
1556
+ }
1557
+
1558
+ return { input, modified, inference: modified ? inference : undefined };
1559
+ }
1560
+
1380
1561
  // Handle edit_document - infer sourcePath from recently created documents
1381
1562
  if (toolName === 'edit_document') {
1382
1563
  let modified = false;
@@ -1458,9 +1639,101 @@ export class TaskExecutor {
1458
1639
  return { input, modified, inference: modified ? inference : undefined };
1459
1640
  }
1460
1641
 
1642
+ // Handle web_search - normalize region/country inputs
1643
+ if (toolName === 'web_search') {
1644
+ let modified = false;
1645
+ let inference = '';
1646
+
1647
+ if (!input?.region && input?.country && typeof input.country === 'string') {
1648
+ input.region = input.country;
1649
+ modified = true;
1650
+ inference = 'Normalized country -> region';
1651
+ }
1652
+
1653
+ if (input?.region && typeof input.region === 'string') {
1654
+ const raw = input.region.trim();
1655
+ const upper = raw.toUpperCase();
1656
+ let normalized = upper;
1657
+ if (upper === 'UK') normalized = 'GB';
1658
+ if (upper === 'USA') normalized = 'US';
1659
+ if (normalized !== raw) {
1660
+ input.region = normalized;
1661
+ modified = true;
1662
+ inference = `${inference ? `${inference}; ` : ''}Normalized region "${raw}" -> "${normalized}"`;
1663
+ }
1664
+ }
1665
+
1666
+ if (modified) {
1667
+ return { input, modified, inference };
1668
+ }
1669
+ }
1670
+
1461
1671
  return { input, modified: false };
1462
1672
  }
1463
1673
 
1674
+ private getContentFallback(): string | undefined {
1675
+ const candidates = [
1676
+ this.lastAssistantText,
1677
+ this.lastNonVerificationOutput,
1678
+ this.lastAssistantOutput,
1679
+ ];
1680
+ const placeholders = new Set([
1681
+ 'I understand. Let me continue.',
1682
+ ]);
1683
+ for (const candidate of candidates) {
1684
+ if (!candidate) continue;
1685
+ const trimmed = candidate.trim();
1686
+ if (trimmed.length < 20) continue;
1687
+ if (placeholders.has(trimmed)) continue;
1688
+ return trimmed;
1689
+ }
1690
+ return undefined;
1691
+ }
1692
+
1693
+ private getToolInputValidationError(toolName: string, input: any): string | null {
1694
+ if (toolName === 'create_document') {
1695
+ if (!input?.filename) return 'create_document requires a filename';
1696
+ if (!input?.format) return 'create_document requires a format (docx or pdf)';
1697
+ if (!input?.content) return 'create_document requires content';
1698
+ }
1699
+ if (toolName === 'write_file') {
1700
+ if (!input?.path) return 'write_file requires a path';
1701
+ if (!input?.content) return 'write_file requires content';
1702
+ }
1703
+ return null;
1704
+ }
1705
+
1706
+ private async handleCanvasPushFallback(content: LLMToolUse, assistantText: string): Promise<void> {
1707
+ if (content.name !== 'canvas_push') {
1708
+ return;
1709
+ }
1710
+
1711
+ const inputContent = content.input?.content;
1712
+ const hasContent = typeof inputContent === 'string' && inputContent.trim().length > 0;
1713
+ const filename = content.input?.filename;
1714
+ const isHtmlTarget = !filename || filename === 'index.html';
1715
+ if (hasContent || !isHtmlTarget) {
1716
+ return;
1717
+ }
1718
+
1719
+ const extracted = this.extractHtmlFromText(assistantText);
1720
+ const generated = extracted || await this.generateCanvasHtml(this.lastUserMessage || this.task.prompt);
1721
+ if (!generated) {
1722
+ return;
1723
+ }
1724
+
1725
+ content.input = {
1726
+ ...(content.input || {}),
1727
+ content: generated,
1728
+ };
1729
+ this.daemon.logEvent(this.task.id, 'parameter_inference', {
1730
+ tool: content.name,
1731
+ inference: extracted
1732
+ ? 'Recovered HTML from assistant text'
1733
+ : 'Auto-generated HTML from latest user request',
1734
+ });
1735
+ }
1736
+
1464
1737
  /**
1465
1738
  * Get available tools, filtering out disabled ones
1466
1739
  * This prevents the LLM from trying to use tools that have been disabled by the circuit breaker
@@ -1576,6 +1849,7 @@ export class TaskExecutor {
1576
1849
  this.systemPrompt = `You are an AI assistant helping with tasks. Use the available tools to complete the work.
1577
1850
  Current time: ${getCurrentDateTimeContext()}
1578
1851
  Workspace: ${this.workspace.path}
1852
+ Workspace is temporary: ${this.workspace.isTemp ? 'true' : 'false'}
1579
1853
  Always ask for approval before deleting files or making destructive changes.
1580
1854
  Be concise in your responses. When reading files, only read what you need.
1581
1855
 
@@ -1891,6 +2165,9 @@ You are continuing a previous conversation. The context from the previous conver
1891
2165
 
1892
2166
  // Reset tool failure tracker (tools might work on retry)
1893
2167
  this.toolFailureTracker = new ToolFailureTracker();
2168
+ this.toolResultMemory = [];
2169
+ this.lastAssistantOutput = null;
2170
+ this.lastNonVerificationOutput = null;
1894
2171
 
1895
2172
  // Add context for LLM about retry
1896
2173
  this.conversationHistory.push({
@@ -2145,6 +2422,391 @@ You are continuing a previous conversation. The context from the previous conver
2145
2422
  return { additionalContext: additionalContext || undefined, taskType };
2146
2423
  }
2147
2424
 
2425
+ private classifyWorkspaceNeed(prompt: string): 'none' | 'new_ok' | 'ambiguous' | 'needs_existing' {
2426
+ const text = prompt.toLowerCase();
2427
+
2428
+ const newProjectPatterns = [
2429
+ /from\s+scratch/i,
2430
+ /\bnew\s+project\b/i,
2431
+ /\bcreate\s+(?:a|an)\s+new\b/i,
2432
+ /\bstart\s+(?:a|an)\s+new\b/i,
2433
+ /\bscaffold\b/i,
2434
+ /\bbootstrap\b/i,
2435
+ /\binitialize\b/i,
2436
+ /\binit\b/i,
2437
+ /\bgreenfield\b/i,
2438
+ ];
2439
+
2440
+ const existingProjectPatterns = [
2441
+ /\bexisting\b/i,
2442
+ /\bcurrent\b/i,
2443
+ /\balready\b/i,
2444
+ /\bin\s+(?:this|the)\s+(?:repo|repository|project|codebase)\b/i,
2445
+ /\bfix\b/i,
2446
+ /\bbug\b/i,
2447
+ /\bdebug\b/i,
2448
+ /\brefactor\b/i,
2449
+ /\bupdate\b/i,
2450
+ /\bmodify\b/i,
2451
+ // Note: 'add' is intentionally omitted - it's ambiguous (could be new or existing)
2452
+ /\bextend\b/i,
2453
+ /\bmigrate\b/i,
2454
+ /\bpatch\b/i,
2455
+ ];
2456
+
2457
+ const pathOrFilePatterns = [
2458
+ /(?:^|[\s/\\])[\w.\-\/\\]+?\.(ts|tsx|js|jsx|py|rs|go|java|kt|swift|json|yml|yaml|toml|md|sol|c|cpp|h|hpp)\b/i,
2459
+ /\b(?:src|app|apps|packages|programs|frontend|backend|server|client|contracts|lib|services)\//i,
2460
+ ];
2461
+
2462
+ const codeTaskPatterns = [
2463
+ /\bapp\b/i,
2464
+ /\bdapp\b/i,
2465
+ /\bweb\b/i,
2466
+ /\bfrontend\b/i,
2467
+ /\bbackend\b/i,
2468
+ /\bapi\b/i,
2469
+ /\bservice\b/i,
2470
+ /\bprogram\b/i,
2471
+ /\bsmart\s+contract\b/i,
2472
+ /\bcontract\b/i,
2473
+ /\bblockchain\b/i,
2474
+ /\bsolana\b/i,
2475
+ /\breact\b/i,
2476
+ /\bnode\b/i,
2477
+ /\btypescript\b/i,
2478
+ /\bjavascript\b/i,
2479
+ /\bpython\b/i,
2480
+ /\brust\b/i,
2481
+ /\bgo\b/i,
2482
+ /\bjava\b/i,
2483
+ /\bkotlin\b/i,
2484
+ /\bswift\b/i,
2485
+ /\bdatabase\b/i,
2486
+ /\bschema\b/i,
2487
+ /\bmigration\b/i,
2488
+ /\brepo\b/i,
2489
+ /\brepository\b/i,
2490
+ /\bcodebase\b/i,
2491
+ ];
2492
+
2493
+ const mentionsNew = newProjectPatterns.some(pattern => pattern.test(text));
2494
+ const isCodeTask = codeTaskPatterns.some(pattern => pattern.test(text));
2495
+ const mentionsExisting = pathOrFilePatterns.some(pattern => pattern.test(text)) ||
2496
+ (existingProjectPatterns.some(pattern => pattern.test(text)) && isCodeTask);
2497
+
2498
+ if (mentionsExisting) return 'needs_existing';
2499
+ if (mentionsNew) return 'new_ok';
2500
+ if (isCodeTask) return 'ambiguous';
2501
+ return 'none';
2502
+ }
2503
+
2504
+ private getWorkspaceSignals(): { hasProjectMarkers: boolean; hasCodeFiles: boolean; hasAppDirs: boolean } {
2505
+ const projectMarkers = new Set([
2506
+ 'package.json',
2507
+ 'pnpm-lock.yaml',
2508
+ 'yarn.lock',
2509
+ 'package-lock.json',
2510
+ 'Cargo.toml',
2511
+ 'Anchor.toml',
2512
+ 'pyproject.toml',
2513
+ 'requirements.txt',
2514
+ 'go.mod',
2515
+ 'pom.xml',
2516
+ 'build.gradle',
2517
+ 'settings.gradle',
2518
+ 'Gemfile',
2519
+ 'composer.json',
2520
+ 'mix.exs',
2521
+ 'Makefile',
2522
+ 'CMakeLists.txt',
2523
+ ]);
2524
+
2525
+ const codeExtensions = new Set([
2526
+ '.ts', '.tsx', '.js', '.jsx', '.py', '.rs', '.go', '.java', '.kt', '.swift',
2527
+ '.cs', '.cpp', '.c', '.h', '.hpp', '.sol',
2528
+ ]);
2529
+
2530
+ const appDirs = new Set([
2531
+ 'src', 'app', 'apps', 'packages', 'programs', 'frontend', 'backend',
2532
+ 'server', 'client', 'contracts', 'lib', 'services', 'web', 'api',
2533
+ ]);
2534
+
2535
+ try {
2536
+ const entries = fs.readdirSync(this.workspace.path, { withFileTypes: true });
2537
+ let hasProjectMarkers = false;
2538
+ let hasCodeFiles = false;
2539
+ let hasAppDirs = false;
2540
+
2541
+ for (const entry of entries) {
2542
+ if (entry.isFile()) {
2543
+ if (projectMarkers.has(entry.name)) {
2544
+ hasProjectMarkers = true;
2545
+ }
2546
+ const ext = path.extname(entry.name).toLowerCase();
2547
+ if (codeExtensions.has(ext)) {
2548
+ hasCodeFiles = true;
2549
+ }
2550
+ } else if (entry.isDirectory()) {
2551
+ if (appDirs.has(entry.name)) {
2552
+ hasAppDirs = true;
2553
+ }
2554
+ }
2555
+
2556
+ if (hasProjectMarkers && hasCodeFiles && hasAppDirs) break;
2557
+ }
2558
+
2559
+ return { hasProjectMarkers, hasCodeFiles, hasAppDirs };
2560
+ } catch {
2561
+ return { hasProjectMarkers: false, hasCodeFiles: false, hasAppDirs: false };
2562
+ }
2563
+ }
2564
+
2565
+ private pauseForUserInput(message: string, reason: string): void {
2566
+ this.waitingForUserInput = true;
2567
+ this.daemon.updateTaskStatus(this.task.id, 'paused');
2568
+ this.daemon.logEvent(this.task.id, 'assistant_message', { message });
2569
+ this.daemon.logEvent(this.task.id, 'task_paused', { message, reason });
2570
+ this.daemon.logEvent(this.task.id, 'progress_update', {
2571
+ phase: 'execution',
2572
+ completedSteps: this.plan?.steps.filter(s => s.status === 'completed').length ?? 0,
2573
+ totalSteps: this.plan?.steps.length ?? 0,
2574
+ progress: 0,
2575
+ message: 'Paused - awaiting user input',
2576
+ });
2577
+
2578
+ if (this.conversationHistory.length === 0) {
2579
+ this.conversationHistory.push({
2580
+ role: 'user',
2581
+ content: this.task.prompt,
2582
+ });
2583
+ }
2584
+
2585
+ this.conversationHistory.push({
2586
+ role: 'assistant',
2587
+ content: [{ type: 'text', text: message }],
2588
+ });
2589
+ this.saveConversationSnapshot();
2590
+ }
2591
+
2592
+ private preflightWorkspaceCheck(): boolean {
2593
+ const workspaceNeed = this.classifyWorkspaceNeed(this.task.prompt);
2594
+ if (workspaceNeed === 'none') return false;
2595
+
2596
+ const signals = this.getWorkspaceSignals();
2597
+ const looksLikeProject = signals.hasProjectMarkers || signals.hasCodeFiles || signals.hasAppDirs;
2598
+ const isTemp = this.workspace.isTemp || this.workspace.id === TEMP_WORKSPACE_ID;
2599
+
2600
+ if (isTemp && !looksLikeProject) {
2601
+ if (workspaceNeed === 'needs_existing') {
2602
+ this.pauseForUserInput(
2603
+ 'I am in the temporary workspace, but this task looks like it targets an existing project. ' +
2604
+ 'Please select the project folder or provide its path so I can switch to it. ' +
2605
+ 'If you want a new project created here instead, say so.',
2606
+ 'workspace_required'
2607
+ );
2608
+ return true;
2609
+ }
2610
+
2611
+ if (workspaceNeed === 'ambiguous') {
2612
+ this.pauseForUserInput(
2613
+ 'I am in the temporary workspace and this task could be a new project or changes to an existing one. ' +
2614
+ 'Choose one:\n' +
2615
+ '1. Create a new project in the temporary workspace\n' +
2616
+ '2. Switch to an existing project folder (share the path or select a workspace)',
2617
+ 'workspace_selection'
2618
+ );
2619
+ return true;
2620
+ }
2621
+ }
2622
+
2623
+ if (!isTemp && workspaceNeed === 'needs_existing' && !looksLikeProject) {
2624
+ this.pauseForUserInput(
2625
+ 'I am in the selected workspace, but I do not see typical project files here. ' +
2626
+ 'If this task targets an existing project, please confirm the correct folder or provide its path. ' +
2627
+ 'If this is a new project, tell me to scaffold it here.',
2628
+ 'workspace_mismatch'
2629
+ );
2630
+ return true;
2631
+ }
2632
+
2633
+ return false;
2634
+ }
2635
+
2636
+ private summarizeToolResult(toolName: string, result: any): string | null {
2637
+ if (!result) return null;
2638
+
2639
+ if (toolName === 'web_search') {
2640
+ const query = typeof result.query === 'string' ? result.query : '';
2641
+ const items = Array.isArray(result.results) ? result.results : [];
2642
+ if (items.length === 0) {
2643
+ return query ? `query "${query}": no results` : 'no results';
2644
+ }
2645
+ const formatted = items.slice(0, 5).map((item: any) => {
2646
+ const title = item?.title ? String(item.title).trim() : 'Untitled';
2647
+ const url = item?.url ? String(item.url) : '';
2648
+ let host = '';
2649
+ if (url) {
2650
+ try {
2651
+ host = new URL(url).hostname.replace(/^www\./, '');
2652
+ } catch {
2653
+ host = '';
2654
+ }
2655
+ }
2656
+ return host ? `${title} (${host})` : title;
2657
+ });
2658
+ const prefix = query ? `query "${query}": ` : '';
2659
+ return `${prefix}${formatted.join(' | ')}`;
2660
+ }
2661
+
2662
+ if (toolName === 'web_fetch') {
2663
+ const url = typeof result.url === 'string' ? result.url : '';
2664
+ const content = typeof result.content === 'string' ? result.content : '';
2665
+ const snippet = content
2666
+ ? content.replace(/\s+/g, ' ').slice(0, 300)
2667
+ : '';
2668
+ if (url && snippet) return `${url} — ${snippet}`;
2669
+ if (url) return url;
2670
+ if (snippet) return snippet;
2671
+ return null;
2672
+ }
2673
+
2674
+ if (toolName === 'search_files') {
2675
+ const totalFound = typeof result.totalFound === 'number' ? result.totalFound : undefined;
2676
+ if (totalFound !== undefined) return `matches found: ${totalFound}`;
2677
+ }
2678
+
2679
+ if (toolName === 'glob') {
2680
+ const totalMatches = typeof result.totalMatches === 'number' ? result.totalMatches : undefined;
2681
+ const pattern = typeof result.pattern === 'string' ? result.pattern : '';
2682
+ if (totalMatches !== undefined) {
2683
+ return pattern ? `pattern "${pattern}" matched ${totalMatches} item(s)` : `matched ${totalMatches} item(s)`;
2684
+ }
2685
+ }
2686
+
2687
+ return null;
2688
+ }
2689
+
2690
+ private recordToolResult(toolName: string, result: any): void {
2691
+ const summary = this.summarizeToolResult(toolName, result);
2692
+ if (!summary) return;
2693
+ this.toolResultMemory.push({ tool: toolName, summary, timestamp: Date.now() });
2694
+ if (this.toolResultMemory.length > this.toolResultMemoryLimit) {
2695
+ this.toolResultMemory.splice(0, this.toolResultMemory.length - this.toolResultMemoryLimit);
2696
+ }
2697
+ }
2698
+
2699
+ private getRecentToolResultSummary(maxEntries = 6): string {
2700
+ if (this.toolResultMemory.length === 0) return '';
2701
+ const entries = this.toolResultMemory.slice(-maxEntries);
2702
+ return entries.map(entry => `- ${entry.tool}: ${entry.summary}`).join('\n');
2703
+ }
2704
+
2705
+ private isVerificationStep(step: PlanStep): boolean {
2706
+ const desc = step.description.toLowerCase().trim();
2707
+ if (desc.startsWith('verify')) return true;
2708
+ if (desc.startsWith('review')) return true;
2709
+ return desc.includes('verify:') || desc.includes('verification') || desc.includes('verify ');
2710
+ }
2711
+
2712
+ private isSummaryStep(step: PlanStep): boolean {
2713
+ const desc = step.description.toLowerCase();
2714
+ return desc.includes('summary') || desc.includes('summarize') || desc.includes('compile') || desc.includes('report');
2715
+ }
2716
+
2717
+ private isLastPlanStep(step: PlanStep): boolean {
2718
+ if (!this.plan || this.plan.steps.length === 0) return false;
2719
+ const last = this.plan.steps[this.plan.steps.length - 1];
2720
+ return last?.id === step.id;
2721
+ }
2722
+
2723
+ private taskLikelyNeedsWebEvidence(): boolean {
2724
+ const prompt = `${this.task.title}\n${this.task.prompt}`.toLowerCase();
2725
+ const signals = [
2726
+ 'news',
2727
+ 'latest',
2728
+ 'today',
2729
+ 'trending',
2730
+ 'breaking',
2731
+ 'reddit',
2732
+ 'search',
2733
+ 'headline',
2734
+ 'current events',
2735
+ ];
2736
+ return signals.some(signal => prompt.includes(signal));
2737
+ }
2738
+
2739
+ private taskRequiresTodayContext(): boolean {
2740
+ const prompt = `${this.task.title}\n${this.task.prompt}`.toLowerCase();
2741
+ return prompt.includes('today');
2742
+ }
2743
+
2744
+ private hasWebEvidence(): boolean {
2745
+ return this.toolResultMemory.some(entry =>
2746
+ entry.tool === 'web_search' || entry.tool === 'web_fetch'
2747
+ );
2748
+ }
2749
+
2750
+ private normalizeToolName(name: string): { name: string; modified: boolean; original: string } {
2751
+ if (!name) return { name, modified: false, original: name };
2752
+ if (!name.includes('.')) return { name, modified: false, original: name };
2753
+ const [prefix, ...rest] = name.split('.');
2754
+ if (rest.length === 0) return { name, modified: false, original: name };
2755
+ if (['functions', 'tool', 'tools'].includes(prefix)) {
2756
+ const normalized = rest.join('.');
2757
+ return { name: normalized, modified: normalized !== name, original: name };
2758
+ }
2759
+ return { name, modified: false, original: name };
2760
+ }
2761
+
2762
+ private recordAssistantOutput(messages: LLMMessage[], step: PlanStep): void {
2763
+ if (!messages || messages.length === 0) return;
2764
+ const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant');
2765
+ if (!lastAssistant || !lastAssistant.content) return;
2766
+ const text = (Array.isArray(lastAssistant.content) ? lastAssistant.content : [])
2767
+ .filter((item: any) => item.type === 'text' && item.text)
2768
+ .map((item: any) => String(item.text))
2769
+ .join('\n')
2770
+ .trim();
2771
+ if (!text) return;
2772
+ const truncated = text.length > 1500 ? `${text.slice(0, 1500)}…` : text;
2773
+ if (!this.isVerificationStep(step)) {
2774
+ this.lastAssistantOutput = truncated;
2775
+ this.lastNonVerificationOutput = truncated;
2776
+ } else {
2777
+ if (!this.lastAssistantOutput) {
2778
+ this.lastAssistantOutput = truncated;
2779
+ }
2780
+ // Preserve lastNonVerificationOutput for future steps/follow-ups.
2781
+ }
2782
+ }
2783
+
2784
+ private isTransientProviderError(error: any): boolean {
2785
+ if (!error) return false;
2786
+ const message = String(error.message || '').toLowerCase();
2787
+ const code = error.cause?.code || error.code;
2788
+ const retryableCodes = new Set(['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED']);
2789
+ if (code && retryableCodes.has(code)) return true;
2790
+ return (
2791
+ message.includes('fetch failed') ||
2792
+ message.includes('network') ||
2793
+ message.includes('timeout') ||
2794
+ message.includes('socket hang up')
2795
+ );
2796
+ }
2797
+
2798
+ private async dispatchMentionedAgentsAfterPlanning(): Promise<void> {
2799
+ if (this.dispatchedMentionedAgents) return;
2800
+ if (!this.shouldPauseForQuestions) return;
2801
+ if (!this.plan) return;
2802
+ try {
2803
+ await this.daemon.dispatchMentionedAgents(this.task.id, this.plan);
2804
+ this.dispatchedMentionedAgents = true;
2805
+ } catch (error) {
2806
+ console.warn('[TaskExecutor] Failed to dispatch mentioned agents:', error);
2807
+ }
2808
+ }
2809
+
2148
2810
  /**
2149
2811
  * Main execution loop
2150
2812
  */
@@ -2181,6 +2843,8 @@ You are continuing a previous conversation. The context from the previous conver
2181
2843
  this.daemon.updateTaskStatus(this.task.id, 'planning');
2182
2844
  await this.createPlan();
2183
2845
 
2846
+ await this.dispatchMentionedAgentsAfterPlanning();
2847
+
2184
2848
  if (this.cancelled) return;
2185
2849
 
2186
2850
  // Phase 2: Execution with Goal Mode retry loop
@@ -2260,6 +2924,13 @@ You are continuing a previous conversation. The context from the previous conver
2260
2924
  return;
2261
2925
  }
2262
2926
 
2927
+ if (this.isTransientProviderError(error)) {
2928
+ const scheduled = this.daemon.handleTransientTaskFailure(this.task.id, error.message || 'Transient LLM error');
2929
+ if (scheduled) {
2930
+ return;
2931
+ }
2932
+ }
2933
+
2263
2934
  console.error(`Task execution failed:`, error);
2264
2935
  // Save conversation snapshot even on failure for potential recovery
2265
2936
  this.saveConversationSnapshot();
@@ -2295,6 +2966,7 @@ You are continuing a previous conversation. The context from the previous conver
2295
2966
 
2296
2967
  Current time: ${getCurrentDateTimeContext()}
2297
2968
  You have access to a workspace folder at: ${this.workspace.path}
2969
+ Workspace is temporary: ${this.workspace.isTemp ? 'true' : 'false'}
2298
2970
  Workspace permissions: ${JSON.stringify(this.workspace.permissions)}
2299
2971
 
2300
2972
  Available tools:
@@ -2307,6 +2979,12 @@ PLANNING RULES:
2307
2979
  - DO NOT plan to create multiple versions of files - pick ONE target file.
2308
2980
  - DO NOT plan to read the same file multiple times in different steps.
2309
2981
 
2982
+ WORKSPACE MODE (CRITICAL):
2983
+ - There are two modes: temporary workspace (no user-selected folder) and user-selected workspace.
2984
+ - If the workspace is temporary and the task likely targets an existing project, your FIRST step must be to ask for the correct folder or to switch workspaces.
2985
+ - If the task could be new or existing, ask the user to choose between scaffolding here or switching to an existing folder.
2986
+ - Do NOT assume a repo exists in the temporary workspace unless you find it.
2987
+
2310
2988
  PATH DISCOVERY (CRITICAL):
2311
2989
  - When users mention a folder or path (e.g., "electron/agent folder"), they may give a PARTIAL path, not the full path.
2312
2990
  - NEVER assume a path doesn't exist just because it's not in your workspace root.
@@ -2346,6 +3024,11 @@ WEB RESEARCH & CONTENT EXTRACTION (IMPORTANT):
2346
3024
  - NEVER create a plan that says "cannot be done" if alternative tools are available.
2347
3025
  - NEVER plan to ask the user for content you can extract yourself.
2348
3026
 
3027
+ REDDIT POSTS (WHEN UPVOTE COUNTS REQUIRED):
3028
+ - Prefer web_fetch against Reddit's JSON endpoints to get reliable titles and upvote counts.
3029
+ - Example: https://www.reddit.com/r/<sub>/top/.json?t=day&limit=5
3030
+ - Use web_search only to discover the right subreddit if needed, not for score counts.
3031
+
2349
3032
  TOOL SELECTION GUIDE (web tools):
2350
3033
  - web_search: Best for research, news, finding information, exploring topics (PREFERRED for most research)
2351
3034
  - web_fetch: Best for reading a specific known URL without interaction
@@ -2563,23 +3246,26 @@ Format your plan as a JSON object with this structure:
2563
3246
  throw new Error('No plan available');
2564
3247
  }
2565
3248
 
2566
- const totalSteps = this.plan.steps.length;
2567
- let completedSteps = 0;
3249
+ if (this.preflightWorkspaceCheck()) {
3250
+ return;
3251
+ }
2568
3252
 
2569
3253
  // Emit initial progress event
2570
3254
  this.daemon.logEvent(this.task.id, 'progress_update', {
2571
3255
  phase: 'execution',
2572
- completedSteps,
2573
- totalSteps,
3256
+ completedSteps: this.plan.steps.filter(s => s.status === 'completed').length,
3257
+ totalSteps: this.plan.steps.length,
2574
3258
  progress: 0,
2575
- message: `Starting execution of ${totalSteps} steps`,
3259
+ message: `Starting execution of ${this.plan.steps.length} steps`,
2576
3260
  });
2577
3261
 
2578
- for (const step of this.plan.steps) {
3262
+ let index = 0;
3263
+ while (index < this.plan.steps.length) {
3264
+ const step = this.plan.steps[index];
2579
3265
  if (this.cancelled) break;
2580
3266
 
2581
3267
  if (step.status === 'completed') {
2582
- completedSteps++;
3268
+ index++;
2583
3269
  continue;
2584
3270
  }
2585
3271
 
@@ -2588,6 +3274,9 @@ Format your plan as a JSON object with this structure:
2588
3274
  await new Promise(resolve => setTimeout(resolve, 100));
2589
3275
  }
2590
3276
 
3277
+ const completedSteps = this.plan.steps.filter(s => s.status === 'completed').length;
3278
+ const totalSteps = this.plan.steps.length;
3279
+
2591
3280
  // Emit step starting progress
2592
3281
  this.daemon.logEvent(this.task.id, 'progress_update', {
2593
3282
  phase: 'execution',
@@ -2645,22 +3334,34 @@ Format your plan as a JSON object with this structure:
2645
3334
  message: `Step timed out after ${STEP_TIMEOUT_MS / 1000}s`,
2646
3335
  });
2647
3336
  // Continue with next step instead of failing entire task
2648
- completedSteps++;
3337
+ const updatedIndex = this.plan.steps.findIndex(s => s.id === step.id);
3338
+ if (updatedIndex === -1) {
3339
+ index = Math.min(index + 1, this.plan.steps.length);
3340
+ } else {
3341
+ index = updatedIndex + 1;
3342
+ }
2649
3343
  continue;
2650
3344
  }
2651
3345
  throw error;
2652
3346
  }
2653
3347
 
2654
- completedSteps++;
3348
+ const updatedIndex = this.plan.steps.findIndex(s => s.id === step.id);
3349
+ if (updatedIndex === -1) {
3350
+ index = Math.min(index + 1, this.plan.steps.length);
3351
+ } else {
3352
+ index = updatedIndex + 1;
3353
+ }
3354
+ const completedAfterStep = this.plan.steps.filter(s => s.status === 'completed').length;
3355
+ const totalAfterStep = this.plan.steps.length;
2655
3356
 
2656
3357
  // Emit step completed progress
2657
3358
  this.daemon.logEvent(this.task.id, 'progress_update', {
2658
3359
  phase: 'execution',
2659
3360
  currentStep: step.id,
2660
- completedSteps,
2661
- totalSteps,
2662
- progress: Math.round((completedSteps / totalSteps) * 100),
2663
- message: `Completed step ${completedSteps}/${totalSteps}`,
3361
+ completedSteps: completedAfterStep,
3362
+ totalSteps: totalAfterStep,
3363
+ progress: totalAfterStep > 0 ? Math.round((completedAfterStep / totalAfterStep) * 100) : 100,
3364
+ message: `Completed step ${step.id}: ${step.description}`,
2664
3365
  });
2665
3366
  }
2666
3367
 
@@ -2676,11 +3377,13 @@ Format your plan as a JSON object with this structure:
2676
3377
  // If critical steps failed (not just verification), this should be marked
2677
3378
  const criticalFailures = failedSteps.filter(s => !s.description.toLowerCase().includes('verify'));
2678
3379
  if (criticalFailures.length > 0) {
3380
+ const totalSteps = this.plan.steps.length;
3381
+ const progress = totalSteps > 0 ? Math.round((successfulSteps.length / totalSteps) * 100) : 0;
2679
3382
  this.daemon.logEvent(this.task.id, 'progress_update', {
2680
3383
  phase: 'execution',
2681
3384
  completedSteps: successfulSteps.length,
2682
3385
  totalSteps,
2683
- progress: Math.round((successfulSteps.length / totalSteps) * 100),
3386
+ progress,
2684
3387
  message: `Completed with ${criticalFailures.length} failed step(s)`,
2685
3388
  hasFailures: true,
2686
3389
  });
@@ -2692,8 +3395,8 @@ Format your plan as a JSON object with this structure:
2692
3395
  // Emit completion progress (only if no critical failures)
2693
3396
  this.daemon.logEvent(this.task.id, 'progress_update', {
2694
3397
  phase: 'execution',
2695
- completedSteps,
2696
- totalSteps,
3398
+ completedSteps: successfulSteps.length,
3399
+ totalSteps: this.plan.steps.length,
2697
3400
  progress: 100,
2698
3401
  message: 'All steps completed',
2699
3402
  });
@@ -2761,6 +3464,11 @@ IMPORTANT INSTRUCTIONS:
2761
3464
  - The delete_file tool has a built-in approval mechanism that will prompt the user. Just call the tool directly.
2762
3465
  - Do NOT ask "Should I proceed?" or wait for permission in text - the tools handle approvals automatically.
2763
3466
 
3467
+ USER INPUT GATE (CRITICAL):
3468
+ - If you ask the user for required information or a decision, STOP and wait.
3469
+ - Do NOT continue executing steps or call tools after asking such questions.
3470
+ - If safe defaults exist, state the assumption and proceed without asking.
3471
+
2764
3472
  PATH DISCOVERY (CRITICAL):
2765
3473
  - When a task mentions a folder or path (e.g., "electron/agent folder"), users often give PARTIAL paths.
2766
3474
  - NEVER conclude a path doesn't exist without SEARCHING for it first.
@@ -2850,6 +3558,11 @@ RESEARCH WORKFLOW:
2850
3558
  - Only fall back to browser_navigate if web_fetch fails (e.g., JavaScript-required content)
2851
3559
  - Many sites (X/Twitter, Reddit logged-in content, LinkedIn) require authentication - web_search can still find public discussions
2852
3560
 
3561
+ REDDIT POSTS (WHEN UPVOTE COUNTS REQUIRED):
3562
+ - Prefer web_fetch against Reddit's JSON endpoints to get reliable titles and upvote counts.
3563
+ - Example: https://www.reddit.com/r/<sub>/top/.json?t=day&limit=5
3564
+ - Use web_search only to discover the right subreddit if needed, not for score counts.
3565
+
2853
3566
  BROWSER TOOLS (when needed):
2854
3567
  - Treat browser_navigate + browser_get_content as ONE ATOMIC OPERATION
2855
3568
  - For dynamic content, use browser_wait then browser_get_content
@@ -2908,12 +3621,48 @@ SCHEDULING & REMINDERS:
2908
3621
  stepContext += `\n\nDo NOT repeat work from previous steps. Focus only on: ${step.description}`;
2909
3622
  }
2910
3623
 
3624
+ const isVerifyStep = this.isVerificationStep(step);
3625
+ const isSummaryStep = this.isSummaryStep(step);
3626
+ const isLastStep = this.isLastPlanStep(step);
3627
+
2911
3628
  // Add accumulated knowledge from previous steps (discovered files, directories, etc.)
2912
3629
  const knowledgeSummary = this.fileOperationTracker.getKnowledgeSummary();
2913
3630
  if (knowledgeSummary) {
2914
3631
  stepContext += `\n\nKNOWLEDGE FROM PREVIOUS STEPS (use this instead of re-reading/re-listing):\n${knowledgeSummary}`;
2915
3632
  }
2916
3633
 
3634
+ const toolResultSummary = this.getRecentToolResultSummary();
3635
+ if (toolResultSummary) {
3636
+ stepContext += `\n\nRECENT TOOL RESULTS (from previous steps; do not look in the filesystem for these):\n${toolResultSummary}`;
3637
+ }
3638
+
3639
+ const shouldIncludePreviousOutput = !isVerifyStep || !this.lastNonVerificationOutput;
3640
+ if (this.lastAssistantOutput && shouldIncludePreviousOutput) {
3641
+ stepContext += `\n\nPREVIOUS STEP OUTPUT:\n${this.lastAssistantOutput}`;
3642
+ }
3643
+
3644
+ if (isVerifyStep) {
3645
+ stepContext += `\n\nVERIFICATION MODE:\n- This is a verification step. Keep the response brief (1-3 sentences).\n- Do NOT output a checklist. Do NOT restate the full deliverable.\n- If the deliverable has NOT been provided earlier, provide it now, then add a one-sentence verification note.\n`;
3646
+ if (isLastStep) {
3647
+ stepContext += `- This is the FINAL step. Include a very short recap (2-4 sentences) of the deliverable before the verification note so the last message still answers the user.\n`;
3648
+ }
3649
+ if (this.lastNonVerificationOutput) {
3650
+ stepContext += `\n\nMOST RECENT DELIVERABLE (use this for verification):\n${this.lastNonVerificationOutput}`;
3651
+ } else if (this.lastAssistantOutput) {
3652
+ stepContext += `\n\nMOST RECENT DELIVERABLE (use this for verification):\n${this.lastAssistantOutput}`;
3653
+ }
3654
+ }
3655
+
3656
+ if (isSummaryStep) {
3657
+ stepContext += `\n\nDELIVERABLE RULES:\n- If you write a file, you MUST also provide the key summary in your response.\n- Do not defer the answer to a verification step.\n`;
3658
+ if (this.taskLikelyNeedsWebEvidence() && !this.hasWebEvidence()) {
3659
+ stepContext += `\n\nEVIDENCE REQUIRED:\n- No web evidence has been gathered yet. Use web_search/web_fetch now before summarizing.\n- If you find no results, say so explicitly instead of guessing.\n`;
3660
+ }
3661
+ if (this.taskRequiresTodayContext()) {
3662
+ stepContext += `\n\nDATE REQUIREMENT:\n- This task explicitly asks for “today.” Only present items as “today” if you can confirm the date from sources.\n- If you cannot confirm any items from today, state that clearly, then optionally list the most recent items as “recent (not today)”.\n`;
3663
+ }
3664
+ }
3665
+
2917
3666
  // Start fresh messages for this step
2918
3667
  let messages: LLMMessage[] = [
2919
3668
  {
@@ -2929,8 +3678,18 @@ SCHEDULING & REMINDERS:
2929
3678
  let lastFailureReason = ''; // Track the reason for failure
2930
3679
  let hadToolError = false;
2931
3680
  let hadToolSuccessAfterError = false;
3681
+ let hadAnyToolSuccess = false;
3682
+ const toolErrors = new Set<string>();
2932
3683
  let lastToolErrorReason = '';
2933
3684
  let awaitingUserInput = false;
3685
+ let hadRunCommandFailure = false;
3686
+ let hadToolSuccessAfterRunCommandFailure = false;
3687
+ const expectsImageVerification = this.stepRequiresImageVerification(step);
3688
+ const imageVerificationSince =
3689
+ typeof this.task.createdAt === 'number'
3690
+ ? this.task.createdAt
3691
+ : (step.startedAt ?? Date.now());
3692
+ let foundNewImage = false;
2934
3693
  const maxIterations = 5; // Reduced from 10 to prevent excessive iterations per step
2935
3694
  const maxEmptyResponses = 3;
2936
3695
 
@@ -2954,6 +3713,8 @@ SCHEDULING & REMINDERS:
2954
3713
  // Compact messages if context is getting too large
2955
3714
  messages = this.contextManager.compactMessages(messages, systemPromptTokens);
2956
3715
 
3716
+ const availableTools = this.getAvailableTools();
3717
+
2957
3718
  // Use retry wrapper for resilient API calls
2958
3719
  const response = await this.callLLMWithRetry(
2959
3720
  () => withTimeout(
@@ -2961,7 +3722,7 @@ SCHEDULING & REMINDERS:
2961
3722
  model: this.modelId,
2962
3723
  maxTokens: 4096,
2963
3724
  system: this.systemPrompt,
2964
- tools: this.getAvailableTools(),
3725
+ tools: availableTools,
2965
3726
  messages,
2966
3727
  signal: this.abortController.signal,
2967
3728
  }),
@@ -2984,6 +3745,16 @@ SCHEDULING & REMINDERS:
2984
3745
 
2985
3746
  // Log any text responses from the assistant and check if asking a question
2986
3747
  let assistantAskedQuestion = false;
3748
+ const assistantText = (response.content || [])
3749
+ .filter((item: any) => item.type === 'text' && item.text)
3750
+ .map((item: any) => item.text)
3751
+ .join('\n');
3752
+ if (assistantText && assistantText.trim().length > 0) {
3753
+ this.lastAssistantText = assistantText.trim();
3754
+ }
3755
+ if (assistantText && assistantText.trim().length > 0) {
3756
+ this.lastAssistantText = assistantText.trim();
3757
+ }
2987
3758
  if (response.content) {
2988
3759
  for (const content of response.content) {
2989
3760
  if (content.type === 'text' && content.text) {
@@ -3031,9 +3802,21 @@ SCHEDULING & REMINDERS:
3031
3802
  const toolResults: LLMToolResult[] = [];
3032
3803
  let hasDisabledToolAttempt = false;
3033
3804
  let hasDuplicateToolAttempt = false;
3805
+ let hasUnavailableToolAttempt = false;
3806
+ const availableToolNames = new Set(availableTools.map(tool => tool.name));
3034
3807
 
3035
3808
  for (const content of response.content || []) {
3036
3809
  if (content.type === 'tool_use') {
3810
+ // Normalize tool names like "functions.web_fetch" -> "web_fetch"
3811
+ const normalizedTool = this.normalizeToolName(content.name);
3812
+ if (normalizedTool.modified) {
3813
+ this.daemon.logEvent(this.task.id, 'parameter_inference', {
3814
+ tool: content.name,
3815
+ inference: `Normalized tool name "${normalizedTool.original}" -> "${normalizedTool.name}"`,
3816
+ });
3817
+ content.name = normalizedTool.name;
3818
+ }
3819
+
3037
3820
  // Check if this tool is disabled (circuit breaker tripped)
3038
3821
  if (this.toolFailureTracker.isDisabled(content.name)) {
3039
3822
  const lastError = this.toolFailureTracker.getLastError(content.name);
@@ -3056,6 +3839,60 @@ SCHEDULING & REMINDERS:
3056
3839
  continue;
3057
3840
  }
3058
3841
 
3842
+ // Validate tool availability before attempting any inference
3843
+ if (!availableToolNames.has(content.name)) {
3844
+ console.log(`[TaskExecutor] Tool not available in this context: ${content.name}`);
3845
+ this.daemon.logEvent(this.task.id, 'tool_error', {
3846
+ tool: content.name,
3847
+ error: 'Tool not available in current context or permissions',
3848
+ blocked: true,
3849
+ });
3850
+ toolResults.push({
3851
+ type: 'tool_result',
3852
+ tool_use_id: content.id,
3853
+ content: JSON.stringify({
3854
+ error: `Tool "${content.name}" is not available in this context. Please choose a different tool or check permissions/integrations.`,
3855
+ unavailable: true,
3856
+ }),
3857
+ is_error: true,
3858
+ });
3859
+ hasUnavailableToolAttempt = true;
3860
+ continue;
3861
+ }
3862
+
3863
+ // Infer missing parameters for weaker models (normalize inputs before deduplication)
3864
+ const inference = this.inferMissingParameters(content.name, content.input);
3865
+ if (inference.modified) {
3866
+ content.input = inference.input;
3867
+ this.daemon.logEvent(this.task.id, 'parameter_inference', {
3868
+ tool: content.name,
3869
+ inference: inference.inference,
3870
+ });
3871
+ }
3872
+
3873
+ // If canvas_push is missing content, try extracting HTML from assistant text or auto-generate
3874
+ await this.handleCanvasPushFallback(content, assistantText);
3875
+
3876
+ const validationError = this.getToolInputValidationError(content.name, content.input);
3877
+ if (validationError) {
3878
+ this.daemon.logEvent(this.task.id, 'tool_warning', {
3879
+ tool: content.name,
3880
+ error: validationError,
3881
+ input: content.input,
3882
+ });
3883
+ toolResults.push({
3884
+ type: 'tool_result',
3885
+ tool_use_id: content.id,
3886
+ content: JSON.stringify({
3887
+ error: validationError,
3888
+ suggestion: 'Include all required fields in the tool call (e.g., content for create_document/write_file).',
3889
+ invalid_input: true,
3890
+ }),
3891
+ is_error: true,
3892
+ });
3893
+ continue;
3894
+ }
3895
+
3059
3896
  // Check for duplicate tool calls (prevents stuck loops)
3060
3897
  const duplicateCheck = this.toolCallDeduplicator.checkDuplicate(content.name, content.input);
3061
3898
  if (duplicateCheck.isDuplicate) {
@@ -3129,16 +3966,6 @@ SCHEDULING & REMINDERS:
3129
3966
  continue;
3130
3967
  }
3131
3968
 
3132
- // Infer missing parameters for weaker models
3133
- const inference = this.inferMissingParameters(content.name, content.input);
3134
- if (inference.modified) {
3135
- content.input = inference.input;
3136
- this.daemon.logEvent(this.task.id, 'parameter_inference', {
3137
- tool: content.name,
3138
- inference: inference.inference,
3139
- });
3140
- }
3141
-
3142
3969
  this.daemon.logEvent(this.task.id, 'tool_call', {
3143
3970
  tool: content.name,
3144
3971
  input: content.input,
@@ -3146,15 +3973,42 @@ SCHEDULING & REMINDERS:
3146
3973
 
3147
3974
  try {
3148
3975
  // Execute tool with timeout to prevent hanging
3149
- const result = await withTimeout(
3976
+ const toolTimeoutMs = this.getToolTimeoutMs(content.name, content.input);
3977
+ let result = await withTimeout(
3150
3978
  this.toolRegistry.executeTool(
3151
3979
  content.name,
3152
3980
  content.input as any
3153
3981
  ),
3154
- TOOL_TIMEOUT_MS,
3982
+ toolTimeoutMs,
3155
3983
  `Tool ${content.name}`
3156
3984
  );
3157
3985
 
3986
+ // Fallback: retry grep without glob if the glob produced an invalid regex
3987
+ if (content.name === 'grep' && result && result.success === false && content.input?.glob) {
3988
+ const errorText = String(result.error || '');
3989
+ if (/invalid regex pattern|nothing to repeat/i.test(errorText)) {
3990
+ this.daemon.logEvent(this.task.id, 'tool_fallback', {
3991
+ tool: 'grep',
3992
+ reason: 'invalid_glob_regex',
3993
+ originalGlob: content.input.glob,
3994
+ });
3995
+ const fallbackInput = { ...content.input };
3996
+ delete (fallbackInput as any).glob;
3997
+ try {
3998
+ const fallbackResult = await withTimeout(
3999
+ this.toolRegistry.executeTool('grep', fallbackInput as any),
4000
+ toolTimeoutMs,
4001
+ 'Tool grep (fallback)'
4002
+ );
4003
+ if (fallbackResult && fallbackResult.success !== false) {
4004
+ result = fallbackResult;
4005
+ }
4006
+ } catch {
4007
+ // Keep original error if fallback fails
4008
+ }
4009
+ }
4010
+ }
4011
+
3158
4012
  // Tool succeeded - reset failure counter
3159
4013
  this.toolFailureTracker.recordSuccess(content.name);
3160
4014
 
@@ -3165,7 +4019,25 @@ SCHEDULING & REMINDERS:
3165
4019
  // Record file operation for tracking
3166
4020
  this.recordFileOperation(content.name, content.input, result);
3167
4021
  this.recordCommandExecution(content.name, content.input, result);
3168
- this.recordCommandExecution(content.name, content.input, result);
4022
+
4023
+ const toolSucceeded = !(result && result.success === false);
4024
+
4025
+ if (toolSucceeded) {
4026
+ hadAnyToolSuccess = true;
4027
+ this.recordToolResult(content.name, result);
4028
+ }
4029
+
4030
+ if (content.name === 'run_command' && !toolSucceeded) {
4031
+ hadRunCommandFailure = true;
4032
+ } else if (hadRunCommandFailure && toolSucceeded) {
4033
+ hadToolSuccessAfterRunCommandFailure = true;
4034
+ }
4035
+
4036
+ if (expectsImageVerification && content.name === 'glob' && !foundNewImage) {
4037
+ if (this.hasNewImageFromGlobResult(result, imageVerificationSince)) {
4038
+ foundNewImage = true;
4039
+ }
4040
+ }
3169
4041
 
3170
4042
  // Check if the result indicates an error (some tools return error in result)
3171
4043
  if (result && result.success === false) {
@@ -3174,6 +4046,7 @@ SCHEDULING & REMINDERS:
3174
4046
  || (typeof result.exitCode === 'number' ? `exit code ${result.exitCode}` : undefined)
3175
4047
  || 'unknown error';
3176
4048
  hadToolError = true;
4049
+ toolErrors.add(content.name);
3177
4050
  lastToolErrorReason = `Tool ${content.name} failed: ${reason}`;
3178
4051
  // Check if this is a non-retryable error
3179
4052
  const shouldDisable = this.toolFailureTracker.recordFailure(content.name, result.error || reason);
@@ -3233,7 +4106,11 @@ SCHEDULING & REMINDERS:
3233
4106
  console.error(`Tool execution failed:`, error);
3234
4107
 
3235
4108
  hadToolError = true;
4109
+ toolErrors.add(content.name);
3236
4110
  lastToolErrorReason = `Tool ${content.name} failed: ${error.message}`;
4111
+ if (content.name === 'run_command') {
4112
+ hadRunCommandFailure = true;
4113
+ }
3237
4114
 
3238
4115
  // Track the failure
3239
4116
  const shouldDisable = this.toolFailureTracker.recordFailure(content.name, error.message);
@@ -3266,7 +4143,7 @@ SCHEDULING & REMINDERS:
3266
4143
  // If all tool attempts were for disabled or duplicate tools, don't continue looping
3267
4144
  // This prevents infinite retry loops
3268
4145
  const allToolsFailed = toolResults.every(r => r.is_error);
3269
- if ((hasDisabledToolAttempt || hasDuplicateToolAttempt) && allToolsFailed) {
4146
+ if ((hasDisabledToolAttempt || hasDuplicateToolAttempt || hasUnavailableToolAttempt) && allToolsFailed) {
3270
4147
  console.log('[TaskExecutor] All tool calls failed, were disabled, or duplicates - stopping iteration');
3271
4148
  if (hasDuplicateToolAttempt) {
3272
4149
  // Duplicate detection triggered - step is likely complete
@@ -3282,8 +4159,8 @@ SCHEDULING & REMINDERS:
3282
4159
  }
3283
4160
  }
3284
4161
 
3285
- // If assistant asked a question and there are no tool calls, stop and wait for user
3286
- if (assistantAskedQuestion && toolResults.length === 0) {
4162
+ // If assistant asked a blocking question, stop and wait for user
4163
+ if (assistantAskedQuestion && this.shouldPauseForQuestions) {
3287
4164
  console.log('[TaskExecutor] Assistant asked a question, pausing for user input');
3288
4165
  awaitingUserInput = true;
3289
4166
  continueLoop = false;
@@ -3291,14 +4168,34 @@ SCHEDULING & REMINDERS:
3291
4168
  }
3292
4169
 
3293
4170
  if (hadToolError && !hadToolSuccessAfterError) {
4171
+ const nonCriticalErrorTools = new Set(['web_search', 'web_fetch']);
4172
+ const onlyNonCriticalErrors = toolErrors.size > 0 && Array.from(toolErrors).every(t => nonCriticalErrorTools.has(t));
4173
+ if (!(hadAnyToolSuccess && onlyNonCriticalErrors)) {
4174
+ stepFailed = true;
4175
+ if (!lastFailureReason) {
4176
+ lastFailureReason = lastToolErrorReason || 'One or more tools failed without recovery.';
4177
+ }
4178
+ }
4179
+ }
4180
+
4181
+ if (hadRunCommandFailure && !hadToolSuccessAfterRunCommandFailure) {
3294
4182
  stepFailed = true;
3295
4183
  if (!lastFailureReason) {
3296
- lastFailureReason = lastToolErrorReason || 'One or more tools failed without recovery.';
4184
+ lastFailureReason = 'run_command failed and no subsequent tool succeeded.';
4185
+ }
4186
+ }
4187
+
4188
+ if (expectsImageVerification && !foundNewImage) {
4189
+ stepFailed = true;
4190
+ if (!lastFailureReason) {
4191
+ lastFailureReason = 'Verification failed: no newly generated image was found.';
3297
4192
  }
3298
4193
  }
3299
4194
 
3300
4195
  // Step completed or failed
3301
4196
 
4197
+ this.recordAssistantOutput(messages, step);
4198
+
3302
4199
  // Save conversation history for follow-up messages
3303
4200
  this.conversationHistory = messages;
3304
4201
 
@@ -3383,6 +4280,60 @@ SCHEDULING & REMINDERS:
3383
4280
  }
3384
4281
  }
3385
4282
 
4283
+ private extractHtmlFromText(text: string): string | null {
4284
+ if (!text) return null;
4285
+ const fenceMatch = text.match(/```html([\s\S]*?)```/i);
4286
+ const raw = fenceMatch ? fenceMatch[1].trim() : text;
4287
+ const doctypeIndex = raw.indexOf('<!DOCTYPE html');
4288
+ if (doctypeIndex >= 0) {
4289
+ const endIndex = raw.lastIndexOf('</html>');
4290
+ if (endIndex > doctypeIndex) {
4291
+ return raw.slice(doctypeIndex, endIndex + '</html>'.length).trim();
4292
+ }
4293
+ }
4294
+ const htmlIndex = raw.indexOf('<html');
4295
+ if (htmlIndex >= 0) {
4296
+ const endIndex = raw.lastIndexOf('</html>');
4297
+ if (endIndex > htmlIndex) {
4298
+ return raw.slice(htmlIndex, endIndex + '</html>'.length).trim();
4299
+ }
4300
+ }
4301
+ return null;
4302
+ }
4303
+
4304
+ private async generateCanvasHtml(prompt: string): Promise<string | null> {
4305
+ const system = [
4306
+ 'You generate a single self-contained HTML document for an in-app canvas.',
4307
+ 'Output ONLY the HTML document (no markdown, no commentary).',
4308
+ 'Use inline CSS and JS. Do not reference external assets or remote URLs.',
4309
+ 'Keep it reasonably compact and interactive where appropriate.',
4310
+ ].join(' ');
4311
+
4312
+ try {
4313
+ const response = await this.provider.createMessage({
4314
+ model: this.modelId,
4315
+ maxTokens: 1800,
4316
+ system,
4317
+ messages: [
4318
+ {
4319
+ role: 'user',
4320
+ content: `Build an interactive HTML demo for this request:\n${prompt}`,
4321
+ },
4322
+ ],
4323
+ });
4324
+
4325
+ const text = (response.content || [])
4326
+ .filter((c) => c.type === 'text')
4327
+ .map((c) => c.text)
4328
+ .join('\n');
4329
+
4330
+ return this.extractHtmlFromText(text);
4331
+ } catch (error) {
4332
+ console.error('[TaskExecutor] Failed to auto-generate canvas HTML:', error);
4333
+ return null;
4334
+ }
4335
+ }
4336
+
3386
4337
  /**
3387
4338
  * Send a follow-up message to continue the conversation
3388
4339
  */
@@ -3391,8 +4342,13 @@ SCHEDULING & REMINDERS:
3391
4342
  const shouldResumeAfterFollowup = previousStatus === 'paused' || this.waitingForUserInput;
3392
4343
  const shouldStartNewCanvasSession = ['completed', 'failed', 'cancelled'].includes(previousStatus);
3393
4344
  let resumeAttempted = false;
4345
+ let pausedForUserInput = false;
3394
4346
  this.waitingForUserInput = false;
3395
4347
  this.paused = false;
4348
+ this.lastUserMessage = message;
4349
+ if (shouldResumeAfterFollowup) {
4350
+ this.task.prompt = `${this.task.prompt}\n\nUSER UPDATE:\n${message}`;
4351
+ }
3396
4352
  this.toolRegistry.setCanvasSessionCutoff(shouldStartNewCanvasSession ? Date.now() : null);
3397
4353
  this.daemon.updateTaskStatus(this.task.id, 'executing');
3398
4354
  this.daemon.logEvent(this.task.id, 'executing', { message: 'Processing follow-up message' });
@@ -3444,6 +4400,11 @@ IMPORTANT INSTRUCTIONS:
3444
4400
  - The delete_file tool has a built-in approval mechanism that will prompt the user. Just call the tool directly.
3445
4401
  - Do NOT ask "Should I proceed?" or wait for permission in text - the tools handle approvals automatically.
3446
4402
 
4403
+ USER INPUT GATE (CRITICAL):
4404
+ - If you ask the user for required information or a decision, STOP and wait.
4405
+ - Do NOT continue executing steps or call tools after asking such questions.
4406
+ - If safe defaults exist, state the assumption and proceed without asking.
4407
+
3447
4408
  PATH DISCOVERY (CRITICAL):
3448
4409
  - When a task mentions a folder or path (e.g., "electron/agent folder"), users often give PARTIAL paths.
3449
4410
  - NEVER conclude a path doesn't exist without SEARCHING for it first.
@@ -3602,6 +4563,9 @@ SCHEDULING & REMINDERS:
3602
4563
  // Compact messages if context is getting too large
3603
4564
  messages = this.contextManager.compactMessages(messages, systemPromptTokens);
3604
4565
 
4566
+ const availableTools = this.getAvailableTools();
4567
+ const availableToolNames = new Set(availableTools.map(tool => tool.name));
4568
+
3605
4569
  // Use retry wrapper for resilient API calls
3606
4570
  const response = await this.callLLMWithRetry(
3607
4571
  () => withTimeout(
@@ -3609,7 +4573,7 @@ SCHEDULING & REMINDERS:
3609
4573
  model: this.modelId,
3610
4574
  maxTokens: 4096,
3611
4575
  system: this.systemPrompt,
3612
- tools: this.getAvailableTools(),
4576
+ tools: availableTools,
3613
4577
  messages,
3614
4578
  signal: this.abortController.signal,
3615
4579
  }),
@@ -3630,6 +4594,10 @@ SCHEDULING & REMINDERS:
3630
4594
  // Log any text responses from the assistant and check if asking a question
3631
4595
  let assistantAskedQuestion = false;
3632
4596
  let hasTextInThisResponse = false;
4597
+ const assistantText = (response.content || [])
4598
+ .filter((item: any) => item.type === 'text' && item.text)
4599
+ .map((item: any) => item.text)
4600
+ .join('\n');
3633
4601
  if (response.content) {
3634
4602
  for (const content of response.content) {
3635
4603
  if (content.type === 'text' && content.text && content.text.trim().length > 0) {
@@ -3679,9 +4647,20 @@ SCHEDULING & REMINDERS:
3679
4647
  const toolResults: LLMToolResult[] = [];
3680
4648
  let hasDisabledToolAttempt = false;
3681
4649
  let hasDuplicateToolAttempt = false;
4650
+ let hasUnavailableToolAttempt = false;
3682
4651
 
3683
4652
  for (const content of response.content || []) {
3684
4653
  if (content.type === 'tool_use') {
4654
+ // Normalize tool names like "functions.web_fetch" -> "web_fetch"
4655
+ const normalizedTool = this.normalizeToolName(content.name);
4656
+ if (normalizedTool.modified) {
4657
+ this.daemon.logEvent(this.task.id, 'parameter_inference', {
4658
+ tool: content.name,
4659
+ inference: `Normalized tool name "${normalizedTool.original}" -> "${normalizedTool.name}"`,
4660
+ });
4661
+ content.name = normalizedTool.name;
4662
+ }
4663
+
3685
4664
  // Check if this tool is disabled (circuit breaker tripped)
3686
4665
  if (this.toolFailureTracker.isDisabled(content.name)) {
3687
4666
  const lastError = this.toolFailureTracker.getLastError(content.name);
@@ -3704,6 +4683,60 @@ SCHEDULING & REMINDERS:
3704
4683
  continue;
3705
4684
  }
3706
4685
 
4686
+ // Validate tool availability before attempting any inference
4687
+ if (!availableToolNames.has(content.name)) {
4688
+ console.log(`[TaskExecutor] Tool not available in this context: ${content.name}`);
4689
+ this.daemon.logEvent(this.task.id, 'tool_error', {
4690
+ tool: content.name,
4691
+ error: 'Tool not available in current context or permissions',
4692
+ blocked: true,
4693
+ });
4694
+ toolResults.push({
4695
+ type: 'tool_result',
4696
+ tool_use_id: content.id,
4697
+ content: JSON.stringify({
4698
+ error: `Tool "${content.name}" is not available in this context. Please choose a different tool or check permissions/integrations.`,
4699
+ unavailable: true,
4700
+ }),
4701
+ is_error: true,
4702
+ });
4703
+ hasUnavailableToolAttempt = true;
4704
+ continue;
4705
+ }
4706
+
4707
+ // Infer missing parameters for weaker models (normalize inputs before deduplication)
4708
+ const inference = this.inferMissingParameters(content.name, content.input);
4709
+ if (inference.modified) {
4710
+ content.input = inference.input;
4711
+ this.daemon.logEvent(this.task.id, 'parameter_inference', {
4712
+ tool: content.name,
4713
+ inference: inference.inference,
4714
+ });
4715
+ }
4716
+
4717
+ // If canvas_push is missing content, try extracting HTML from assistant text or auto-generate
4718
+ await this.handleCanvasPushFallback(content, assistantText);
4719
+
4720
+ const validationError = this.getToolInputValidationError(content.name, content.input);
4721
+ if (validationError) {
4722
+ this.daemon.logEvent(this.task.id, 'tool_warning', {
4723
+ tool: content.name,
4724
+ error: validationError,
4725
+ input: content.input,
4726
+ });
4727
+ toolResults.push({
4728
+ type: 'tool_result',
4729
+ tool_use_id: content.id,
4730
+ content: JSON.stringify({
4731
+ error: validationError,
4732
+ suggestion: 'Include all required fields in the tool call (e.g., content for create_document/write_file).',
4733
+ invalid_input: true,
4734
+ }),
4735
+ is_error: true,
4736
+ });
4737
+ continue;
4738
+ }
4739
+
3707
4740
  // Check for duplicate tool calls (prevents stuck loops)
3708
4741
  const duplicateCheck = this.toolCallDeduplicator.checkDuplicate(content.name, content.input);
3709
4742
  if (duplicateCheck.isDuplicate) {
@@ -3775,16 +4808,6 @@ SCHEDULING & REMINDERS:
3775
4808
  continue;
3776
4809
  }
3777
4810
 
3778
- // Infer missing parameters for weaker models
3779
- const inference = this.inferMissingParameters(content.name, content.input);
3780
- if (inference.modified) {
3781
- content.input = inference.input;
3782
- this.daemon.logEvent(this.task.id, 'parameter_inference', {
3783
- tool: content.name,
3784
- inference: inference.inference,
3785
- });
3786
- }
3787
-
3788
4811
  this.daemon.logEvent(this.task.id, 'tool_call', {
3789
4812
  tool: content.name,
3790
4813
  input: content.input,
@@ -3792,12 +4815,13 @@ SCHEDULING & REMINDERS:
3792
4815
 
3793
4816
  try {
3794
4817
  // Execute tool with timeout to prevent hanging
4818
+ const toolTimeoutMs = this.getToolTimeoutMs(content.name, content.input);
3795
4819
  const result = await withTimeout(
3796
4820
  this.toolRegistry.executeTool(
3797
4821
  content.name,
3798
4822
  content.input as any
3799
4823
  ),
3800
- TOOL_TIMEOUT_MS,
4824
+ toolTimeoutMs,
3801
4825
  `Tool ${content.name}`
3802
4826
  );
3803
4827
 
@@ -3873,7 +4897,7 @@ SCHEDULING & REMINDERS:
3873
4897
 
3874
4898
  // If all tool attempts were for disabled or duplicate tools, don't continue looping
3875
4899
  const allToolsFailed = toolResults.every(r => r.is_error);
3876
- if ((hasDisabledToolAttempt || hasDuplicateToolAttempt) && allToolsFailed) {
4900
+ if ((hasDisabledToolAttempt || hasDuplicateToolAttempt || hasUnavailableToolAttempt) && allToolsFailed) {
3877
4901
  console.log('[TaskExecutor] All tool calls failed, were disabled, or duplicates - stopping iteration');
3878
4902
  continueLoop = false;
3879
4903
  } else {
@@ -3881,6 +4905,13 @@ SCHEDULING & REMINDERS:
3881
4905
  }
3882
4906
  }
3883
4907
 
4908
+ if (assistantAskedQuestion && shouldResumeAfterFollowup && this.shouldPauseForQuestions) {
4909
+ console.log('[TaskExecutor] Assistant asked a question during follow-up, pausing for user input');
4910
+ this.waitingForUserInput = true;
4911
+ pausedForUserInput = true;
4912
+ continueLoop = false;
4913
+ }
4914
+
3884
4915
  // Check if agent wants to end but hasn't provided a text response yet
3885
4916
  // If tools were called but no summary was given, request one
3886
4917
  if (wantsToEnd && !hasTextInThisResponse && hadToolCalls && !hasProvidedTextResponse) {
@@ -3911,6 +4942,14 @@ SCHEDULING & REMINDERS:
3911
4942
  message: 'Follow-up message processed',
3912
4943
  });
3913
4944
 
4945
+ if (pausedForUserInput) {
4946
+ this.daemon.updateTaskStatus(this.task.id, 'paused');
4947
+ this.daemon.logEvent(this.task.id, 'task_paused', {
4948
+ message: 'Paused - awaiting user input',
4949
+ });
4950
+ return;
4951
+ }
4952
+
3914
4953
  if (shouldResumeAfterFollowup && this.plan) {
3915
4954
  resumeAttempted = true;
3916
4955
  await this.resumeAfterPause();