@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
@@ -0,0 +1,174 @@
1
+ import type { MessageToolEvent } from '@/types'
2
+ import { canonicalizePluginId } from './tool-aliases'
3
+ import { extractSuggestions } from './suggestions'
4
+ import { isDirectMemoryWriteRequest } from './memory-policy'
5
+ import {
6
+ isBroadGoal,
7
+ looksLikeExternalWalletTask,
8
+ looksLikeOpenEndedDeliverableTask,
9
+ } from './stream-continuation'
10
+
11
+ export function getExplicitRequiredToolNames(userMessage: string, enabledPlugins: string[]): string[] {
12
+ const normalized = userMessage.toLowerCase()
13
+ const required: string[] = []
14
+ const hasEnabledTool = (toolId: string) => enabledPlugins.some((enabled) => (canonicalizePluginId(enabled) || enabled) === toolId)
15
+
16
+ if (hasEnabledTool('ask_human')
17
+ && (/\bask_human\b/.test(normalized) || /ask the human/.test(normalized) || /request_input/.test(normalized))) {
18
+ required.push('ask_human')
19
+ }
20
+
21
+ if (hasEnabledTool('email')
22
+ && (/\bemail\b/.test(normalized) || /send a welcome email/.test(normalized) || /send an email/.test(normalized))) {
23
+ required.push('email')
24
+ }
25
+
26
+ if (
27
+ hasEnabledTool('shell')
28
+ && (
29
+ /\bcurl request\b/.test(normalized)
30
+ || /\b(?:run|execute|do|use|try)\b[\s\S]{0,40}\bcurl\b/.test(normalized)
31
+ || /\brun (?:this )?command\b/.test(normalized)
32
+ || /\buse (?:the )?(?:shell|terminal)\b/.test(normalized)
33
+ || /\bin (?:the )?terminal\b/.test(normalized)
34
+ )
35
+ ) {
36
+ required.push('shell')
37
+ }
38
+
39
+ return required
40
+ }
41
+
42
+ export function shouldForceExternalServiceSummary(params: {
43
+ userMessage: string
44
+ finalResponse: string
45
+ hasToolCalls: boolean
46
+ toolEventCount: number
47
+ }): boolean {
48
+ if (!looksLikeExternalWalletTask(params.userMessage)) return false
49
+ if (!params.hasToolCalls || params.toolEventCount === 0) return false
50
+ const trimmed = params.finalResponse.trim()
51
+ if (!trimmed) return true
52
+ if (/\b(blocker|blocked|cannot|can't|requires|need|missing|last reversible step|next step)\b/i.test(trimmed)) return false
53
+ if (trimmed.length >= 240 && !/(let me|i'll|i will|checking|verify|promising|look into|explore|access their interface)/i.test(trimmed)) return false
54
+ return /:$/.test(trimmed) || /(let me|i'll|i will|checking|verify|promising|look into|explore|access their interface)/i.test(trimmed) || trimmed.length < 240
55
+ }
56
+
57
+ export function resolveToolAction(input: unknown): string {
58
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
59
+ const action = (input as Record<string, unknown>).action
60
+ return typeof action === 'string' ? action.trim().toLowerCase() : ''
61
+ }
62
+ if (typeof input !== 'string') return ''
63
+ const trimmed = input.trim()
64
+ if (!trimmed.startsWith('{')) return ''
65
+ try {
66
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>
67
+ return typeof parsed.action === 'string' ? parsed.action.trim().toLowerCase() : ''
68
+ } catch {
69
+ return ''
70
+ }
71
+ }
72
+
73
+ export function shouldTerminateOnSuccessfulMemoryMutation(params: {
74
+ toolName: string
75
+ toolInput: unknown
76
+ toolOutput: string
77
+ }): boolean {
78
+ const canonicalToolName = canonicalizePluginId(params.toolName) || params.toolName
79
+ if (canonicalToolName !== 'memory') return false
80
+ const exactToolName = String(params.toolName || '').trim().toLowerCase()
81
+ const action = exactToolName === 'memory_store'
82
+ ? 'store'
83
+ : exactToolName === 'memory_update'
84
+ ? 'update'
85
+ : resolveToolAction(params.toolInput)
86
+ if (action !== 'store' && action !== 'update') return false
87
+ const output = extractSuggestions(params.toolOutput || '').clean.trim()
88
+ if (!output || /^error[:\s]/i.test(output)) return false
89
+ if (!/^(stored|updated) memory\b/i.test(output)) return false
90
+ return /no further memory lookup is needed unless the user asked you to verify/i.test(output)
91
+ }
92
+
93
+ export function getWalletApprovalBoundaryAction(output: string): string | null {
94
+ if (!output.includes('plugin_wallet_')) return null
95
+ if (/"type":"plugin_wallet_transfer_request"/.test(output)) return 'send'
96
+ const actionMatch = output.match(/"action":"([^"]+)"/)
97
+ const action = actionMatch?.[1] || ''
98
+ if (!action) return null
99
+ const readOnlyActions = new Set([
100
+ 'balance',
101
+ 'address',
102
+ 'transactions',
103
+ 'encode_contract_call',
104
+ 'simulate_transaction',
105
+ ])
106
+ return readOnlyActions.has(action) ? null : action
107
+ }
108
+
109
+ export function isWalletSimulationResult(toolName: string, output: string): boolean {
110
+ return toolName === 'wallet_tool' && /"status":"simulated"/.test(output)
111
+ }
112
+
113
+ export function updateStreamedToolEvents(
114
+ events: MessageToolEvent[],
115
+ event: { type: 'call' | 'result'; name: string; input?: string; output?: string; toolCallId?: string },
116
+ ) {
117
+ if (event.type === 'call') {
118
+ events.push({
119
+ name: event.name,
120
+ input: event.input || '',
121
+ toolCallId: event.toolCallId,
122
+ })
123
+ return
124
+ }
125
+ const index = event.toolCallId
126
+ ? events.findLastIndex((entry) => entry.toolCallId === event.toolCallId && !entry.output)
127
+ : events.findLastIndex((entry) => entry.name === event.name && !entry.output)
128
+ if (index === -1) return
129
+ events[index] = {
130
+ ...events[index],
131
+ output: event.output || '',
132
+ }
133
+ }
134
+
135
+ export function compactThreadRecallText(text: string, maxChars = 180): string {
136
+ const compact = extractSuggestions(text || '').clean.replace(/\s+/g, ' ').trim()
137
+ if (!compact) return ''
138
+ return compact.length > maxChars ? `${compact.slice(0, maxChars - 3)}...` : compact
139
+ }
140
+
141
+ const DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE = /\b(?:then|and then|after that)?\s*(?:confirm|recap|repeat|summarize|tell me|say)\b[\s\S]{0,120}\b(?:stored|saved|updated|remembered|wrote|write)\b/i
142
+ const DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE = /\b(?:then|and then|after that|also)\b[\s\S]{0,160}\b(?:write|create|send|email|message|delegate|research|search|browse|open|edit|build|schedule|plan|review|analy[sz]e)\b/i
143
+
144
+ export function isNarrowDirectMemoryWriteTurn(message: string): boolean {
145
+ const trimmed = String(message || '').trim()
146
+ if (!trimmed || !isDirectMemoryWriteRequest(trimmed)) return false
147
+ if (looksLikeOpenEndedDeliverableTask(trimmed)) return false
148
+ if (DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE.test(trimmed) && !DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed)) {
149
+ return false
150
+ }
151
+ return !isBroadGoal(trimmed) || DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed) || !/[?]$/.test(trimmed)
152
+ }
153
+
154
+ const CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS = new Set([
155
+ 'memory',
156
+ 'manage_sessions',
157
+ 'web',
158
+ 'context_mgmt',
159
+ ])
160
+
161
+ export function shouldAllowToolForCurrentThreadRecall(toolName: string): boolean {
162
+ const canonicalToolName = canonicalizePluginId(toolName) || toolName.trim().toLowerCase()
163
+ return !CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS.has(canonicalToolName)
164
+ }
165
+
166
+ const DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS = new Set([
167
+ 'memory_store',
168
+ 'memory_update',
169
+ ])
170
+
171
+ export function shouldAllowToolForDirectMemoryWrite(toolName: string): boolean {
172
+ const rawToolName = toolName.trim().toLowerCase()
173
+ return DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS.has(rawToolName)
174
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Chat Turn — Post-LLM Tool Routing
3
+ *
4
+ * After the LLM produces a response, this module handles forced tool
5
+ * invocations (explicitly requested by the user), auto-delegation
6
+ * (routing coding tasks to CLI backends), and auto-routing (browsing,
7
+ * research, memory intents).
8
+ *
9
+ * Extracted from chat-execution.ts for testability and readability.
10
+ */
11
+
12
+ import type { StructuredToolInterface } from '@langchain/core/tools'
13
+ import type { MessageToolEvent, SSEEvent } from '@/types'
14
+ import { loadAgents } from './storage'
15
+ import { buildSessionTools } from './session-tools'
16
+ import { resolveConcreteToolPolicyBlock, type PluginPolicyDecision } from './tool-capability-policy'
17
+ import { resolveActiveProjectContext } from './project-context'
18
+ import { genId } from '@/lib/id'
19
+ import { rankDelegatesByHealth } from './provider-health'
20
+ import { routeTaskIntent, type CapabilityRoutingDecision } from './capability-router'
21
+ import {
22
+ type DelegateTool,
23
+ type SessionWithTools,
24
+ enabledDelegationTools,
25
+ extractConnectorMessageArgs,
26
+ extractDelegationTask,
27
+ findFirstUrl,
28
+ hasToolEnabled,
29
+ hasDirectLocalCodingTools,
30
+ isMemoryListIntent,
31
+ requestedToolNamesFromMessage,
32
+ translateRequestedToolInvocation,
33
+ } from './chat-execution-utils'
34
+ import { errorMessage } from '@/lib/shared-utils'
35
+
36
+ interface ToolRoutingSession extends SessionWithTools {
37
+ agentId?: string | null
38
+ cwd: string
39
+ memoryScopeMode?: string | null
40
+ }
41
+
42
+ export interface ToolRoutingContext {
43
+ session: ToolRoutingSession
44
+ sessionId: string
45
+ message: string
46
+ effectiveMessage: string
47
+ enabledPlugins: string[]
48
+ toolPolicy: PluginPolicyDecision
49
+ appSettings: Record<string, unknown>
50
+ internal: boolean
51
+ source: string
52
+ toolEvents: MessageToolEvent[]
53
+ emit: (ev: SSEEvent) => void
54
+ }
55
+
56
+ export interface ToolRoutingResult {
57
+ /** Tool names that were actually invoked */
58
+ calledNames: Set<string>
59
+ /** Updated full response (may be modified by delegate output) */
60
+ fullResponse: string
61
+ /** Updated error message (may be cleared on failover success) */
62
+ errorMessage: string | undefined
63
+ /** Missed requested tools (for warning) */
64
+ missedRequestedTools: string[]
65
+ }
66
+
67
+ function extractDelegateResponse(outputText: string): string | null {
68
+ try {
69
+ const parsed = JSON.parse(outputText) as Record<string, unknown>
70
+ if (typeof parsed.response === 'string' && parsed.response.trim()) return parsed.response.trim()
71
+ if (typeof parsed.result === 'string' && parsed.result.trim()) return parsed.result.trim()
72
+ return null
73
+ } catch {
74
+ return null
75
+ }
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Core: Invoke a single session tool
80
+ // ---------------------------------------------------------------------------
81
+
82
+ async function invokeSessionTool(
83
+ ctx: ToolRoutingContext,
84
+ toolName: string,
85
+ args: Record<string, unknown>,
86
+ failurePrefix: string,
87
+ calledNames: Set<string>,
88
+ ): Promise<{ invoked: boolean; responseOverride: string | null }> {
89
+ const blockedReason = resolveConcreteToolPolicyBlock(toolName, ctx.toolPolicy, ctx.appSettings)
90
+ if (blockedReason) {
91
+ ctx.emit({ t: 'err', text: `Capability policy blocked tool invocation "${toolName}": ${blockedReason}` })
92
+ return { invoked: false, responseOverride: null }
93
+ }
94
+ if (
95
+ (ctx.appSettings as Record<string, unknown>).safetyRequireApprovalForOutbound === true
96
+ && toolName === 'connector_message_tool'
97
+ && ctx.source !== 'chat'
98
+ ) {
99
+ ctx.emit({ t: 'err', text: 'Outbound connector messaging requires explicit user approval.' })
100
+ return { invoked: false, responseOverride: null }
101
+ }
102
+
103
+ const agent = ctx.session.agentId ? loadAgents()[ctx.session.agentId] : null
104
+ const agentRecord = agent as Record<string, unknown> | null
105
+ const activeProjectContext = resolveActiveProjectContext(ctx.session as unknown as { agentId?: string | null; cwd?: string | null; projectId?: string | null })
106
+ const { tools, cleanup } = await buildSessionTools(ctx.session.cwd, ctx.enabledPlugins, {
107
+ agentId: ctx.session.agentId || null,
108
+ sessionId: ctx.sessionId,
109
+ platformAssignScope: (agentRecord?.platformAssignScope as 'self' | 'all') || 'self',
110
+ mcpServerIds: agentRecord?.mcpServerIds as string[] | undefined,
111
+ mcpDisabledTools: agentRecord?.mcpDisabledTools as string[] | undefined,
112
+ projectId: activeProjectContext.projectId,
113
+ projectRoot: activeProjectContext.projectRoot,
114
+ projectName: activeProjectContext.project?.name || null,
115
+ projectDescription: activeProjectContext.project?.description || null,
116
+ memoryScopeMode: (ctx.session.memoryScopeMode ?? agentRecord?.memoryScopeMode as string | null ?? null) as 'all' | 'auto' | 'global' | 'agent' | 'session' | 'project' | null,
117
+ })
118
+
119
+ try {
120
+ const directTool = tools.find((t) => t?.name === toolName) as StructuredToolInterface | undefined
121
+ const availableToolNames = tools.map((candidate) => candidate?.name).filter(Boolean)
122
+ const translated = directTool
123
+ ? { toolName, args }
124
+ : translateRequestedToolInvocation(toolName, args, ctx.message, availableToolNames)
125
+ const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
126
+ if (!selectedTool?.invoke) return { invoked: false, responseOverride: null }
127
+
128
+ const toolCallId = genId()
129
+ ctx.emit({ t: 'tool_call', toolName, toolInput: JSON.stringify(translated.args), toolCallId })
130
+ const toolOutput = await selectedTool.invoke(translated.args)
131
+ const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
132
+ ctx.emit({ t: 'tool_result', toolName, toolOutput: outputText, toolCallId })
133
+
134
+ const delegateResponse = (
135
+ toolName === 'delegate'
136
+ || toolName.startsWith('delegate_to_')
137
+ ) ? extractDelegateResponse(outputText) : null
138
+
139
+ calledNames.add(toolName)
140
+
141
+ if (delegateResponse) {
142
+ return { invoked: true, responseOverride: delegateResponse }
143
+ }
144
+ return { invoked: true, responseOverride: null }
145
+ } catch (forceErr: unknown) {
146
+ ctx.emit({ t: 'err', text: `${failurePrefix}: ${errorMessage(forceErr)}` })
147
+ return { invoked: false, responseOverride: null }
148
+ } finally {
149
+ await cleanup()
150
+ }
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Main: Run all post-LLM tool routing
155
+ // ---------------------------------------------------------------------------
156
+
157
+ const FORCED_DELEGATION_TOOLS: DelegateTool[] = [
158
+ 'delegate_to_claude_code',
159
+ 'delegate_to_codex_cli',
160
+ 'delegate_to_opencode_cli',
161
+ 'delegate_to_gemini_cli',
162
+ ]
163
+
164
+ export async function runPostLlmToolRouting(
165
+ ctx: ToolRoutingContext,
166
+ currentResponse: string,
167
+ currentError: string | undefined,
168
+ ): Promise<ToolRoutingResult> {
169
+ const calledNames = new Set((ctx.toolEvents || []).map((t) => t.name))
170
+ let fullResponse = currentResponse
171
+ let errorMessage = currentError
172
+
173
+ const requestedToolNames = (!ctx.internal && ctx.source === 'chat')
174
+ ? requestedToolNamesFromMessage(ctx.message)
175
+ : []
176
+ const routingDecision: CapabilityRoutingDecision | null = (!ctx.internal && ctx.source === 'chat')
177
+ ? routeTaskIntent(ctx.message, ctx.enabledPlugins, ctx.appSettings)
178
+ : null
179
+
180
+ // --- Forced connector_message_tool ---
181
+ if (requestedToolNames.includes('connector_message_tool') && !calledNames.has('connector_message_tool')) {
182
+ const forcedArgs = extractConnectorMessageArgs(ctx.message)
183
+ if (forcedArgs) {
184
+ const result = await invokeSessionTool(
185
+ ctx, 'connector_message_tool',
186
+ forcedArgs as unknown as Record<string, unknown>,
187
+ 'Forced connector_message_tool invocation failed',
188
+ calledNames,
189
+ )
190
+ if (result.responseOverride) fullResponse = result.responseOverride
191
+ }
192
+ }
193
+
194
+ // --- Forced delegation tools ---
195
+ for (const toolName of FORCED_DELEGATION_TOOLS) {
196
+ if (!requestedToolNames.includes(toolName)) continue
197
+ if (calledNames.has(toolName)) continue
198
+ const task = extractDelegationTask(ctx.message, toolName)
199
+ if (!task) continue
200
+ const result = await invokeSessionTool(ctx, toolName, { task }, `Forced ${toolName} invocation failed`, calledNames)
201
+ if (result.responseOverride) fullResponse = result.responseOverride
202
+ }
203
+
204
+ // --- Auto-delegation for coding intent ---
205
+ const hasDelegationCall = FORCED_DELEGATION_TOOLS.some((t) => calledNames.has(t))
206
+ const enabledDelegates = enabledDelegationTools(ctx.session)
207
+ const shouldAutoDelegateCoding = (!ctx.internal && ctx.source === 'chat')
208
+ && enabledDelegates.length > 0
209
+ && !hasDelegationCall
210
+ && calledNames.size === 0
211
+ && !requestedToolNames.length
212
+ && !hasDirectLocalCodingTools(ctx.session)
213
+ && routingDecision?.intent === 'coding'
214
+
215
+ if (shouldAutoDelegateCoding) {
216
+ const baseDelegationOrder = routingDecision?.preferredDelegates?.length
217
+ ? routingDecision.preferredDelegates
218
+ : FORCED_DELEGATION_TOOLS
219
+ const delegationOrder = rankDelegatesByHealth(baseDelegationOrder as DelegateTool[])
220
+ .filter((tool) => enabledDelegates.includes(tool))
221
+ for (const delegateTool of delegationOrder) {
222
+ const result = await invokeSessionTool(ctx, delegateTool, { task: ctx.effectiveMessage.trim() }, 'Auto-delegation failed', calledNames)
223
+ if (result.invoked) {
224
+ if (result.responseOverride) fullResponse = result.responseOverride
225
+ break
226
+ }
227
+ }
228
+ }
229
+
230
+ // --- Provider failover via delegation ---
231
+ const shouldFailoverDelegate = (!ctx.internal && ctx.source === 'chat')
232
+ && !!errorMessage
233
+ && !(fullResponse || '').trim()
234
+ && enabledDelegates.length > 0
235
+ && !hasDelegationCall
236
+ && (routingDecision?.intent === 'coding' || routingDecision?.intent === 'general')
237
+ if (shouldFailoverDelegate) {
238
+ const preferred = routingDecision?.preferredDelegates?.length
239
+ ? routingDecision.preferredDelegates
240
+ : FORCED_DELEGATION_TOOLS
241
+ const fallbackOrder = rankDelegatesByHealth(preferred as DelegateTool[])
242
+ .filter((tool) => enabledDelegates.includes(tool))
243
+ for (const delegateTool of fallbackOrder) {
244
+ const result = await invokeSessionTool(
245
+ ctx, delegateTool,
246
+ { task: ctx.effectiveMessage.trim() },
247
+ `Provider failover via ${delegateTool} failed`,
248
+ calledNames,
249
+ )
250
+ if (result.invoked) {
251
+ if (result.responseOverride) fullResponse = result.responseOverride
252
+ errorMessage = undefined
253
+ break
254
+ }
255
+ }
256
+ }
257
+
258
+ // --- Auto-routing: browsing, research, memory ---
259
+ const canAutoRoute = (!ctx.internal && ctx.source === 'chat')
260
+ && !!routingDecision
261
+ && calledNames.size === 0
262
+ && requestedToolNames.length === 0
263
+
264
+ if (canAutoRoute && routingDecision?.intent === 'browsing' && routingDecision.primaryUrl && hasToolEnabled(ctx.session, 'browser')) {
265
+ const result = await invokeSessionTool(
266
+ ctx, 'browser',
267
+ { action: 'read_page', url: routingDecision.primaryUrl },
268
+ 'Auto browser routing failed',
269
+ calledNames,
270
+ )
271
+ if (result.responseOverride) fullResponse = result.responseOverride
272
+ }
273
+
274
+ if (canAutoRoute && routingDecision?.intent === 'research') {
275
+ const routeUrl = routingDecision.primaryUrl || findFirstUrl(ctx.message)
276
+ if (routeUrl && hasToolEnabled(ctx.session, 'web_fetch')) {
277
+ const result = await invokeSessionTool(ctx, 'web_fetch', { url: routeUrl }, 'Auto web_fetch routing failed', calledNames)
278
+ if (result.responseOverride) fullResponse = result.responseOverride
279
+ } else if (hasToolEnabled(ctx.session, 'web_search')) {
280
+ const result = await invokeSessionTool(ctx, 'web_search', { query: ctx.effectiveMessage.trim(), maxResults: 5 }, 'Auto web_search routing failed', calledNames)
281
+ if (result.responseOverride) fullResponse = result.responseOverride
282
+ }
283
+ }
284
+
285
+ if (canAutoRoute && calledNames.size === 0 && hasToolEnabled(ctx.session, 'memory') && isMemoryListIntent(ctx.message)) {
286
+ const result = await invokeSessionTool(
287
+ ctx, 'memory_tool',
288
+ { action: 'list', key: '', scope: 'auto' },
289
+ 'Auto memory listing failed',
290
+ calledNames,
291
+ )
292
+ if (result.responseOverride) fullResponse = result.responseOverride
293
+ }
294
+
295
+ // --- Missed requested tools ---
296
+ const missed = requestedToolNames.filter((name) => !calledNames.has(name))
297
+
298
+ // When tool output is the only content and LLM produced nothing, provide a brief notice
299
+ if (calledNames.size > 0 && !fullResponse.trim()) {
300
+ const toolLabel = Array.from(calledNames).pop()?.replace(/_/g, ' ') || 'tool'
301
+ fullResponse = `Used **${toolLabel}** — see tool output above for details.`
302
+ }
303
+
304
+ return {
305
+ calledNames,
306
+ fullResponse,
307
+ errorMessage,
308
+ missedRequestedTools: missed,
309
+ }
310
+ }
@@ -35,9 +35,9 @@ function runWithTempDataDir(script: string) {
35
35
  describe('chatroom synthetic session persistence', () => {
36
36
  it('reuses stored synthetic sessions and preserves delegate resume state', () => {
37
37
  const output = runWithTempDataDir(`
38
- const helpersMod = await import('./src/lib/server/chatroom-helpers.ts')
38
+ const helpersMod = await import('./src/lib/server/chatroom-helpers')
39
39
  const helpers = helpersMod.default || helpersMod
40
- const storageMod = await import('./src/lib/server/storage.ts')
40
+ const storageMod = await import('./src/lib/server/storage')
41
41
  const storage = storageMod.default || storageMod
42
42
  const now = Date.now()
43
43
  const agent = {
@@ -1,4 +1,5 @@
1
1
  import type { ClawHubSkill } from '@/types'
2
+ import { errorMessage } from '@/lib/shared-utils'
2
3
 
3
4
  export interface ClawHubSearchResult {
4
5
  skills: ClawHubSkill[]
@@ -71,7 +72,7 @@ export async function searchClawHub(query: string, page = 1, limit = 20): Promis
71
72
 
72
73
  return { skills, total, page, nextCursor: data.nextCursor }
73
74
  } catch (err: unknown) {
74
- console.warn('[clawhub] search failed:', err instanceof Error ? err.message : String(err))
75
+ console.warn('[clawhub] search failed:', errorMessage(err))
75
76
  return { skills: [], total: 0, page }
76
77
  }
77
78
  }
@@ -0,0 +1,92 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { mutateItem, deleteItem, type CollectionOps } from './collection-helpers'
5
+
6
+ /**
7
+ * Tests for collection-helpers reliability fixes:
8
+ * - mutateItem uses atomic patchStoredItem when ops.table is set
9
+ * - deleteItem uses row-level deleteStoredItem when ops.table is set
10
+ *
11
+ * Since patchStoredItem requires a real SQLite connection, we test the
12
+ * logic branching by verifying that the table-based path is taken when
13
+ * ops.table is set, and the legacy path when it is not.
14
+ */
15
+
16
+ function makeInMemoryOps<T>(initial: Record<string, T>): CollectionOps<T> & { data: Record<string, T>; saveCount: number } {
17
+ const data = { ...initial }
18
+ const ops = {
19
+ data,
20
+ saveCount: 0,
21
+ load: () => ({ ...data }),
22
+ save: (next: Record<string, T>) => {
23
+ Object.keys(data).forEach((k) => delete data[k])
24
+ Object.assign(data, next)
25
+ ops.saveCount++
26
+ },
27
+ topic: 'test',
28
+ }
29
+ return ops
30
+ }
31
+
32
+ describe('collection-helpers', () => {
33
+ describe('mutateItem (legacy path — no table)', () => {
34
+ it('mutates an existing item via load-all/save-all', () => {
35
+ const ops = makeInMemoryOps({ a: { name: 'Alice', score: 10 } })
36
+ const result = mutateItem(ops, 'a', (item) => ({ ...item, score: 20 }))
37
+
38
+ assert.ok(result)
39
+ assert.equal((result as Record<string, unknown>).score, 20)
40
+ assert.equal(ops.saveCount, 1)
41
+ assert.equal(ops.data.a.score, 20)
42
+ })
43
+
44
+ it('returns null for missing item', () => {
45
+ const ops = makeInMemoryOps<Record<string, unknown>>({})
46
+ const result = mutateItem(ops, 'missing', (item) => item)
47
+
48
+ assert.equal(result, null)
49
+ assert.equal(ops.saveCount, 0)
50
+ })
51
+
52
+ it('passes full collection to the mutation function', () => {
53
+ const ops = makeInMemoryOps({ a: { v: 1 }, b: { v: 2 } })
54
+ let capturedAll: Record<string, unknown> | null = null
55
+ mutateItem(ops, 'a', (item, all) => {
56
+ capturedAll = all as Record<string, unknown>
57
+ return item
58
+ })
59
+
60
+ assert.ok(capturedAll)
61
+ assert.ok('a' in capturedAll!)
62
+ assert.ok('b' in capturedAll!)
63
+ })
64
+ })
65
+
66
+ describe('deleteItem (legacy path — no table)', () => {
67
+ it('deletes an existing item', () => {
68
+ const ops = makeInMemoryOps({ a: { v: 1 }, b: { v: 2 } })
69
+ const result = deleteItem(ops, 'a')
70
+
71
+ assert.equal(result, true)
72
+ assert.equal(ops.data.a, undefined)
73
+ assert.equal(ops.data.b.v, 2)
74
+ })
75
+
76
+ it('returns false for missing item', () => {
77
+ const ops = makeInMemoryOps<Record<string, unknown>>({})
78
+ const result = deleteItem(ops, 'missing')
79
+ assert.equal(result, false)
80
+ })
81
+
82
+ it('uses custom deleteFn when provided', () => {
83
+ let deletedId: string | null = null
84
+ const ops = makeInMemoryOps({ a: { v: 1 } })
85
+ ops.deleteFn = (id: string) => { deletedId = id }
86
+
87
+ const result = deleteItem(ops, 'a')
88
+ assert.equal(result, true)
89
+ assert.equal(deletedId, 'a')
90
+ })
91
+ })
92
+ })
@@ -1,22 +1,41 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { notify } from './ws-hub'
3
+ import { deleteStoredItem, patchStoredItem, type StorageCollection } from './storage'
3
4
 
4
5
  export interface CollectionOps<T> {
5
6
  load: () => Record<string, T>
6
7
  save: (data: Record<string, T>) => void
7
8
  deleteFn?: (id: string) => void
8
9
  topic?: string
10
+ /** When set, mutateItem/deleteItem use row-level upsert/delete instead of save-all. */
11
+ table?: StorageCollection
9
12
  }
10
13
 
11
14
  /**
12
- * Load → 404 check → mutate → save → notify.
15
+ * Load → 404 check → mutate → upsert single row → notify.
13
16
  * `fn` receives the item and the full collection, returns the updated item.
17
+ *
18
+ * When `ops.table` is set, uses an atomic read-modify-write transaction via
19
+ * patchStoredItem to prevent concurrent writers from losing each other's updates.
14
20
  */
15
21
  export function mutateItem<T>(
16
22
  ops: CollectionOps<T>,
17
23
  id: string,
18
24
  fn: (item: T, all: Record<string, T>) => T,
19
25
  ): T | null {
26
+ if (ops.table) {
27
+ // Atomic path: read + mutate + write inside a single SQLite transaction
28
+ const result = patchStoredItem<T>(ops.table, id, (current) => {
29
+ if (current === null) return null
30
+ // Load full collection for the fn callback (rare code paths need it)
31
+ const all = ops.load()
32
+ all[id] = current
33
+ return fn(current, all)
34
+ })
35
+ if (result !== null && ops.topic) notify(ops.topic)
36
+ return result
37
+ }
38
+ // Legacy path: load-all → mutate → save-all (no table set)
20
39
  const all = ops.load()
21
40
  if (!all[id]) return null
22
41
  all[id] = fn(all[id], all)
@@ -26,8 +45,9 @@ export function mutateItem<T>(
26
45
  }
27
46
 
28
47
  /**
29
- * Load → 404 check → delete → notify.
30
- * Uses `ops.deleteFn` if provided, otherwise inline `delete` + `save`.
48
+ * Load → 404 check → delete single row → notify.
49
+ * Uses `ops.deleteFn` if provided, then `ops.table` for row-level delete,
50
+ * otherwise inline `delete` + `save`.
31
51
  */
32
52
  export function deleteItem<T>(
33
53
  ops: CollectionOps<T>,
@@ -37,6 +57,8 @@ export function deleteItem<T>(
37
57
  if (!all[id]) return false
38
58
  if (ops.deleteFn) {
39
59
  ops.deleteFn(id)
60
+ } else if (ops.table) {
61
+ deleteStoredItem(ops.table, id)
40
62
  } else {
41
63
  delete all[id]
42
64
  ops.save(all)