@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,58 +1,58 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadAgents, loadSchedules, loadSessions, saveSchedules, deleteSchedule } from '@/lib/server/storage'
2
+ import { deleteSchedule, loadAgents, loadSchedules, loadSessions, upsertSchedules } from '@/lib/server/storage'
3
3
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
4
- import { resolveScheduleName } from '@/lib/schedule-name'
5
- import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
6
- import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
7
-
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- const ops: CollectionOps<any> = { load: loadSchedules, save: saveSchedules, deleteFn: deleteSchedule, topic: 'schedules' }
4
+ import { notFound } from '@/lib/server/collection-helpers'
5
+ import { getScheduleClusterIds, prepareScheduleUpdate } from '@/lib/server/schedule-service'
6
+ import { errorMessage } from '@/lib/shared-utils'
7
+ import { notify } from '@/lib/server/ws-hub'
10
8
 
11
9
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
12
10
  const { id } = await params
13
11
  const body = await req.json()
12
+ const schedules = loadSchedules()
13
+ const current = schedules[id]
14
+ if (!current) return notFound()
14
15
  const sessions = loadSessions()
15
16
  const agents = loadAgents()
16
- let result = null
17
- try {
18
- result = mutateItem(ops, id, (schedule) => {
19
- const sessionCwd = typeof schedule.createdInSessionId === 'string'
20
- ? sessions[schedule.createdInSessionId]?.cwd
21
- : null
22
- const normalized = normalizeSchedulePayload({
23
- ...schedule,
24
- ...(body as Record<string, unknown>),
25
- id,
26
- }, {
27
- cwd: sessionCwd || WORKSPACE_DIR,
28
- now: Date.now(),
29
- })
30
- if (!normalized.ok) throw new Error(normalized.error)
31
- const nextSchedule = {
32
- ...schedule,
33
- ...normalized.value,
34
- id,
35
- updatedAt: Date.now(),
36
- }
37
- if (!agents[String(nextSchedule.agentId)]) {
38
- throw new Error(`Agent not found: ${String(nextSchedule.agentId)}`)
39
- }
40
- nextSchedule.name = resolveScheduleName({
41
- name: nextSchedule.name,
42
- taskPrompt: nextSchedule.taskPrompt,
43
- })
44
- return nextSchedule
45
- })
46
- } catch (error: unknown) {
47
- const message = error instanceof Error ? error.message : String(error)
17
+ const sessionCwd = typeof current.createdInSessionId === 'string'
18
+ ? sessions[current.createdInSessionId]?.cwd
19
+ : null
20
+ const prepared = prepareScheduleUpdate({
21
+ id,
22
+ current,
23
+ patch: body as Record<string, unknown>,
24
+ schedules,
25
+ now: Date.now(),
26
+ cwd: sessionCwd || WORKSPACE_DIR,
27
+ agentExists: (agentId) => Boolean(agents[agentId]),
28
+ propagateEquivalentStatuses: true,
29
+ propagationSource: current,
30
+ })
31
+ if (!prepared.ok) {
32
+ const message = errorMessage(prepared.error)
48
33
  return NextResponse.json({ error: message }, { status: 400 })
49
34
  }
50
- if (!result) return notFound()
51
- return NextResponse.json(result)
35
+ upsertSchedules(prepared.entries)
36
+ notify('schedules')
37
+ return NextResponse.json(
38
+ prepared.affectedScheduleIds.length > 1
39
+ ? { ...prepared.schedule, affectedScheduleIds: prepared.affectedScheduleIds }
40
+ : prepared.schedule,
41
+ )
52
42
  }
53
43
 
54
44
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
55
45
  const { id } = await params
56
- if (!deleteItem(ops, id)) return notFound()
57
- return NextResponse.json({ ok: true })
46
+ const schedules = loadSchedules()
47
+ const current = schedules[id]
48
+ if (!current) return notFound()
49
+ const deleteIds = getScheduleClusterIds(schedules, current)
50
+ for (const deleteId of deleteIds) {
51
+ deleteSchedule(deleteId)
52
+ }
53
+ notify('schedules')
54
+ return NextResponse.json({
55
+ ok: true,
56
+ deletedIds: deleteIds,
57
+ })
58
58
  }
