machinaos 0.0.76 → 0.0.78

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 (393) hide show
  1. package/README.md +143 -107
  2. package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
  3. package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
  4. package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
  5. package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
  6. package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
  7. package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
  8. package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
  9. package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
  10. package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
  11. package/client/dist/assets/index-DQ0nwhec.js +257 -0
  12. package/client/dist/assets/index-DxmbVskS.css +1 -0
  13. package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
  14. package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
  15. package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
  16. package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
  17. package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
  18. package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
  19. package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
  20. package/client/dist/index.html +37 -3
  21. package/client/index.html +28 -1
  22. package/client/package.json +44 -40
  23. package/client/src/App.tsx +2 -0
  24. package/client/src/Dashboard.tsx +157 -45
  25. package/client/src/ParameterPanel.tsx +3 -5
  26. package/client/src/adapters/nodeSpecToDescription.ts +1 -0
  27. package/client/src/assets/icons/NodeIcon.tsx +32 -0
  28. package/client/src/assets/icons/index.ts +4 -0
  29. package/client/src/assets/icons/stripe.svg +1 -0
  30. package/client/src/assets/icons/themedGlyphs.ts +404 -0
  31. package/client/src/components/AIAgentNode.tsx +77 -53
  32. package/client/src/components/GenericNode.tsx +34 -52
  33. package/client/src/components/OutputPanel.tsx +64 -147
  34. package/client/src/components/ParameterRenderer.tsx +5 -3
  35. package/client/src/components/SkillEditorModal.tsx +9 -18
  36. package/client/src/components/SquareNode.tsx +97 -115
  37. package/client/src/components/StartNode.tsx +32 -42
  38. package/client/src/components/SvgFilterDefs.tsx +54 -0
  39. package/client/src/components/TeamMonitorNode.tsx +12 -14
  40. package/client/src/components/ToolkitNode.tsx +35 -60
  41. package/client/src/components/TriggerNode.tsx +43 -77
  42. package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
  43. package/client/src/components/credentials/CredentialsModal.tsx +98 -30
  44. package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
  45. package/client/src/components/credentials/catalogueAdapter.ts +17 -1
  46. package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
  47. package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
  48. package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
  49. package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
  50. package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
  51. package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
  52. package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
  53. package/client/src/components/credentials/types.ts +12 -2
  54. package/client/src/components/credentials/useCredentialPanel.ts +43 -19
  55. package/client/src/components/icons/AIProviderIcons.tsx +16 -0
  56. package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
  57. package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
  58. package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
  59. package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
  60. package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
  61. package/client/src/components/parameterPanel/InputSection.tsx +9 -7
  62. package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
  63. package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
  64. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
  65. package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
  66. package/client/src/components/ui/AIResultModal.tsx +1 -1
  67. package/client/src/components/ui/CollapsibleSection.tsx +9 -5
  68. package/client/src/components/ui/CommandPalette.tsx +147 -0
  69. package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
  70. package/client/src/components/ui/ComponentItem.tsx +13 -7
  71. package/client/src/components/ui/ComponentPalette.tsx +24 -13
  72. package/client/src/components/ui/ConsolePanel.tsx +19 -11
  73. package/client/src/components/ui/DropCap.tsx +28 -0
  74. package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
  75. package/client/src/components/ui/InputNodesPanel.tsx +1 -1
  76. package/client/src/components/ui/Modal.tsx +38 -6
  77. package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
  78. package/client/src/components/ui/SettingsPanel.tsx +42 -13
  79. package/client/src/components/ui/StatusBar.tsx +108 -0
  80. package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
  81. package/client/src/components/ui/TopToolbar.tsx +42 -25
  82. package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
  83. package/client/src/components/ui/action-button.tsx +40 -15
  84. package/client/src/components/ui/button.tsx +24 -1
  85. package/client/src/components/ui/dropdown-menu.tsx +24 -2
  86. package/client/src/components/ui/input.tsx +19 -2
  87. package/client/src/components/ui/select.tsx +15 -0
  88. package/client/src/components/ui/textarea.tsx +15 -2
  89. package/client/src/contexts/AuthContext.tsx +148 -109
  90. package/client/src/contexts/ThemeContext.tsx +93 -17
  91. package/client/src/contexts/WebSocketContext.tsx +373 -206
  92. package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
  93. package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
  94. package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
  95. package/client/src/hooks/useAppTheme.ts +209 -7
  96. package/client/src/hooks/useAutoSkillEdges.ts +7 -2
  97. package/client/src/hooks/useCatalogueQuery.ts +67 -1
  98. package/client/src/hooks/useDragVariable.ts +1 -1
  99. package/client/src/hooks/useNodeAllowlist.ts +115 -8
  100. package/client/src/hooks/useOnboarding.ts +20 -8
  101. package/client/src/hooks/useParameterPanel.ts +2 -1
  102. package/client/src/hooks/useReactFlowNodes.ts +2 -1
  103. package/client/src/hooks/useSound.ts +185 -0
  104. package/client/src/hooks/useWorkflowManagement.ts +6 -8
  105. package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
  106. package/client/src/index.css +65 -3
  107. package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
  108. package/client/src/lib/aiModelProviders.ts +8 -0
  109. package/client/src/lib/connectionConfig.ts +107 -0
  110. package/client/src/lib/queryPersist.ts +13 -5
  111. package/client/src/lib/sound.ts +393 -0
  112. package/client/src/main.tsx +20 -0
  113. package/client/src/store/useAppStore.ts +26 -0
  114. package/client/src/styles/canvasAnimations.ts +37 -36
  115. package/client/src/styles/theme.ts +36 -20
  116. package/client/src/test/setup.ts +1 -0
  117. package/client/src/themes/atomic.css +253 -0
  118. package/client/src/themes/base.css +373 -0
  119. package/client/src/themes/cyber.css +890 -0
  120. package/client/src/themes/dark.css +70 -0
  121. package/client/src/themes/edo.css +246 -0
  122. package/client/src/themes/greek.css +293 -0
  123. package/client/src/themes/light.css +78 -0
  124. package/client/src/themes/plague.css +253 -0
  125. package/client/src/themes/renaissance.css +727 -0
  126. package/client/src/themes/rot.css +249 -0
  127. package/client/src/themes/steampunk.css +272 -0
  128. package/client/src/themes/surveillance.css +289 -0
  129. package/client/src/themes/wasteland.css +250 -0
  130. package/client/src/types/INodeProperties.ts +5 -0
  131. package/client/src/types/NodeTypes.ts +11 -1
  132. package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
  133. package/client/src/types/cloudEvents.ts +78 -0
  134. package/client/src/vite-env.d.ts +7 -0
  135. package/client/tsconfig.json +1 -1
  136. package/client/vite.config.js +62 -2
  137. package/install.ps1 +1 -1
  138. package/install.sh +1 -1
  139. package/machina/commands/build.py +51 -7
  140. package/machina/pyproject.toml +4 -0
  141. package/machina/supervisor.py +12 -2
  142. package/machina/tree.py +71 -21
  143. package/package.json +4 -4
  144. package/scripts/install.js +16 -1
  145. package/server/config/ai_cli_providers.json +54 -0
  146. package/server/config/credential_providers.json +109 -2
  147. package/server/config/llm_defaults.json +24 -0
  148. package/server/config/model_registry.json +338 -499
  149. package/server/config/node_allowlist.json +16 -1
  150. package/server/config/pricing.json +8 -0
  151. package/server/constants.py +38 -15
  152. package/server/core/container.py +2 -2
  153. package/server/core/credentials_database.py +35 -2
  154. package/server/core/logging.py +4 -3
  155. package/server/main.py +99 -13
  156. package/server/models/node_metadata.py +1 -0
  157. package/server/nodejs/package.json +8 -6
  158. package/server/nodejs/src/index.ts +22 -5
  159. package/server/nodes/README.md +31 -4
  160. package/server/nodes/agent/_inline.py +2 -0
  161. package/server/nodes/agent/_specialized.py +6 -3
  162. package/server/nodes/agent/ai_agent.py +13 -3
  163. package/server/nodes/agent/chat_agent.py +6 -3
  164. package/server/nodes/agent/claude_code_agent.py +287 -75
  165. package/server/nodes/agent/codex_agent.py +239 -0
  166. package/server/nodes/agent/deep_agent.py +3 -3
  167. package/server/nodes/agent/rlm_agent.py +3 -3
  168. package/server/nodes/android/__init__.py +31 -1
  169. package/server/nodes/android/_base.py +9 -5
  170. package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
  171. package/server/nodes/android/_handlers.py +154 -0
  172. package/server/nodes/android/_option_loaders.py +44 -0
  173. package/server/nodes/android/_refresh.py +127 -0
  174. package/server/{services/android → nodes/android/_relay}/client.py +4 -4
  175. package/server/{routers/android.py → nodes/android/_router.py} +27 -8
  176. package/server/nodes/browser/browser.py +2 -2
  177. package/server/nodes/code/_base.py +6 -2
  178. package/server/nodes/code/_claude_code.py +134 -0
  179. package/server/nodes/document/embedding_generator.py +3 -3
  180. package/server/nodes/document/http_scraper.py +3 -3
  181. package/server/nodes/document/vector_store.py +5 -5
  182. package/server/nodes/email/__init__.py +11 -1
  183. package/server/nodes/email/_filters.py +21 -0
  184. package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
  185. package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
  186. package/server/nodes/email/email_read.py +1 -1
  187. package/server/nodes/email/email_receive.py +54 -5
  188. package/server/nodes/email/email_send.py +1 -1
  189. package/server/nodes/filesystem/shell.py +24 -1
  190. package/server/nodes/google/__init__.py +55 -1
  191. package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
  192. package/server/nodes/google/_base.py +2 -2
  193. package/server/nodes/google/_credentials.py +5 -5
  194. package/server/nodes/google/_filters.py +25 -0
  195. package/server/nodes/google/_handlers.py +57 -0
  196. package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
  197. package/server/nodes/google/_option_loaders.py +107 -0
  198. package/server/nodes/google/_refresh.py +66 -0
  199. package/server/nodes/google/_router.py +131 -0
  200. package/server/nodes/google/gmail_receive.py +41 -4
  201. package/server/nodes/groups.py +1 -0
  202. package/server/nodes/location/_credentials.py +45 -1
  203. package/server/{services/maps.py → nodes/location/_service.py} +18 -3
  204. package/server/nodes/location/gmaps_create.py +4 -4
  205. package/server/nodes/location/gmaps_locations.py +4 -4
  206. package/server/nodes/location/gmaps_nearby_places.py +4 -4
  207. package/server/nodes/model/_base.py +8 -3
  208. package/server/nodes/model/_credentials.py +96 -8
  209. package/server/nodes/model/_local_validator.py +345 -0
  210. package/server/nodes/model/lmstudio_chat_model.py +23 -0
  211. package/server/nodes/model/ollama_chat_model.py +25 -0
  212. package/server/nodes/proxy/_usage.py +2 -2
  213. package/server/nodes/proxy/proxy_config.py +14 -14
  214. package/server/nodes/proxy/proxy_request.py +4 -4
  215. package/server/nodes/scraper/_credentials.py +29 -1
  216. package/server/nodes/scraper/apify_actor.py +9 -9
  217. package/server/nodes/scraper/crawlee_scraper.py +5 -5
  218. package/server/nodes/search/brave_search.py +4 -0
  219. package/server/nodes/search/perplexity_search.py +9 -0
  220. package/server/nodes/search/serper_search.py +3 -0
  221. package/server/nodes/skill/simple_memory.py +12 -0
  222. package/server/nodes/social/_base.py +2 -2
  223. package/server/nodes/stripe/__init__.py +46 -0
  224. package/server/nodes/stripe/_credentials.py +33 -0
  225. package/server/nodes/stripe/_handlers.py +270 -0
  226. package/server/nodes/stripe/_install.py +127 -0
  227. package/server/nodes/stripe/_source.py +174 -0
  228. package/server/nodes/stripe/stripe_action.py +81 -0
  229. package/server/nodes/stripe/stripe_receive.py +92 -0
  230. package/server/nodes/telegram/_credentials.py +52 -1
  231. package/server/nodes/telegram/_handlers.py +19 -18
  232. package/server/nodes/telegram/_service.py +134 -32
  233. package/server/nodes/telegram/telegram_send.py +5 -6
  234. package/server/nodes/text/file_handler.py +2 -2
  235. package/server/nodes/text/text_generator.py +2 -2
  236. package/server/nodes/tool/agent_builder.py +630 -0
  237. package/server/nodes/tool/task_manager.py +144 -2
  238. package/server/nodes/twitter/__init__.py +38 -1
  239. package/server/nodes/twitter/_base.py +7 -7
  240. package/server/nodes/twitter/_credentials.py +1 -1
  241. package/server/nodes/twitter/_filters.py +37 -0
  242. package/server/nodes/twitter/_handlers.py +77 -0
  243. package/server/nodes/twitter/_oauth.py +124 -0
  244. package/server/nodes/twitter/_refresh.py +78 -0
  245. package/server/nodes/twitter/_router.py +29 -0
  246. package/server/nodes/twitter/twitter_receive.py +4 -0
  247. package/server/nodes/visuals.json +64 -19
  248. package/server/nodes/whatsapp/__init__.py +45 -5
  249. package/server/nodes/whatsapp/_base.py +3 -3
  250. package/server/nodes/whatsapp/_filters.py +137 -0
  251. package/server/nodes/whatsapp/_handlers.py +167 -0
  252. package/server/nodes/whatsapp/_option_loaders.py +68 -0
  253. package/server/nodes/whatsapp/_refresh.py +62 -0
  254. package/server/nodes/whatsapp/_runtime.py +1 -1
  255. package/server/pyproject.toml +29 -7
  256. package/server/routers/schemas.py +2 -2
  257. package/server/routers/webhook.py +26 -9
  258. package/server/routers/websocket.py +149 -810
  259. package/server/services/ai.py +89 -8
  260. package/server/services/auth.py +220 -43
  261. package/server/services/claude_oauth.py +126 -100
  262. package/server/services/cli_agent/__init__.py +78 -0
  263. package/server/services/cli_agent/_handlers.py +237 -0
  264. package/server/services/cli_agent/config.py +112 -0
  265. package/server/services/cli_agent/factory.py +48 -0
  266. package/server/services/cli_agent/lockfile.py +141 -0
  267. package/server/services/cli_agent/mcp_server.py +482 -0
  268. package/server/services/cli_agent/protocol.py +173 -0
  269. package/server/services/cli_agent/providers/__init__.py +9 -0
  270. package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
  271. package/server/services/cli_agent/providers/google_gemini.py +80 -0
  272. package/server/services/cli_agent/providers/openai_codex.py +310 -0
  273. package/server/services/cli_agent/service.py +607 -0
  274. package/server/services/cli_agent/session.py +618 -0
  275. package/server/services/cli_agent/types.py +227 -0
  276. package/server/services/cli_agent/workflow_tools.py +233 -0
  277. package/server/services/credential_registry.py +26 -1
  278. package/server/services/deployment/manager.py +26 -145
  279. package/server/services/deployment/poll_registry.py +59 -0
  280. package/server/services/event_waiter.py +76 -246
  281. package/server/services/events/__init__.py +54 -0
  282. package/server/services/events/cli.py +78 -0
  283. package/server/services/events/daemon.py +163 -0
  284. package/server/services/events/envelope.py +281 -0
  285. package/server/services/events/lifecycle.py +99 -0
  286. package/server/services/events/oauth_lifecycle.py +534 -0
  287. package/server/services/events/polling.py +60 -0
  288. package/server/services/events/push.py +36 -0
  289. package/server/services/events/source.py +63 -0
  290. package/server/services/events/triggers.py +118 -0
  291. package/server/services/events/verifiers/__init__.py +25 -0
  292. package/server/services/events/verifiers/base.py +28 -0
  293. package/server/services/events/verifiers/github.py +25 -0
  294. package/server/services/events/verifiers/hmac_basic.py +32 -0
  295. package/server/services/events/verifiers/standard_webhooks.py +47 -0
  296. package/server/services/events/verifiers/stripe.py +42 -0
  297. package/server/services/events/webhook.py +105 -0
  298. package/server/services/handlers/tools.py +28 -186
  299. package/server/services/llm/config.py +7 -0
  300. package/server/services/llm/factory.py +8 -2
  301. package/server/services/memory/__init__.py +52 -0
  302. package/server/services/memory/jsonl.py +80 -0
  303. package/server/services/memory/markdown.py +65 -0
  304. package/server/services/memory/state.py +112 -0
  305. package/server/services/memory/vector_store.py +40 -0
  306. package/server/services/model_registry.py +76 -0
  307. package/server/services/node_allowlist.py +71 -15
  308. package/server/services/node_executor.py +2 -2
  309. package/server/services/node_output_schemas.py +21 -10
  310. package/server/services/node_spec.py +1 -1
  311. package/server/services/oauth_utils.py +1 -1
  312. package/server/services/plugin/__init__.py +2 -0
  313. package/server/services/plugin/base.py +44 -2
  314. package/server/services/plugin/credential.py +288 -1
  315. package/server/services/plugin/deps.py +105 -0
  316. package/server/services/plugin/edge_walker.py +12 -4
  317. package/server/services/plugin/oauth.py +381 -0
  318. package/server/services/plugin/polling.py +247 -0
  319. package/server/services/plugin/registry.py +145 -0
  320. package/server/services/plugin/singleton.py +65 -0
  321. package/server/services/plugin/ws.py +81 -0
  322. package/server/services/process_service.py +31 -2
  323. package/server/services/status_broadcaster.py +155 -238
  324. package/server/services/temporal/workflow.py +7 -7
  325. package/server/services/workflow.py +21 -3
  326. package/server/services/ws_handler_registry.py +111 -28
  327. package/server/skills/GUIDE.md +16 -1
  328. package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
  329. package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
  330. package/server/tests/credentials/test_auth_service.py +16 -9
  331. package/server/tests/credentials/test_credential_broadcasts.py +219 -0
  332. package/server/tests/credentials/test_google_oauth.py +6 -6
  333. package/server/tests/credentials/test_oauth_utils.py +1 -1
  334. package/server/tests/credentials/test_twitter_oauth.py +2 -2
  335. package/server/tests/credentials/test_websocket_handlers.py +44 -20
  336. package/server/tests/llm/test_factory.py +1 -0
  337. package/server/tests/llm/test_wiring.py +5 -1
  338. package/server/tests/nodes/_compat.py +24 -24
  339. package/server/tests/nodes/test_agent_builder.py +439 -0
  340. package/server/tests/nodes/test_ai_tools.py +18 -14
  341. package/server/tests/nodes/test_code_fs_process.py +17 -8
  342. package/server/tests/nodes/test_email.py +10 -9
  343. package/server/tests/nodes/test_google_workspace.py +2 -2
  344. package/server/tests/nodes/test_specialized_agents.py +100 -53
  345. package/server/tests/nodes/test_stripe_plugin.py +293 -0
  346. package/server/tests/nodes/test_telegram_social.py +4 -4
  347. package/server/tests/nodes/test_twitter.py +1 -1
  348. package/server/tests/nodes/test_web_automation.py +2 -2
  349. package/server/tests/nodes/test_whatsapp.py +9 -9
  350. package/server/tests/services/cli_agent/__init__.py +0 -0
  351. package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
  352. package/server/tests/services/cli_agent/test_providers.py +358 -0
  353. package/server/tests/services/cli_agent/test_service.py +298 -0
  354. package/server/tests/services/memory/__init__.py +0 -0
  355. package/server/tests/services/memory/test_jsonl.py +188 -0
  356. package/server/tests/services/test_events.py +333 -0
  357. package/server/tests/test_node_spec.py +56 -16
  358. package/server/tests/test_plugin_helpers.py +116 -0
  359. package/server/tests/test_plugin_self_containment.py +486 -0
  360. package/server/tests/test_status_broadcasts.py +425 -0
  361. package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
  362. package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
  363. package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
  364. package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
  365. package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
  366. package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
  367. package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
  368. package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
  369. package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
  370. package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
  371. package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
  372. package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
  373. package/client/dist/assets/index-D9tZfgvi.js +0 -363
  374. package/client/dist/assets/index-al7snTkG.css +0 -1
  375. package/client/src/components/credentials/providers.tsx +0 -177
  376. package/server/routers/google.py +0 -277
  377. package/server/routers/maps.py +0 -142
  378. package/server/routers/twitter.py +0 -365
  379. package/server/services/claude_code_service.py +0 -106
  380. package/server/services/memory.py +0 -159
  381. package/server/services/node_option_loaders/__init__.py +0 -77
  382. package/server/services/node_option_loaders/android_loaders.py +0 -55
  383. package/server/services/node_option_loaders/google_loaders.py +0 -97
  384. package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
  385. package/server/services/twitter_oauth.py +0 -411
  386. package/server/services/websocket_client.py +0 -29
  387. /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
  388. /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
  389. /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
  390. /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
  391. /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
  392. /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
  393. /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
