@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,318 @@
1
+ /**
2
+ * Wake Dispatcher — central routing for wake requests by WakeMode.
3
+ *
4
+ * Instead of callers directly choosing between `requestHeartbeatNow()` (immediate)
5
+ * and the heartbeat-service timer (deferred), they call `dispatchWake()` with an
6
+ * explicit WakeMode. The dispatcher routes to the correct execution path.
7
+ *
8
+ * This replaces the pattern where scheduler.ts both creates a task AND calls
9
+ * requestHeartbeatNow() — the dispatcher handles that routing centrally.
10
+ */
11
+
12
+ import { genId } from '@/lib/id'
13
+ import { log } from './logger'
14
+ import type { WakeModeRequest, JobContext } from './wake-mode'
15
+ import {
16
+ computeWakePriority,
17
+ resolveRunAt,
18
+ wakeModeToSource,
19
+ createJobContext,
20
+ } from './wake-mode'
21
+ import type { WakeRequestInput } from './heartbeat-wake'
22
+ import { requestHeartbeatNow } from './heartbeat-wake'
23
+ import { enqueueSystemEvent } from './system-events'
24
+ import { errorMessage, hmrSingleton } from '@/lib/shared-utils'
25
+
26
+ // ── Deferred queue for `next_heartbeat` mode ────────────────────────────
27
+
28
+ interface DeferredWake {
29
+ request: WakeModeRequest
30
+ priority: number
31
+ enqueuedAt: number
32
+ }
33
+
34
+ const state = hmrSingleton('__swarmclaw_wake_dispatcher__', () => ({
35
+ deferredQueue: new Map<string, DeferredWake[]>(),
36
+ scheduledTimers: new Map<string, ReturnType<typeof setTimeout>>(),
37
+ activeJobs: new Map<string, JobContext>(),
38
+ }))
39
+
40
+ function deferredKey(request: WakeModeRequest): string {
41
+ return `${request.agentId || ''}::${request.sessionId || ''}`
42
+ }
43
+
44
+ // ── Public API ──────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Dispatch a wake request through the appropriate execution path.
48
+ *
49
+ * - `immediate`: forwards to requestHeartbeatNow with coalesce
50
+ * - `next_heartbeat`: queues for the next heartbeat-service tick to drain
51
+ * - `scheduled`: sets a timer to fire requestHeartbeatNow at the target time
52
+ */
53
+ export function dispatchWake(request: WakeModeRequest): {
54
+ mode: string
55
+ priority: number
56
+ runAt: number | null
57
+ jobId: string
58
+ } {
59
+ const priority = computeWakePriority(request)
60
+ const runAt = resolveRunAt(request)
61
+ const jobId = genId(8)
62
+
63
+ switch (request.mode) {
64
+ case 'immediate':
65
+ dispatchImmediate(request, priority, jobId)
66
+ break
67
+ case 'next_heartbeat':
68
+ dispatchDeferred(request, priority, jobId)
69
+ break
70
+ case 'scheduled':
71
+ dispatchScheduled(request, priority, runAt, jobId)
72
+ break
73
+ }
74
+
75
+ return { mode: request.mode, priority, runAt, jobId }
76
+ }
77
+
78
+ function dispatchImmediate(request: WakeModeRequest, priority: number, jobId: string): void {
79
+ const wakeInput: WakeRequestInput = {
80
+ eventId: request.eventId || jobId,
81
+ agentId: request.agentId,
82
+ sessionId: request.sessionId,
83
+ reason: request.reason || 'immediate-wake',
84
+ source: request.source,
85
+ resumeMessage: request.resumeMessage,
86
+ detail: request.detail,
87
+ priority,
88
+ }
89
+ requestHeartbeatNow(wakeInput)
90
+ log.info('wake-dispatcher', `Dispatched immediate wake ${jobId}`, {
91
+ agentId: request.agentId,
92
+ sessionId: request.sessionId,
93
+ reason: request.reason,
94
+ priority,
95
+ })
96
+ }
97
+
98
+ function dispatchDeferred(request: WakeModeRequest, priority: number, jobId: string): void {
99
+ const key = deferredKey(request)
100
+ const queue = state.deferredQueue.get(key) || []
101
+
102
+ // Deduplicate by eventId or reason+source
103
+ const existingIndex = queue.findIndex((entry: any) => {
104
+ if (request.eventId && entry.request.eventId) {
105
+ return request.eventId === entry.request.eventId
106
+ }
107
+ return entry.request.reason === request.reason
108
+ && entry.request.source === request.source
109
+ })
110
+
111
+ const entry: DeferredWake = { request, priority, enqueuedAt: Date.now() }
112
+
113
+ if (existingIndex >= 0) {
114
+ // Replace with higher-priority version
115
+ if (priority >= queue[existingIndex].priority) {
116
+ queue[existingIndex] = entry
117
+ }
118
+ } else {
119
+ queue.push(entry)
120
+ }
121
+
122
+ // Keep sorted by priority (highest first)
123
+ queue.sort((a: any, b: any) => b.priority - a.priority)
124
+ state.deferredQueue.set(key, queue)
125
+
126
+ log.info('wake-dispatcher', `Deferred wake ${jobId} queued for next heartbeat`, {
127
+ agentId: request.agentId,
128
+ sessionId: request.sessionId,
129
+ reason: request.reason,
130
+ queueDepth: queue.length,
131
+ })
132
+ }
133
+
134
+ function dispatchScheduled(
135
+ request: WakeModeRequest,
136
+ priority: number,
137
+ runAt: number | null,
138
+ jobId: string,
139
+ ): void {
140
+ const now = Date.now()
141
+ const targetTime = runAt ?? now
142
+ const delayMs = Math.max(0, targetTime - now)
143
+
144
+ // If the target time is now or in the past, dispatch immediately
145
+ if (delayMs <= 0) {
146
+ dispatchImmediate(request, priority, jobId)
147
+ return
148
+ }
149
+
150
+ // Set a timer to fire at the target time
151
+ const timerId = setTimeout(() => {
152
+ state.scheduledTimers.delete(jobId)
153
+ try {
154
+ dispatchImmediate(request, priority, jobId)
155
+ } catch (err: unknown) {
156
+ log.error('wake-dispatcher', `Scheduled wake ${jobId} failed, retrying once`, {
157
+ error: errorMessage(err),
158
+ agentId: request.agentId,
159
+ sessionId: request.sessionId,
160
+ })
161
+ // Single retry after 5s — if this also fails, the wake is lost (logged above)
162
+ setTimeout(() => {
163
+ try { dispatchImmediate(request, priority, jobId) } catch { /* give up */ }
164
+ }, 5000)
165
+ }
166
+ log.info('wake-dispatcher', `Scheduled wake ${jobId} fired after ${delayMs}ms delay`, {
167
+ agentId: request.agentId,
168
+ sessionId: request.sessionId,
169
+ reason: request.reason,
170
+ })
171
+ }, delayMs)
172
+
173
+ state.scheduledTimers.set(jobId, timerId)
174
+
175
+ log.info('wake-dispatcher', `Scheduled wake ${jobId} for ${new Date(targetTime).toISOString()}`, {
176
+ agentId: request.agentId,
177
+ sessionId: request.sessionId,
178
+ reason: request.reason,
179
+ delayMs,
180
+ })
181
+
182
+ // Also enqueue a system event so the agent knows something is scheduled
183
+ if (request.sessionId && request.resumeMessage) {
184
+ enqueueSystemEvent(
185
+ request.sessionId,
186
+ `[Scheduled] ${request.resumeMessage} (fires at ${new Date(targetTime).toISOString()})`,
187
+ )
188
+ }
189
+ }
190
+
191
+ // ── Deferred queue drain (called by heartbeat-service on each tick) ─────
192
+
193
+ /**
194
+ * Drain all deferred wakes for a given session/agent. Called by heartbeat-service
195
+ * during its periodic tick to pick up `next_heartbeat` mode requests.
196
+ *
197
+ * Returns the deferred events so the heartbeat-service can include them in
198
+ * the prompt context alongside normal heartbeat content.
199
+ */
200
+ export function drainDeferredWakes(agentId?: string, sessionId?: string): WakeModeRequest[] {
201
+ const key = `${agentId || ''}::${sessionId || ''}`
202
+ const queue = state.deferredQueue.get(key)
203
+ if (!queue || queue.length === 0) return []
204
+
205
+ const drained = queue.map((entry: any) => entry.request)
206
+ state.deferredQueue.delete(key)
207
+
208
+ log.info('wake-dispatcher', `Drained ${drained.length} deferred wakes`, {
209
+ agentId,
210
+ sessionId,
211
+ reasons: drained.map((r: any) => r.reason),
212
+ })
213
+
214
+ return drained
215
+ }
216
+
217
+ /**
218
+ * Check if there are pending deferred wakes for a target.
219
+ */
220
+ export function hasDeferredWakes(agentId?: string, sessionId?: string): boolean {
221
+ const key = `${agentId || ''}::${sessionId || ''}`
222
+ const queue = state.deferredQueue.get(key)
223
+ return !!queue && queue.length > 0
224
+ }
225
+
226
+ // ── Job context management ──────────────────────────────────────────────
227
+
228
+ /**
229
+ * Create and register an isolated job context for a wake execution.
230
+ * The job context provides per-job scratchpad, abort signal, and
231
+ * heartbeat snapshot isolation.
232
+ */
233
+ export function startJobExecution(params: {
234
+ sessionId: string
235
+ agentId?: string
236
+ mode: WakeModeRequest['mode']
237
+ signal: AbortSignal
238
+ source?: string
239
+ reason?: string
240
+ heartbeatSnapshot?: string
241
+ }): JobContext {
242
+ const jobId = genId(8)
243
+ const ctx = createJobContext({
244
+ jobId,
245
+ sessionId: params.sessionId,
246
+ agentId: params.agentId,
247
+ mode: params.mode,
248
+ signal: params.signal,
249
+ source: params.source,
250
+ reason: params.reason,
251
+ heartbeatSnapshot: params.heartbeatSnapshot,
252
+ })
253
+ ctx.startedAt = Date.now()
254
+ state.activeJobs.set(jobId, ctx)
255
+ return ctx
256
+ }
257
+
258
+ /**
259
+ * Mark a job as completed and remove from active tracking.
260
+ */
261
+ export function endJobExecution(jobId: string): JobContext | null {
262
+ const ctx = state.activeJobs.get(jobId)
263
+ if (!ctx) return null
264
+ ctx.endedAt = Date.now()
265
+ state.activeJobs.delete(jobId)
266
+ return ctx
267
+ }
268
+
269
+ /**
270
+ * Get the active job context by ID.
271
+ */
272
+ export function getActiveJob(jobId: string): JobContext | null {
273
+ return state.activeJobs.get(jobId) || null
274
+ }
275
+
276
+ /**
277
+ * List all active job contexts for a session.
278
+ */
279
+ export function getActiveJobsForSession(sessionId: string): JobContext[] {
280
+ return [...state.activeJobs.values()].filter((ctx) => ctx.sessionId === sessionId)
281
+ }
282
+
283
+ // ── Cleanup ─────────────────────────────────────────────────────────────
284
+
285
+ export function cancelScheduledWake(jobId: string): boolean {
286
+ const timer = state.scheduledTimers.get(jobId)
287
+ if (!timer) return false
288
+ clearTimeout(timer)
289
+ state.scheduledTimers.delete(jobId)
290
+ return true
291
+ }
292
+
293
+ export function resetWakeDispatcherForTests(): void {
294
+ for (const timer of state.scheduledTimers.values()) {
295
+ clearTimeout(timer)
296
+ }
297
+ state.deferredQueue.clear()
298
+ state.scheduledTimers.clear()
299
+ state.activeJobs.clear()
300
+ }
301
+
302
+ // ── Diagnostics ─────────────────────────────────────────────────────────
303
+
304
+ export function getWakeDispatcherStatus(): {
305
+ deferredQueueCount: number
306
+ scheduledTimerCount: number
307
+ activeJobCount: number
308
+ } {
309
+ let deferredCount = 0
310
+ for (const queue of state.deferredQueue.values()) {
311
+ deferredCount += queue.length
312
+ }
313
+ return {
314
+ deferredQueueCount: deferredCount,
315
+ scheduledTimerCount: state.scheduledTimers.size,
316
+ activeJobCount: state.activeJobs.size,
317
+ }
318
+ }
@@ -0,0 +1,161 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import {
5
+ computeWakePriority,
6
+ createJobContext,
7
+ resolveRunAt,
8
+ sourceToWakeMode,
9
+ wakeModeToSource,
10
+ } from './wake-mode'
11
+ import type { WakeModeRequest } from './wake-mode'
12
+
13
+ describe('WakeMode', () => {
14
+ describe('computeWakePriority', () => {
15
+ it('returns mode-based default priority when none specified', () => {
16
+ assert.equal(computeWakePriority({ mode: 'immediate' }), 80)
17
+ assert.equal(computeWakePriority({ mode: 'next_heartbeat' }), 40)
18
+ assert.equal(computeWakePriority({ mode: 'scheduled' }), 60)
19
+ })
20
+
21
+ it('uses explicit priority when provided', () => {
22
+ assert.equal(computeWakePriority({ mode: 'immediate', priority: 95 }), 95)
23
+ assert.equal(computeWakePriority({ mode: 'next_heartbeat', priority: 10 }), 10)
24
+ })
25
+
26
+ it('clamps priority to [0, 100]', () => {
27
+ assert.equal(computeWakePriority({ mode: 'immediate', priority: 150 }), 100)
28
+ assert.equal(computeWakePriority({ mode: 'immediate', priority: -5 }), 0)
29
+ })
30
+
31
+ it('ignores non-finite priority values', () => {
32
+ assert.equal(computeWakePriority({ mode: 'immediate', priority: NaN }), 80)
33
+ assert.equal(computeWakePriority({ mode: 'immediate', priority: Infinity }), 80)
34
+ })
35
+ })
36
+
37
+ describe('resolveRunAt', () => {
38
+ const NOW = 1_700_000_000_000
39
+
40
+ it('returns now for immediate mode', () => {
41
+ assert.equal(resolveRunAt({ mode: 'immediate' }, NOW), NOW)
42
+ })
43
+
44
+ it('returns null for next_heartbeat mode (deferred)', () => {
45
+ assert.equal(resolveRunAt({ mode: 'next_heartbeat' }, NOW), null)
46
+ })
47
+
48
+ it('returns absolute runAt for scheduled mode', () => {
49
+ const target = NOW + 60_000
50
+ assert.equal(resolveRunAt({ mode: 'scheduled', runAt: target }, NOW), target)
51
+ })
52
+
53
+ it('computes runAt from delayMs for scheduled mode', () => {
54
+ assert.equal(resolveRunAt({ mode: 'scheduled', delayMs: 5_000 }, NOW), NOW + 5_000)
55
+ })
56
+
57
+ it('clamps scheduled runAt to at least now', () => {
58
+ const pastTime = NOW - 10_000
59
+ assert.equal(resolveRunAt({ mode: 'scheduled', runAt: pastTime }, NOW), NOW)
60
+ })
61
+
62
+ it('falls back to now for scheduled mode without runAt or delayMs', () => {
63
+ assert.equal(resolveRunAt({ mode: 'scheduled' }, NOW), NOW)
64
+ })
65
+ })
66
+
67
+ describe('wakeModeToSource (backward compat)', () => {
68
+ it('maps immediate to heartbeat-wake', () => {
69
+ assert.equal(wakeModeToSource('immediate'), 'heartbeat-wake')
70
+ })
71
+
72
+ it('maps next_heartbeat to heartbeat', () => {
73
+ assert.equal(wakeModeToSource('next_heartbeat'), 'heartbeat')
74
+ })
75
+
76
+ it('maps scheduled to heartbeat-wake', () => {
77
+ assert.equal(wakeModeToSource('scheduled'), 'heartbeat-wake')
78
+ })
79
+ })
80
+
81
+ describe('sourceToWakeMode (legacy migration)', () => {
82
+ it('infers next_heartbeat from heartbeat source', () => {
83
+ assert.equal(sourceToWakeMode('heartbeat'), 'next_heartbeat')
84
+ })
85
+
86
+ it('infers immediate from heartbeat-wake source', () => {
87
+ assert.equal(sourceToWakeMode('heartbeat-wake'), 'immediate')
88
+ })
89
+
90
+ it('infers scheduled from schedule-prefixed source', () => {
91
+ assert.equal(sourceToWakeMode('schedule:nightly'), 'scheduled')
92
+ })
93
+
94
+ it('defaults to immediate for unknown sources', () => {
95
+ assert.equal(sourceToWakeMode('connector:slack'), 'immediate')
96
+ })
97
+ })
98
+
99
+ describe('createJobContext', () => {
100
+ it('creates an isolated context with scratchpad', () => {
101
+ const controller = new AbortController()
102
+ const ctx = createJobContext({
103
+ jobId: 'job-1',
104
+ sessionId: 'sess-1',
105
+ agentId: 'agent-1',
106
+ mode: 'immediate',
107
+ signal: controller.signal,
108
+ source: 'connector:slack',
109
+ reason: 'New message arrived',
110
+ })
111
+
112
+ assert.equal(ctx.jobId, 'job-1')
113
+ assert.equal(ctx.sessionId, 'sess-1')
114
+ assert.equal(ctx.agentId, 'agent-1')
115
+ assert.equal(ctx.mode, 'immediate')
116
+ assert.equal(ctx.source, 'connector:slack')
117
+ assert.equal(ctx.reason, 'New message arrived')
118
+ assert.ok(ctx.createdAt > 0)
119
+ assert.equal(ctx.startedAt, undefined)
120
+ assert.equal(ctx.endedAt, undefined)
121
+ assert.ok(ctx.scratchpad instanceof Map)
122
+ assert.equal(ctx.scratchpad.size, 0)
123
+ })
124
+
125
+ it('scratchpad isolates state between jobs', () => {
126
+ const controller = new AbortController()
127
+ const ctx1 = createJobContext({
128
+ jobId: 'job-a',
129
+ sessionId: 'sess-1',
130
+ mode: 'immediate',
131
+ signal: controller.signal,
132
+ })
133
+ const ctx2 = createJobContext({
134
+ jobId: 'job-b',
135
+ sessionId: 'sess-1',
136
+ mode: 'next_heartbeat',
137
+ signal: controller.signal,
138
+ })
139
+
140
+ ctx1.scratchpad.set('key', 'value-a')
141
+ ctx2.scratchpad.set('key', 'value-b')
142
+
143
+ assert.equal(ctx1.scratchpad.get('key'), 'value-a')
144
+ assert.equal(ctx2.scratchpad.get('key'), 'value-b')
145
+ })
146
+
147
+ it('captures heartbeat snapshot for isolation', () => {
148
+ const controller = new AbortController()
149
+ const snapshot = '# Heartbeat Tasks\n## Active\n- [ ] Send report'
150
+ const ctx = createJobContext({
151
+ jobId: 'job-snap',
152
+ sessionId: 'sess-1',
153
+ mode: 'next_heartbeat',
154
+ signal: controller.signal,
155
+ heartbeatSnapshot: snapshot,
156
+ })
157
+
158
+ assert.equal(ctx.heartbeatSnapshot, snapshot)
159
+ })
160
+ })
161
+ })
@@ -0,0 +1,174 @@
1
+ /**
2
+ * WakeMode — explicit scheduling semantics for heartbeat and task execution.
3
+ *
4
+ * Replaces the implicit `source: 'heartbeat' | 'heartbeat-wake'` convention
5
+ * with a formal enum that determines routing, priority, and isolation behavior.
6
+ *
7
+ * Inspired by OpenClaw's separation of "run now" vs "queue next heartbeat" vs
8
+ * scheduled execution with proper isolation.
9
+ */
10
+
11
+ // ── WakeMode enum ───────────────────────────────────────────────────────
12
+
13
+ export type WakeMode = 'immediate' | 'next_heartbeat' | 'scheduled'
14
+
15
+ /**
16
+ * `immediate` — Run now. Coalesced within a short window (250ms default),
17
+ * then dispatched. Used for connector events, watch-job
18
+ * triggers, approvals, webhooks.
19
+ *
20
+ * `next_heartbeat` — Queue for the next periodic heartbeat tick. No coalesce
21
+ * window; the job waits until the heartbeat-service timer
22
+ * fires. Used for low-urgency background polling, system
23
+ * events that don't need instant reaction.
24
+ *
25
+ * `scheduled` — Run at a specific future time (absolute or relative).
26
+ * Managed by the scheduler tick. Used for cron jobs,
27
+ * interval schedules, one-shot delayed wakes.
28
+ */
29
+
30
+ // ── Job Context ─────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Isolated execution context for a wake job. Each job gets its own context
34
+ * so that failures, side effects, and state are contained.
35
+ */
36
+ export interface JobContext {
37
+ /** Unique job execution ID. */
38
+ jobId: string
39
+ /** Which session this job targets. */
40
+ sessionId: string
41
+ /** Which agent (if any) owns this job. */
42
+ agentId?: string
43
+ /** The wake mode that created this job. */
44
+ mode: WakeMode
45
+ /** When the job was created/requested. */
46
+ createdAt: number
47
+ /** When the job actually started executing. */
48
+ startedAt?: number
49
+ /** When the job finished (success or failure). */
50
+ endedAt?: number
51
+ /** Abort controller for this specific job. */
52
+ signal: AbortSignal
53
+ /** Source identifier (e.g. 'connector:slack', 'schedule:nightly'). */
54
+ source?: string
55
+ /** Human-readable reason for this wake. */
56
+ reason?: string
57
+ /** Snapshot of HEARTBEAT.md at job start (for isolation). */
58
+ heartbeatSnapshot?: string
59
+ /** Job-scoped metadata accumulator — tools can stash results here without
60
+ * polluting session state until the job completes successfully. */
61
+ scratchpad: Map<string, unknown>
62
+ }
63
+
64
+ export function createJobContext(params: {
65
+ jobId: string
66
+ sessionId: string
67
+ agentId?: string
68
+ mode: WakeMode
69
+ signal: AbortSignal
70
+ source?: string
71
+ reason?: string
72
+ heartbeatSnapshot?: string
73
+ }): JobContext {
74
+ return {
75
+ jobId: params.jobId,
76
+ sessionId: params.sessionId,
77
+ agentId: params.agentId,
78
+ mode: params.mode,
79
+ createdAt: Date.now(),
80
+ signal: params.signal,
81
+ source: params.source,
82
+ reason: params.reason,
83
+ heartbeatSnapshot: params.heartbeatSnapshot,
84
+ scratchpad: new Map(),
85
+ }
86
+ }
87
+
88
+ // ── Wake Request with explicit mode ─────────────────────────────────────
89
+
90
+ export interface WakeModeRequest {
91
+ mode: WakeMode
92
+ agentId?: string
93
+ sessionId?: string
94
+ reason?: string
95
+ source?: string
96
+ resumeMessage?: string
97
+ detail?: string
98
+ priority?: number
99
+ /** For `scheduled` mode: absolute timestamp (ms) to execute at. */
100
+ runAt?: number
101
+ /** For `scheduled` mode: relative delay (ms) from now. */
102
+ delayMs?: number
103
+ /** Event ID for deduplication. */
104
+ eventId?: string
105
+ }
106
+
107
+ // ── Priority mapping per mode ───────────────────────────────────────────
108
+
109
+ const MODE_BASE_PRIORITY: Record<WakeMode, number> = {
110
+ immediate: 80,
111
+ scheduled: 60,
112
+ next_heartbeat: 40,
113
+ }
114
+
115
+ /**
116
+ * Compute effective priority for a wake request. Explicit priority overrides
117
+ * the mode-based default; otherwise the mode determines the base.
118
+ */
119
+ export function computeWakePriority(request: WakeModeRequest): number {
120
+ if (typeof request.priority === 'number' && Number.isFinite(request.priority)) {
121
+ return Math.max(0, Math.min(100, Math.trunc(request.priority)))
122
+ }
123
+ return MODE_BASE_PRIORITY[request.mode]
124
+ }
125
+
126
+ /**
127
+ * Resolve the target execution time for a wake request.
128
+ * - `immediate`: now (or within coalesce window)
129
+ * - `next_heartbeat`: null (deferred to next tick)
130
+ * - `scheduled`: absolute time from runAt or now + delayMs
131
+ */
132
+ export function resolveRunAt(request: WakeModeRequest, now = Date.now()): number | null {
133
+ switch (request.mode) {
134
+ case 'immediate':
135
+ return now
136
+ case 'next_heartbeat':
137
+ return null
138
+ case 'scheduled': {
139
+ if (typeof request.runAt === 'number' && Number.isFinite(request.runAt)) {
140
+ return Math.max(now, Math.trunc(request.runAt))
141
+ }
142
+ if (typeof request.delayMs === 'number' && Number.isFinite(request.delayMs)) {
143
+ return now + Math.max(0, Math.trunc(request.delayMs))
144
+ }
145
+ return now
146
+ }
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Map a WakeMode to the session-run-manager source string.
152
+ * Maintains backward compatibility with existing heartbeat-source checks.
153
+ */
154
+ export function wakeModeToSource(mode: WakeMode): string {
155
+ switch (mode) {
156
+ case 'immediate':
157
+ return 'heartbeat-wake'
158
+ case 'next_heartbeat':
159
+ return 'heartbeat'
160
+ case 'scheduled':
161
+ return 'heartbeat-wake'
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Infer WakeMode from a legacy source string.
167
+ * Used during migration to preserve backward compat with existing callers.
168
+ */
169
+ export function sourceToWakeMode(source: string): WakeMode {
170
+ if (source === 'heartbeat') return 'next_heartbeat'
171
+ if (source === 'heartbeat-wake') return 'immediate'
172
+ if (source.startsWith('schedule')) return 'scheduled'
173
+ return 'immediate'
174
+ }
@@ -9,7 +9,8 @@ import {
9
9
  normalizeAtomicString,
10
10
  } from '@/lib/wallet'
11
11
  import type { Agent, AgentWallet, WalletChain, WalletTransaction } from '@/types'
12
- import { loadAgents, loadWalletTransactions, loadWallets, saveAgents, upsertWallet } from './storage'
12
+ import { dedup } from '@/lib/shared-utils'
13
+ import { loadAgent, loadAgents, loadWalletTransactions, loadWallets, upsertAgent, upsertWallet } from './storage'
13
14
  import { generateEthereumWallet, isValidEthereumAddress, sendEth } from './ethereum'
14
15
  import { generateSolanaKeypair, isValidSolanaAddress, sendSol } from './solana'
15
16
  import { notify } from './ws-hub'
@@ -31,7 +32,7 @@ export function getAgentWalletIds(agent: Pick<Agent, 'walletIds' | 'walletId'> |
31
32
  const legacy = typeof agent?.walletId === 'string' && agent.walletId.trim()
32
33
  ? [agent.walletId.trim()]
33
34
  : []
34
- return [...new Set([...ids, ...legacy])]
35
+ return dedup([...ids, ...legacy])
35
36
  }
36
37
 
37
38
  export function getAgentActiveWalletId(
@@ -44,7 +45,7 @@ export function getAgentActiveWalletId(
44
45
  }
45
46
 
46
47
  function syncAgentWalletPointers(agent: Agent, walletIds: string[], activeWalletId?: string | null): Agent {
47
- const normalizedIds = [...new Set(walletIds.filter(Boolean))]
48
+ const normalizedIds = dedup(walletIds.filter(Boolean))
48
49
  const normalizedActive = activeWalletId && normalizedIds.includes(activeWalletId)
49
50
  ? activeWalletId
50
51
  : normalizedIds[0] || null
@@ -102,8 +103,8 @@ export function createAgentWallet(input: {
102
103
  const agentId = String(input.agentId || '').trim()
103
104
  if (!agentId) throw new Error('agentId is required')
104
105
 
105
- const agents = loadAgents()
106
- if (!agents[agentId]) throw new Error('Agent not found')
106
+ const agent = loadAgent(agentId)
107
+ if (!agent) throw new Error('Agent not found')
107
108
 
108
109
  const chain = getWalletChainOrDefault(input.chain ?? input.provider, 'solana')
109
110
  const existing = getWalletByAgentId(agentId, chain)
@@ -128,11 +129,9 @@ export function createAgentWallet(input: {
128
129
  upsertWallet(id, wallet)
129
130
  clearWalletPortfolioCache(id)
130
131
 
131
- const agent = agents[agentId]
132
- linkWalletToAgent(agent, id, getAgentActiveWalletId(agent) == null)
132
+ linkWalletToAgent(agent as any, id, getAgentActiveWalletId(agent as any) == null)
133
133
  agent.updatedAt = now
134
- agents[agentId] = agent
135
- saveAgents(agents)
134
+ upsertAgent(agentId, agent)
136
135
 
137
136
  notify('wallets')
138
137
  notify('agents')