@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -1,32 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getPluginManager } from '@/lib/server/plugins'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
-
5
- // Ensure all builtin plugins are registered by importing their modules
6
- import '@/lib/server/session-tools/shell'
7
- import '@/lib/server/session-tools/file'
8
- import '@/lib/server/session-tools/edit_file'
9
- import '@/lib/server/session-tools/web'
10
- import '@/lib/server/session-tools/memory'
11
- import '@/lib/server/session-tools/platform'
12
- import '@/lib/server/session-tools/monitor'
13
- import '@/lib/server/session-tools/discovery'
14
- import '@/lib/server/session-tools/sample-ui'
15
- import '@/lib/server/session-tools/git'
16
- import '@/lib/server/session-tools/wallet'
17
- import '@/lib/server/session-tools/connector'
18
- import '@/lib/server/session-tools/http'
19
- import '@/lib/server/session-tools/sandbox'
20
- import '@/lib/server/session-tools/canvas'
21
- import '@/lib/server/session-tools/chatroom'
22
- import '@/lib/server/session-tools/delegate'
23
- import '@/lib/server/session-tools/schedule'
24
- import '@/lib/server/session-tools/session-info'
25
- import '@/lib/server/session-tools/openclaw-nodes'
26
- import '@/lib/server/session-tools/openclaw-workspace'
27
- import '@/lib/server/session-tools/context-mgmt'
28
- import '@/lib/server/session-tools/subagent'
29
- import '@/lib/server/session-tools/plugin-creator'
4
+ import '@/lib/server/builtin-plugins'
30
5
 
31
6
  export const dynamic = 'force-dynamic'
32
7
 
@@ -74,11 +49,13 @@ export async function PATCH(req: Request) {
74
49
 
75
50
  if (all) {
76
51
  await manager.updateAllPlugins()
52
+ notify('plugins')
77
53
  return NextResponse.json({ ok: true, message: 'All plugins updated' })
78
54
  }
79
55
 
80
56
  if (id) {
81
57
  await manager.updatePlugin(id)
58
+ notify('plugins')
82
59
  return NextResponse.json({ ok: true, message: `Plugin ${id} updated` })
83
60
  }
84
61
 
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadSettings, saveSettings } from '@/lib/server/storage'
2
+ import { getPluginManager } from '@/lib/server/plugins'
3
+ import '@/lib/server/builtin-plugins'
3
4
 
4
5
  export const dynamic = 'force-dynamic'
5
6
 
@@ -11,9 +12,7 @@ export async function GET(req: Request) {
11
12
  return NextResponse.json({ error: 'pluginId required' }, { status: 400 })
12
13
  }
13
14
 
14
- const settings = loadSettings()
15
- const pluginSettings = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined) ?? {}
16
- return NextResponse.json(pluginSettings[pluginId] ?? {})
15
+ return NextResponse.json(getPluginManager().getPublicPluginSettings(pluginId))
17
16
  }
18
17
 
19
18
  /** PUT /api/plugins/settings?pluginId=X — write per-plugin settings */
@@ -24,12 +23,18 @@ export async function PUT(req: Request) {
24
23
  return NextResponse.json({ error: 'pluginId required' }, { status: 400 })
25
24
  }
26
25
 
27
- const body = await req.json() as Record<string, unknown>
28
- const settings = loadSettings()
29
- const pluginSettings = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined) ?? {}
30
- pluginSettings[pluginId] = body
31
- settings.pluginSettings = pluginSettings
32
- saveSettings(settings)
33
-
34
- return NextResponse.json({ ok: true })
26
+ try {
27
+ const body = await req.json() as Record<string, unknown>
28
+ const saved = getPluginManager().setPluginSettings(pluginId, body)
29
+ return NextResponse.json({
30
+ ok: true,
31
+ values: saved,
32
+ configuredSecretFields: getPluginManager().getPublicPluginSettings(pluginId).configuredSecretFields,
33
+ })
34
+ } catch (err: unknown) {
35
+ return NextResponse.json(
36
+ { error: err instanceof Error ? err.message : String(err) },
37
+ { status: 400 },
38
+ )
39
+ }
35
40
  }
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getPluginManager } from '@/lib/server/plugins'
3
+ import '@/lib/server/builtin-plugins'
3
4
 