@@ -0,0 +1,618 @@
1
+ """One CLI session per task — `BaseProcessSupervisor` subclass.
2
+
3
+ Each session is bound to:
4
+ - one provider (Claude or Codex)
5
+ - one task spec (`ClaudeTaskSpec` / `CodexTaskSpec`)
6
+ - one git worktree (created in `_pre_spawn`, removed in `cleanup`)
7
+ - one IDE lockfile (written in `_pre_spawn` when the provider supports it)
8
+
9
+ Inherits `BaseProcessSupervisor`'s locked idempotent start/stop, recursive
10
+ `kill_tree`, `terminate_then_kill(5s)` grace, and Windows
11
+ `CTRL_BREAK_EVENT` path. We override `_do_start` to wire NDJSON consumers
12
+ instead of the parent's generic `drain_stream(logger.info)`.
13
+
14
+ Sessions are NOT registered in the global supervisor registry — they're
15
+ owned by `AICliService.run_batch()` for the lifetime of one batch.
16
+
17
+ Liveness: relies on `wait_for_completion(timeout_seconds)` as the watchdog
18
+ and the per-broadcast Temporal heartbeat fired by every
19
+ `update_node_status()` call. We do not run our own per-second heartbeat
20
+ loop or write diagnostic dump files.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import json
27
+ import os
28
+ import shutil
29
+ import subprocess
30
+ import sys
31
+ import uuid
32
+ from pathlib import Path
33
+ from typing import Any, Dict, List, Optional
34
+
35
+ import anyio
36
+ import yaml
37
+
38
+ from core.logging import get_logger
39
+ from services._supervisor.process import BaseProcessSupervisor
40
+ from services.cli_agent.lockfile import remove_ide_lockfile, write_ide_lockfile
41
+ from services.cli_agent.protocol import AICliProvider, CanonicalUsage, SessionResult
42
+ from services.cli_agent.types import BaseAICliTaskSpec
43
+
44
+ logger = get_logger(__name__)
45
+
46
+
47
+ class AICliSession(BaseProcessSupervisor):
48
+ """One CLI subprocess for one task."""
49
+
50
+ pipe_streams = True
51
+ terminate_grace_seconds = 5.0
52
+ graceful_shutdown = sys.platform == "win32"
53
+
54
+ def __init__(
55
+ self,
56
+ *,
57
+ provider: AICliProvider,
58
+ task: BaseAICliTaskSpec,
59
+ repo_root: Path,
60
+ workspace_dir: Path,
61
+ node_id: str,
62
+ workflow_id: str,
63
+ broadcaster: Any,
64
+ defaults: Dict[str, Any],
65
+ mcp_port: int,
66
+ batch_token: str,
67
+ connected_tool_names: Optional[List[str]] = None,
68
+ connected_skill_names: Optional[List[str]] = None,
69
+ memory_bound: bool = False,
70
+ ) -> None:
71
+ super().__init__()
72
+ self._provider = provider
73
+ self._task = task
74
+ self._task_id = task.task_id or f"t_{uuid.uuid4().hex[:8]}"
75
+ self._repo_root = Path(repo_root).resolve()
76
+ self._worktree_dir = (
77
+ Path(workspace_dir).resolve() / node_id / f"wt_{self._task_id}"
78
+ )
79
+ self._branch = task.branch or f"machina/{self._task_id}"
80
+ self._broadcaster = broadcaster
81
+ self._defaults = defaults
82
+ self._mcp_port = mcp_port
83
+ self._batch_token = batch_token
84
+ self._node_id = node_id
85
+ self._workflow_id = workflow_id
86
+ # Names of `mcp__machinaos__*` tools to add to `--allowedTools`.
87
+ self._connected_tool_names: List[str] = list(connected_tool_names or [])
88
+ # Skills to materialise into `<worktree>/.claude/skills/<name>/SKILL.md`
89
+ # so claude auto-discovers them per the documented project-scope
90
+ # path (https://code.claude.com/docs/en/skills#where-skills-live).
91
+ self._connected_skill_names: List[str] = list(connected_skill_names or [])
92
+ # Memory-bound runs use ``cwd=repo_root`` so claude's project_key
93
+ # (derived from cwd via `[^a-zA-Z0-9.-] -> -`) stays stable
94
+ # across spawns. With a stable project_key, ``--resume <UUID>``
95
+ # finds the prior session JSONL claude wrote on its previous
96
+ # turn under ``<CLAUDE_CONFIG_DIR>/projects/<key>/<UUID>.jsonl``.
97
+ # The per-task git worktree (random `wt_t_<rand>` suffix) is
98
+ # incompatible with this — every spawn would land under a
99
+ # brand-new project_key with no prior JSONL.
100
+ self._memory_bound: bool = bool(memory_bound)
101
+
102
+ # Streaming state
103
+ self._events: List[Dict[str, Any]] = []
104
+ self._stderr_lines: List[str] = []
105
+ self._exit_code: Optional[int] = None
106
+ self._lockfile_path: Optional[Path] = None
107
+
108
+ # ------------------------------------------------------------------
109
+ # Identity
110
+ # ------------------------------------------------------------------
111
+
112
+ @property
113
+ def label(self) -> str:
114
+ return f"AICliSession_{self._provider.name}_{self._task_id}"
115
+
116
+ @property
117
+ def task_id(self) -> str:
118
+ return self._task_id
119
+
120
+ @property
121
+ def branch(self) -> str:
122
+ return self._branch
123
+
124
+ @property
125
+ def worktree_dir(self) -> Path:
126
+ return self._worktree_dir
127
+
128
+ # ------------------------------------------------------------------
129
+ # BaseProcessSupervisor surface
130
+ # ------------------------------------------------------------------
131
+
132
+ def binary_path(self) -> Path:
133
+ return self._provider.binary_path()
134
+
135
+ def argv(self) -> List[str]:
136
+ return self._provider.headless_argv(
137
+ self._task,
138
+ defaults=self._defaults,
139
+ mcp_endpoint_url=self._mcp_endpoint_url(),
140
+ mcp_bearer_token=self._batch_token,
141
+ connected_tool_names=self._connected_tool_names,
142
+ )
143
+
144
+ def _mcp_endpoint_url(self) -> str:
145
+ """Absolute URL of MachinaOs's FastMCP JSON-RPC endpoint.
146
+
147
+ Mirrors the ``url`` written into the IDE lockfile. FastMCP serves
148
+ at ``/mcp`` of the sub-app; ``main.py`` mounts it at ``/mcp/ide``;
149
+ the JSON-RPC URL is therefore ``/mcp/ide/mcp``."""
150
+ return f"http://127.0.0.1:{self._mcp_port}/mcp/ide/mcp"
151
+
152
+ def cwd(self) -> Optional[Path]:
153
+ # Memory-bound runs spawn directly under repo_root so claude's
154
+ # project_key stays constant and `--resume` finds the prior
155
+ # JSONL across runs. Non-memory runs use the per-task worktree.
156
+ if self._memory_bound:
157
+ return self._repo_root
158
+ return self._worktree_dir
159
+
160
+ def env(self) -> Dict[str, str]:
161
+ # Inherit parent env, then redirect provider config to MachinaOs's
162
+ # project-local isolation dir for providers that support it (Claude
163
+ # via CLAUDE_CONFIG_DIR; Codex/Gemini still use their native auth
164
+ # paths until isolation is wired). Without this, the agent's
165
+ # spawned CLI reads the user's personal `~/.claude/` credentials
166
+ # instead of the ones the credentials modal's Login button wrote.
167
+ e: Dict[str, str] = {**os.environ, "PYTHONUNBUFFERED": "1"}
168
+ if self._provider.name == "claude":
169
+ from services.claude_oauth import MACHINA_CLAUDE_DIR
170
+ e["CLAUDE_CONFIG_DIR"] = str(MACHINA_CLAUDE_DIR)
171
+ if self._lockfile_path and self._provider.ide_lock_env_var:
172
+ e[self._provider.ide_lock_env_var] = str(self._lockfile_path)
173
+ # Composio-style parent-run-ID for MCP correlation
174
+ e["MACHINA_PARENT_RUN_ID"] = (
175
+ f"{self._workflow_id}:{self._node_id}:{self._batch_token[:8]}"
176
+ )
177
+ return e
178
+
179
+ async def _pre_spawn(self) -> None:
180
+ """Create the per-task git worktree (non-memory-bound runs only)
181
+ and (if supported) write the IDE lockfile. Failures abort
182
+ `_do_start` cleanly via RuntimeError."""
183
+ # 1. Per-task git worktree — skipped for memory-bound runs
184
+ # which use cwd=repo_root to keep claude's project_key stable.
185
+ if not self._memory_bound:
186
+ self._worktree_dir.parent.mkdir(parents=True, exist_ok=True)
187
+ wt_proc = await anyio.run_process(
188
+ [
189
+ "git", "-C", str(self._repo_root),
190
+ "worktree", "add",
191
+ str(self._worktree_dir),
192
+ "-b", self._branch,
193
+ ],
194
+ check=False,
195
+ )
196
+ if wt_proc.returncode != 0:
197
+ err = (wt_proc.stderr or b"").decode(
198
+ "utf-8", errors="replace",
199
+ ).strip()
200
+ raise RuntimeError(f"git worktree add failed: {err}")
201
+ else:
202
+ logger.info(
203
+ "[%s] memory-bound: skipping worktree, using cwd=%s",
204
+ self.label, self._repo_root,
205
+ )
206
+
207
+ # 2. IDE lockfile (VSCode pattern) — providers that support it.
208
+ # `workspace_dir` in the lockfile points at whatever cwd will
209
+ # actually be — repo_root for memory-bound runs, else worktree.
210
+ lockfile_workspace = (
211
+ self._repo_root if self._memory_bound else self._worktree_dir
212
+ )
213
+ if self._provider.supports("ide_lockfile") and self._provider.ide_lockfile_dir:
214
+ try:
215
+ self._lockfile_path = write_ide_lockfile(
216
+ ide_lockfile_dir=self._provider.ide_lockfile_dir,
217
+ pid=os.getpid(),
218
+ port=self._mcp_port,
219
+ token=self._batch_token,
220
+ workspace_dir=lockfile_workspace,
221
+ ide_name=self._provider.name,
222
+ )
223
+ except OSError as exc:
224
+ logger.warning(
225
+ "[%s] IDE lockfile write failed (%s) — continuing without MCP tools",
226
+ self.label, exc,
227
+ )
228
+
229
+ # 3. Materialise connected skills under cwd's `.claude/skills/`.
230
+ # cwd is repo_root for memory-bound, worktree otherwise — both
231
+ # are project-scope per the spec.
232
+ if self._connected_skill_names:
233
+ await self._materialise_skills()
234
+
235
+ async def _materialise_skills(self) -> None:
236
+ """Write `<worktree>/.claude/skills/<name>/SKILL.md` for each
237
+ connected skill so the spawned claude can invoke them via the
238
+ built-in `Skill` tool. Filesystem skills are copied wholesale
239
+ (preserves `scripts/` + `references/`); DB skills are
240
+ reconstructed from frontmatter."""
241
+ from services.skill_loader import get_skill_loader
242
+
243
+ loader = get_skill_loader()
244
+ # cwd-relative — repo_root for memory-bound runs, worktree otherwise.
245
+ skills_dir = self.cwd() / ".claude" / "skills"
246
+ try:
247
+ skills_dir.mkdir(parents=True, exist_ok=True)
248
+ except OSError as exc:
249
+ logger.warning(
250
+ "[%s] cannot create skills dir %s: %s — skipping",
251
+ self.label, skills_dir, exc,
252
+ )
253
+ return
254
+
255
+ for name in self._connected_skill_names:
256
+ try:
257
+ skill = await loader.load_skill_async(name)
258
+ except Exception as exc:
259
+ logger.warning(
260
+ "[%s] load_skill_async(%r) failed: %s",
261
+ self.label, name, exc,
262
+ )
263
+ continue
264
+ if skill is None:
265
+ logger.warning(
266
+ "[%s] skill %r not found — skipping materialisation",
267
+ self.label, name,
268
+ )
269
+ continue
270
+
271
+ dest = skills_dir / name
272
+ try:
273
+ if skill.metadata.path is not None:
274
+ # Filesystem skill: copy whole directory tree.
275
+ shutil.copytree(
276
+ skill.metadata.path, dest, dirs_exist_ok=True,
277
+ )
278
+ else:
279
+ # DB skill: reconstruct frontmatter + body.
280
+ dest.mkdir(parents=True, exist_ok=True)
281
+ frontmatter = {
282
+ "name": skill.metadata.name,
283
+ "description": skill.metadata.description,
284
+ "allowed-tools": " ".join(skill.metadata.allowed_tools),
285
+ "metadata": skill.metadata.metadata,
286
+ }
287
+ body = (
288
+ f"---\n"
289
+ f"{yaml.safe_dump(frontmatter, sort_keys=False)}"
290
+ f"---\n\n"
291
+ f"{skill.instructions}"
292
+ )
293
+ (dest / "SKILL.md").write_text(body, encoding="utf-8")
294
+ logger.info(
295
+ "[CC-Agent _pre_spawn] materialised skill %r -> %s",
296
+ name, dest,
297
+ )
298
+ except OSError as exc:
299
+ logger.warning(
300
+ "[%s] failed to materialise skill %r at %s: %s",
301
+ self.label, name, dest, exc,
302
+ )
303
+
304
+ # ------------------------------------------------------------------
305
+ # _do_start: replace parent's drain tasks with NDJSON consumers
306
+ # ------------------------------------------------------------------
307
+
308
+ async def _do_start(self) -> None:
309
+ binary = self.binary_path()
310
+ if not binary.exists():
311
+ raise FileNotFoundError(f"{self.label} binary not found at {binary}")
312
+
313
+ await self._pre_spawn()
314
+
315
+ kwargs: Dict[str, Any] = {
316
+ "cwd": str(self.cwd()),
317
+ "env": self.env(),
318
+ # `claude -p` opens stdin to read piped input; we never pipe
319
+ # anything, so without DEVNULL the CLI waits 3s and prints
320
+ # "Warning: no stdin data received in 3s, proceeding without
321
+ # it." Tools come over MCP, not stdin — close the handle
322
+ # explicitly to skip the warning + the 3s stall.
323
+ "stdin": subprocess.DEVNULL,
324
+ "stdout": subprocess.PIPE,
325
+ "stderr": subprocess.PIPE,
326
+ }
327
+ if sys.platform == "win32" and self.graceful_shutdown:
328
+ kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined]
329
+
330
+ self._proc = await anyio.open_process(self.argv(), **kwargs)
331
+ self._logger.info(
332
+ "[%s] spawned pid=%s task=%s branch=%s",
333
+ self.label, self._proc.pid, self._task_id, self._branch,
334
+ )
335
+
336
+ # NDJSON consumers replace the parent's `drain_stream(logger.info)`.
337
+ # We populate `self._drain_tasks` so the parent's `_do_stop`
338
+ # cancels them on stop.
339
+ self._drain_tasks = [
340
+ asyncio.create_task(self._consume_stdout(self._proc.stdout)),
341
+ asyncio.create_task(self._consume_stderr(self._proc.stderr)),
342
+ ]
343
+
344
+ # ------------------------------------------------------------------
345
+ # Stream consumers
346
+ # ------------------------------------------------------------------
347
+
348
+ async def _consume_stdout(self, stream: Optional[anyio.abc.ByteReceiveStream]) -> None:
349
+ if stream is None:
350
+ return
351
+ buf = b""
352
+ try:
353
+ async for chunk in stream:
354
+ buf += chunk
355
+ while b"\n" in buf:
356
+ raw, buf = buf.split(b"\n", 1)
357
+ text = raw.decode("utf-8", errors="replace").strip()
358
+ if not text:
359
+ continue
360
+ event = self._provider.parse_event(text)
361
+ if event is None:
362
+ continue
363
+ self._events.append(event)
364
+ await self._on_event(event)
365
+ if buf:
366
+ text = buf.decode("utf-8", errors="replace").strip()
367
+ if text:
368
+ event = self._provider.parse_event(text)
369
+ if event is not None:
370
+ self._events.append(event)
371
+ await self._on_event(event)
372
+ except (anyio.ClosedResourceError, anyio.EndOfStream, asyncio.CancelledError):
373
+ pass
374
+ except Exception as exc: # pragma: no cover — defensive
375
+ self._logger.debug("[%s] stdout consumer ended: %s", self.label, exc)
376
+
377
+ async def _consume_stderr(self, stream: Optional[anyio.abc.ByteReceiveStream]) -> None:
378
+ if stream is None:
379
+ return
380
+ buf = b""
381
+ try:
382
+ async for chunk in stream:
383
+ buf += chunk
384
+ while b"\n" in buf:
385
+ raw, buf = buf.split(b"\n", 1)
386
+ text = raw.decode("utf-8", errors="replace").rstrip()
387
+ if not text:
388
+ continue
389
+ self._stderr_lines.append(text)
390
+ # Mirror to backend log too — without this the spawned
391
+ # CLI's MCP-discovery / auth-failure / debug output
392
+ # only reaches the Terminal panel, leaving the
393
+ # operator log blind during runtime debugging.
394
+ self._logger.info("[CC-Agent stderr] %s", text)
395
+ await self._safe_terminal_log(text, level="error")
396
+ if buf:
397
+ text = buf.decode("utf-8", errors="replace").rstrip()
398
+ if text:
399
+ self._stderr_lines.append(text)
400
+ self._logger.info("[CC-Agent stderr] %s", text)
401
+ await self._safe_terminal_log(text, level="error")
402
+ except (anyio.ClosedResourceError, anyio.EndOfStream, asyncio.CancelledError):
403
+ pass
404
+ except Exception as exc: # pragma: no cover
405
+ self._logger.debug("[%s] stderr consumer ended: %s", self.label, exc)
406
+
407
+ # ------------------------------------------------------------------
408
+ # Event dispatch (UI broadcasts)
409
+ # ------------------------------------------------------------------
410
+
411
+ async def _on_event(self, event: Dict[str, Any]) -> None:
412
+ # Tag every interesting event in the backend log so the operator
413
+ # can see what claude is actually doing — tool calls, assistant
414
+ # text, hook events. Without this the stream is invisible from
415
+ # the backend (only the Terminal panel saw it).
416
+ self._log_event_summary(event)
417
+
418
+ if self._provider.is_final_event(event):
419
+ payload = {
420
+ "phase": "ai_cli_subtask",
421
+ "task_id": self._task_id,
422
+ "provider": self._provider.name,
423
+ "status": "finalising",
424
+ }
425
+ for k in ("total_cost_usd", "duration_ms", "num_turns", "session_id"):
426
+ v = event.get(k)
427
+ if v is not None:
428
+ payload[k] = v
429
+ await self._safe_node_status("executing", payload)
430
+ else:
431
+ msg = (
432
+ event.get("message")
433
+ or event.get("text")
434
+ or event.get("delta")
435
+ or json.dumps(event)
436
+ )
437
+ text = msg if isinstance(msg, str) else json.dumps(msg)
438
+ await self._safe_terminal_log(text[:500], level="info")
439
+
440
+ def _log_event_summary(self, event: Dict[str, Any]) -> None:
441
+ """One-line summary per claude stream event. Picks out the event
442
+ types that matter for tool-call debugging."""
443
+ etype = event.get("type", "?")
444
+ try:
445
+ if etype == "system" and event.get("subtype") == "init":
446
+ tools = event.get("tools") or []
447
+ mcp_servers = event.get("mcp_servers") or []
448
+ self._logger.info(
449
+ "[CC-Agent stream] system.init tools=%d (sample=%s) "
450
+ "mcp_servers=%s",
451
+ len(tools), tools[:8],
452
+ [s.get("name") for s in mcp_servers if isinstance(s, dict)],
453
+ )
454
+ elif etype == "assistant":
455
+ msg = event.get("message") or {}
456
+ content = msg.get("content") or []
457
+ tool_uses = [
458
+ c for c in content
459
+ if isinstance(c, dict) and c.get("type") == "tool_use"
460
+ ]
461
+ texts = [
462
+ c.get("text", "") for c in content
463
+ if isinstance(c, dict) and c.get("type") == "text"
464
+ ]
465
+ if tool_uses:
466
+ for tu in tool_uses:
467
+ self._logger.info(
468
+ "[CC-Agent stream] assistant->tool_use name=%s "
469
+ "input_keys=%s",
470
+ tu.get("name"),
471
+ list((tu.get("input") or {}).keys()),
472
+ )
473
+ elif texts:
474
+ sample = " ".join(t for t in texts if isinstance(t, str))[:300]
475
+ self._logger.info(
476
+ "[CC-Agent stream] assistant.text: %r", sample,
477
+ )
478
+ elif etype == "tool_use":
479
+ self._logger.info(
480
+ "[CC-Agent stream] tool_use name=%s input_keys=%s",
481
+ event.get("name"),
482
+ list((event.get("input") or {}).keys()),
483
+ )
484
+ elif etype == "tool_result":
485
+ content = event.get("content") or ""
486
+ preview = content if isinstance(content, str) else json.dumps(content)
487
+ self._logger.info(
488
+ "[CC-Agent stream] tool_result is_error=%s content=%r",
489
+ event.get("is_error", False), preview[:300],
490
+ )
491
+ elif etype == "hook":
492
+ self._logger.info(
493
+ "[CC-Agent stream] hook %s",
494
+ event.get("hook_event_name") or event.get("subtype") or "?",
495
+ )
496
+ elif etype == "result":
497
+ self._logger.info(
498
+ "[CC-Agent stream] result is_error=%s subtype=%s "
499
+ "session_id=%s duration_ms=%s num_turns=%s cost=%s",
500
+ event.get("is_error", False), event.get("subtype"),
501
+ event.get("session_id"),
502
+ event.get("duration_ms"), event.get("num_turns"),
503
+ event.get("total_cost_usd"),
504
+ )
505
+ except Exception:
506
+ self._logger.debug("[CC-Agent stream] log-summary failed for event")
507
+
508
+ async def _safe_terminal_log(self, message: str, *, level: str) -> None:
509
+ if not self._broadcaster:
510
+ return
511
+ try:
512
+ await self._broadcaster.broadcast_terminal_log({
513
+ "source": f"{self._provider.name}:{self._task_id}",
514
+ "level": level,
515
+ "message": message,
516
+ })
517
+ except Exception:
518
+ pass
519
+
520
+ async def _safe_node_status(self, status: str, data: Dict[str, Any]) -> None:
521
+ if not self._broadcaster:
522
+ return
523
+ try:
524
+ await self._broadcaster.update_node_status(
525
+ self._node_id, status, data,
526
+ workflow_id=self._workflow_id,
527
+ )
528
+ except Exception:
529
+ pass
530
+
531
+ # ------------------------------------------------------------------
532
+ # Public lifecycle
533
+ # ------------------------------------------------------------------
534
+
535
+ async def wait_for_completion(self, timeout_seconds: int) -> SessionResult:
536
+ """Wait for the CLI to exit, with hard timeout watchdog."""
537
+ if self._proc is None:
538
+ return self._build_result(
539
+ success=False, error="session never started",
540
+ )
541
+
542
+ try:
543
+ await asyncio.wait_for(self._proc.wait(), timeout=timeout_seconds)
544
+ self._exit_code = self._proc.returncode
545
+ return self._build_result(success=(self._exit_code == 0))
546
+ except asyncio.TimeoutError:
547
+ await self.stop()
548
+ return self._build_result(
549
+ success=False,
550
+ error=f"timeout after {timeout_seconds}s",
551
+ )
552
+
553
+ async def cleanup(self) -> None:
554
+ """Stop the process and remove the worktree + lockfile."""
555
+ try:
556
+ await self.stop()
557
+ except Exception as exc:
558
+ self._logger.debug("[%s] stop during cleanup: %s", self.label, exc)
559
+
560
+ if self._lockfile_path:
561
+ remove_ide_lockfile(self._lockfile_path)
562
+ self._lockfile_path = None
563
+
564
+ # Remove the per-task worktree (only created for non-memory
565
+ # runs; memory-bound spawns ran directly under repo_root).
566
+ if not self._memory_bound:
567
+ try:
568
+ await anyio.run_process(
569
+ [
570
+ "git", "-C", str(self._repo_root),
571
+ "worktree", "remove", "--force",
572
+ str(self._worktree_dir),
573
+ ],
574
+ check=False,
575
+ )
576
+ except Exception as exc:
577
+ self._logger.debug("[%s] worktree remove: %s", self.label, exc)
578
+
579
+ # ------------------------------------------------------------------
580
+ # Result construction
581
+ # ------------------------------------------------------------------
582
+
583
+ def _build_result(
584
+ self,
585
+ *,
586
+ success: bool,
587
+ error: Optional[str] = None,
588
+ ) -> SessionResult:
589
+ provider_result = self._provider.event_to_session_result(
590
+ self._events,
591
+ "\n".join(self._stderr_lines),
592
+ self._exit_code if self._exit_code is not None else -1,
593
+ )
594
+
595
+ canonical = provider_result.get("canonical_usage")
596
+ if not isinstance(canonical, CanonicalUsage):
597
+ canonical = self._provider.canonical_usage(self._events)
598
+
599
+ final_success = success and provider_result.get("success", True)
600
+ final_error = error or provider_result.get("error")
601
+
602
+ return SessionResult(
603
+ task_id=self._task_id,
604
+ session_id=provider_result.get("session_id"),
605
+ provider=self._provider.name,
606
+ prompt=self._task.prompt,
607
+ branch=self._branch,
608
+ worktree_path=str(self._worktree_dir),
609
+ response=str(provider_result.get("response") or "")[:4000],
610
+ cost_usd=provider_result.get("cost_usd"),
611
+ duration_ms=provider_result.get("duration_ms"),
612
+ num_turns=provider_result.get("num_turns"),
613
+ tool_calls=int(provider_result.get("tool_calls", 0)),
614
+ canonical_usage=canonical,
615
+ provider_data=dict(provider_result.get("provider_data") or {}),
616
+ success=final_success,
617
+ error=final_error if not final_success else None,
618
+ )