@@ -1,11 +1,11 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { genId } from '@/lib/id'
3
2
  import { notFound } from '@/lib/server/collection-helpers'
4
- import { loadSchedules, saveSchedules, loadAgents, loadTasks, saveTasks } from '@/lib/server/storage'
3
+ import { loadSchedule, loadAgents, loadTasks, upsertSchedule, upsertTask } from '@/lib/server/storage'
5
4
  import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agent-availability'
6
5
  import { enqueueTask } from '@/lib/server/queue'
7
6
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
8
7
  import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
8
+ import { prepareScheduledTaskRun } from '@/lib/server/task-lifecycle'
9
9
 
10
10
  type InFlightTask = {
11
11
  status?: string
@@ -14,8 +14,7 @@ type InFlightTask = {
14
14
 
15
15
  export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
16
16
  const { id } = await params
17
- const schedules = loadSchedules()
18
- const schedule = schedules[id]
17
+ const schedule = loadSchedule(id)
19
18
  if (!schedule) return notFound()
20
19
 
21
20
  const agents = loadAgents()
@@ -41,64 +40,14 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
41
40
  const now = Date.now()
42
41
  schedule.runNumber = (schedule.runNumber || 0) + 1
43
42
 
44
- // Reuse linked task if it exists and is not in-flight
45
- let taskId = ''
46
- const existingTaskId = typeof schedule.linkedTaskId === 'string' ? schedule.linkedTaskId : ''
47
- const existingTask = existingTaskId ? tasks[existingTaskId] : null
48
-
49
- if (existingTask && existingTask.status !== 'queued' && existingTask.status !== 'running') {
50
- taskId = existingTaskId
51
- const prev = existingTask as Record<string, unknown>
52
- prev.totalRuns = ((prev.totalRuns as number) || 0) + 1
53
- if (existingTask.status === 'completed') prev.totalCompleted = ((prev.totalCompleted as number) || 0) + 1
54
- if (existingTask.status === 'failed') prev.totalFailed = ((prev.totalFailed as number) || 0) + 1
55
-
56
- existingTask.status = 'backlog'
57
- existingTask.title = `[Sched] ${schedule.name} (run #${schedule.runNumber})`
58
- existingTask.result = null
59
- existingTask.error = null
60
- existingTask.outputFiles = []
61
- existingTask.artifacts = []
62
- existingTask.sessionId = null
63
- existingTask.completionReportPath = null
64
- existingTask.updatedAt = now
65
- existingTask.queuedAt = null
66
- existingTask.startedAt = null
67
- existingTask.completedAt = null
68
- existingTask.archivedAt = null
69
- existingTask.attempts = 0
70
- existingTask.retryScheduledAt = null
71
- existingTask.deadLetteredAt = null
72
- existingTask.validation = null
73
- prev.runNumber = schedule.runNumber
74
- } else {
75
- taskId = genId()
76
- tasks[taskId] = {
77
- id: taskId,
78
- title: `[Sched] ${schedule.name} (run #${schedule.runNumber})`,
79
- description: schedule.taskPrompt || '',
80
- status: 'backlog',
81
- agentId: schedule.agentId,
82
- sessionId: null,
83
- result: null,
84
- error: null,
85
- createdAt: now,
86
- updatedAt: now,
87
- queuedAt: null,
88
- startedAt: null,
89
- completedAt: null,
90
- sourceType: 'schedule',
91
- sourceScheduleId: schedule.id,
92
- sourceScheduleName: schedule.name,
93
- sourceScheduleKey: scheduleSignature || null,
94
- createdInSessionId: schedule.createdInSessionId || null,
95
- createdByAgentId: schedule.createdByAgentId || null,
96
- runNumber: schedule.runNumber,
97
- }
98
- schedule.linkedTaskId = taskId
99
- }
43
+ const { taskId } = prepareScheduledTaskRun({
44
+ schedule,
45
+ tasks,
46
+ now,
47
+ scheduleSignature,
48
+ })
100
49
 
101
- saveTasks(tasks)
50
+ upsertTask(taskId, tasks[taskId])
102
51
  enqueueTask(taskId)
103
52
  pushMainLoopEventToMainSessions({
104
53
  type: 'schedule_fired',
@@ -106,7 +55,7 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
106
55
  })
107
56
 
108
57
  schedule.lastRunAt = now
109
- saveSchedules(schedules)
58
+ upsertSchedule(schedule.id, schedule)
110
59
 
111
60
  return NextResponse.json({ ok: true, queued: true, taskId, runNumber: schedule.runNumber })
112
61
  }
@@ -1,33 +1,11 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { genId } from '@/lib/id'
3
- import { loadAgents, loadSchedules, saveSchedules } from '@/lib/server/storage'
4
- import { WORKSPACE_DIR } from '@/lib/server/data-dir'
5
- import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
2
+ import { loadAgents, loadSchedules, upsertSchedule, upsertSchedules } from '@/lib/server/storage'
6
3
  import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agent-availability'
7
- import { resolveScheduleName } from '@/lib/schedule-name'
8
- import { findDuplicateSchedule } from '@/lib/schedule-dedupe'
4
+ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
5
+ import { prepareScheduleCreate } from '@/lib/server/schedule-service'
9
6
  import { notify } from '@/lib/server/ws-hub'
10
7
  export const dynamic = 'force-dynamic'
11
8
 
12
- function asString(value: unknown): string {
13
- return typeof value === 'string' ? value.trim() : ''
14
- }
15
-
16
- function asPositiveInt(value: unknown): number | null {
17
- const parsed = typeof value === 'number'
18
- ? value
19
- : typeof value === 'string'
20
- ? Number.parseInt(value, 10)
21
- : Number.NaN
22
- if (!Number.isFinite(parsed)) return null
23
- const intValue = Math.trunc(parsed)
24
- return intValue > 0 ? intValue : null
25
- }
26
-
27
- function asScheduleType(value: unknown): 'cron' | 'interval' | 'once' {
28
- return value === 'cron' || value === 'interval' || value === 'once' ? value : 'cron'
29
- }
30
-
31
9
  export async function GET(_req: Request) {
32
10
  return NextResponse.json(loadSchedules())
33
11
  }
@@ -36,76 +14,32 @@ export async function POST(req: Request) {
36
14
  const body = await req.json()
37
15
  const now = Date.now()
38
16
  const schedules = loadSchedules()
39
- const normalizedSchedule = normalizeSchedulePayload(body as Record<string, unknown>, {
40
- cwd: WORKSPACE_DIR,
41
- now,
42
- })
43
- if (!normalizedSchedule.ok) {
44
- return NextResponse.json({ error: normalizedSchedule.error }, { status: 400 })
45
- }
46
-
47
- const candidate = normalizedSchedule.value
48
17
  const agents = loadAgents()
49
- const agent = agents[String(candidate.agentId)]
18
+ const candidateAgentId = typeof body?.agentId === 'string' ? body.agentId.trim() : ''
19
+ const agent = agents[candidateAgentId]
50
20
  if (!agent) {
51
- return NextResponse.json({ error: `Agent not found: ${String(candidate.agentId)}` }, { status: 400 })
21
+ return NextResponse.json({ error: `Agent not found: ${String(body?.agentId)}` }, { status: 400 })
52
22
  }
53
23
  if (isAgentDisabled(agent)) {
54
24
  return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'take scheduled work') }, { status: 409 })
