@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,391 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { BoardTask, Connector } from '@/types'
4
+ import { normalizeWhatsappTarget } from './connectors/response-media'
5
+ import { WORKSPACE_DIR } from './data-dir'
6
+ import { loadConnectors, loadSessions, UPLOAD_DIR } from './storage'
7
+ import { errorMessage } from '@/lib/shared-utils'
8
+
9
+ export { normalizeWhatsappTarget }
10
+
11
+ export interface SessionMessageLike {
12
+ role?: string
13
+ text?: string
14
+ time?: number
15
+ kind?: string
16
+ historyExcluded?: boolean
17
+ source?: {
18
+ connectorId?: string
19
+ channelId?: string
20
+ threadId?: string
21
+ }
22
+ toolEvents?: Array<{ name?: string; output?: string }>
23
+ streaming?: boolean
24
+ imageUrl?: string
25
+ }
26
+
27
+ export interface SessionLike {
28
+ id?: string
29
+ name?: string
30
+ user?: string
31
+ cwd?: string
32
+ agentId?: string | null
33
+ messages?: SessionMessageLike[]
34
+ connectorContext?: {
35
+ connectorId?: string | null
36
+ channelId?: string | null
37
+ threadId?: string | null
38
+ senderId?: string | null
39
+ senderName?: string | null
40
+ }
41
+ lastActiveAt?: number
42
+ heartbeatEnabled?: boolean | null
43
+ active?: boolean
44
+ currentRunId?: string | null
45
+ }
46
+
47
+ export interface ScheduleTaskMeta extends BoardTask {
48
+ user?: string | null
49
+ createdInSessionId?: string | null
50
+ }
51
+
52
+ export interface RunningConnectorLike {
53
+ id: string
54
+ platform: string
55
+ agentId: string | null
56
+ supportsSend: boolean
57
+ configuredTargets: string[]
58
+ recentChannelId: string | null
59
+ }
60
+
61
+ export interface ConnectorTaskFollowupTarget {
62
+ connectorId: string
63
+ channelId: string
64
+ threadId?: string | null
65
+ }
66
+
67
+ function isEnabledFlag(value: unknown): boolean {
68
+ if (typeof value === 'boolean') return value
69
+ if (typeof value !== 'string') return false
70
+ const normalized = value.trim().toLowerCase()
71
+ return normalized === '1'
72
+ || normalized === 'true'
73
+ || normalized === 'yes'
74
+ || normalized === 'on'
75
+ || normalized === 'enabled'
76
+ }
77
+
78
+ export function fillTaskFollowupTemplate(template: string, data: {
79
+ status: string
80
+ title: string
81
+ summary: string
82
+ taskId: string
83
+ }): string {
84
+ return template
85
+ .replaceAll('{status}', data.status)
86
+ .replaceAll('{title}', data.title)
87
+ .replaceAll('{summary}', data.summary)
88
+ .replaceAll('{taskId}', data.taskId)
89
+ }
90
+
91
+ export function maybeResolveUploadMediaPathFromUrl(url: string | undefined): string | undefined {
92
+ if (!url || !url.startsWith('/api/uploads/')) return undefined
93
+ const rawName = url.slice('/api/uploads/'.length).split(/[?#]/)[0] || ''
94
+ let decoded: string
95
+ try { decoded = decodeURIComponent(rawName) } catch { decoded = rawName }
96
+ const safeName = decoded.replace(/[^a-zA-Z0-9._-]/g, '')
97
+ if (!safeName) return undefined
98
+ const fullPath = path.join(UPLOAD_DIR, safeName)
99
+ return fs.existsSync(fullPath) ? fullPath : undefined
100
+ }
101
+
102
+ const OUTPUT_FILE_BACKTICK_RE = /`([^`\n]+\.(?:txt|md|json|csv|pdf|png|jpe?g|webp|gif|svg|mp4|webm|mov|zip|tar|gz|log|yml|yaml|xml|html|css|js|ts|tsx|jsx|py|go|rs|java|swift|kt|sql))`/gi
103
+ const OUTPUT_FILE_PATH_RE = /\b((?:\.{1,2}\/|~\/|\/)?[\w./-]+\.(?:txt|md|json|csv|pdf|png|jpe?g|webp|gif|svg|mp4|webm|mov|zip|tar|gz|log|yml|yaml|xml|html|css|js|ts|tsx|jsx|py|go|rs|java|swift|kt|sql))\b/gi
104
+ const MAX_CONNECTOR_ATTACHMENT_BYTES = 25 * 1024 * 1024
105
+
106
+ export function extractLikelyOutputFiles(text: string): string[] {
107
+ const out: string[] = []
108
+ const seen = new Set<string>()
109
+ const push = (raw: string) => {
110
+ const value = raw.trim().replace(/^['"]|['"]$/g, '')
111
+ if (!value || /^https?:\/\//i.test(value)) return
112
+ if (value.startsWith('/api/uploads/')) return
113
+ const key = value.toLowerCase()
114
+ if (seen.has(key)) return
115
+ seen.add(key)
116
+ out.push(value)
117
+ }
118
+
119
+ for (const match of text.matchAll(OUTPUT_FILE_BACKTICK_RE)) {
120
+ push(match[1] || '')
121
+ if (out.length >= 8) return out
122
+ }
123
+ for (const match of text.matchAll(OUTPUT_FILE_PATH_RE)) {
124
+ push(match[1] || '')
125
+ if (out.length >= 8) return out
126
+ }
127
+
128
+ return out
129
+ }
130
+
131
+ export function resolveExistingOutputFilePath(fileRef: string, cwd: string): string | null {
132
+ const ref = (fileRef || '').trim()
133
+ if (!ref) return null
134
+ if (ref.startsWith('/api/uploads/')) {
135
+ return maybeResolveUploadMediaPathFromUrl(ref) || null
136
+ }
137
+ const withoutFileScheme = ref.replace(/^file:\/\//i, '')
138
+ const candidates = path.isAbsolute(withoutFileScheme)
139
+ ? [withoutFileScheme]
140
+ : [
141
+ cwd ? path.resolve(cwd, withoutFileScheme) : '',
142
+ path.resolve(WORKSPACE_DIR, withoutFileScheme),
143
+ ].filter(Boolean)
144
+
145
+ for (const candidate of candidates) {
146
+ try {
147
+ const stat = fs.statSync(candidate)
148
+ if (stat.isFile()) return candidate
149
+ } catch {
150
+ // ignore missing candidate
151
+ }
152
+ }
153
+ return null
154
+ }
155
+
156
+ export function isSendableAttachment(filePath: string): boolean {
157
+ try {
158
+ const stat = fs.statSync(filePath)
159
+ return stat.isFile() && stat.size <= MAX_CONNECTOR_ATTACHMENT_BYTES
160
+ } catch {
161
+ return false
162
+ }
163
+ }
164
+
165
+ export function resolveTaskOriginConnectorFollowupTarget(params: {
166
+ task: BoardTask
167
+ sessions: Record<string, SessionLike>
168
+ connectors: Record<string, Connector>
169
+ running: RunningConnectorLike[]
170
+ }): ConnectorTaskFollowupTarget | null {
171
+ const { task, sessions, connectors, running } = params
172
+ const metaTask = task as ScheduleTaskMeta
173
+ const delegatedByAgentId = typeof metaTask.delegatedByAgentId === 'string'
174
+ ? metaTask.delegatedByAgentId.trim()
175
+ : ''
176
+ const allowedOwners = new Set([task.agentId, delegatedByAgentId].filter(Boolean))
177
+ const sourceSessionId = typeof metaTask.createdInSessionId === 'string'
178
+ ? metaTask.createdInSessionId.trim()
179
+ : ''
180
+
181
+ const runningById = new Map<string, RunningConnectorLike>()
182
+ for (const entry of running) {
183
+ if (!entry?.id) continue
184
+ runningById.set(entry.id, entry)
185
+ }
186
+
187
+ const normalizeTarget = (raw: {
188
+ connectorId?: string | null
189
+ channelId?: string | null
190
+ threadId?: string | null
191
+ }): ConnectorTaskFollowupTarget | null => {
192
+ const connectorId = typeof raw.connectorId === 'string' ? raw.connectorId.trim() : ''
193
+ if (!connectorId) return null
194
+ const connector = connectors[connectorId]
195
+ if (!connector) return null
196
+ const ownerId = typeof connector.agentId === 'string' ? connector.agentId.trim() : ''
197
+ if (ownerId && !allowedOwners.has(ownerId)) return null
198
+
199
+ const runtime = runningById.get(connectorId)
200
+ if (runtime && !runtime.supportsSend) return null
201
+
202
+ const channelId = typeof raw.channelId === 'string' ? raw.channelId.trim() : ''
203
+ if (!channelId) return null
204
+ const normalizedChannelId = connector.platform === 'whatsapp'
205
+ ? normalizeWhatsappTarget(channelId)
206
+ : channelId
207
+ const threadId = typeof raw.threadId === 'string' ? raw.threadId.trim() : ''
208
+ return {
209
+ connectorId,
210
+ channelId: normalizedChannelId,
211
+ ...(threadId ? { threadId } : {}),
212
+ }
213
+ }
214
+
215
+ const explicitTarget = normalizeTarget({
216
+ connectorId: typeof metaTask.followupConnectorId === 'string' ? metaTask.followupConnectorId : null,
217
+ channelId: typeof metaTask.followupChannelId === 'string' ? metaTask.followupChannelId : null,
218
+ threadId: typeof metaTask.followupThreadId === 'string' ? metaTask.followupThreadId : null,
219
+ })
220
+ if (explicitTarget) return explicitTarget
221
+
222
+ if (!sourceSessionId) return null
223
+ const sourceSession = sessions[sourceSessionId]
224
+ if (!sourceSession) return null
225
+
226
+ const sessionContextTarget = normalizeTarget({
227
+ connectorId: typeof sourceSession.connectorContext?.connectorId === 'string'
228
+ ? sourceSession.connectorContext.connectorId
229
+ : null,
230
+ channelId: typeof sourceSession.connectorContext?.channelId === 'string'
231
+ ? sourceSession.connectorContext.channelId
232
+ : null,
233
+ threadId: typeof sourceSession.connectorContext?.threadId === 'string'
234
+ ? sourceSession.connectorContext.threadId
235
+ : null,
236
+ })
237
+ if (sessionContextTarget) return sessionContextTarget
238
+
239
+ if (!Array.isArray(sourceSession.messages)) return null
240
+
241
+ for (let index = sourceSession.messages.length - 1; index >= 0; index -= 1) {
242
+ const message = sourceSession.messages[index]
243
+ if (!message || message.role !== 'user') continue
244
+ if (message.historyExcluded === true) continue
245
+
246
+ const connectorId = typeof message.source?.connectorId === 'string'
247
+ ? message.source.connectorId.trim()
248
+ : ''
249
+ if (!connectorId) continue
250
+
251
+ const connector = connectors[connectorId]
252
+ if (!connector) continue
253
+ const runtime = runningById.get(connectorId)
254
+ const sourceChannel = typeof message.source?.channelId === 'string'
255
+ ? message.source.channelId.trim()
256
+ : ''
257
+ const fallbackChannel = runtime?.recentChannelId
258
+ || runtime?.configuredTargets?.[0]
259
+ || connector.config?.outboundJid
260
+ || connector.config?.outboundTarget
261
+ || ''
262
+ const target = normalizeTarget({
263
+ connectorId,
264
+ channelId: sourceChannel || fallbackChannel,
265
+ threadId: typeof message.source?.threadId === 'string' ? message.source.threadId : null,
266
+ })
267
+ if (target) return target
268
+ }
269
+
270
+ return null
271
+ }
272
+
273
+ export function collectTaskConnectorFollowupTargets(params: {
274
+ task: BoardTask
275
+ sessions: Record<string, SessionLike>
276
+ connectors: Record<string, Connector>
277
+ running: RunningConnectorLike[]
278
+ }): ConnectorTaskFollowupTarget[] {
279
+ const { task, sessions, connectors, running } = params
280
+ const originTarget = resolveTaskOriginConnectorFollowupTarget({ task, sessions, connectors, running })
281
+ if (originTarget) return [originTarget]
282
+
283
+ const targets: ConnectorTaskFollowupTarget[] = []
284
+ const seen = new Set<string>()
285
+ const pushTarget = (target: ConnectorTaskFollowupTarget | null | undefined) => {
286
+ if (!target?.connectorId || !target?.channelId) return
287
+ const key = `${target.connectorId}|${target.channelId}|${target.threadId || ''}`
288
+ if (seen.has(key)) return
289
+ seen.add(key)
290
+ targets.push(target)
291
+ }
292
+
293
+ for (const entry of running) {
294
+ if (!entry.supportsSend || !entry.id) continue
295
+ const connector = connectors[entry.id]
296
+ if (!connector) continue
297
+ if (connector.agentId !== task.agentId) continue
298
+ if (!isEnabledFlag(connector.config?.taskFollowups)) continue
299
+ const channelTargetRaw = entry.configuredTargets[0]
300
+ || connector.config?.outboundJid
301
+ || connector.config?.outboundTarget
302
+ || ''
303
+ if (!channelTargetRaw) continue
304
+ pushTarget({
305
+ connectorId: entry.id,
306
+ channelId: connector.platform === 'whatsapp'
307
+ ? normalizeWhatsappTarget(channelTargetRaw)
308
+ : channelTargetRaw,
309
+ })
310
+ }
311
+
312
+ return targets
313
+ }
314
+
315
+ export async function notifyConnectorTaskFollowups(params: {
316
+ task: BoardTask
317
+ statusLabel: string
318
+ summaryText: string
319
+ imageUrl?: string
320
+ mediaPath?: string
321
+ mediaFileName?: string
322
+ }) {
323
+ const { task, statusLabel, summaryText, imageUrl, mediaPath, mediaFileName } = params
324
+
325
+ const connectors = loadConnectors()
326
+ const running = (await import('./connectors/manager')).listRunningConnectors()
327
+ const manager = await import('./connectors/manager')
328
+ const sessions = loadSessions()
329
+ const targets = collectTaskConnectorFollowupTargets({
330
+ task,
331
+ sessions: sessions as Record<string, SessionLike>,
332
+ connectors: connectors as any,
333
+ running: running as RunningConnectorLike[],
334
+ })
335
+ if (!targets.length) return
336
+ const originTarget = resolveTaskOriginConnectorFollowupTarget({
337
+ task,
338
+ sessions: sessions as Record<string, SessionLike>,
339
+ connectors: connectors as any,
340
+ running: running as RunningConnectorLike[],
341
+ })
342
+ const preferredTargetKey = originTarget
343
+ ? `${originTarget.connectorId}|${originTarget.channelId}|${originTarget.threadId || ''}`
344
+ : ''
345
+
346
+ const summary = summaryText.trim().slice(0, 1400)
347
+ for (const target of targets) {
348
+ const connector = connectors[target.connectorId]
349
+ if (!connector) continue
350
+
351
+ const template = typeof (connector as any).config?.taskFollowupTemplate === 'string'
352
+ ? (connector as any).config.taskFollowupTemplate.trim()
353
+ : ''
354
+ const message = template
355
+ ? fillTaskFollowupTemplate(template, {
356
+ status: statusLabel,
357
+ title: task.title || task.id,
358
+ summary,
359
+ taskId: task.id,
360
+ })
361
+ : [
362
+ `Task ${statusLabel}: ${task.title}`,
363
+ summary || 'No summary provided.',
364
+ ].join('\n\n')
365
+ const targetKey = `${target.connectorId}|${target.channelId}|${target.threadId || ''}`
366
+ const preferredChannelNote = !template && preferredTargetKey && targetKey === preferredTargetKey
367
+ ? '\n\n(Update sent in the same channel that requested this task.)'
368
+ : ''
369
+ const outboundMessage = `${message}${preferredChannelNote}`
370
+
371
+ const resolvedMediaPath = mediaPath || maybeResolveUploadMediaPathFromUrl(imageUrl)
372
+ try {
373
+ await manager.sendConnectorMessage({
374
+ connectorId: target.connectorId,
375
+ channelId: target.channelId,
376
+ threadId: target.threadId || undefined,
377
+ text: outboundMessage,
378
+ ...(resolvedMediaPath
379
+ ? {
380
+ mediaPath: resolvedMediaPath,
381
+ fileName: mediaFileName || path.basename(resolvedMediaPath),
382
+ caption: outboundMessage,
383
+ }
384
+ : {}),
385
+ })
386
+ } catch (err: unknown) {
387
+ const errMsg = errorMessage(err)
388
+ console.warn(`[queue] Failed task follow-up send on connector ${target.connectorId}: ${errMsg}`)
389
+ }
390
+ }
391
+ }
@@ -0,0 +1,205 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import type { BoardTask, Schedule } from '@/types'
5
+
6
+ import {
7
+ buildBoardTask,
8
+ didTaskValidationChange,
9
+ markInvalidCompletedTaskFailed,
10
+ markValidatedTaskCompleted,
11
+ prepareScheduledTaskRun,
12
+ resetTaskForRerun,
13
+ } from './task-lifecycle'
14
+
15
+ function makeTask(overrides: Partial<BoardTask> = {}): BoardTask {
16
+ return {
17
+ id: 'task-1',
18
+ title: 'Example Task',
19
+ description: 'Do the work',
20
+ status: 'backlog',
21
+ agentId: 'agent-1',
22
+ sessionId: null,
23
+ result: null,
24
+ error: null,
25
+ createdAt: 1,
26
+ updatedAt: 1,
27
+ queuedAt: null,
28
+ startedAt: null,
29
+ completedAt: null,
30
+ ...overrides,
31
+ }
32
+ }
33
+
34
+ function makeSchedule(overrides: Partial<Schedule> = {}): Schedule {
35
+ return {
36
+ id: 'schedule-1',
37
+ name: 'Nightly backup',
38
+ agentId: 'agent-1',
39
+ taskPrompt: 'Run nightly backup',
40
+ scheduleType: 'cron',
41
+ cron: '0 0 * * *',
42
+ status: 'active',
43
+ runNumber: 2,
44
+ createdAt: 1,
45
+ updatedAt: 1,
46
+ ...overrides,
47
+ }
48
+ }
49
+
50
+ describe('task lifecycle helpers', () => {
51
+ it('buildBoardTask applies shared defaults and preserves seed metadata', () => {
52
+ const task = buildBoardTask({
53
+ id: 'task-build',
54
+ title: 'Build',
55
+ description: 'Compile project',
56
+ agentId: 'agent-1',
57
+ now: 123,
58
+ seed: {
59
+ comments: [],
60
+ sourceType: 'manual',
61
+ },
62
+ })
63
+
64
+ assert.equal(task.id, 'task-build')
65
+ assert.equal(task.status, 'backlog')
66
+ assert.equal(task.sessionId, null)
67
+ assert.equal(task.result, null)
68
+ assert.equal(task.createdAt, 123)
69
+ assert.equal(task.sourceType, 'manual')
70
+ })
71
+
72
+ it('resetTaskForRerun clears terminal fields and rolls stats forward', () => {
73
+ const task = makeTask({
74
+ status: 'completed',
75
+ result: 'done',
76
+ error: 'old error',
77
+ sessionId: 'session-1',
78
+ completionReportPath: 'report.md',
79
+ outputFiles: ['a.txt'],
80
+ artifacts: [{ url: '/x', type: 'file', filename: 'x.txt' }],
81
+ attempts: 3,
82
+ retryScheduledAt: 10,
83
+ deadLetteredAt: 20,
84
+ validation: { ok: true, reasons: [], checkedAt: 5 },
85
+ runNumber: 1,
86
+ totalRuns: 4,
87
+ totalCompleted: 2,
88
+ })
89
+
90
+ resetTaskForRerun(task, {
91
+ title: '[Sched] Nightly backup (run #2)',
92
+ now: 99,
93
+ runNumber: 2,
94
+ })
95
+
96
+ assert.equal(task.status, 'backlog')
97
+ assert.equal(task.title, '[Sched] Nightly backup (run #2)')
98
+ assert.equal(task.result, null)
99
+ assert.equal(task.error, null)
100
+ assert.equal(task.sessionId, null)
101
+ assert.equal(task.completionReportPath, null)
102
+ assert.equal(task.completedAt, null)
103
+ assert.equal(task.attempts, 0)
104
+ assert.equal(task.retryScheduledAt, null)
105
+ assert.equal(task.deadLetteredAt, null)
106
+ assert.equal(task.validation, null)
107
+ assert.equal(task.totalRuns, 5)
108
+ assert.equal(task.totalCompleted, 3)
109
+ assert.equal(task.runNumber, 2)
110
+ })
111
+
112
+ it('prepareScheduledTaskRun creates a schedule-backed task when no reusable task exists', () => {
113
+ const schedule = makeSchedule({
114
+ createdInSessionId: 'session-1',
115
+ createdByAgentId: 'agent-owner',
116
+ followupConnectorId: 'connector-1',
117
+ })
118
+ const tasks: Record<string, BoardTask> = {}
119
+
120
+ const { taskId, task } = prepareScheduledTaskRun({
121
+ schedule,
122
+ tasks,
123
+ now: 200,
124
+ scheduleSignature: 'sig-1',
125
+ })
126
+
127
+ assert.equal(taskId, task.id)
128
+ assert.equal(task.sourceType, 'schedule')
129
+ assert.equal(task.sourceScheduleId, schedule.id)
130
+ assert.equal(task.sourceScheduleKey, 'sig-1')
131
+ assert.equal(task.createdInSessionId, 'session-1')
132
+ assert.equal(task.followupConnectorId, 'connector-1')
133
+ assert.equal(schedule.linkedTaskId, taskId)
134
+ assert.equal(tasks[taskId], task)
135
+ })
136
+
137
+ it('prepareScheduledTaskRun reuses and resets a terminal linked task', () => {
138
+ const existing = makeTask({
139
+ id: 'linked-task',
140
+ status: 'failed',
141
+ result: 'failed before',
142
+ runNumber: 1,
143
+ })
144
+ const tasks: Record<string, BoardTask> = { 'linked-task': existing }
145
+ const schedule = makeSchedule({
146
+ linkedTaskId: 'linked-task',
147
+ runNumber: 7,
148
+ })
149
+
150
+ const { taskId, task } = prepareScheduledTaskRun({
151
+ schedule,
152
+ tasks,
153
+ now: 300,
154
+ scheduleSignature: 'sig-2',
155
+ })
156
+
157
+ assert.equal(taskId, 'linked-task')
158
+ assert.equal(task, existing)
159
+ assert.equal(task.status, 'backlog')
160
+ assert.equal(task.runNumber, 7)
161
+ assert.equal(task.result, null)
162
+ })
163
+
164
+ it('markValidatedTaskCompleted preserves or sets completedAt as requested', () => {
165
+ const task = makeTask({ status: 'completed', completedAt: 10, error: 'nope' })
166
+ markValidatedTaskCompleted(task, { now: 50, preserveCompletedAt: true })
167
+ assert.equal(task.completedAt, 10)
168
+ assert.equal(task.error, null)
169
+
170
+ markValidatedTaskCompleted(task, { now: 75 })
171
+ assert.equal(task.completedAt, 75)
172
+ })
173
+
174
+ it('markInvalidCompletedTaskFailed records failure state and comment', () => {
175
+ const task = makeTask({ status: 'completed' })
176
+ markInvalidCompletedTaskFailed(task, {
177
+ ok: false,
178
+ reasons: ['Missing evidence'],
179
+ checkedAt: 20,
180
+ }, {
181
+ now: 30,
182
+ comment: {
183
+ author: 'System',
184
+ text: 'Validation failed.',
185
+ },
186
+ })
187
+
188
+ assert.equal(task.status, 'failed')
189
+ assert.equal(task.completedAt, null)
190
+ assert.match(task.error || '', /Completion validation failed/)
191
+ assert.equal(task.comments?.[0]?.author, 'System')
192
+ })
193
+
194
+ it('didTaskValidationChange compares both status and reasons', () => {
195
+ assert.equal(didTaskValidationChange(null, { ok: true, reasons: [], checkedAt: 1 }), true)
196
+ assert.equal(didTaskValidationChange(
197
+ { ok: true, reasons: [], checkedAt: 1 },
198
+ { ok: true, reasons: [], checkedAt: 2 },
199
+ ), false)
200
+ assert.equal(didTaskValidationChange(
201
+ { ok: true, reasons: [], checkedAt: 1 },
202
+ { ok: false, reasons: ['x'], checkedAt: 2 },
203
+ ), true)
204
+ })
205
+ })