@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,30 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { UPLOAD_DIR } from './storage'
4
+
5
+ const UPLOAD_URL_PREFIX = '/api/uploads/'
6
+
7
+ /**
8
+ * Resolve an image to a valid filesystem path.
9
+ *
10
+ * Tries, in order:
11
+ * 1. `imagePath` (the absolute filesystem path returned by the upload API)
12
+ * 2. `imageUrl` mapped back to the uploads dir (e.g. `/api/uploads/foo.jpeg` → `UPLOAD_DIR/foo.jpeg`)
13
+ *
14
+ * Returns `null` if neither resolves to an existing file.
15
+ */
16
+ export function resolveImagePath(imagePath?: string, imageUrl?: string): string | null {
17
+ if (imagePath && fs.existsSync(imagePath)) return imagePath
18
+
19
+ // Fall back: resolve relative API URL to filesystem
20
+ if (imageUrl?.startsWith(UPLOAD_URL_PREFIX)) {
21
+ const filename = imageUrl.slice(UPLOAD_URL_PREFIX.length)
22
+ const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '')
23
+ if (safeName) {
24
+ const resolved = path.join(UPLOAD_DIR, safeName)
25
+ if (fs.existsSync(resolved)) return resolved
26
+ }
27
+ }
28
+
29
+ return null
30
+ }
@@ -35,8 +35,8 @@ function runWithTempDataDir(script: string) {
35
35
  describe('runtime settings defaults', () => {
36
36
  it('backfills explicit runtime defaults for clean installs', () => {
37
37
  const output = runWithTempDataDir(`
38
- const storageMod = await import('./src/lib/server/storage.ts')
39
- const runtimeMod = await import('./src/lib/server/runtime-settings.ts')
38
+ const storageMod = await import('./src/lib/server/storage')
39
+ const runtimeMod = await import('./src/lib/server/runtime-settings')
40
40
  const storage = storageMod.default || storageMod
41
41
  const runtime = runtimeMod.default || runtimeMod
42
42
  console.log(JSON.stringify({
@@ -68,8 +68,8 @@ describe('runtime settings defaults', () => {
68
68
 
69
69
  it('clamps invalid persisted runtime settings into the supported range', () => {
70
70
  const output = runWithTempDataDir(`
71
- const storageMod = await import('./src/lib/server/storage.ts')
72
- const runtimeMod = await import('./src/lib/server/runtime-settings.ts')
71
+ const storageMod = await import('./src/lib/server/storage')
72
+ const runtimeMod = await import('./src/lib/server/runtime-settings')
73
73
  const storage = storageMod.default || storageMod
74
74
  const runtime = runtimeMod.default || runtimeMod
75
75
 
@@ -0,0 +1,60 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import test from 'node:test'
5
+
6
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
7
+
8
+ function readRepoSource(relativePath: string): string {
9
+ return fs.readFileSync(path.join(repoRoot, relativePath), 'utf-8')
10
+ }
11
+
12
+ test('runtime hot paths use row-level task, schedule, and agent writes', () => {
13
+ const expectations = [
14
+ {
15
+ file: 'src/lib/server/queue',
16
+ required: ['patchTask(', 'upsertTask(', 'upsertTasks(', 'upsertSchedule(', 'deleteSchedule('],
17
+ forbidden: ['saveTasks(', 'saveSchedules('],
18
+ },
19
+ {
20
+ file: 'src/lib/server/scheduler',
21
+ required: ['upsertTask(', 'upsertSchedule(', 'upsertSchedules('],
22
+ forbidden: ['saveTasks(', 'saveSchedules('],
23
+ },
24
+ {
25
+ file: 'src/lib/server/orchestrator-lg',
26
+ required: ['patchTask(', 'upsertTask('],
27
+ forbidden: ['saveTasks('],
28
+ },
29
+ {
30
+ file: 'src/app/api/orchestrator/run/route',
31
+ required: ['upsertTask('],
32
+ forbidden: ['saveTasks('],
33
+ },
34
+ {
35
+ file: 'src/app/api/schedules/[id]/run/route',
36
+ required: ['upsertTask(', 'upsertSchedule('],
37
+ forbidden: ['saveTasks(', 'saveSchedules('],
38
+ },
39
+ {
40
+ file: 'src/app/api/tasks/[id]/approve/route',
41
+ required: ['patchTask('],
42
+ forbidden: ['saveTasks('],
43
+ },
44
+ {
45
+ file: 'src/lib/server/wallet-service',
46
+ required: ['loadAgent(', 'upsertAgent('],
47
+ forbidden: ['saveAgents('],
48
+ },
49
+ ] as const
50
+
51
+ for (const expectation of expectations) {
52
+ const src = readRepoSource(expectation.file)
53
+ for (const token of expectation.required) {
54
+ assert.equal(src.includes(token), true, `${expectation.file} should use ${token}`)
55
+ }
56
+ for (const token of expectation.forbidden) {
57
+ assert.equal(src.includes(token), false, `${expectation.file} should not use ${token}`)
58
+ }
59
+ }
60
+ })
@@ -0,0 +1,279 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { after, before, describe, it } from 'node:test'
6
+
7
+ const originalEnv = {
8
+ DATA_DIR: process.env.DATA_DIR,
9
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
10
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
11
+ }
12
+
13
+ let tempDir = ''
14
+ let workspaceDir = ''
15
+ let mod: typeof import('./schedule-normalization')
16
+
17
+ before(async () => {
18
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-schedule-norm-'))
19
+ workspaceDir = path.join(tempDir, 'workspace')
20
+ fs.mkdirSync(workspaceDir, { recursive: true })
21
+ process.env.DATA_DIR = path.join(tempDir, 'data')
22
+ process.env.WORKSPACE_DIR = workspaceDir
23
+ process.env.SWARMCLAW_BUILD_MODE = '1'
24
+ mod = await import('./schedule-normalization')
25
+ })
26
+
27
+ after(() => {
28
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
29
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
30
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
31
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
32
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
33
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
34
+ fs.rmSync(tempDir, { recursive: true, force: true })
35
+ })
36
+
37
+ describe('extractScheduleCommandScriptPath', () => {
38
+ it('extracts script path from python command', () => {
39
+ assert.equal(mod.extractScheduleCommandScriptPath('python3 scripts/run.py'), 'scripts/run.py')
40
+ })
41
+
42
+ it('extracts script path from node command', () => {
43
+ assert.equal(mod.extractScheduleCommandScriptPath('node ./build/index.js'), './build/index.js')
44
+ })
45
+
46
+ it('extracts script path from bash command', () => {
47
+ assert.equal(mod.extractScheduleCommandScriptPath('bash deploy.sh'), 'deploy.sh')
48
+ })
49
+
50
+ it('extracts script path from npx tsx command', () => {
51
+ assert.equal(mod.extractScheduleCommandScriptPath('npx tsx src/worker.ts'), 'src/worker.ts')
52
+ })
53
+
54
+ it('extracts script path from deno run command', () => {
55
+ assert.equal(mod.extractScheduleCommandScriptPath('deno run main.ts'), 'main.ts')
56
+ })
57
+
58
+ it('skips flags before finding path', () => {
59
+ assert.equal(mod.extractScheduleCommandScriptPath('python3 -u scripts/run.py'), 'scripts/run.py')
60
+ })
61
+
62
+ it('returns null for bare command with no script-like argument', () => {
63
+ assert.equal(mod.extractScheduleCommandScriptPath('echo hello'), null)
64
+ })
65
+
66
+ it('returns null for empty command', () => {
67
+ assert.equal(mod.extractScheduleCommandScriptPath(''), null)
68
+ })
69
+
70
+ it('handles quoted paths', () => {
71
+ assert.equal(mod.extractScheduleCommandScriptPath('python3 "my script.py"'), 'my script.py')
72
+ })
73
+ })
74
+
75
+ describe('normalizeSchedulePayload', () => {
76
+ it('rejects missing agentId', () => {
77
+ const result = mod.normalizeSchedulePayload({ taskPrompt: 'do stuff' })
78
+ assert.equal(result.ok, false)
79
+ if (!result.ok) assert.match(result.error, /agentId/)
80
+ })
81
+
82
+ it('rejects missing taskPrompt/command/path', () => {
83
+ const result = mod.normalizeSchedulePayload({ agentId: 'agent-1' })
84
+ assert.equal(result.ok, false)
85
+ if (!result.ok) assert.match(result.error, /taskPrompt/)
86
+ })
87
+
88
+ it('accepts valid payload with taskPrompt', () => {
89
+ const result = mod.normalizeSchedulePayload({
90
+ agentId: 'agent-1',
91
+ taskPrompt: 'Run the daily report',
92
+ })
93
+ assert.equal(result.ok, true)
94
+ if (result.ok) {
95
+ assert.equal(result.value.agentId, 'agent-1')
96
+ assert.equal(result.value.taskPrompt, 'Run the daily report')
97
+ }
98
+ })
99
+
100
+ it('derives taskPrompt from command when not explicit', () => {
101
+ const result = mod.normalizeSchedulePayload({
102
+ agentId: 'agent-1',
103
+ command: 'echo hello',
104
+ })
105
+ assert.equal(result.ok, true)
106
+ if (result.ok) {
107
+ assert.match(result.value.taskPrompt as string, /echo hello/)
108
+ }
109
+ })
110
+
111
+ it('normalizes scheduleType to interval by default', () => {
112
+ const result = mod.normalizeSchedulePayload({
113
+ agentId: 'agent-1',
114
+ taskPrompt: 'test',
115
+ })
116
+ assert.equal(result.ok, true)
117
+ if (result.ok) assert.equal(result.value.scheduleType, 'interval')
118
+ })
119
+
120
+ it('preserves valid scheduleType values', () => {
121
+ for (const t of ['cron', 'interval', 'once'] as const) {
122
+ const result = mod.normalizeSchedulePayload({
123
+ agentId: 'agent-1',
124
+ taskPrompt: 'test',
125
+ scheduleType: t,
126
+ })
127
+ assert.equal(result.ok, true)
128
+ if (result.ok) assert.equal(result.value.scheduleType, t)
129
+ }
130
+ })
131
+
132
+ it('falls back invalid scheduleType to interval', () => {
133
+ const result = mod.normalizeSchedulePayload({
134
+ agentId: 'agent-1',
135
+ taskPrompt: 'test',
136
+ scheduleType: 'bogus',
137
+ })
138
+ assert.equal(result.ok, true)
139
+ if (result.ok) assert.equal(result.value.scheduleType, 'interval')
140
+ })
141
+
142
+ it('normalizes status to active for unknown values', () => {
143
+ const result = mod.normalizeSchedulePayload({
144
+ agentId: 'agent-1',
145
+ taskPrompt: 'test',
146
+ status: 'invalid-status',
147
+ })
148
+ assert.equal(result.ok, true)
149
+ if (result.ok) assert.equal(result.value.status, 'active')
150
+ })
151
+
152
+ it('preserves valid status values', () => {
153
+ for (const s of ['active', 'paused', 'completed', 'failed']) {
154
+ const result = mod.normalizeSchedulePayload({
155
+ agentId: 'agent-1',
156
+ taskPrompt: 'test',
157
+ status: s,
158
+ })
159
+ assert.equal(result.ok, true)
160
+ if (result.ok) assert.equal(result.value.status, s)
161
+ }
162
+ })
163
+
164
+ it('calculates nextRunAt for interval type when intervalMs is set', () => {
165
+ const now = 1_000_000
166
+ const result = mod.normalizeSchedulePayload(
167
+ { agentId: 'agent-1', taskPrompt: 'test', scheduleType: 'interval', intervalMs: 5000 },
168
+ { now },
169
+ )
170
+ assert.equal(result.ok, true)
171
+ if (result.ok) assert.equal(result.value.nextRunAt, 1_005_000)
172
+ })
173
+
174
+ it('uses runAt for once type', () => {
175
+ const result = mod.normalizeSchedulePayload(
176
+ { agentId: 'agent-1', taskPrompt: 'test', scheduleType: 'once', runAt: 9_999_999 },
177
+ { now: 1_000_000 },
178
+ )
179
+ assert.equal(result.ok, true)
180
+ if (result.ok) assert.equal(result.value.nextRunAt, 9_999_999)
181
+ })
182
+
183
+ it('does not overwrite existing nextRunAt', () => {
184
+ const result = mod.normalizeSchedulePayload(
185
+ { agentId: 'agent-1', taskPrompt: 'test', scheduleType: 'interval', intervalMs: 5000, nextRunAt: 42 },
186
+ { now: 1_000_000 },
187
+ )
188
+ assert.equal(result.ok, true)
189
+ if (result.ok) assert.equal(result.value.nextRunAt, 42)
190
+ })
191
+
192
+ it('trims whitespace from agentId', () => {
193
+ const result = mod.normalizeSchedulePayload({
194
+ agentId: ' agent-1 ',
195
+ taskPrompt: 'test',
196
+ })
197
+ assert.equal(result.ok, true)
198
+ if (result.ok) assert.equal(result.value.agentId, 'agent-1')
199
+ })
200
+
201
+ it('rejects run_script action without path', () => {
202
+ const result = mod.normalizeSchedulePayload({
203
+ agentId: 'agent-1',
204
+ taskPrompt: 'test',
205
+ action: 'run_script',
206
+ })
207
+ assert.equal(result.ok, false)
208
+ if (!result.ok) assert.match(result.error, /run_script/)
209
+ })
210
+
211
+ it('rejects path outside workspace', () => {
212
+ const result = mod.normalizeSchedulePayload(
213
+ { agentId: 'agent-1', taskPrompt: 'test', path: '/etc/passwd' },
214
+ { cwd: workspaceDir },
215
+ )
216
+ assert.equal(result.ok, false)
217
+ if (!result.ok) assert.match(result.error, /must stay inside/)
218
+ })
219
+
220
+ it('validates path exists for file-based schedules', () => {
221
+ const result = mod.normalizeSchedulePayload(
222
+ { agentId: 'agent-1', taskPrompt: 'test', path: 'nonexistent.py' },
223
+ { cwd: workspaceDir },
224
+ )
225
+ assert.equal(result.ok, false)
226
+ if (!result.ok) assert.match(result.error, /not found/)
227
+ })
228
+
229
+ it('accepts path that exists inside workspace', () => {
230
+ const scriptPath = path.join(workspaceDir, 'test-script.py')
231
+ fs.writeFileSync(scriptPath, '#!/usr/bin/env python3\nprint("ok")')
232
+ const result = mod.normalizeSchedulePayload(
233
+ { agentId: 'agent-1', taskPrompt: 'test', path: 'test-script.py' },
234
+ { cwd: workspaceDir },
235
+ )
236
+ assert.equal(result.ok, true)
237
+ })
238
+
239
+ it('derives taskPrompt from path with run_script action', () => {
240
+ const scriptPath = path.join(workspaceDir, 'runner.sh')
241
+ fs.writeFileSync(scriptPath, '#!/bin/bash\necho ok')
242
+ const result = mod.normalizeSchedulePayload(
243
+ { agentId: 'agent-1', action: 'run_script', path: 'runner.sh' },
244
+ { cwd: workspaceDir },
245
+ )
246
+ assert.equal(result.ok, true)
247
+ if (result.ok) assert.match(result.value.taskPrompt as string, /Run the script/)
248
+ })
249
+
250
+ it('derives taskPrompt from path without specific action', () => {
251
+ const filePath = path.join(workspaceDir, 'data.csv')
252
+ fs.writeFileSync(filePath, 'a,b\n1,2')
253
+ const result = mod.normalizeSchedulePayload(
254
+ { agentId: 'agent-1', path: 'data.csv' },
255
+ { cwd: workspaceDir },
256
+ )
257
+ assert.equal(result.ok, true)
258
+ if (result.ok) assert.match(result.value.taskPrompt as string, /Use the file/)
259
+ })
260
+
261
+ it('rejects command referencing a missing script file', () => {
262
+ const result = mod.normalizeSchedulePayload(
263
+ { agentId: 'agent-1', taskPrompt: 'test', command: 'python3 missing_script.py' },
264
+ { cwd: workspaceDir },
265
+ )
266
+ assert.equal(result.ok, false)
267
+ if (!result.ok) assert.match(result.error, /missing file/)
268
+ })
269
+
270
+ it('accepts command referencing an existing script file', () => {
271
+ const scriptPath = path.join(workspaceDir, 'existing.py')
272
+ fs.writeFileSync(scriptPath, 'print("ok")')
273
+ const result = mod.normalizeSchedulePayload(
274
+ { agentId: 'agent-1', taskPrompt: 'test', command: 'python3 existing.py' },
275
+ { cwd: workspaceDir },
276
+ )
277
+ assert.equal(result.ok, true)
278
+ })
279
+ })
@@ -0,0 +1,263 @@
1
+ import { genId } from '@/lib/id'
2
+ import {
3
+ findDuplicateSchedule,
4
+ findEquivalentSchedules,
5
+ type ScheduleLike,
6
+ } from '@/lib/schedule-dedupe'
7
+ import { resolveScheduleName } from '@/lib/schedule-name'
8
+ import type { Schedule, ScheduleStatus } from '@/types'
9
+ import { dedup } from '@/lib/shared-utils'
10
+
11
+ import { normalizeSchedulePayload } from './schedule-normalization'
12
+
13
+ export interface ScheduleCreatorScope {
14
+ agentId?: string | null
15
+ sessionId?: string | null
16
+ }
17
+
18
+ export function buildScheduleCreatorScope(schedule: Record<string, unknown> | null | undefined): ScheduleCreatorScope | null {
19
+ if (!schedule || typeof schedule !== 'object') return null
20
+ const agentId = typeof schedule.createdByAgentId === 'string' && schedule.createdByAgentId.trim()
21
+ ? schedule.createdByAgentId.trim()
22
+ : null
23
+ const sessionId = typeof schedule.createdInSessionId === 'string' && schedule.createdInSessionId.trim()
24
+ ? schedule.createdInSessionId.trim()
25
+ : null
26
+ if (!agentId && !sessionId) return null
27
+ return { agentId, sessionId }
28
+ }
29
+
30
+ export function findRelatedScheduleIds(
31
+ schedules: Record<string, ScheduleLike>,
32
+ schedule: Record<string, unknown> | null | undefined,
33
+ opts: { ignoreId?: string | null } = {},
34
+ ): string[] {
35
+ if (!schedule || typeof schedule !== 'object') return []
36
+ const scope = buildScheduleCreatorScope(schedule)
37
+ if (!scope?.sessionId) return []
38
+ const matches = findEquivalentSchedules(schedules, {
39
+ id: typeof schedule.id === 'string' ? schedule.id : null,
40
+ agentId: typeof schedule.agentId === 'string' ? schedule.agentId : null,
41
+ taskPrompt: typeof schedule.taskPrompt === 'string' ? schedule.taskPrompt : null,
42
+ scheduleType: typeof schedule.scheduleType === 'string' ? schedule.scheduleType : null,
43
+ cron: typeof schedule.cron === 'string' ? schedule.cron : null,
44
+ intervalMs: typeof schedule.intervalMs === 'number' ? schedule.intervalMs : null,
45
+ runAt: typeof schedule.runAt === 'number' ? schedule.runAt : null,
46
+ createdByAgentId: scope.agentId,
47
+ createdInSessionId: scope.sessionId,
48
+ }, {
49
+ ignoreId: opts.ignoreId || (typeof schedule.id === 'string' ? schedule.id : null),
50
+ creatorScope: scope,
51
+ })
52
+ return dedup(matches
53
+ .map((entry) => (typeof entry.id === 'string' ? entry.id : ''))
54
+ .filter(Boolean))
55
+ }
56
+
57
+ export function getScheduleClusterIds(
58
+ schedules: Record<string, ScheduleLike>,
59
+ schedule: Record<string, unknown> | null | undefined,
60
+ opts: { ignoreId?: string | null } = {},
61
+ ): string[] {
62
+ const id = typeof schedule?.id === 'string' ? schedule.id : ''
63
+ const relatedIds = findRelatedScheduleIds(schedules, schedule, {
64
+ ignoreId: opts.ignoreId || id || null,
65
+ })
66
+ const ids = [
67
+ ...(!opts.ignoreId && id ? [id] : []),
68
+ ...relatedIds,
69
+ ]
70
+ return dedup(ids.filter(Boolean))
71
+ }
72
+
73
+ function normalizeScheduleStatus(value: unknown): ScheduleStatus | '' {
74
+ if (typeof value !== 'string') return ''
75
+ const normalized = value.trim().toLowerCase()
76
+ return normalized === 'active'
77
+ || normalized === 'paused'
78
+ || normalized === 'completed'
79
+ || normalized === 'failed'
80
+ ? normalized
81
+ : ''
82
+ }
83
+
84
+ export interface PrepareScheduleCreateOptions {
85
+ input: Record<string, unknown>
86
+ schedules: Record<string, ScheduleLike>
87
+ now: number
88
+ cwd?: string | null
89
+ creatorScope?: ScheduleCreatorScope | null
90
+ dedupeCreatorScope?: ScheduleCreatorScope | null
91
+ followupTarget?: Partial<Schedule>
92
+ createId?: () => string
93
+ }
94
+
95
+ export type PrepareScheduleCreateResult =
96
+ | { ok: false; error: string }
97
+ | {
98
+ ok: true
99
+ kind: 'duplicate'
100
+ scheduleId: string
101
+ schedule: ScheduleLike
102
+ changed: boolean
103
+ entries: Array<[string, ScheduleLike]>
104
+ }
105
+ | {
106
+ ok: true
107
+ kind: 'created'
108
+ scheduleId: string
109
+ schedule: Schedule
110
+ entries: Array<[string, Schedule]>
111
+ }
112
+
113
+ export function prepareScheduleCreate(options: PrepareScheduleCreateOptions): PrepareScheduleCreateResult {
114
+ const normalized = normalizeSchedulePayload(options.input, {
115
+ cwd: options.cwd,
116
+ now: options.now,
117
+ })
118
+ if (!normalized.ok) return { ok: false, error: normalized.error }
119
+
120
+ const candidate = normalized.value
121
+ const dedupeScope = options.dedupeCreatorScope || null
122
+ const duplicate = findDuplicateSchedule(options.schedules, {
123
+ agentId: typeof candidate.agentId === 'string' ? candidate.agentId : null,
124
+ taskPrompt: typeof candidate.taskPrompt === 'string' ? candidate.taskPrompt : '',
125
+ scheduleType: typeof candidate.scheduleType === 'string' ? candidate.scheduleType : 'interval',
126
+ cron: typeof candidate.cron === 'string' ? candidate.cron : null,
127
+ intervalMs: typeof candidate.intervalMs === 'number' ? candidate.intervalMs : null,
128
+ runAt: typeof candidate.runAt === 'number' ? candidate.runAt : null,
129
+ createdByAgentId: dedupeScope?.agentId || null,
130
+ createdInSessionId: dedupeScope?.sessionId || null,
131
+ }, dedupeScope
132
+ ? { creatorScope: dedupeScope }
133
+ : undefined)
134
+
135
+ if (duplicate) {
136
+ const duplicateId = typeof duplicate.id === 'string' ? duplicate.id : ''
137
+ const nextSchedule = { ...duplicate }
138
+ let changed = false
139
+ const nextName = resolveScheduleName({
140
+ name: candidate.name ?? nextSchedule.name,
141
+ taskPrompt: candidate.taskPrompt ?? nextSchedule.taskPrompt,
142
+ })
143
+ if (nextName && nextName !== nextSchedule.name) {
144
+ nextSchedule.name = nextName
145
+ changed = true
146
+ }
147
+ const nextStatus = normalizeScheduleStatus(candidate.status)
148
+ if ((nextStatus === 'active' || nextStatus === 'paused') && nextSchedule.status !== nextStatus) {
149
+ nextSchedule.status = nextStatus
150
+ changed = true
151
+ }
152
+ if (changed) nextSchedule.updatedAt = options.now
153
+ return {
154
+ ok: true,
155
+ kind: 'duplicate',
156
+ scheduleId: duplicateId,
157
+ schedule: nextSchedule,
158
+ changed,
159
+ entries: changed && duplicateId ? [[duplicateId, nextSchedule]] : [],
160
+ }
161
+ }
162
+
163
+ const id = options.createId ? options.createId() : genId()
164
+ const creatorFields = options.creatorScope
165
+ ? {
166
+ createdByAgentId: options.creatorScope.agentId || null,
167
+ createdInSessionId: options.creatorScope.sessionId || null,
168
+ }
169
+ : {}
170
+ const schedule = {
171
+ id,
172
+ ...candidate,
173
+ ...creatorFields,
174
+ ...(options.followupTarget || {}),
175
+ name: resolveScheduleName({ name: candidate.name, taskPrompt: candidate.taskPrompt }),
176
+ scheduleType: candidate.scheduleType === 'cron' || candidate.scheduleType === 'once' ? candidate.scheduleType : 'interval',
177
+ lastRunAt: undefined,
178
+ createdAt: options.now,
179
+ updatedAt: options.now,
180
+ } as Schedule
181
+
182
+ return {
183
+ ok: true,
184
+ kind: 'created',
185
+ scheduleId: id,
186
+ schedule,
187
+ entries: [[id, schedule]],
188
+ }
189
+ }
190
+
191
+ export interface PrepareScheduleUpdateOptions {
192
+ id: string
193
+ current: ScheduleLike
194
+ patch: Record<string, unknown>
195
+ schedules: Record<string, ScheduleLike>
196
+ now: number
197
+ cwd?: string | null
198
+ agentExists?: (agentId: string) => boolean
199
+ propagateEquivalentStatuses?: boolean
200
+ propagationSource?: Record<string, unknown> | null
201
+ }
202
+
203
+ export type PrepareScheduleUpdateResult =
204
+ | { ok: false; error: string }
205
+ | {
206
+ ok: true
207
+ schedule: ScheduleLike
208
+ entries: Array<[string, ScheduleLike]>
209
+ affectedScheduleIds: string[]
210
+ }
211
+
212
+ export function prepareScheduleUpdate(options: PrepareScheduleUpdateOptions): PrepareScheduleUpdateResult {
213
+ const normalized = normalizeSchedulePayload({
214
+ ...options.current,
215
+ ...options.patch,
216
+ id: options.id,
217
+ }, {
218
+ cwd: options.cwd,
219
+ now: options.now,
220
+ })
221
+ if (!normalized.ok) return { ok: false, error: normalized.error }
222
+
223
+ const nextSchedule = {
224
+ ...options.current,
225
+ ...normalized.value,
226
+ id: options.id,
227
+ updatedAt: options.now,
228
+ }
229
+ const agentId = typeof nextSchedule.agentId === 'string' ? nextSchedule.agentId : ''
230
+ if (options.agentExists && (!agentId || !options.agentExists(agentId))) {
231
+ return { ok: false, error: `Agent not found: ${String(nextSchedule.agentId)}` }
232
+ }
233
+ nextSchedule.name = resolveScheduleName({
234
+ name: nextSchedule.name,
235
+ taskPrompt: nextSchedule.taskPrompt,
236
+ })
237
+
238
+ const entries: Array<[string, ScheduleLike]> = [[options.id, nextSchedule]]
239
+ const nextStatus = normalizeScheduleStatus(nextSchedule.status)
240
+ if (options.propagateEquivalentStatuses && (nextStatus === 'paused' || nextStatus === 'completed' || nextStatus === 'failed')) {
241
+ const relatedIds = findRelatedScheduleIds(
242
+ options.schedules,
243
+ options.propagationSource || options.current,
244
+ { ignoreId: options.id },
245
+ )
246
+ for (const relatedId of relatedIds) {
247
+ const related = options.schedules[relatedId]
248
+ if (!related) continue
249
+ entries.push([relatedId, {
250
+ ...related,
251
+ status: nextStatus,
252
+ updatedAt: options.now,
253
+ }])
254
+ }
255
+ }
256
+
257
+ return {
258
+ ok: true,
259
+ schedule: nextSchedule,
260
+ entries,
261
+ affectedScheduleIds: dedup(entries.map(([id]) => id)),
262
+ }
263
+ }