@swarmclawai/swarmclaw 0.8.4 → 0.8.7

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 (394) hide show
  1. package/README.md +9 -9
  2. package/bin/swarmclaw.js +5 -1
  3. package/bin/worker-cmd.js +73 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +17 -7
  6. package/src/app/api/agents/route.ts +21 -8
  7. package/src/app/api/approvals/route.test.ts +6 -6
  8. package/src/app/api/approvals/route.ts +2 -1
  9. package/src/app/api/auth/route.ts +2 -3
  10. package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
  11. package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
  12. package/src/app/api/chatrooms/[id]/route.ts +7 -6
  13. package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
  14. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  15. package/src/app/api/chats/[id]/clear/route.ts +9 -9
  16. package/src/app/api/chats/[id]/devserver/route.ts +2 -1
  17. package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
  18. package/src/app/api/chats/[id]/fork/route.ts +3 -5
  19. package/src/app/api/chats/[id]/restore/route.ts +6 -7
  20. package/src/app/api/chats/[id]/retry/route.ts +3 -4
  21. package/src/app/api/chats/[id]/route.ts +61 -62
  22. package/src/app/api/chats/route.ts +7 -1
  23. package/src/app/api/connectors/[id]/route.ts +7 -8
  24. package/src/app/api/connectors/route.ts +5 -4
  25. package/src/app/api/eval/run/route.ts +2 -1
  26. package/src/app/api/eval/suite/route.ts +2 -1
  27. package/src/app/api/external-agents/route.test.ts +1 -1
  28. package/src/app/api/external-agents/route.ts +2 -2
  29. package/src/app/api/files/serve/route.ts +1 -1
  30. package/src/app/api/gateways/[id]/route.ts +7 -5
  31. package/src/app/api/gateways/route.ts +1 -1
  32. package/src/app/api/knowledge/upload/route.ts +1 -1
  33. package/src/app/api/logs/route.ts +5 -7
  34. package/src/app/api/memory-images/[filename]/route.ts +2 -3
  35. package/src/app/api/openclaw/agent-files/route.ts +4 -3
  36. package/src/app/api/openclaw/approvals/route.ts +3 -4
  37. package/src/app/api/openclaw/config-sync/route.ts +3 -2
  38. package/src/app/api/openclaw/cron/route.ts +3 -2
  39. package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
  40. package/src/app/api/openclaw/exec-config/route.ts +3 -2
  41. package/src/app/api/openclaw/gateway/route.ts +5 -4
  42. package/src/app/api/openclaw/history/route.ts +3 -2
  43. package/src/app/api/openclaw/media/route.ts +2 -1
  44. package/src/app/api/openclaw/permissions/route.ts +3 -2
  45. package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
  46. package/src/app/api/openclaw/skills/install/route.ts +2 -1
  47. package/src/app/api/openclaw/skills/remove/route.ts +2 -1
  48. package/src/app/api/openclaw/skills/route.ts +3 -2
  49. package/src/app/api/orchestrator/run/route.ts +5 -14
  50. package/src/app/api/perf/route.ts +43 -0
  51. package/src/app/api/plugins/dependencies/route.ts +2 -1
  52. package/src/app/api/plugins/install/route.ts +2 -1
  53. package/src/app/api/plugins/marketplace/route.ts +3 -2
  54. package/src/app/api/plugins/settings/route.ts +2 -1
  55. package/src/app/api/preview-server/route.ts +11 -10
  56. package/src/app/api/projects/[id]/route.ts +1 -1
  57. package/src/app/api/schedules/[id]/route.test.ts +128 -0
  58. package/src/app/api/schedules/[id]/route.ts +43 -43
  59. package/src/app/api/schedules/[id]/run/route.ts +11 -62
  60. package/src/app/api/schedules/route.ts +21 -87
  61. package/src/app/api/settings/route.ts +2 -0
  62. package/src/app/api/setup/doctor/route.ts +9 -8
  63. package/src/app/api/tasks/[id]/approve/route.ts +33 -30
  64. package/src/app/api/tasks/[id]/route.ts +12 -35
  65. package/src/app/api/tasks/import/github/route.ts +2 -1
  66. package/src/app/api/tasks/route.ts +79 -91
  67. package/src/app/api/wallets/[id]/approve/route.ts +2 -1
  68. package/src/app/api/wallets/[id]/route.ts +13 -19
  69. package/src/app/api/wallets/[id]/send/route.ts +2 -1
  70. package/src/app/api/wallets/route.ts +2 -1
  71. package/src/app/api/webhooks/[id]/route.ts +2 -1
  72. package/src/app/api/webhooks/route.test.ts +3 -1
  73. package/src/app/page.tsx +23 -331
  74. package/src/cli/index.js +19 -0
  75. package/src/cli/index.ts +38 -7
  76. package/src/cli/spec.js +9 -0
  77. package/src/components/activity/activity-feed.tsx +7 -4
  78. package/src/components/agents/agent-card.tsx +32 -6
  79. package/src/components/agents/agent-chat-list.tsx +55 -22
  80. package/src/components/agents/agent-files-editor.tsx +3 -2
  81. package/src/components/agents/agent-sheet.tsx +123 -22
  82. package/src/components/agents/inspector-panel.tsx +1 -1
  83. package/src/components/agents/openclaw-skills-panel.tsx +2 -1
  84. package/src/components/agents/trash-list.tsx +1 -1
  85. package/src/components/auth/access-key-gate.tsx +8 -2
  86. package/src/components/auth/setup-wizard.tsx +10 -9
  87. package/src/components/auth/user-picker.tsx +3 -2
  88. package/src/components/chat/chat-area.tsx +20 -1
  89. package/src/components/chat/chat-card.tsx +18 -3
  90. package/src/components/chat/chat-header.tsx +24 -4
  91. package/src/components/chat/chat-list.tsx +2 -11
  92. package/src/components/chat/heartbeat-history-panel.tsx +2 -1
  93. package/src/components/chat/message-bubble.tsx +45 -6
  94. package/src/components/chat/message-list.tsx +280 -145
  95. package/src/components/chat/streaming-bubble.tsx +217 -60
  96. package/src/components/chat/swarm-panel.test.ts +274 -0
  97. package/src/components/chat/swarm-panel.tsx +410 -0
  98. package/src/components/chat/swarm-status-card.tsx +346 -0
  99. package/src/components/chat/tool-call-bubble.tsx +48 -23
  100. package/src/components/chatrooms/chatroom-list.tsx +8 -5
  101. package/src/components/chatrooms/chatroom-message.tsx +10 -7
  102. package/src/components/chatrooms/chatroom-view.tsx +12 -9
  103. package/src/components/connectors/connector-health.tsx +6 -4
  104. package/src/components/connectors/connector-list.tsx +16 -11
  105. package/src/components/connectors/connector-sheet.tsx +12 -6
  106. package/src/components/home/home-view.tsx +38 -24
  107. package/src/components/input/chat-input.tsx +10 -1
  108. package/src/components/layout/app-layout.tsx +2 -38
  109. package/src/components/layout/sheet-layer.tsx +50 -0
  110. package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
  111. package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
  112. package/src/components/plugins/plugin-list.tsx +8 -4
  113. package/src/components/plugins/plugin-sheet.tsx +2 -1
  114. package/src/components/providers/provider-list.tsx +3 -2
  115. package/src/components/providers/provider-sheet.tsx +2 -1
  116. package/src/components/runs/run-list.tsx +11 -7
  117. package/src/components/schedules/schedule-card.tsx +5 -3
  118. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  119. package/src/components/shared/attachment-chip.tsx +19 -3
  120. package/src/components/shared/notification-center.tsx +6 -3
  121. package/src/components/shared/settings/plugin-manager.tsx +3 -2
  122. package/src/components/shared/settings/section-embedding.tsx +2 -1
  123. package/src/components/shared/settings/section-orchestrator.tsx +2 -1
  124. package/src/components/shared/settings/section-user-preferences.tsx +107 -0
  125. package/src/components/shared/settings/settings-page.tsx +13 -9
  126. package/src/components/skills/clawhub-browser.tsx +15 -4
  127. package/src/components/skills/skill-list.tsx +15 -4
  128. package/src/components/tasks/approvals-panel.tsx +2 -1
  129. package/src/components/tasks/task-board.tsx +35 -37
  130. package/src/components/tasks/task-sheet.tsx +4 -3
  131. package/src/components/ui/full-screen-loader.tsx +164 -0
  132. package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
  133. package/src/components/wallets/wallet-panel.tsx +6 -5
  134. package/src/components/wallets/wallet-section.tsx +3 -2
  135. package/src/components/webhooks/webhook-list.tsx +4 -5
  136. package/src/components/webhooks/webhook-sheet.tsx +6 -6
  137. package/src/hooks/use-app-bootstrap.ts +202 -0
  138. package/src/hooks/use-mounted-ref.ts +14 -0
  139. package/src/hooks/use-now.ts +31 -0
  140. package/src/hooks/use-openclaw-gateway.ts +2 -1
  141. package/src/instrumentation.ts +20 -8
  142. package/src/lib/agent-default-tools.test.ts +52 -0
  143. package/src/lib/agent-default-tools.ts +40 -0
  144. package/src/lib/api-client.test.ts +21 -0
  145. package/src/lib/api-client.ts +6 -11
  146. package/src/lib/canvas-content.test.ts +360 -0
  147. package/src/lib/chat-streaming-state.test.ts +49 -2
  148. package/src/lib/chat-streaming-state.ts +26 -10
  149. package/src/lib/fetch-timeout.test.ts +54 -0
  150. package/src/lib/fetch-timeout.ts +60 -3
  151. package/src/lib/live-tool-events.test.ts +77 -0
  152. package/src/lib/live-tool-events.ts +73 -0
  153. package/src/lib/local-observability.test.ts +2 -2
  154. package/src/lib/openclaw-endpoint.test.ts +1 -1
  155. package/src/lib/providers/anthropic.ts +12 -16
  156. package/src/lib/providers/index.ts +4 -2
  157. package/src/lib/providers/ollama.ts +9 -6
  158. package/src/lib/providers/openai.ts +11 -14
  159. package/src/lib/runtime-env.test.ts +8 -8
  160. package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
  161. package/src/lib/schedule-dedupe.test.ts +1 -1
  162. package/src/lib/schedule-dedupe.ts +3 -2
  163. package/src/lib/server/agent-thread-session.test.ts +6 -6
  164. package/src/lib/server/agent-thread-session.ts +6 -9
  165. package/src/lib/server/alert-dispatch.ts +2 -1
  166. package/src/lib/server/api-routes.test.ts +6 -6
  167. package/src/lib/server/approval-connector-notify.test.ts +4 -4
  168. package/src/lib/server/approvals-auto-approve.test.ts +29 -29
  169. package/src/lib/server/approvals.test.ts +317 -0
  170. package/src/lib/server/approvals.ts +5 -4
  171. package/src/lib/server/autonomy-runtime.test.ts +11 -11
  172. package/src/lib/server/browser-state.ts +2 -2
  173. package/src/lib/server/capability-router.test.ts +1 -1
  174. package/src/lib/server/capability-router.ts +3 -2
  175. package/src/lib/server/chat-execution-advanced.test.ts +15 -2
  176. package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
  177. package/src/lib/server/chat-execution-disabled.test.ts +3 -3
  178. package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
  179. package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
  180. package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
  181. package/src/lib/server/chat-execution-tool-events.ts +116 -0
  182. package/src/lib/server/chat-execution-utils.test.ts +479 -0
  183. package/src/lib/server/chat-execution-utils.ts +533 -0
  184. package/src/lib/server/chat-execution.ts +153 -748
  185. package/src/lib/server/chat-streaming-utils.ts +174 -0
  186. package/src/lib/server/chat-turn-tool-routing.ts +310 -0
  187. package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
  188. package/src/lib/server/clawhub-client.ts +2 -1
  189. package/src/lib/server/collection-helpers.test.ts +92 -0
  190. package/src/lib/server/collection-helpers.ts +25 -3
  191. package/src/lib/server/connectors/access.ts +146 -0
  192. package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
  193. package/src/lib/server/connectors/bluebubbles.ts +4 -4
  194. package/src/lib/server/connectors/commands.ts +367 -0
  195. package/src/lib/server/connectors/connector-routing.test.ts +4 -4
  196. package/src/lib/server/connectors/delivery.ts +142 -0
  197. package/src/lib/server/connectors/discord.ts +37 -40
  198. package/src/lib/server/connectors/email.ts +11 -10
  199. package/src/lib/server/connectors/googlechat.ts +4 -4
  200. package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
  201. package/src/lib/server/connectors/ingress-delivery.ts +23 -0
  202. package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
  203. package/src/lib/server/connectors/manager.test.ts +352 -77
  204. package/src/lib/server/connectors/manager.ts +134 -673
  205. package/src/lib/server/connectors/matrix.ts +4 -4
  206. package/src/lib/server/connectors/message-sentinel.ts +7 -0
  207. package/src/lib/server/connectors/openclaw.test.ts +1 -1
  208. package/src/lib/server/connectors/openclaw.ts +8 -10
  209. package/src/lib/server/connectors/outbox.test.ts +192 -0
  210. package/src/lib/server/connectors/outbox.ts +369 -0
  211. package/src/lib/server/connectors/pairing.test.ts +18 -1
  212. package/src/lib/server/connectors/pairing.ts +49 -4
  213. package/src/lib/server/connectors/policy.ts +9 -3
  214. package/src/lib/server/connectors/reconnect-state.ts +71 -0
  215. package/src/lib/server/connectors/response-media.ts +256 -0
  216. package/src/lib/server/connectors/runtime-state.ts +67 -0
  217. package/src/lib/server/connectors/session.test.ts +357 -0
  218. package/src/lib/server/connectors/session.ts +422 -0
  219. package/src/lib/server/connectors/signal.ts +7 -7
  220. package/src/lib/server/connectors/slack.ts +43 -43
  221. package/src/lib/server/connectors/teams.ts +4 -4
  222. package/src/lib/server/connectors/telegram.ts +37 -43
  223. package/src/lib/server/connectors/types.ts +31 -1
  224. package/src/lib/server/connectors/whatsapp.test.ts +108 -0
  225. package/src/lib/server/connectors/whatsapp.ts +106 -34
  226. package/src/lib/server/context-manager.test.ts +409 -0
  227. package/src/lib/server/cost.test.ts +1 -1
  228. package/src/lib/server/daemon-policy.ts +78 -0
  229. package/src/lib/server/daemon-state-connectors.test.ts +167 -0
  230. package/src/lib/server/daemon-state.test.ts +283 -55
  231. package/src/lib/server/daemon-state.ts +106 -109
  232. package/src/lib/server/data-dir.test.ts +5 -5
  233. package/src/lib/server/data-dir.ts +4 -0
  234. package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
  235. package/src/lib/server/delegation-jobs.test.ts +87 -0
  236. package/src/lib/server/delegation-jobs.ts +42 -48
  237. package/src/lib/server/devserver-launch.ts +1 -1
  238. package/src/lib/server/document-utils.ts +7 -9
  239. package/src/lib/server/elevenlabs.ts +2 -1
  240. package/src/lib/server/embeddings.test.ts +105 -0
  241. package/src/lib/server/ethereum.ts +3 -2
  242. package/src/lib/server/eval/agent-regression.ts +3 -2
  243. package/src/lib/server/eval/runner.ts +2 -1
  244. package/src/lib/server/eval/scorer.ts +2 -1
  245. package/src/lib/server/evm-swap.ts +2 -1
  246. package/src/lib/server/gateway/protocol.test.ts +1 -1
  247. package/src/lib/server/guardian.ts +2 -1
  248. package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
  249. package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
  250. package/src/lib/server/heartbeat-service.test.ts +406 -0
  251. package/src/lib/server/heartbeat-service.ts +54 -7
  252. package/src/lib/server/heartbeat-wake.test.ts +19 -0
  253. package/src/lib/server/heartbeat-wake.ts +17 -16
  254. package/src/lib/server/integrity-monitor.test.ts +149 -0
  255. package/src/lib/server/json-utils.ts +22 -0
  256. package/src/lib/server/knowledge-db.test.ts +13 -13
  257. package/src/lib/server/link-understanding.ts +2 -1
  258. package/src/lib/server/llm-response-cache.test.ts +1 -1
  259. package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
  260. package/src/lib/server/main-agent-loop.test.ts +6 -6
  261. package/src/lib/server/main-agent-loop.ts +21 -7
  262. package/src/lib/server/mcp-client.test.ts +1 -1
  263. package/src/lib/server/mcp-conformance.test.ts +1 -1
  264. package/src/lib/server/mcp-conformance.ts +3 -2
  265. package/src/lib/server/memory-consolidation.ts +2 -1
  266. package/src/lib/server/memory-db.test.ts +485 -0
  267. package/src/lib/server/memory-db.ts +39 -26
  268. package/src/lib/server/memory-graph.test.ts +2 -2
  269. package/src/lib/server/memory-policy.test.ts +7 -7
  270. package/src/lib/server/memory-retrieval.test.ts +1 -1
  271. package/src/lib/server/openclaw-config-sync.ts +2 -1
  272. package/src/lib/server/openclaw-deploy.test.ts +1 -1
  273. package/src/lib/server/openclaw-deploy.ts +8 -12
  274. package/src/lib/server/openclaw-exec-config.ts +2 -1
  275. package/src/lib/server/openclaw-gateway.ts +6 -7
  276. package/src/lib/server/openclaw-skills-normalize.ts +2 -1
  277. package/src/lib/server/openclaw-sync.ts +7 -5
  278. package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
  279. package/src/lib/server/orchestrator-lg.ts +199 -327
  280. package/src/lib/server/path-utils.ts +31 -0
  281. package/src/lib/server/perf.ts +161 -0
  282. package/src/lib/server/plugins-approval-guidance.ts +115 -0
  283. package/src/lib/server/plugins.test.ts +1 -1
  284. package/src/lib/server/plugins.ts +22 -132
  285. package/src/lib/server/process-manager.ts +5 -8
  286. package/src/lib/server/provider-health.test.ts +137 -0
  287. package/src/lib/server/provider-health.ts +3 -3
  288. package/src/lib/server/provider-model-discovery.ts +3 -12
  289. package/src/lib/server/queue-followups.test.ts +9 -9
  290. package/src/lib/server/queue-reconcile.test.ts +2 -2
  291. package/src/lib/server/queue-recovery.test.ts +269 -0
  292. package/src/lib/server/queue.test.ts +570 -0
  293. package/src/lib/server/queue.ts +62 -455
  294. package/src/lib/server/resolve-image.ts +30 -0
  295. package/src/lib/server/runtime-settings.test.ts +4 -4
  296. package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
  297. package/src/lib/server/schedule-normalization.test.ts +279 -0
  298. package/src/lib/server/schedule-service.ts +263 -0
  299. package/src/lib/server/scheduler.ts +17 -74
  300. package/src/lib/server/session-mailbox.test.ts +191 -0
  301. package/src/lib/server/session-run-manager.test.ts +640 -0
  302. package/src/lib/server/session-run-manager.ts +59 -15
  303. package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
  304. package/src/lib/server/session-tools/calendar.ts +2 -1
  305. package/src/lib/server/session-tools/canvas.ts +2 -1
  306. package/src/lib/server/session-tools/chatroom.ts +2 -1
  307. package/src/lib/server/session-tools/connector.ts +26 -28
  308. package/src/lib/server/session-tools/context-mgmt.ts +3 -2
  309. package/src/lib/server/session-tools/crawl.ts +4 -3
  310. package/src/lib/server/session-tools/crud.ts +105 -324
  311. package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
  312. package/src/lib/server/session-tools/delegate.ts +6 -8
  313. package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
  314. package/src/lib/server/session-tools/discovery.ts +4 -3
  315. package/src/lib/server/session-tools/document.ts +2 -1
  316. package/src/lib/server/session-tools/email.ts +2 -1
  317. package/src/lib/server/session-tools/extract.ts +2 -1
  318. package/src/lib/server/session-tools/file.ts +4 -3
  319. package/src/lib/server/session-tools/http.ts +2 -1
  320. package/src/lib/server/session-tools/human-loop.ts +2 -1
  321. package/src/lib/server/session-tools/image-gen.ts +4 -3
  322. package/src/lib/server/session-tools/index.ts +26 -30
  323. package/src/lib/server/session-tools/mailbox.ts +2 -1
  324. package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
  325. package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
  326. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
  327. package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
  328. package/src/lib/server/session-tools/monitor.ts +2 -1
  329. package/src/lib/server/session-tools/platform.ts +2 -1
  330. package/src/lib/server/session-tools/plugin-creator.ts +2 -1
  331. package/src/lib/server/session-tools/replicate.ts +3 -2
  332. package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
  333. package/src/lib/server/session-tools/shell.ts +4 -9
  334. package/src/lib/server/session-tools/subagent.ts +322 -170
  335. package/src/lib/server/session-tools/table.ts +6 -5
  336. package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
  337. package/src/lib/server/session-tools/wallet.ts +7 -6
  338. package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
  339. package/src/lib/server/session-tools/web-utils.ts +317 -0
  340. package/src/lib/server/session-tools/web.ts +62 -328
  341. package/src/lib/server/skill-prompt-budget.test.ts +1 -1
  342. package/src/lib/server/skills-normalize.ts +2 -1
  343. package/src/lib/server/storage-item-access.test.ts +302 -0
  344. package/src/lib/server/storage.ts +366 -314
  345. package/src/lib/server/stream-agent-chat.test.ts +82 -3
  346. package/src/lib/server/stream-agent-chat.ts +146 -510
  347. package/src/lib/server/stream-continuation.ts +412 -0
  348. package/src/lib/server/subagent-lineage.test.ts +647 -0
  349. package/src/lib/server/subagent-lineage.ts +435 -0
  350. package/src/lib/server/subagent-runtime.test.ts +484 -0
  351. package/src/lib/server/subagent-runtime.ts +419 -0
  352. package/src/lib/server/subagent-swarm.test.ts +391 -0
  353. package/src/lib/server/subagent-swarm.ts +564 -0
  354. package/src/lib/server/system-events.ts +3 -3
  355. package/src/lib/server/task-followups.test.ts +491 -0
  356. package/src/lib/server/task-followups.ts +391 -0
  357. package/src/lib/server/task-lifecycle.test.ts +205 -0
  358. package/src/lib/server/task-lifecycle.ts +200 -0
  359. package/src/lib/server/task-quality-gate.test.ts +1 -1
  360. package/src/lib/server/task-resume.ts +208 -0
  361. package/src/lib/server/task-service.test.ts +108 -0
  362. package/src/lib/server/task-service.ts +264 -0
  363. package/src/lib/server/task-validation.test.ts +1 -1
  364. package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
  365. package/src/lib/server/tool-capability-policy.test.ts +2 -2
  366. package/src/lib/server/tool-capability-policy.ts +3 -2
  367. package/src/lib/server/tool-planning.ts +2 -1
  368. package/src/lib/server/tool-retry.ts +2 -3
  369. package/src/lib/server/wake-dispatcher.test.ts +303 -0
  370. package/src/lib/server/wake-dispatcher.ts +318 -0
  371. package/src/lib/server/wake-mode.test.ts +161 -0
  372. package/src/lib/server/wake-mode.ts +174 -0
  373. package/src/lib/server/wallet-service.ts +8 -9
  374. package/src/lib/server/watch-jobs.ts +2 -1
  375. package/src/lib/server/workspace-context.ts +2 -2
  376. package/src/lib/shared-utils.test.ts +142 -0
  377. package/src/lib/shared-utils.ts +62 -0
  378. package/src/lib/tool-event-summary.ts +2 -1
  379. package/src/lib/view-routes.test.ts +100 -0
  380. package/src/lib/wallet.test.ts +322 -6
  381. package/src/proxy.test.ts +4 -4
  382. package/src/proxy.ts +2 -3
  383. package/src/stores/set-if-changed.ts +40 -0
  384. package/src/stores/slices/agent-slice.ts +111 -0
  385. package/src/stores/slices/auth-slice.ts +25 -0
  386. package/src/stores/slices/data-slice.ts +301 -0
  387. package/src/stores/slices/index.ts +7 -0
  388. package/src/stores/slices/session-slice.ts +112 -0
  389. package/src/stores/slices/task-slice.ts +63 -0
  390. package/src/stores/slices/ui-slice.ts +192 -0
  391. package/src/stores/use-app-store.ts +17 -822
  392. package/src/stores/use-approval-store.ts +2 -1
  393. package/src/stores/use-chat-store.ts +8 -1
  394. package/src/types/index.ts +10 -0
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs'
2
2
  import os from 'os'