55
25
  }
56
- const scheduleType = asScheduleType(candidate.scheduleType)
57
- const candidateAgentId = asString(candidate.agentId) || null
58
- const candidateTaskPrompt = asString(candidate.taskPrompt)
59
- const candidateCron = asString(candidate.cron) || null
60
- const candidateIntervalMs = asPositiveInt(candidate.intervalMs)
61
- const candidateRunAt = asPositiveInt(candidate.runAt)
62
-
63
- const duplicate = findDuplicateSchedule(schedules, {
64
- agentId: candidateAgentId,
65
- taskPrompt: candidateTaskPrompt,
66
- scheduleType,
67
- cron: candidateCron,
68
- intervalMs: candidateIntervalMs,
69
- runAt: candidateRunAt,
26
+ const prepared = prepareScheduleCreate({
27
+ input: body as Record<string, unknown>,
28
+ schedules,
29
+ now,
30
+ cwd: WORKSPACE_DIR,
70
31
  })
71
- if (duplicate) {
72
- const duplicateId = duplicate.id || ''
73
- let changed = false
74
- const nextName = resolveScheduleName({
75
- name: candidate.name ?? duplicate.name,
76
- taskPrompt: candidate.taskPrompt ?? duplicate.taskPrompt,
77
- })
78
- if (nextName && nextName !== duplicate.name) {
79
- duplicate.name = nextName
80
- changed = true
81
- }
82
- const normalizedStatus = typeof candidate.status === 'string' ? candidate.status.trim().toLowerCase() : ''
83
- if ((normalizedStatus === 'active' || normalizedStatus === 'paused') && duplicate.status !== normalizedStatus) {
84
- duplicate.status = normalizedStatus as 'active' | 'paused'
85
- changed = true
86
- }
87
- if (changed) {
88
- const mutableDuplicate = duplicate as Record<string, unknown>
89
- mutableDuplicate.updatedAt = now
90
- if (duplicateId) schedules[duplicateId] = duplicate
91
- saveSchedules(schedules)
92
- notify('schedules')
93
- }
94
- return NextResponse.json(duplicate)
32
+ if (!prepared.ok) {
33
+ return NextResponse.json({ error: prepared.error }, { status: 400 })
95
34
  }
96
-
97
- const id = genId()
98
-
99
- schedules[id] = {
100
- id,
101
- ...candidate,
102
- name: resolveScheduleName({ name: candidate.name, taskPrompt: candidate.taskPrompt }),
103
- scheduleType,
104
- lastRunAt: undefined,
105
- createdAt: now,
106
- updatedAt: now,
35
+ if (prepared.kind === 'duplicate') {
36
+ if (prepared.entries.length === 1) upsertSchedule(prepared.scheduleId, prepared.schedule)
37
+ else if (prepared.entries.length > 1) upsertSchedules(prepared.entries)
38
+ if (prepared.entries.length > 0) notify('schedules')
39
+ return NextResponse.json(prepared.schedule)
107
40
  }
108
- saveSchedules(schedules)
41
+
42
+ upsertSchedule(prepared.scheduleId, prepared.schedule)
109
43
  notify('schedules')
110
- return NextResponse.json(schedules[id])
44
+ return NextResponse.json(prepared.schedule)
111
45
  }
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { normalizeHeartbeatSettingFields } from '@/lib/heartbeat-defaults'
3
+ import { normalizeWhatsAppApprovedContacts } from '@/lib/server/connectors/pairing'
3
4
  import { loadPublicSettings, loadSettings, saveSettings } from '@/lib/server/storage'
4
5
  import { normalizeRuntimeSettingFields } from '@/lib/runtime-loop'
5
6
  export const dynamic = 'force-dynamic'
@@ -130,6 +131,7 @@ export async function PUT(req: Request) {
130
131
  settings.projectManagementEnabled = parseBoolSetting(settings.projectManagementEnabled, true)
131
132
  settings.integrityMonitorEnabled = parseBoolSetting(settings.integrityMonitorEnabled, true)
132
133
  settings.sessionResetMode = settings.sessionResetMode === 'daily' ? 'daily' : settings.sessionResetMode === 'idle' ? 'idle' : null
134
+ settings.whatsappApprovedContacts = normalizeWhatsAppApprovedContacts(settings.whatsappApprovedContacts)
133
135
  settings.sessionIdleTimeoutSec = parseIntSetting(
134
136
  settings.sessionIdleTimeoutSec,
135
137
  12 * 60 * 60,
@@ -2,7 +2,9 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { spawnSync } from 'node:child_process'
4
4
  import { NextResponse } from 'next/server'
5
+ import { DATA_DIR } from '@/lib/server/data-dir'
5
6
  import { loadAgents, loadCredentials, loadSettings } from '@/lib/server/storage'
7
+ import { dedup, errorMessage } from '@/lib/shared-utils'
6
8
 
7
9
  type CheckStatus = 'pass' | 'warn' | 'fail'
8
10
 
@@ -38,7 +40,7 @@ function run(command: string, args: string[], timeoutMs = 8_000): CommandResult
38
40
  }
39
41
  return { ok: true, output: (result.stdout || '').trim() }
40
42
  } catch (err: unknown) {
41
- const message = err instanceof Error ? err.message : String(err)
43
+ const message = errorMessage(err)
42
44
  return { ok: false, output: '', error: message }
43
45
  }
44
46
  }
@@ -77,7 +79,7 @@ function testDataWriteAccess(dataDir: string): { ok: boolean; error?: string } {
77
79
  fs.unlinkSync(probe)
78
80
  return { ok: true }
79
81
  } catch (err: unknown) {
80
- const message = err instanceof Error ? err.message : String(err)
82
+ const message = errorMessage(err)
81
83
  return { ok: false, error: message }
82
84
  }
83
85
  }
@@ -124,13 +126,12 @@ export async function GET(req: Request) {
124
126
  actions.push('Run `npm run setup:easy` to install Deno automatically, or install Deno from https://deno.land/#installation.')
125
127
  }
126
128
 
127
- const dataDir = path.join(process.cwd(), 'data')
128
- const dataWrite = testDataWriteAccess(dataDir)
129
+ const dataWrite = testDataWriteAccess(DATA_DIR)
129
130
  if (dataWrite.ok) {
130
- pushCheck(checks, 'data-dir', 'Data directory permissions', 'pass', `Writable: ${dataDir}`, true)
131
+ pushCheck(checks, 'data-dir', 'Data directory permissions', 'pass', `Writable: ${DATA_DIR}`, true)
131
132
  } else {
132
- pushCheck(checks, 'data-dir', 'Data directory permissions', 'fail', dataWrite.error || `Cannot write to ${dataDir}`, true)
133
- actions.push(`Fix filesystem permissions for ${dataDir}.`)
133
+ pushCheck(checks, 'data-dir', 'Data directory permissions', 'fail', dataWrite.error || `Cannot write to ${DATA_DIR}`, true)
134
+ actions.push(`Fix filesystem permissions for ${DATA_DIR}.`)
134
135
  }
135
136
 
136
137
  const envFile = path.join(process.cwd(), '.env.local')
@@ -259,7 +260,7 @@ export async function GET(req: Request) {
259
260
  checkedAt,
260
261
  summary,
261
262
  checks,
262
- actions: Array.from(new Set(actions)),
263
+ actions: dedup(actions),
263
264
  git: {
264
265
  localSha,
265
266
  remoteSha,
@@ -1,16 +1,16 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadTasks, saveTasks, loadAgents } from '@/lib/server/storage'
2
+ import { loadTask, patchTask, loadAgents } from '@/lib/server/storage'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
  import { notify } from '@/lib/server/ws-hub'
5
5
  import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
6
+ import { errorMessage } from '@/lib/shared-utils'
6
7
 
7
8
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
9
  const { id } = await params
9
10
  const body = await req.json()
10
11
  const approved = body.approved === true
11
12
 
12
- const tasks = loadTasks()
13
- const task = tasks[id]
13
+ const task = loadTask(id)
14
14
  if (!task) return notFound()
15
15
  if (!task.pendingApproval) {
16
16
  return NextResponse.json({ error: 'No pending approval on this task' }, { status: 400 })
@@ -20,11 +20,14 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
20
20
 
21
21
  if (!approved) {
22
22
  // Reject: clear approval, delete checkpoint, fail the task
23
- task.pendingApproval = null
24
- task.status = 'failed'
25
- task.error = 'Tool execution rejected by user'
26
- task.updatedAt = Date.now()
27
- saveTasks(tasks)
23
+ patchTask(id, (current) => {
24
+ if (!current) return current
25
+ current.pendingApproval = null
26
+ current.status = 'failed'
27
+ current.error = 'Tool execution rejected by user'
28
+ current.updatedAt = Date.now()
29
+ return current
30
+ })
28
31
  await getCheckpointSaver().deleteThread(threadId)
29
32
  notify('tasks')
30
33
  return NextResponse.json({ status: 'rejected' })
@@ -37,37 +40,37 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
37
40
  return NextResponse.json({ error: 'Agent not found' }, { status: 400 })
38
41
  }
39
42
 
40
- task.pendingApproval = null
41
- task.updatedAt = Date.now()
42
- saveTasks(tasks)
43
+ const approvedTask = patchTask(id, (current) => {
44
+ if (!current) return current
45
+ current.pendingApproval = null
46
+ current.updatedAt = Date.now()
47
+ return current
48
+ })
43
49
  notify('tasks')
44
50
 
45
51
  // Resume in the background
46
- const sessionId = task.sessionId || ''
52
+ const sessionId = approvedTask?.sessionId || task.sessionId || ''
47
53
  setImmediate(async () => {
48
54
  try {
49
55
  const { resumeLangGraphOrchestrator } = await import('@/lib/server/orchestrator-lg')
50
56
  const result = await resumeLangGraphOrchestrator(agent, sessionId, threadId)
51
- const t2 = loadTasks()
52
- if (t2[id] && !t2[id].pendingApproval) {
53
- // Only mark completed if not paused again
54
- if (t2[id].status === 'running') {
55
- t2[id].result = result
56
- }
57
- t2[id].updatedAt = Date.now()
58
- saveTasks(t2)
59
- notify('tasks')
60
- }
57
+ const updated = patchTask(id, (current) => {
58
+ if (!current || current.pendingApproval) return current
59
+ if (current.status === 'running') current.result = result
60
+ current.updatedAt = Date.now()
61
+ return current
62
+ })
63
+ if (updated) notify('tasks')
61
64
  } catch (err: unknown) {
62
- const errMsg = err instanceof Error ? err.message : String(err)
65
+ const errMsg = errorMessage(err)
63
66
  console.error(`[approve] Resume failed for task ${id}:`, errMsg)
64
- const t2 = loadTasks()
65
- if (t2[id]) {
66
- t2[id].error = errMsg
67
- t2[id].updatedAt = Date.now()
68
- saveTasks(t2)
69
- notify('tasks')
70
- }
67
+ const updated = patchTask(id, (current) => {
68
+ if (!current) return current
69
+ current.error = errMsg
70
+ current.updatedAt = Date.now()
71
+ return current
72
+ })
73
+ if (updated) notify('tasks')
71
74
  }
72
75
  })
73
76
 
@@ -1,10 +1,7 @@
1
- import { genId } from '@/lib/id'
2
1
  import { NextResponse } from 'next/server'
3
2
  import { loadAgents, loadSettings, loadTasks, logActivity, upsertStoredItems, upsertTask } from '@/lib/server/storage'
4
3
  import { notFound } from '@/lib/server/collection-helpers'
5
4
  import { disableSessionHeartbeat, enqueueTask, recoverStalledRunningTasks, validateCompletedTasksQueue } from '@/lib/server/queue'
6
- import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
7
- import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
8
5
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
9
6
  import { notify } from '@/lib/server/ws-hub'
10
7
  import { createNotification } from '@/lib/server/create-notification'
@@ -12,7 +9,9 @@ import { enqueueSystemEvent } from '@/lib/server/system-events'
12
9
  import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
13
10
  import { validateDag, cascadeUnblock } from '@/lib/server/dag-validation'
14
11
  import { getPluginManager } from '@/lib/server/plugins'
15
- import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
12
+ import {
13
+ applyTaskPatch,
14
+ } from '@/lib/server/task-service'
16
15
  import type { BoardTask } from '@/types'
17
16
  import '@/lib/server/builtin-plugins'
18
17
 
@@ -53,14 +52,15 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
53
52
  tasks[id].comments.push(body.appendComment)
54
53
  tasks[id].updatedAt = Date.now()
55
54
  } else {
56
- if (Object.prototype.hasOwnProperty.call(body, 'qualityGate')) {
57
- body.qualityGate = body.qualityGate
58
- ? normalizeTaskQualityGate(body.qualityGate, settings)
59
- : null
60
- }
61
- Object.assign(tasks[id], body, { updatedAt: Date.now() })
62
- // Explicitly clear nullable fields when sent as null (Object.assign copies null but not undefined)
63
- if (body.projectId === null) delete tasks[id].projectId
55
+ applyTaskPatch({
56
+ task: tasks[id],
57
+ patch: body as Record<string, unknown>,
58
+ now: Date.now(),
59
+ settings,
60
+ preserveCompletedAt: true,
61
+ clearProjectIdWhenNull: true,
62
+ invalidCompletionCommentAuthor: 'System',
63
+ })
64
64
  }
65
65
  tasks[id].id = id // prevent id overwrite
66
66
 
@@ -69,29 +69,6 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
69
69
  tasks[id].archivedAt = Date.now()
70
70
  }
71
71
 
72
- // Re-validate any completed task updates so "completed" always means actually done.
73
- if (tasks[id].status === 'completed') {
74
- const report = ensureTaskCompletionReport(tasks[id])
75
- if (report?.relativePath) tasks[id].completionReportPath = report.relativePath
76
- const validation = validateTaskCompletion(tasks[id], { report, settings })
77
- tasks[id].validation = validation
78
- if (validation.ok) {
79
- tasks[id].completedAt = tasks[id].completedAt || Date.now()
80
- tasks[id].error = null
81
- } else {
82
- tasks[id].status = 'failed'
83
- tasks[id].completedAt = null
84
- tasks[id].error = formatValidationFailure(validation.reasons).slice(0, 500)
85
- if (!tasks[id].comments) tasks[id].comments = []
86
- tasks[id].comments.push({
87
- id: genId(),
88
- author: 'System',
89
- text: `Completion validation failed.\n\n${validation.reasons.map((r) => `- ${r}`).join('\n')}`,
90
- createdAt: Date.now(),
91
- })
92
- }
93
- }
94
-
95
72
  upsertTask(id, tasks[id])
96
73
  logActivity({ entityType: 'task', entityId: id, action: 'updated', actor: 'user', summary: `Task updated: "${tasks[id].title}" (${prevStatus} → ${tasks[id].status})` })
97
74
  if (prevStatus !== tasks[id].status) {
@@ -6,6 +6,7 @@ import { formatZodError } from '@/lib/validation/schemas'
6
6
  import { loadSettings, loadTasks, logActivity, upsertStoredItems } from '@/lib/server/storage'
7
7
  import { notify } from '@/lib/server/ws-hub'
8
8
  import type { BoardTask } from '@/types'
9
+ import { dedup } from '@/lib/shared-utils'
9
10
 
10
11
  const MAX_IMPORT_LIMIT = 200
11
12
  const BODY_CHAR_LIMIT = 12_000
@@ -131,7 +132,7 @@ export function buildGitHubIssueTaskTags(issue: GitHubIssueRecord, repoFullName:
131
132
  repoFullName,
132
133
  ...(issue.labels || []).map(normalizeLabelName),
133
134
  ]
134
- return Array.from(new Set(raw.map(normalizeTag).filter(Boolean))).slice(0, 8)
135
+ return dedup(raw.map(normalizeTag).filter(Boolean)).slice(0, 8)
135
136
  }
136
137
 
137
138
  function findExistingImportedTask(