4
5
  export const dynamic = 'force-dynamic'
5
6
 
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { discoverProviderModels } from '@/lib/server/provider-model-discovery'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET(
7
+ req: Request,
8
+ { params }: { params: Promise<{ id: string }> },
9
+ ) {
10
+ const { id } = await params
11
+ const { searchParams } = new URL(req.url)
12
+ const result = await discoverProviderModels({
13
+ providerId: id,
14
+ credentialId: searchParams.get('credentialId'),
15
+ endpoint: searchParams.get('endpoint'),
16
+ force: searchParams.get('force') === '1',
17
+ requiresApiKey: searchParams.has('requiresApiKey')
18
+ ? searchParams.get('requiresApiKey') !== '0'
19
+ : undefined,
20
+ })
21
+
22
+ return NextResponse.json(result, {
23
+ headers: {
24
+ 'Cache-Control': 'private, no-store',
25
+ },
26
+ })
27
+ }
@@ -1,7 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadSchedules, saveSchedules, deleteSchedule } from '@/lib/server/storage'
2
+ import { loadAgents, loadSchedules, loadSessions, saveSchedules, deleteSchedule } from '@/lib/server/storage'
3
+ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
3
4
  import { resolveScheduleName } from '@/lib/schedule-name'
4
5
  import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
6
+ import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
5
7
 
6
8
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
9
  const ops: CollectionOps<any> = { load: loadSchedules, save: saveSchedules, deleteFn: deleteSchedule, topic: 'schedules' }