3
- import path from 'path'
3
+ import { perf } from './perf'
4
4
  import {
5
5
  loadSessions,
6
6
  saveSessions,
@@ -10,7 +10,6 @@ import {
10
10
  loadAgents,
11
11
  loadSkills,
12
12
  loadSettings,
13
- loadUsage,
14
13
  appendUsage,
15
14
  active,
16
15
  } from './storage'
@@ -20,52 +19,102 @@ import { log } from './logger'
20
19
  import { logExecution } from './execution-log'
21
20
  import { buildToolDisciplineLines, streamAgentChat } from './stream-agent-chat'
22
21
  import { runLinkUnderstanding } from './link-understanding'
23
- import { buildSessionTools } from './session-tools'
24
- import type { StructuredToolInterface } from '@langchain/core/tools'
25
22
  import type { Session } from '@/types'
23
+ import type { ApprovalCategory } from '@/types'
26
24
  import { stripMainLoopMetaForPersistence } from './main-agent-loop'
27
25
  import { getPluginManager } from './plugins'
28
26
  import { isLocalOpenClawEndpoint, normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
29
- import { routeTaskIntent } from './capability-router'
30
27
  import { notify } from './ws-hub'
31
28
  import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
32
- import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
33
- import { pluginIdMatches } from './tool-aliases'
29
+ import { resolveSessionToolPolicy } from './tool-capability-policy'
34
30
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
31
+ import { buildWorkspaceContext } from './workspace-context'
32
+ import { resolveImagePath } from './resolve-image'
33
+ import {
34
+ applyContextClearBoundary,
35
+ shouldApplySessionFreshnessReset,
36
+ shouldAutoRouteHeartbeatAlerts,
37
+ shouldPersistInboundUserMessage,
38
+ translateRequestedToolInvocation,
39
+ normalizeAssistantArtifactLinks,
40
+ extractHeartbeatStatus,
41
+ shouldReplaceRecentAssistantMessage,
42
+ hasPersistableAssistantPayload,
43
+ getPersistedAssistantText,
44
+ getToolEventsSnapshotKey,
45
+ requestedToolNamesFromMessage,
46
+ hasDirectLocalCodingTools,
47
+ parseUsdLimit,
48
+ getTodaySpendUsd,
49
+ classifyHeartbeatResponse,
50
+ estimateConversationTone,
51
+ } from './chat-execution-utils'
52
+ import { runPostLlmToolRouting } from './chat-turn-tool-routing'
35
53
  import {
36
54
  getCachedLlmResponse,
37
55
  resolveLlmResponseCacheConfig,
38
56
  setCachedLlmResponse,
39
57
  type LlmResponseCacheKeyInput,
40
58
  } from './llm-response-cache'
41
- import { genId } from '@/lib/id'
42
59
  import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
43
- import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
60
+ import { markProviderFailure, markProviderSuccess } from './provider-health'
44
61
  import { isHeartbeatSource, isInternalHeartbeatRun } from './heartbeat-source'
45
62
  import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
46
63
  import { buildIdentityContinuityContext, refreshSessionIdentityState } from './identity-continuity'
47
64
  import { syncSessionArchiveMemory } from './session-archive-memory'
48
65
  import { evaluateSessionFreshness, resetSessionRuntime, resolveSessionResetPolicy } from './session-reset-policy'
49
66
  import { pruneStreamingAssistantArtifacts, upsertStreamingAssistantArtifact } from '@/lib/chat-streaming-state'
50
- import { resolveActiveProjectContext } from './project-context'
51
67
  import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from './assistant-control'
52
- import { buildToolEventAssistantSummary } from '@/lib/tool-event-summary'
53
68
  import { buildAgentDisabledMessage, isAgentDisabled } from './agent-availability'
54
- type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
69
+ import { errorMessage as toErrorMessage } from '@/lib/shared-utils'
70
+
71
+ export {
72
+ shouldApplySessionFreshnessReset,
73
+ shouldAutoRouteHeartbeatAlerts,
74
+ translateRequestedToolInvocation,
75
+ normalizeAssistantArtifactLinks,
76
+ requestedToolNamesFromMessage,
77
+ hasDirectLocalCodingTools,
78
+ }
55
79
 
56
- /** Slice history from the most recent context-clear marker forward */
57
- function applyContextClearBoundary(messages: Message[]): Message[] {
58
- const filterModelHistory = (items: Message[]) => items.filter((message) => message.historyExcluded !== true)
59
- for (let i = messages.length - 1; i >= 0; i--) {
60
- if (messages[i].kind === 'context-clear') return filterModelHistory(messages.slice(i + 1))
61
- }
62
- return filterModelHistory(messages)
80
+ export function buildAgentRuntimeCapabilities(enabledPlugins: string[]): string[] {
81
+ const capabilities = ['heartbeats', 'autonomous_loop', 'multi_agent_chat']
82
+ if (enabledPlugins.length > 0) capabilities.unshift('tools')
83
+ return capabilities
63
84
  }
64
85
 
65
- interface SessionWithTools {
66
- plugins?: string[] | null
67
- /** @deprecated Use plugins */
68
- tools?: string[] | null
86
+ export function buildNoToolsGuidance(): string[] {
87
+ return [
88
+ '## Tool Availability',
89
+ 'No session tools are enabled in this chat.',
90
+ 'Do not imply that a normal read-only action is waiting on user permission or approval when the real blocker is missing tool access.',
91
+ 'If browsing, web fetches, file edits, or other actions are unavailable, state that the capability is not enabled in this session.',
92
+ 'Only mention approval when a real runtime tool explicitly returned an approval requirement for a concrete action.',
93
+ ]
94
+ }
95
+
96
+ export function buildEnabledToolsAutonomyGuidance(options?: {
97
+ approvalsEnabled?: boolean
98
+ approvalAutoApproveCategories?: ApprovalCategory[] | null
99
+ }): string[] {
100
+ const lines = [
101
+ '## Tool Autonomy',
102
+ 'Enabled session tools are already available for normal use in this chat.',
103
+ 'Do not ask the user for permission before using enabled tools for ordinary read-only work, routine diagnostics, or reversible execution steps that are clearly part of the request.',
104
+ 'If the user asks you to use an enabled tool or to perform a task that clearly maps to an enabled tool, attempt that tool path before asking the user to do the work manually.',
105
+ 'Only surface approval or permission as a blocker when a real runtime tool result explicitly requires approval for a concrete state-changing action.',
106
+ ]
107
+ if (options?.approvalsEnabled === false) {
108
+ lines.push('Approvals are disabled platform-wide in this runtime. Do not tell the user that enabled tool use is waiting on approval.')
109
+ }
110
+ const autoApproved = Array.isArray(options?.approvalAutoApproveCategories)
111
+ ? options!.approvalAutoApproveCategories.filter((value): value is ApprovalCategory => typeof value === 'string' && value.trim().length > 0)
112
+ : []
113
+ if (autoApproved.length > 0) {
114
+ lines.push(`These approval categories auto-approve in this runtime: ${autoApproved.join(', ')}.`)
115
+ lines.push('If a tool-backed action falls into one of those categories, call the tool and let the runtime auto-approve it instead of asking the user first in prose.')
116
+ }
117
+ return lines
69
118
  }
70
119
 
71
120
  interface SessionWithCredentials {
@@ -105,15 +154,6 @@ export interface ExecuteChatTurnResult {
105
154
  estimatedCost?: number
106
155
  }
107
156
 
108
- export function shouldApplySessionFreshnessReset(source: string): boolean {
109
- return source !== 'eval'
110
- }
111
-
112
- function shouldPersistInboundUserMessage(internal: boolean, source: string): boolean {
113
- if (!internal) return true
114
- return source === 'eval'
115
- }
116
-
117
157
  function extractEventJson(line: string): SSEEvent | null {
118
158
  if (!line.startsWith('data: ')) return null
119
159
  try {
@@ -156,25 +196,6 @@ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
156
196
  }
157
197
  }
158
198
 
159
- function escapeRegExp(value: string): string {
160
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
161
- }
162
-
163
- function hasExplicitToolMention(message: string, toolName: string): boolean {
164
- const escaped = escapeRegExp(toolName)
165
- const negated = new RegExp(`\\b(?:do not|don't|dont|avoid|skip|without|never)\\s+(?:use\\s+|call\\s+|invoke\\s+)?(?:the\\s+)?\`?${escaped}\`?(?:\\s+tool)?\\b`, 'i')
166
- if (negated.test(message)) return false
167
- const boundary = new RegExp(`(^|[^a-z0-9_])\`?${escaped}\`?([^a-z0-9_]|$)`, 'i')
168
- return boundary.test(message)
169
- }
170
-
171
- function hasExplicitGenericToolRequest(message: string, toolName: string): boolean {
172
- const escaped = escapeRegExp(toolName)
173
- const negated = new RegExp(`\\b(?:do not|don't|dont|avoid|skip|without|never)\\s+(?:use\\s+|call\\s+|invoke\\s+)?(?:the\\s+)?${escaped}(?:\\s+tool)?\\b`, 'i')
174
- if (negated.test(message)) return false
175
- return new RegExp(`(^|[\\s(])\`${escaped}\`([\\s).,!?]|$)|\\b${escaped}\\s+tool\\b|\\buse\\s+(?:the\\s+)?${escaped}\\b|\\bcall\\s+(?:the\\s+)?${escaped}\\b|\\binvoke\\s+(?:the\\s+)?${escaped}\\b`, 'i').test(message)
176
- }
177
-
178
199
  export function dedupeConsecutiveToolEvents(events: MessageToolEvent[]): MessageToolEvent[] {
179
200
  const sameEvent = (left: MessageToolEvent, right: MessageToolEvent): boolean => (
180
201
  left.name === right.name
@@ -231,111 +252,6 @@ export function deriveTerminalRunError(params: {
231
252
  return undefined
232
253
  }
233
254
 
234
- function extractDelegateResponse(outputText: string): string | null {
235
- try {
236
- const parsed = JSON.parse(outputText) as Record<string, unknown>
237
- if (typeof parsed.response === 'string' && parsed.response.trim()) return parsed.response.trim()
238
- if (typeof parsed.result === 'string' && parsed.result.trim()) return parsed.result.trim()
239
- return null
240
- } catch {
241
- return null
242
- }
243
- }
244
-
245
- const MANAGE_PLATFORM_RESOURCE_TO_TOOL: Record<string, string> = {
246
- agent: 'manage_agents',
247
- agents: 'manage_agents',
248
- project: 'manage_projects',
249
- projects: 'manage_projects',
250
- task: 'manage_tasks',
251
- tasks: 'manage_tasks',
252
- schedule: 'manage_schedules',
253
- schedules: 'manage_schedules',
254
- skill: 'manage_skills',
255
- skills: 'manage_skills',
256
- document: 'manage_documents',
257
- documents: 'manage_documents',
258
- secret: 'manage_secrets',
259
- secrets: 'manage_secrets',
260
- connector: 'manage_connectors',
261
- connectors: 'manage_connectors',
262
- session: 'manage_sessions',
263
- sessions: 'manage_sessions',
264
- }
265
-
266
- export function translateRequestedToolInvocation(
267
- requestedName: string,
268
- rawArgs: Record<string, unknown>,
269
- messageFallback: string,
270
- availableToolNames?: Iterable<string>,
271
- ): { toolName: string; args: Record<string, unknown> } {
272
- const available = new Set(availableToolNames || [])
273
-
274
- if (requestedName === 'web_search') {
275
- return {
276
- toolName: 'web',
277
- args: {
278
- action: 'search',
279
- query: typeof rawArgs.query === 'string' ? rawArgs.query : messageFallback.trim(),
280
- maxResults: typeof rawArgs.maxResults === 'number' ? rawArgs.maxResults : 5,
281
- },
282
- }
283
- }
284
- if (requestedName === 'web_fetch') {
285
- return {
286
- toolName: 'web',
287
- args: {
288
- action: 'fetch',
289
- url: rawArgs.url,
290
- },
291
- }
292
- }
293
- if (requestedName === 'delegate_to_claude_code') {
294
- return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
295
- }
296
- if (requestedName === 'delegate_to_codex_cli') {
297
- return { toolName: 'delegate', args: { ...rawArgs, backend: 'codex' } }
298
- }
299
- if (requestedName === 'delegate_to_opencode_cli') {
300
- return { toolName: 'delegate', args: { ...rawArgs, backend: 'opencode' } }
301
- }
302
- if (requestedName === 'delegate_to_gemini_cli') {
303
- return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
304
- }
305
-
306
- const managePrefix = 'manage_'
307
- if (requestedName === 'manage_platform') {
308
- const resource = typeof rawArgs.resource === 'string'
309
- ? rawArgs.resource.trim().toLowerCase()
310
- : ''
311
- const specificTool = MANAGE_PLATFORM_RESOURCE_TO_TOOL[resource]
312
- if (specificTool && available.has(specificTool) && !available.has('manage_platform')) {
313
- return { toolName: specificTool, args: rawArgs }
314
- }
315
- return { toolName: requestedName, args: rawArgs }
316
- }
317
-
318
- if (requestedName.startsWith(managePrefix) && requestedName !== 'manage_platform') {
319
- if (!available.has(requestedName) && available.has('manage_platform')) {
320
- const resource = requestedName.slice(managePrefix.length)
321
- if (resource) {
322
- const { action, id, data, ...rest } = rawArgs
323
- const nextArgs: Record<string, unknown> = { resource, ...rest }
324
- if (action !== undefined) nextArgs.action = action
325
- if (id !== undefined) nextArgs.id = id
326
- if (data !== undefined) nextArgs.data = data
327
- return {
328
- toolName: 'manage_platform',
329
- args: nextArgs,
330
- }
331
- }
332
- }
333
- return { toolName: requestedName, args: rawArgs }
334
- }
335
-
336
- return { toolName: requestedName, args: rawArgs }
337
- }
338
-
339
255
  export function isLikelyToolErrorOutput(output: string): boolean {
340
256
  const trimmed = String(output || '').trim()
341
257
  if (!trimmed) return false
@@ -353,156 +269,10 @@ export function isLikelyToolErrorOutput(output: string): boolean {
353
269
  return false
354
270
  }
355
271
 
356
- function normalizeWorkspaceSandboxLinks(text: string, cwd: string): string {
357
- return text.replace(/\[([^\]]+)\]\(sandbox:\/workspace\/([^)]+)\)/g, (raw, label: string, relativePath: string) => {
358
- const normalized = String(relativePath || '').replace(/^\/+/, '')
359
- if (!normalized) return raw
360
- const resolvedCwd = path.resolve(cwd)
361
- const resolved = path.resolve(resolvedCwd, normalized)
362
- if (!resolved.startsWith(resolvedCwd)) return raw
363
- if (!fs.existsSync(resolved)) return raw
364
- return `[${label}](/api/files/serve?path=${encodeURIComponent(resolved)})`
365
- })
366
- }
367
-
368
- function normalizeAbsoluteFileMarkdownLinks(text: string): string {
369
- return text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (raw, label: string, target: string) => {
370
- if (!path.isAbsolute(target)) return raw
371
- const resolved = path.resolve(target)
372
- if (!fs.existsSync(resolved)) return raw
373
- return `[${label}](/api/files/serve?path=${encodeURIComponent(resolved)})`
374
- })
375
- }
376
-
377
- export function normalizeAssistantArtifactLinks(text: string, cwd: string): string {
378
- const uploadsNormalized = text.replace(/sandbox:\/api\/uploads\//g, '/api/uploads/')
379
- const workspaceNormalized = normalizeWorkspaceSandboxLinks(uploadsNormalized, cwd)
380
- return normalizeAbsoluteFileMarkdownLinks(workspaceNormalized)
381
- }
382
-
383
- function extractHeartbeatStatus(text: string): { goal?: string; status?: string; summary?: string; nextAction?: string } | null {
384
- const match = text.match(/\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i)
385
- if (!match) return null
386
- try {
387
- const meta = JSON.parse(match[1]) as Record<string, unknown>
388
- const payload: { goal?: string; status?: string; summary?: string; nextAction?: string } = {}
389
- if (typeof meta.goal === 'string' && meta.goal.trim()) payload.goal = meta.goal.trim()
390
- if (typeof meta.status === 'string' && meta.status.trim()) payload.status = meta.status.trim()
391
- if (typeof meta.summary === 'string' && meta.summary.trim()) payload.summary = meta.summary.trim()
392
- if (typeof meta.next_action === 'string' && meta.next_action.trim()) payload.nextAction = meta.next_action.trim()
393
- return Object.keys(payload).length > 0 ? payload : null
394
- } catch {
395
- return null
396
- }
397
- }
398
-
399
- function shouldReplaceRecentAssistantMessage(params: {
400
- previous: Message | null | undefined
401
- nextToolEvents: MessageToolEvent[]
402
- nextKind: Message['kind']
403
- now: number
404
- }): boolean {
405
- const { previous, nextToolEvents, nextKind, now } = params
406
- if (!previous || previous.role !== 'assistant') return false
407
- if (nextToolEvents.length === 0) return false
408
- if (previous.kind && nextKind && previous.kind !== nextKind) return false
409
- if (typeof previous.time === 'number' && now - previous.time > 45_000) return false
410
- const prevTools = Array.isArray(previous.toolEvents) ? previous.toolEvents.length : 0
411
- return prevTools === 0
412
- }
413
-
414
- function hasPersistableAssistantPayload(text: string, thinking: string, toolEvents: MessageToolEvent[]): boolean {
415
- return text.trim().length > 0 || thinking.trim().length > 0 || toolEvents.length > 0
416
- }
417
-
418
- function getPersistedAssistantText(text: string, toolEvents: MessageToolEvent[]): string {
419
- const trimmed = text.trim()
420
- if (trimmed) return trimmed
421
- return buildToolEventAssistantSummary(toolEvents)
422
- }
423
-
424
- function getToolEventsSnapshotKey(toolEvents: MessageToolEvent[]): string {
425
- return JSON.stringify(toolEvents.map((event) => [
426
- event.name,
427
- event.input,
428
- event.output || '',
429
- event.error === true,
430
- event.toolCallId || '',
431
- ]))
432
- }
433
-
434
272
  export function pruneSuppressedHeartbeatStreamMessage(messages: Message[]): boolean {
435
273
  return pruneStreamingAssistantArtifacts(messages)
436
274
  }
437
275
 
438
- export function requestedToolNamesFromMessage(message: string): string[] {
439
- const explicitCandidates = [
440
- 'delegate_to_claude_code',
441
- 'delegate_to_codex_cli',
442
- 'delegate_to_opencode_cli',
443
- 'delegate_to_gemini_cli',
444
- 'connector_message_tool',
445
- 'sessions_tool',
446
- 'whoami_tool',
447
- 'search_history_tool',
448
- 'manage_agents',
449
- 'manage_tasks',
450
- 'manage_schedules',
451
- 'manage_documents',
452
- 'manage_webhooks',
453
- 'manage_skills',
454
- 'manage_connectors',
455
- 'manage_sessions',
456
- 'manage_secrets',
457
- 'manage_capabilities',
458
- 'manage_platform',
459
- 'manage_chatrooms',
460
- 'search_marketplace',
461
- 'monitor_tool',
462
- 'plugin_creator_tool',
463
- 'memory_tool',
464
- 'memory_search',
465
- 'memory_get',
466
- 'memory_store',
467
- 'memory_update',
468
- 'wallet_tool',
469
- 'http_request',
470
- 'send_file',
471
- 'sandbox_exec',
472
- 'sandbox_list_runtimes',
473
- 'schedule_wake',
474
- 'spawn_subagent',
475
- 'ask_human',
476
- 'context_status',
477
- 'context_summarize',
478
- 'openclaw_nodes',
479
- 'openclaw_workspace',
480
- ]
481
- const genericCandidates = [
482
- 'browser',
483
- 'web',
484
- 'shell',
485
- 'files',
486
- 'edit_file',
487
- 'git',
488
- 'canvas',
489
- 'mailbox',
490
- 'document',
491
- 'extract',
492
- 'table',
493
- 'crawl',
494
- 'email',
495
- ]
496
- const requested = explicitCandidates.filter((name) => hasExplicitToolMention(message, name))
497
- for (const name of genericCandidates) {
498
- if (hasExplicitGenericToolRequest(message, name)) requested.push(name)
499
- }
500
- if (hasExplicitGenericToolRequest(message, 'delegate')) {
501
- requested.push('delegate')
502
- }
503
- return Array.from(new Set(requested))
504
- }
505
-
506
276
  function parseToolJsonObject(raw: string): Record<string, unknown> | null {
507
277
  const trimmed = raw.trim()
508
278
  if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) return null
@@ -568,216 +338,6 @@ export function reconcileConnectorDeliveryText(text: string, events: MessageTool
568
338
  return `I couldn't send that through the configured connector. ${failureSummary}`.trim()
569
339
  }
570
340
 
571
- function parseKeyValueArgs(raw: string): Record<string, string> {
572
- const out: Record<string, string> = {}
573
- const regex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*("([^"]*)"|'([^']*)'|[^\s,]+)/g
574
- let match: RegExpExecArray | null = null
575
- while ((match = regex.exec(raw)) !== null) {
576
- const key = match[1]
577
- const value = match[3] ?? match[4] ?? match[2] ?? ''
578
- out[key] = value.replace(/^['"]|['"]$/g, '').trim()
579
- }
580
- return out
581
- }
582
-
583
- function extractConnectorMessageArgs(message: string): {
584
- action:
585
- | 'list_running'
586
- | 'list_targets'
587
- | 'start'
588
- | 'stop'
589
- | 'send'
590
- | 'send_voice_note'
591
- | 'schedule_followup'
592
- platform?: string
593
- connectorId?: string
594
- to?: string
595
- message?: string
596
- voiceText?: string
597
- voiceId?: string
598
- imageUrl?: string
599
- fileUrl?: string
600
- mediaPath?: string
601
- mimeType?: string
602
- fileName?: string
603
- caption?: string
604
- delaySec?: number
605
- followUpMessage?: string
606
- followUpDelaySec?: number
607
- ptt?: boolean
608
- approved?: boolean
609
- } | null {
610
- if (!message.toLowerCase().includes('connector_message_tool')) return null
611
- const parsed = parseKeyValueArgs(message)
612
-
613
- let payload = parsed.message
614
- if (!payload) {
615
- const quoted = message.match(/message\s*=\s*("(.*?)"|'(.*?)')/i)
616
- if (quoted) payload = (quoted[2] || quoted[3] || '').trim()
617
- }
618
- if (!payload) {
619
- const raw = message.match(/message\s*=\s*([^\n]+)/i)
620
- if (raw?.[1]) {
621
- payload = raw[1]
622
- .replace(/\b(Return|Output|Then|Respond)\b[\s\S]*$/i, '')
623
- .trim()
624
- .replace(/^['"]|['"]$/g, '')
625
- }
626
- }
627
-
628
- const actionRaw = (parsed.action || 'send').toLowerCase()
629
- const action = (
630
- actionRaw === 'list_running'
631
- || actionRaw === 'list_targets'
632
- || actionRaw === 'start'
633
- || actionRaw === 'stop'
634
- || actionRaw === 'send'
635
- || actionRaw === 'send_voice_note'
636
- || actionRaw === 'schedule_followup'
637
- )
638
- ? actionRaw
639
- : 'send'
640
- const args: {
641
- action:
642
- | 'list_running'
643
- | 'list_targets'
644
- | 'start'
645
- | 'stop'
646
- | 'send'
647
- | 'send_voice_note'
648
- | 'schedule_followup'
649
- platform?: string
650
- connectorId?: string
651
- to?: string
652
- message?: string
653
- voiceText?: string
654
- voiceId?: string
655
- imageUrl?: string
656
- fileUrl?: string
657
- mediaPath?: string
658
- mimeType?: string
659
- fileName?: string
660
- caption?: string
661
- delaySec?: number
662
- followUpMessage?: string
663
- followUpDelaySec?: number
664
- ptt?: boolean
665
- approved?: boolean
666
- } = { action }
667
- const quoted = (key: string): string | undefined => {
668
- const m = message.match(new RegExp(`${key}\\s*=\\s*(\"([^\"]*)\"|'([^']*)')`, 'i'))
669
- return (m?.[2] || m?.[3] || '').trim() || undefined
670
- }
671
- if (parsed.platform) args.platform = parsed.platform
672
- if (parsed.connectorId) args.connectorId = parsed.connectorId
673
- if (parsed.to) args.to = parsed.to
674
- if (payload) args.message = payload
675
- if (parsed.voiceText) args.voiceText = parsed.voiceText
676
- if (parsed.voiceId) args.voiceId = parsed.voiceId
677
- args.imageUrl = parsed.imageUrl || quoted('imageUrl')
678
- args.fileUrl = parsed.fileUrl || quoted('fileUrl')
679
- args.mediaPath = parsed.mediaPath || quoted('mediaPath')
680
- args.mimeType = parsed.mimeType || quoted('mimeType')
681
- args.fileName = parsed.fileName || quoted('fileName')
682
- args.caption = parsed.caption || quoted('caption')
683
- if (parsed.followUpMessage) args.followUpMessage = parsed.followUpMessage
684
- if (parsed.delaySec && Number.isFinite(Number(parsed.delaySec))) args.delaySec = Number(parsed.delaySec)
685
- if (parsed.followUpDelaySec && Number.isFinite(Number(parsed.followUpDelaySec))) args.followUpDelaySec = Number(parsed.followUpDelaySec)
686
- if (parsed.ptt) args.ptt = ['true', '1', 'yes', 'on'].includes(parsed.ptt.toLowerCase())
687
- if (parsed.approved) args.approved = ['true', '1', 'yes', 'on'].includes(parsed.approved.toLowerCase())
688
- return args
689
- }
690
-
691
- function extractDelegationTask(message: string, toolName: string): string | null {
692
- if (!message.toLowerCase().includes(toolName.toLowerCase())) return null
693
- const patterns = [
694
- /task\s+exactly\s*:\s*"([^"]+)"/i,
695
- /task\s+exactly\s*:\s*'([^']+)'/i,
696
- /task\s+exactly\s*:\s*([^\n]+?)(?:\.\s|$)/i,
697
- /task\s*:\s*"([^"]+)"/i,
698
- /task\s*:\s*'([^']+)'/i,
699
- /task\s*:\s*([^\n]+?)(?:\.\s|$)/i,
700
- ]
701
- for (const re of patterns) {
702
- const m = message.match(re)
703
- const task = (m?.[1] || '').trim()
704
- if (task) return task
705
- }
706
- return null
707
- }
708
-
709
- function hasToolEnabled(session: SessionWithTools, toolName: string): boolean {
710
- return pluginIdMatches(session?.plugins || session?.tools || [], toolName)
711
- }
712
-
713
- export function hasDirectLocalCodingTools(session: SessionWithTools): boolean {
714
- return [
715
- 'shell',
716
- 'execute_command',
717
- 'files',
718
- 'edit_file',
719
- 'openclaw_workspace',
720
- 'sandbox',
721
- ].some((toolName) => hasToolEnabled(session, toolName))
722
- }
723
-
724
- function enabledDelegationTools(session: SessionWithTools): DelegateTool[] {
725
- const tools: DelegateTool[] = []
726
- if (hasToolEnabled(session, 'claude_code') || hasToolEnabled(session, 'delegate')) tools.push('delegate_to_claude_code')
727
- if (hasToolEnabled(session, 'codex_cli')) tools.push('delegate_to_codex_cli')
728
- if (hasToolEnabled(session, 'opencode_cli')) tools.push('delegate_to_opencode_cli')
729
- if (hasToolEnabled(session, 'gemini_cli')) tools.push('delegate_to_gemini_cli')
730
- return tools
731
- }
732
-
733
- function parseUsdLimit(value: unknown): number | null {
734
- const parsed = typeof value === 'number'
735
- ? value
736
- : typeof value === 'string'
737
- ? Number.parseFloat(value)
738
- : Number.NaN
739
- if (!Number.isFinite(parsed) || parsed <= 0) return null
740
- return Math.max(0.01, Math.min(1_000_000, parsed))
741
- }
742
-
743
- function getTodaySpendUsd(): number {
744
- const usage = loadUsage()
745
- const dayStart = new Date()
746
- dayStart.setHours(0, 0, 0, 0)
747
- const minTs = dayStart.getTime()
748
- let total = 0
749
- for (const records of Object.values(usage)) {
750
- for (const record of records || []) {
751
- const rec = record as Record<string, unknown>
752
- const ts = typeof rec?.timestamp === 'number' ? rec.timestamp : 0
753
- if (ts < minTs) continue
754
- const cost = typeof rec?.estimatedCost === 'number' ? rec.estimatedCost : 0
755
- if (Number.isFinite(cost) && cost > 0) total += cost
756
- }
757
- }
758
- return total
759
- }
760
-
761
- function findFirstUrl(text: string): string | null {
762
- const m = text.match(/https?:\/\/[^\s<>"')]+/i)
763
- return m?.[0] || null
764
- }
765
-
766
- function isMemoryListIntent(message: string): boolean {
767
- const text = message.toLowerCase()
768
- if (!/\bmemory|memories|remember\b/.test(text)) return false
769
- if (/\b(save|store|memorize|add to memory|write to memory|remember this)\b/.test(text)) return false
770
- if (/\bmemory_tool\b/.test(text)) return true
771
- return (
772
- /\blist\b[\s\w]{0,24}\bmemories\b/.test(text)
773
- || /\bshow\b[\s\w]{0,24}\bmemories\b/.test(text)
774
- || /\bget\b[\s\w]{0,24}\bmemories\b/.test(text)
775
- || /\bwhat\b[\s\w]{0,40}\bmemories\b/.test(text)
776
- || /\bwhat do you remember\b/.test(text)
777
- || /\brecall\b[\s\w]{0,24}\bmemories?\b/.test(text)
778
- )
779
- }
780
-
781
341
  function syncSessionFromAgent(sessionId: string): void {
782
342
  const sessions = loadSessions()
783
343
  const session = sessions[sessionId]
@@ -886,6 +446,7 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
886
446
 
887
447
  const settings = loadSettings()
888
448
  const parts: string[] = []
449
+ const enabledPlugins = Array.isArray(session.plugins) ? session.plugins : (Array.isArray(agent.plugins) ? agent.plugins : [])
889
450
 
890
451
  // 1. Identity & Persona (Grounded OpenClaw Style)
891
452
  const identityLines = [`## My Identity`]
@@ -904,7 +465,9 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
904
465
  const runtimeLines = [
905
466
  '## Runtime',
906
467
  `os=${process.platform} | host=${os.hostname()} | agent=${agent.id} | provider=${agent.provider} | model=${agent.model}`,
907
- `capabilities=tools,heartbeats,autonomous_loop,multi_agent_chat`,
468
+ `capabilities=${buildAgentRuntimeCapabilities(enabledPlugins).join(',')}`,
469
+ `approvals=${settings.approvalsEnabled === false ? 'disabled' : 'enabled_or_tool_specific'}`,
470
+ `approval_auto_approve_categories=${Array.isArray(settings.approvalAutoApproveCategories) && settings.approvalAutoApproveCategories.length > 0 ? settings.approvalAutoApproveCategories.join(',') : 'none'}`,
908
471
  ]
909
472
  parts.push(runtimeLines.join('\n'))
910
473
 
@@ -927,7 +490,6 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
927
490
 
928
491
  // 5b. Workspace context files (HEARTBEAT.md, IDENTITY.md, AGENTS.md, etc.)
929
492
  try {
930
- const { buildWorkspaceContext } = require('./workspace-context')
931
493
  const wsCtx = buildWorkspaceContext({ cwd: session.cwd })
932
494
  if (wsCtx.block) parts.push(wsCtx.block)
933
495
  } catch {
@@ -943,7 +505,14 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
943
505
  ]
944
506
  parts.push(thinkingHint.join('\n'))
945
507
 
946
- const enabledPlugins = Array.isArray(session.plugins) ? session.plugins : (Array.isArray(agent.plugins) ? agent.plugins : [])
508
+ if (enabledPlugins.length === 0) {
509
+ parts.push(buildNoToolsGuidance().join('\n'))
510
+ } else {
511
+ parts.push(buildEnabledToolsAutonomyGuidance({
512
+ approvalsEnabled: settings.approvalsEnabled,
513
+ approvalAutoApproveCategories: settings.approvalAutoApproveCategories,
514
+ }).join('\n'))
515
+ }
947
516
  const toolDisciplineLines = buildToolDisciplineLines(enabledPlugins)
948
517
  if (toolDisciplineLines.length > 0) parts.push(['## Tool Discipline', ...toolDisciplineLines].join('\n'))
949
518
  const operatingGuidance = getPluginManager().collectOperatingGuidance(enabledPlugins)
@@ -978,42 +547,6 @@ function resolveApiKeyForSession(session: SessionWithCredentials, provider: Prov
978
547
  return null
979
548
  }
980
549
 
981
- function stripMarkupForHeartbeat(text: string): string {
982
- return text
983
- .replace(/<[^>]*>/g, ' ') // strip HTML tags
984
- .replace(/&nbsp;/gi, ' ') // decode nbsp
985
- .replace(/^[*`~_]+/, '') // strip leading markdown
986
- .replace(/[*`~_]+$/, '') // strip trailing markdown
987
- .trim()
988
- }
989
-
990
- const HEARTBEAT_OK_RE = /HEARTBEAT_OK[^\w]{0,4}$/
991
- const NO_MESSAGE_RE = /NO_MESSAGE[^\w]{0,4}$/
992
-
993
- function classifyHeartbeatResponse(text: string, ackMaxChars: number, hadToolCalls: boolean): 'suppress' | 'strip' | 'keep' {
994
- const cleaned = stripMarkupForHeartbeat(text)
995
- if (cleaned === 'HEARTBEAT_OK' || cleaned === 'NO_MESSAGE') return 'suppress'
996
- if (HEARTBEAT_OK_RE.test(cleaned) || NO_MESSAGE_RE.test(cleaned)) return 'suppress'
997
- const stripped = cleaned.replace(/HEARTBEAT_OK/gi, '').replace(/NO_MESSAGE/gi, '').trim()
998
- if (!stripped) return 'suppress'
999
- if (!hadToolCalls && stripped.length <= ackMaxChars) return 'suppress'
1000
- return stripped.length < cleaned.length ? 'strip' : 'keep'
1001
- }
1002
-
1003
- function estimateConversationTone(text: string): string {
1004
- const t = text || ''
1005
- // Technical: code blocks, function signatures, technical terms
1006
- if (/```/.test(t) || /\b(function|const|let|var|import|export|class|interface|async|await|return)\b/.test(t)) return 'technical'
1007
- if (/\b(error|bug|debug|stack trace|exception|null|undefined|TypeError)\b/i.test(t)) return 'technical'
1008
- // Empathetic: emotional/supportive language
1009
- if (/\b(understand|feel|sorry|empathize|appreciate|grateful|tough|difficult|challenging)\b/i.test(t)) return 'empathetic'
1010
- // Formal: academic/business language
1011
- if (/\b(furthermore|regarding|consequently|therefore|henceforth|pursuant|accordingly|notwithstanding)\b/i.test(t)) return 'formal'
1012
- // Casual: contractions, exclamations, informal language
1013
- if (/\b(gonna|wanna|gotta|yeah|hey|awesome|cool|lol|btw|tbh)\b/i.test(t) || /!{2,}/.test(t)) return 'casual'
1014
- return 'neutral'
1015
- }
1016
-
1017
550
 
1018
551
  export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promise<ExecuteChatTurnResult> {
1019
552
  const { message } = input
@@ -1029,6 +562,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1029
562
  signal,
1030
563
  } = input
1031
564
 
565
+ // Resolve image path early: if the filesystem path is gone, fall back to
566
+ // the upload URL which resolveImagePath maps back to the uploads directory.
567
+ const resolvedImagePath = resolveImagePath(imagePath, imageUrl) ?? undefined
568
+
569
+ const endTurnPerf = perf.start('chat-execution', 'executeSessionChatTurn', { sessionId, source })
570
+
1032
571
  syncSessionFromAgent(sessionId)
1033
572
 
1034
573
  const sessions = loadSessions()
@@ -1266,9 +805,19 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1266
805
  let lastPartialSaveAt = 0
1267
806
  let lastPartialSnapshotKey = ''
1268
807
  let partialSaveTimeout: ReturnType<typeof setTimeout> | null = null
808
+ let partialPersistenceClosed = false
809
+
810
+ const stopPartialAssistantPersistence = () => {
811
+ partialPersistenceClosed = true
812
+ if (partialSaveTimeout) {
813
+ clearTimeout(partialSaveTimeout)
814
+ partialSaveTimeout = null
815
+ }
816
+ }
1269
817
 
1270
818
  const persistStreamingAssistantArtifact = () => {
1271
819
  partialSaveTimeout = null
820
+ if (partialPersistenceClosed) return
1272
821
  const persistedToolEvents = toolEvents.length ? dedupeConsecutiveToolEvents([...toolEvents]) : []
1273
822
  if (!hasPersistableAssistantPayload(streamingPartialText, thinkingText, persistedToolEvents)) return
1274
823
 
@@ -1306,6 +855,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1306
855
  }
1307
856
 
1308
857
  const queuePartialAssistantPersist = (immediate = false) => {
858
+ if (partialPersistenceClosed) return
1309
859
  const now = Date.now()
1310
860
  const minIntervalMs = 400
1311
861
  if (immediate || now - lastPartialSaveAt >= minIntervalMs) {
@@ -1398,12 +948,21 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1398
948
  let responseCacheHit = false
1399
949
  let responseCacheInput: LlmResponseCacheKeyInput | null = null
1400
950
  const useLocalOpenClawNativeRuntime = providerType === 'openclaw' && isLocalOpenClawEndpoint(sessionForRun.apiEndpoint)
1401
- const hasPlugins = !!(sessionForRun.plugins?.length || sessionForRun.tools?.length)
951
+ const enabledSessionPlugins = Array.isArray(sessionForRun.plugins)
952
+ ? sessionForRun.plugins
953
+ : (Array.isArray(sessionForRun.tools) ? sessionForRun.tools : [])
954
+ const hasPlugins = enabledSessionPlugins.length > 0
1402
955
  && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
1403
956
  && !useLocalOpenClawNativeRuntime
1404
957
 
1405
958
  let durationMs = 0
1406
959
  const startTs = Date.now()
960
+ const endLlmPerf = perf.start('chat-execution', 'llm-round-trip', {
961
+ sessionId,
962
+ provider: providerType,
963
+ hasPlugins,
964
+ pluginCount: enabledSessionPlugins.length,
965
+ })
1407
966
  try {
1408
967
  // Heartbeat runs get a small tail of recent messages so the agent can see
1409
968
  // prior findings and avoid repeating the same searches. Full history is
@@ -1412,12 +971,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1412
971
  ? getSessionMessages(sessionId).slice(-6)
1413
972
  : undefined
1414
973
 
1415
- console.log(`[chat-execution] provider=${providerType}, hasPlugins=${hasPlugins}, localOpenClawNative=${useLocalOpenClawNativeRuntime}, imagePath=${imagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, plugins=${(sessionForRun.plugins || sessionForRun.tools || []).length}`)
974
+ console.log(`[chat-execution] provider=${providerType}, hasPlugins=${hasPlugins}, localOpenClawNative=${useLocalOpenClawNativeRuntime}, imagePath=${resolvedImagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, plugins=${enabledSessionPlugins.length}`)
1416
975
  if (hasPlugins) {
1417
976
  const result = await streamAgentChat({
1418
977
  session: sessionForRun,
1419
978
  message: effectiveMessage,
1420
- imagePath,
979
+ imagePath: resolvedImagePath,
1421
980
  attachedFiles,
1422
981
  apiKey,
1423
982
  systemPrompt,
@@ -1464,7 +1023,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1464
1023
  fullResponse = await provider.handler.streamChat({
1465
1024
  session: sessionForRun,
1466
1025
  message: effectiveMessage,
1467
- imagePath,
1026
+ imagePath: resolvedImagePath,
1468
1027
  apiKey,
1469
1028
  systemPrompt,
1470
1029
  write: (raw: string) => parseAndEmit(raw),
@@ -1484,8 +1043,10 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1484
1043
  }
1485
1044
  }
1486
1045
  durationMs = Date.now() - startTs
1046
+ endLlmPerf({ durationMs, cacheHit: responseCacheHit })
1487
1047
  } catch (err: unknown) {
1488
- errorMessage = err instanceof Error ? err.message : String(err)
1048
+ endLlmPerf({ error: true })
1049
+ errorMessage = toErrorMessage(err)
1489
1050
  const failureText = errorMessage || 'Run failed.'
1490
1051
  markProviderFailure(providerType, failureText)
1491
1052
  emit({ t: 'err', text: failureText })
@@ -1497,7 +1058,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1497
1058
  })
1498
1059
  } finally {
1499
1060
  clearInterval(partialSaveTimer)
1500
- if (partialSaveTimeout) clearTimeout(partialSaveTimeout)
1061
+ stopPartialAssistantPersistence()
1501
1062
  active.delete(sessionId)
1502
1063
  if (signal) signal.removeEventListener('abort', abortFromOutside)
1503
1064
  }
@@ -1535,197 +1096,32 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1535
1096
  }
1536
1097
  }
1537
1098
 
1538
- const requestedToolNames = (!internal && source === 'chat')
1539
- ? requestedToolNamesFromMessage(message)
1540
- : []
1541
- const routingDecision = (!internal && source === 'chat')
1542
- ? routeTaskIntent(message, pluginsForRun, appSettings)
1543
- : null
1544
- const calledNames = new Set((toolEvents || []).map((t) => t.name))
1545
-
1546
- const invokeSessionTool = async (toolName: string, args: Record<string, unknown>, failurePrefix: string): Promise<boolean> => {
1547
- const blockedReason = resolveConcreteToolPolicyBlock(toolName, toolPolicy, appSettings)
1548
- if (blockedReason) {
1549
- emit({ t: 'err', text: `Capability policy blocked tool invocation "${toolName}": ${blockedReason}` })
1550
- return false
1551
- }
1552
- if (
1553
- appSettings.safetyRequireApprovalForOutbound === true
1554
- && toolName === 'connector_message_tool'
1555
- && source !== 'chat'
1556
- ) {
1557
- emit({ t: 'err', text: 'Outbound connector messaging requires explicit user approval.' })
1558
- return false
1559
- }
1560
- const agent = session.agentId ? loadAgents()[session.agentId] : null
1561
- const activeProjectContext = resolveActiveProjectContext(session)
1562
- const { tools, cleanup } = await buildSessionTools(session.cwd, sessionForRun.plugins || sessionForRun.tools || [], {
1563
- agentId: session.agentId || null,
1564
- sessionId,
1565
- platformAssignScope: agent?.platformAssignScope || 'self',
1566
- mcpServerIds: agent?.mcpServerIds,
1567
- mcpDisabledTools: agent?.mcpDisabledTools,
1568
- projectId: activeProjectContext.projectId,
1569
- projectRoot: activeProjectContext.projectRoot,
1570
- projectName: activeProjectContext.project?.name || null,
1571
- projectDescription: activeProjectContext.project?.description || null,
1572
- memoryScopeMode: (((session as unknown as Record<string, unknown>).memoryScopeMode as string | null | undefined) ?? agent?.memoryScopeMode ?? null),
1573
- })
1574
- try {
1575
- const directTool = tools.find((t) => t?.name === toolName) as StructuredToolInterface | undefined
1576
- const availableToolNames = tools.map((candidate) => candidate?.name).filter(Boolean)
1577
- const translated = directTool
1578
- ? { toolName, args }
1579
- : translateRequestedToolInvocation(toolName, args, message, availableToolNames)
1580
- const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
1581
- if (!selectedTool?.invoke) return false
1582
- const toolInput = JSON.stringify(translated.args)
1583
- const toolCallId = genId()
1584
- emit({ t: 'tool_call', toolName, toolInput, toolCallId })
1585
- const toolOutput = await selectedTool.invoke(translated.args)
1586
- const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
1587
- emit({ t: 'tool_result', toolName, toolOutput: outputText, toolCallId })
1588
- const delegateResponse = (
1589
- toolName === 'delegate'
1590
- || toolName.startsWith('delegate_to_')
1591
- ) ? extractDelegateResponse(outputText) : null
1592
- if (delegateResponse) {
1593
- fullResponse = delegateResponse
1594
- } else if (!fullResponse.trim() && outputText?.trim()) {
1595
- // Don't overwrite fullResponse with raw tool output — it's already captured
1596
- // in toolEvents. Only set a brief notice when the LLM produced no text,
1597
- // so the message bubble isn't empty.
1598
- const label = toolName.replace(/_/g, ' ')
1599
- fullResponse = `Used **${label}** — see tool output above for details.`
1600
- }
1601
- calledNames.add(toolName)
1602
- return true
1603
- } catch (forceErr: unknown) {
1604
- emit({ t: 'err', text: `${failurePrefix}: ${forceErr instanceof Error ? forceErr.message : String(forceErr)}` })
1605
- return false
1606
- } finally {
1607
- await cleanup()
1608
- }
1609
- }
1610
-
1611
- if (requestedToolNames.includes('connector_message_tool') && !calledNames.has('connector_message_tool')) {
1612
- const forcedArgs = extractConnectorMessageArgs(message)
1613
- if (forcedArgs) {
1614
- await invokeSessionTool(
1615
- 'connector_message_tool',
1616
- forcedArgs as unknown as Record<string, unknown>,
1617
- 'Forced connector_message_tool invocation failed',
1618
- )
1619
- }
1620
- }
1621
-
1622
- const forcedDelegationTools: DelegateTool[] = [
1623
- 'delegate_to_claude_code',
1624
- 'delegate_to_codex_cli',
1625
- 'delegate_to_opencode_cli',
1626
- 'delegate_to_gemini_cli',
1627
- ]
1628
- for (const toolName of forcedDelegationTools) {
1629
- if (!requestedToolNames.includes(toolName)) continue
1630
- if (calledNames.has(toolName)) continue
1631
- const task = extractDelegationTask(message, toolName)
1632
- if (!task) continue
1633
- await invokeSessionTool(toolName, { task }, `Forced ${toolName} invocation failed`)
1634
- }
1635
-
1636
- const hasDelegationCall = forcedDelegationTools.some((toolName) => calledNames.has(toolName))
1637
- const enabledDelegateTools = enabledDelegationTools(sessionForRun)
1638
- const shouldAutoDelegateCoding = (!internal && source === 'chat')
1639
- && enabledDelegateTools.length > 0
1640
- && !hasDelegationCall
1641
- && calledNames.size === 0
1642
- && !requestedToolNames.length
1643
- && !hasDirectLocalCodingTools(sessionForRun)
1644
- && routingDecision?.intent === 'coding'
1645
-
1646
- if (shouldAutoDelegateCoding) {
1647
- const baseDelegationOrder = routingDecision?.preferredDelegates?.length
1648
- ? routingDecision.preferredDelegates
1649
- : forcedDelegationTools
1650
- const delegationOrder = rankDelegatesByHealth(baseDelegationOrder as DelegateTool[])
1651
- .filter((tool) => enabledDelegateTools.includes(tool))
1652
- for (const delegateTool of delegationOrder) {
1653
- const invoked = await invokeSessionTool(delegateTool, { task: effectiveMessage.trim() }, 'Auto-delegation failed')
1654
- if (invoked) break
1655
- }
1656
- }
1657
-
1658
- const shouldFailoverDelegate = (!internal && source === 'chat')
1659
- && !!errorMessage
1660
- && !(fullResponse || '').trim()
1661
- && enabledDelegateTools.length > 0
1662
- && !hasDelegationCall
1663
- && (routingDecision?.intent === 'coding' || routingDecision?.intent === 'general')
1664
- if (shouldFailoverDelegate) {
1665
- const preferred = routingDecision?.preferredDelegates?.length
1666
- ? routingDecision.preferredDelegates
1667
- : forcedDelegationTools
1668
- const fallbackOrder = rankDelegatesByHealth(preferred as DelegateTool[])
1669
- .filter((tool) => enabledDelegateTools.includes(tool))
1670
- for (const delegateTool of fallbackOrder) {
1671
- const invoked = await invokeSessionTool(
1672
- delegateTool,
1673
- { task: effectiveMessage.trim() },
1674
- `Provider failover via ${delegateTool} failed`,
1675
- )
1676
- if (invoked) {
1677
- errorMessage = undefined
1678
- break
1679
- }
1680
- }
1681
- }
1682
-
1683
- const canAutoRouteWithTools = (!internal && source === 'chat')
1684
- && !!routingDecision
1685
- && calledNames.size === 0
1686
- && requestedToolNames.length === 0
1687
-
1688
- if (canAutoRouteWithTools && routingDecision?.intent === 'browsing' && routingDecision.primaryUrl && hasToolEnabled(sessionForRun, 'browser')) {
1689
- await invokeSessionTool(
1690
- 'browser',
1691
- { action: 'read_page', url: routingDecision.primaryUrl },
1692
- 'Auto browser routing failed',
1693
- )
1694
- }
1695
-
1696
- if (canAutoRouteWithTools && routingDecision?.intent === 'research') {
1697
- const routeUrl = routingDecision.primaryUrl || findFirstUrl(message)
1698
- if (routeUrl && hasToolEnabled(sessionForRun, 'web_fetch')) {
1699
- await invokeSessionTool('web_fetch', { url: routeUrl }, 'Auto web_fetch routing failed')
1700
- } else if (hasToolEnabled(sessionForRun, 'web_search')) {
1701
- await invokeSessionTool('web_search', { query: effectiveMessage.trim(), maxResults: 5 }, 'Auto web_search routing failed')
1702
- }
1703
- }
1704
-
1705
- if (
1706
- canAutoRouteWithTools
1707
- && calledNames.size === 0
1708
- && hasToolEnabled(sessionForRun, 'memory')
1709
- && isMemoryListIntent(message)
1710
- ) {
1711
- await invokeSessionTool(
1712
- 'memory_tool',
1713
- { action: 'list', key: '', scope: 'auto' },
1714
- 'Auto memory listing failed',
1715
- )
1716
- }
1717
-
1718
- if (requestedToolNames.length > 0) {
1719
- const missed = requestedToolNames.filter((name) => !calledNames.has(name))
1720
- if (missed.length > 0) {
1721
- const notice = `Tool execution notice: requested tool(s) ${missed.join(', ')} were not actually invoked in this run.`
1722
- emit({ t: 'err', text: notice })
1723
- if (!fullResponse.includes('Tool execution notice:')) {
1724
- const trimmedResponse = (fullResponse || '').trim()
1725
- fullResponse = trimmedResponse
1726
- ? `${trimmedResponse}\n\n${notice}`
1727
- : notice
1728
- }
1099
+ const endPostProcessPerf = perf.start('chat-execution', 'post-process', { sessionId })
1100
+ const toolRoutingResult = await runPostLlmToolRouting({
1101
+ session: sessionForRun,
1102
+ sessionId,
1103
+ message,
1104
+ effectiveMessage,
1105
+ enabledPlugins: pluginsForRun,
1106
+ toolPolicy,
1107
+ appSettings,
1108
+ internal,
1109
+ source,
1110
+ toolEvents,
1111
+ emit,
1112
+ }, fullResponse, errorMessage)
1113
+
1114
+ fullResponse = toolRoutingResult.fullResponse
1115
+ errorMessage = toolRoutingResult.errorMessage
1116
+
1117
+ if (toolRoutingResult.missedRequestedTools.length > 0) {
1118
+ const notice = `Tool execution notice: requested tool(s) ${toolRoutingResult.missedRequestedTools.join(', ')} were not actually invoked in this run.`
1119
+ emit({ t: 'err', text: notice })
1120
+ if (!fullResponse.includes('Tool execution notice:')) {
1121
+ const trimmedResponse = (fullResponse || '').trim()
1122
+ fullResponse = trimmedResponse
1123
+ ? `${trimmedResponse}\n\n${notice}`
1124
+ : notice
1729
1125
  }
1730
1126
  }
1731
1127
 
@@ -1955,6 +1351,15 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1955
1351
  notify(`messages:${sessionId}`)
1956
1352
  }
1957
1353
 
1354
+ endPostProcessPerf({ toolEventCount: persistedToolEvents.length })
1355
+ endTurnPerf({
1356
+ durationMs,
1357
+ toolEventCount: persistedToolEvents.length,
1358
+ inputTokens: accumulatedUsage.inputTokens || 0,
1359
+ outputTokens: accumulatedUsage.outputTokens || 0,
1360
+ error: !!errorMessage,
1361
+ })
1362
+
1958
1363
  return {
1959
1364
  runId,
1960
1365
  sessionId,