@@ -9,15 +11,42 @@ const ops: CollectionOps<any> = { load: loadSchedules, save: saveSchedules, dele
9
11
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
12
  const { id } = await params
11
13
  const body = await req.json()
12
- const result = mutateItem(ops, id, (schedule) => {
13
- Object.assign(schedule, body)
14
- schedule.id = id
15
- schedule.name = resolveScheduleName({
16
- name: schedule.name,
17
- taskPrompt: schedule.taskPrompt,
14
+ const sessions = loadSessions()
15
+ const agents = loadAgents()
16
+ let result = null
17
+ try {
18
+ result = mutateItem(ops, id, (schedule) => {
19
+ const sessionCwd = typeof schedule.createdInSessionId === 'string'
20
+ ? sessions[schedule.createdInSessionId]?.cwd
21
+ : null
22
+ const normalized = normalizeSchedulePayload({
23
+ ...schedule,
24
+ ...(body as Record<string, unknown>),
25
+ id,
26
+ }, {
27
+ cwd: sessionCwd || WORKSPACE_DIR,
28
+ now: Date.now(),
29
+ })
30
+ if (!normalized.ok) throw new Error(normalized.error)
31
+ const nextSchedule = {
32
+ ...schedule,
33
+ ...normalized.value,
34
+ id,
35
+ updatedAt: Date.now(),
36
+ }
37
+ if (!agents[String(nextSchedule.agentId)]) {
38
+ throw new Error(`Agent not found: ${String(nextSchedule.agentId)}`)
39
+ }
40
+ nextSchedule.name = resolveScheduleName({
41
+ name: nextSchedule.name,
42
+ taskPrompt: nextSchedule.taskPrompt,
43
+ })
44
+ return nextSchedule
18
45
  })
19
- return schedule
20
- })
46
+ } catch (error: unknown) {
47
+ const message = error instanceof Error ? error.message : String(error)
48
+ return NextResponse.json({ error: message }, { status: 400 })
49
+ }
21
50
  if (!result) return notFound()
22
51
  return NextResponse.json(result)
23
52
  }
@@ -1,11 +1,31 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
- import { loadSchedules, saveSchedules } from '@/lib/server/storage'
3
+ import { loadAgents, loadSchedules, saveSchedules } from '@/lib/server/storage'
4
+ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
5
+ import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
4
6
  import { resolveScheduleName } from '@/lib/schedule-name'
5
7
  import { findDuplicateSchedule } from '@/lib/schedule-dedupe'
6
8
  import { notify } from '@/lib/server/ws-hub'
7
9
  export const dynamic = 'force-dynamic'
8
10
 
11
+ function asString(value: unknown): string {
12
+ return typeof value === 'string' ? value.trim() : ''
13
+ }
14
+
15
+ function asPositiveInt(value: unknown): number | null {
16
+ const parsed = typeof value === 'number'
17
+ ? value
18
+ : typeof value === 'string'
19
+ ? Number.parseInt(value, 10)
20
+ : Number.NaN
21
+ if (!Number.isFinite(parsed)) return null
22
+ const intValue = Math.trunc(parsed)
23
+ return intValue > 0 ? intValue : null
24
+ }
25
+
26
+ function asScheduleType(value: unknown): 'cron' | 'interval' | 'once' {
27
+ return value === 'cron' || value === 'interval' || value === 'once' ? value : 'cron'
28
+ }
9
29
 
10
30
  export async function GET(_req: Request) {
11
31
  return NextResponse.json(loadSchedules())
@@ -15,28 +35,46 @@ export async function POST(req: Request) {
15
35
  const body = await req.json()
16
36
  const now = Date.now()
17
37
  const schedules = loadSchedules()
18
- const scheduleType = body.scheduleType || 'cron'
38
+ const normalizedSchedule = normalizeSchedulePayload(body as Record<string, unknown>, {
39
+ cwd: WORKSPACE_DIR,
40
+ now,
41
+ })
42
+ if (!normalizedSchedule.ok) {
43
+ return NextResponse.json({ error: normalizedSchedule.error }, { status: 400 })
44
+ }
45
+
46
+ const candidate = normalizedSchedule.value
47
+ const agents = loadAgents()
48
+ if (!agents[String(candidate.agentId)]) {
49
+ return NextResponse.json({ error: `Agent not found: ${String(candidate.agentId)}` }, { status: 400 })
50
+ }
51
+ const scheduleType = asScheduleType(candidate.scheduleType)
52
+ const candidateAgentId = asString(candidate.agentId) || null
53
+ const candidateTaskPrompt = asString(candidate.taskPrompt)
54
+ const candidateCron = asString(candidate.cron) || null
55
+ const candidateIntervalMs = asPositiveInt(candidate.intervalMs)
56
+ const candidateRunAt = asPositiveInt(candidate.runAt)
19
57
 
20
58
  const duplicate = findDuplicateSchedule(schedules, {
21
- agentId: body.agentId || null,
22
- taskPrompt: body.taskPrompt || '',
59
+ agentId: candidateAgentId,
60
+ taskPrompt: candidateTaskPrompt,
23
61
  scheduleType,
24
- cron: body.cron,
25
- intervalMs: body.intervalMs,
26
- runAt: body.runAt,
62
+ cron: candidateCron,
63
+ intervalMs: candidateIntervalMs,
64
+ runAt: candidateRunAt,
27
65
  })
28
66
  if (duplicate) {
29
67
  const duplicateId = duplicate.id || ''
30
68
  let changed = false
31
69
  const nextName = resolveScheduleName({
32
- name: body.name ?? duplicate.name,
33
- taskPrompt: body.taskPrompt ?? duplicate.taskPrompt,
70
+ name: candidate.name ?? duplicate.name,
71
+ taskPrompt: candidate.taskPrompt ?? duplicate.taskPrompt,
34
72
  })
35
73
  if (nextName && nextName !== duplicate.name) {
36
74
  duplicate.name = nextName
37
75
  changed = true
38
76
  }
39
- const normalizedStatus = typeof body.status === 'string' ? body.status.trim().toLowerCase() : ''
77
+ const normalizedStatus = typeof candidate.status === 'string' ? candidate.status.trim().toLowerCase() : ''
40
78
  if ((normalizedStatus === 'active' || normalizedStatus === 'paused') && duplicate.status !== normalizedStatus) {
41
79
  duplicate.status = normalizedStatus as 'active' | 'paused'
42
80
  changed = true
@@ -53,29 +91,14 @@ export async function POST(req: Request) {
53
91
 
54
92
  const id = genId()
55
93
 
56
- let nextRunAt: number | undefined
57
- if (scheduleType === 'once' && body.runAt) {
58
- nextRunAt = body.runAt
59
- } else if (scheduleType === 'interval' && body.intervalMs) {
60
- nextRunAt = now + body.intervalMs
61
- } else if (scheduleType === 'cron') {
62
- // nextRunAt will be computed by the scheduler engine
63
- nextRunAt = undefined
64
- }
65
-
66
94
  schedules[id] = {
67
95
  id,
68
- name: resolveScheduleName({ name: body.name, taskPrompt: body.taskPrompt }),
69
- agentId: body.agentId,
70
- taskPrompt: body.taskPrompt || '',
96
+ ...candidate,
97
+ name: resolveScheduleName({ name: candidate.name, taskPrompt: candidate.taskPrompt }),
71
98
  scheduleType,
72
- cron: body.cron,
73
- intervalMs: body.intervalMs,
74
- runAt: body.runAt,
75
99
  lastRunAt: undefined,
76
- nextRunAt,
77
- status: body.status || 'active',
78
100
  createdAt: now,
101
+ updatedAt: now,
79
102
  }
80
103
  saveSchedules(schedules)
81
104
  notify('schedules')
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadSettings, saveSettings } from '@/lib/server/storage'
3
- import { DEFAULT_DELEGATION_MAX_DEPTH } from '@/lib/runtime-loop'
2
+ import { normalizeHeartbeatSettingFields } from '@/lib/heartbeat-defaults'
3
+ import { loadPublicSettings, loadSettings, saveSettings } from '@/lib/server/storage'
4
+ import { normalizeRuntimeSettingFields } from '@/lib/runtime-loop'
4
5
  export const dynamic = 'force-dynamic'
5
6
 
6
7
 
@@ -10,8 +11,6 @@ const MEMORY_PER_LOOKUP_MIN = 1
10
11
  const MEMORY_PER_LOOKUP_MAX = 200
11
12
  const MEMORY_LINKED_MIN = 0
12
13
  const MEMORY_LINKED_MAX = 1000
13
- const DELEGATION_DEPTH_MIN = 1
14
- const DELEGATION_DEPTH_MAX = 12
15
14
  const RESPONSE_CACHE_TTL_MIN_SEC = 5
16
15
  const RESPONSE_CACHE_TTL_MAX_SEC = 7 * 24 * 3600
17
16
  const RESPONSE_CACHE_MAX_ENTRIES_MIN = 1
@@ -20,6 +19,9 @@ const TASK_QG_MIN_RESULT_MIN = 10
20
19
  const TASK_QG_MIN_RESULT_MAX = 2000
21
20
  const TASK_QG_MIN_EVIDENCE_MIN = 0
22
21
  const TASK_QG_MIN_EVIDENCE_MAX = 8
22
+ const SESSION_RESET_TIMEOUT_MIN = 0
23
+ const SESSION_RESET_TIMEOUT_MAX = 365 * 24 * 60 * 60
24
+ const SECRET_SETTING_KEYS = ['elevenLabsApiKey', 'tavilyApiKey', 'braveApiKey'] as const
23
25
 
24
26
  function parseIntSetting(value: unknown, fallback: number, min: number, max: number): number {
25
27
  const parsed = typeof value === 'number'
@@ -42,13 +44,25 @@ function parseBoolSetting(value: unknown, fallback: boolean): boolean {
42
44
  }
43
45
 
44
46
  export async function GET(_req: Request) {
45
- return NextResponse.json(loadSettings())
47
+ return NextResponse.json(loadPublicSettings())
46
48
  }
47
49
 
48
50
  export async function PUT(req: Request) {
49
- const body = await req.json()
51
+ const body = await req.json() as Record<string, unknown>
52
+ const sanitizedBody: Record<string, unknown> = { ...body }
53
+
54
+ delete sanitizedBody.__encryptedAppSettings
55
+
56
+ for (const key of SECRET_SETTING_KEYS) {
57
+ const configuredKey = `${key}Configured`
58
+ if (sanitizedBody[key] === null && sanitizedBody[configuredKey] === true) {
59
+ delete sanitizedBody[key]
60
+ }
61
+ delete sanitizedBody[configuredKey]
62
+ }
63
+
50
64
  const settings = loadSettings()
51
- Object.assign(settings, body)
65
+ Object.assign(settings, sanitizedBody)
52
66
 
53
67
  const nextDepth = parseIntSetting(
54
68
  settings.memoryReferenceDepth ?? settings.memoryMaxDepth,
@@ -68,12 +82,8 @@ export async function PUT(req: Request) {
68
82
  MEMORY_LINKED_MIN,
69
83
  MEMORY_LINKED_MAX,
70
84
  )
71
- const nextDelegationDepth = parseIntSetting(
72
- settings.delegationMaxDepth,
73
- DEFAULT_DELEGATION_MAX_DEPTH,
74
- DELEGATION_DEPTH_MIN,
75
- DELEGATION_DEPTH_MAX,
76
- )
85
+ const normalizedRuntime = normalizeRuntimeSettingFields(settings)
86
+ const normalizedHeartbeat = normalizeHeartbeatSettingFields(settings)
77
87
  const nextResponseCacheTtlSec = parseIntSetting(
78
88
  settings.responseCacheTtlSec,
79
89
  15 * 60,
@@ -105,7 +115,8 @@ export async function PUT(req: Request) {
105
115
  settings.maxMemoriesPerLookup = nextPerLookup
106
116
  settings.memoryMaxPerLookup = nextPerLookup
107
117
  settings.maxLinkedMemoriesExpanded = nextLinked
108
- settings.delegationMaxDepth = nextDelegationDepth
118
+ Object.assign(settings, normalizedRuntime)
119
+ Object.assign(settings, normalizedHeartbeat)
109
120
  settings.responseCacheTtlSec = nextResponseCacheTtlSec
110
121
  settings.responseCacheMaxEntries = nextResponseCacheMaxEntries
111
122
  settings.responseCacheEnabled = parseBoolSetting(settings.responseCacheEnabled, true)
@@ -116,16 +127,43 @@ export async function PUT(req: Request) {
116
127
  settings.taskQualityGateRequireArtifact = parseBoolSetting(settings.taskQualityGateRequireArtifact, false)
117
128
  settings.taskQualityGateRequireReport = parseBoolSetting(settings.taskQualityGateRequireReport, false)
118
129
  settings.integrityMonitorEnabled = parseBoolSetting(settings.integrityMonitorEnabled, true)
130
+ settings.sessionResetMode = settings.sessionResetMode === 'daily' ? 'daily' : settings.sessionResetMode === 'idle' ? 'idle' : null
131
+ settings.sessionIdleTimeoutSec = parseIntSetting(
132
+ settings.sessionIdleTimeoutSec,
133
+ 12 * 60 * 60,
134
+ SESSION_RESET_TIMEOUT_MIN,
135
+ SESSION_RESET_TIMEOUT_MAX,
136
+ )
137
+ settings.sessionMaxAgeSec = parseIntSetting(
138
+ settings.sessionMaxAgeSec,
139
+ 7 * 24 * 60 * 60,
140
+ SESSION_RESET_TIMEOUT_MIN,
141
+ SESSION_RESET_TIMEOUT_MAX,
142
+ )
143
+ if (typeof settings.sessionDailyResetAt === 'string') settings.sessionDailyResetAt = settings.sessionDailyResetAt.trim() || null
144
+ if (typeof settings.sessionResetTimezone === 'string') settings.sessionResetTimezone = settings.sessionResetTimezone.trim() || null
119
145
 
120
146
  saveSettings(settings)
121
147
 
122
148
  // Restart heartbeat service when heartbeat-related settings change
123
- const heartbeatKeys = ['heartbeatIntervalSec', 'heartbeatInterval', 'heartbeatPrompt', 'heartbeatEnabled', 'heartbeatActiveStart', 'heartbeatActiveEnd']
124
- if (heartbeatKeys.some((k) => k in body)) {
149
+ const heartbeatKeys = [
150
+ 'heartbeatIntervalSec',
151
+ 'heartbeatInterval',
152
+ 'heartbeatPrompt',
153
+ 'heartbeatEnabled',
154
+ 'heartbeatActiveStart',
155
+ 'heartbeatActiveEnd',
156
+ 'sessionResetMode',
157
+ 'sessionIdleTimeoutSec',
158
+ 'sessionMaxAgeSec',
159
+ 'sessionDailyResetAt',
160
+ 'sessionResetTimezone',
161
+ ]
162
+ if (heartbeatKeys.some((k) => k in sanitizedBody)) {
125
163
  import('@/lib/server/heartbeat-service').then(({ restartHeartbeatService }) => {
126
164
  restartHeartbeatService()
127
165
  }).catch(() => { /* heartbeat service may not be initialized yet */ })
128
166
  }
129
167
 
130
- return NextResponse.json(settings)
168
+ return NextResponse.json(loadPublicSettings())
131
169
  }
@@ -90,12 +90,14 @@ export async function GET(req: Request) {
90
90
  const checkedAt = Date.now()
91
91
 
92
92
  const nodeVersion = process.versions.node
93
- const nodeMajor = Number.parseInt(String(nodeVersion).split('.')[0] || '0', 10)
94
- if (nodeMajor >= 20) {
93
+ const [nodeMajorRaw, nodeMinorRaw] = String(nodeVersion).split('.')
94
+ const nodeMajor = Number.parseInt(nodeMajorRaw || '0', 10)
95
+ const nodeMinor = Number.parseInt(nodeMinorRaw || '0', 10)
96
+ if (nodeMajor > 22 || (nodeMajor === 22 && nodeMinor >= 6)) {
95
97
  pushCheck(checks, 'node-version', 'Node.js version', 'pass', `Detected Node ${nodeVersion}.`, true)
96
98
  } else {
97
- pushCheck(checks, 'node-version', 'Node.js version', 'fail', `Detected Node ${nodeVersion}. Node 20+ is required.`, true)
98
- actions.push('Install Node.js 20 or newer from https://nodejs.org and rerun setup.')
99
+ pushCheck(checks, 'node-version', 'Node.js version', 'fail', `Detected Node ${nodeVersion}. Node 22.6+ is required.`, true)
100
+ actions.push('Install Node.js 22.6 or newer from https://nodejs.org and rerun setup.')
99
101
  }
100
102
 
101
103
  const npmCheck = run('npm', ['--version'], 5_000)
@@ -1,6 +1,6 @@
1
1
  import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
- import { loadTasks, saveTasks, logActivity, loadSettings } from '@/lib/server/storage'
3
+ import { loadAgents, loadSettings, loadTasks, logActivity, upsertStoredItems, upsertTask } from '@/lib/server/storage'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
5
  import { disableSessionHeartbeat, enqueueTask, recoverStalledRunningTasks, validateCompletedTasksQueue } from '@/lib/server/queue'
6
6
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
@@ -13,6 +13,8 @@ import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
13
13
  import { validateDag, cascadeUnblock } from '@/lib/server/dag-validation'
14
14
  import { getPluginManager } from '@/lib/server/plugins'
15
15
  import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
16
+ import type { BoardTask } from '@/types'
17
+ import '@/lib/server/builtin-plugins'
16
18
 
17
19
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
18
20
  // Keep completed queue integrity even if daemon is not running.
@@ -90,7 +92,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
90
92
  }
91
93
  }
92
94
 
93
- saveTasks(tasks)
95
+ upsertTask(id, tasks[id])
94
96
  logActivity({ entityType: 'task', entityId: id, action: 'updated', actor: 'user', summary: `Task updated: "${tasks[id].title}" (${prevStatus} → ${tasks[id].status})` })
95
97
  if (prevStatus !== tasks[id].status) {
96
98
  pushMainLoopEventToMainSessions({
@@ -111,7 +113,12 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
111
113
  })
112
114
 
113
115
  if (tasks[id].status === 'completed') {
114
- getPluginManager().runHook('onTaskComplete', { taskId: id, result: tasks[id].result })
116
+ const agentPlugins = tasks[id].agentId ? (loadAgents()[tasks[id].agentId]?.plugins || []) : []
117
+ getPluginManager().runHook(
118
+ 'onTaskComplete',
119
+ { taskId: id, result: tasks[id].result },
120
+ { enabledIds: agentPlugins },
121
+ )
115
122
  }
116
123
 
117
124
  // Enqueue system event + heartbeat wake
@@ -131,7 +138,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
131
138
  // Revert status change and reject
132
139
  tasks[id].status = prevStatus
133
140
  tasks[id].updatedAt = Date.now()
134
- saveTasks(tasks)
141
+ upsertTask(id, tasks[id])
135
142
  return NextResponse.json(
136
143
  { error: 'Cannot queue: blocked by incomplete tasks', blockedBy: incompleteBlocker },
137
144
  { status: 409 },
@@ -143,7 +150,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
143
150
  if (tasks[id].status === 'completed') {
144
151
  const unblockedIds = cascadeUnblock(tasks, id)
145
152
  if (unblockedIds.length > 0) {
146
- saveTasks(tasks)
153
+ upsertStoredItems('tasks', [
154
+ [id, tasks[id]],
155
+ ...unblockedIds.map((uid) => [uid, tasks[uid]] as [string, BoardTask]),
156
+ ])
147
157
  for (const uid of unblockedIds) {
148
158
  enqueueTask(uid)
149
159
  }
@@ -168,7 +178,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
168
178
  tasks[id].status = 'archived'
169
179
  tasks[id].archivedAt = Date.now()
170
180
  tasks[id].updatedAt = Date.now()
171
- saveTasks(tasks)
181
+ upsertTask(id, tasks[id])
172
182
  logActivity({ entityType: 'task', entityId: id, action: 'deleted', actor: 'user', summary: `Task archived: "${tasks[id].title}"` })
173
183
  pushMainLoopEventToMainSessions({
174
184
  type: 'task_archived',
@@ -1,10 +1,10 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadTasks, saveTasks, logActivity } from '@/lib/server/storage'
2
+ import { loadTasks, logActivity, upsertStoredItems } from '@/lib/server/storage'
3
3
  import { enqueueTask, disableSessionHeartbeat } from '@/lib/server/queue'
4
4
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
5
5
  import { notify } from '@/lib/server/ws-hub'
6
6
  import { createNotification } from '@/lib/server/create-notification'
7
- import type { BoardTaskStatus } from '@/types'
7
+ import type { BoardTask, BoardTaskStatus } from '@/types'
8
8
 
9
9
  const VALID_STATUSES: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed', 'archived']
10
10
 
@@ -82,7 +82,7 @@ export async function POST(req: Request) {
82
82
  }
83
83
  }
84
84
 
85
- saveTasks(tasks)
85
+ upsertStoredItems('tasks', results.map((id) => [id, tasks[id]] as [string, BoardTask]))
86
86
 
87
87
  if (updated > 0) {
88
88
  const action = body.status
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
- import { loadTasks, saveTasks, loadSettings, loadAgents, logActivity } from '@/lib/server/storage'
3
+ import { deleteTask, loadAgents, loadSettings, loadTasks, logActivity, upsertTask } from '@/lib/server/storage'
4
4
  import { TaskCreateSchema, formatZodError } from '@/lib/validation/schemas'
5
5
  import { z } from 'zod'
6
6
  import { enqueueTask, recoverStalledRunningTasks, validateCompletedTasksQueue } from '@/lib/server/queue'
@@ -13,6 +13,7 @@ import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
13
13
  import { validateDag } from '@/lib/server/dag-validation'
14
14
  import { getPluginManager } from '@/lib/server/plugins'
15
15
  import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
16
+ import '@/lib/server/builtin-plugins'
16
17
 
17
18
  export async function GET(req: Request) {
18
19
  // Keep completed queue integrity even if daemon is not running.
@@ -49,7 +50,6 @@ export async function DELETE(req: Request) {
49
50
  (filter === 'done' && (task.status === 'completed' || task.status === 'failed')) ||
50
51
  (!filter && task.status === 'archived')
51
52
 
52
- const { deleteTask } = await import('@/lib/server/storage')
53
53
  for (const [id, task] of Object.entries(tasks)) {
54
54
  if (shouldRemove(task as { status: string; sourceType?: string })) {
55
55
  deleteTask(id)
@@ -169,7 +169,12 @@ export async function POST(req: Request) {
169
169
  if (validation.ok) {
170
170
  tasks[id].completedAt = Date.now()
171
171
  tasks[id].error = null
172
- getPluginManager().runHook('onTaskComplete', { taskId: id, result: tasks[id].result })
172
+ const agentPlugins = resolvedAgentId ? (loadAgents()[resolvedAgentId]?.plugins || []) : []
173
+ getPluginManager().runHook(
174
+ 'onTaskComplete',
175
+ { taskId: id, result: tasks[id].result },
176
+ { enabledIds: agentPlugins },
177
+ )
173
178
  } else {
174
179
  tasks[id].status = 'failed'
175
180
  tasks[id].completedAt = null
@@ -177,7 +182,7 @@ export async function POST(req: Request) {
177
182
  }
178
183
  }
179
184
 
180
- saveTasks(tasks)
185
+ upsertTask(id, tasks[id])
181
186
  logActivity({ entityType: 'task', entityId: id, action: 'created', actor: 'user', summary: `Task created: "${tasks[id].title}"` })
182
187
  pushMainLoopEventToMainSessions({
183
188
  type: 'task_created',
@@ -8,6 +8,7 @@ import { enqueueSystemEvent } from '@/lib/server/system-events'
8
8
  import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
9
9
  import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
10
10
  import type { WebhookRetryEntry } from '@/types'
11
+ import { triggerWebhookWatchJobs } from '@/lib/server/watch-jobs'
11
12
 
12
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
14
  const ops: CollectionOps<any> = { load: loadWebhooks, save: saveWebhooks }
@@ -126,6 +127,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
126
127
  })
127
128
  }
128
129
 
130
+ triggerWebhookWatchJobs({
131
+ webhookId: id,
132
+ event: incomingEvent,
133
+ payloadPreview: rawBody,
134
+ })
135
+
129
136
  const agents = loadAgents()
130
137
  const agent = webhook.agentId ? agents[webhook.agentId] : null
131
138
  if (!agent) {
@@ -165,7 +172,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
165
172
  messages: [],
166
173
  createdAt: now,
167
174
  lastActiveAt: now,
168
- sessionType: 'orchestrated',
175
+ sessionType: 'human',
169
176
  agentId: agent.id,
170
177
  parentSessionId: null,
171
178
  tools: agent.tools || [],