@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
@@ -5,7 +5,9 @@ import os from 'os'
5
5
  import Database from 'better-sqlite3'
6
6
 
7
7
  import { DATA_DIR, WORKSPACE_DIR } from './data-dir'
8
- import type { Message } from '@/types'
8
+ import { normalizeHeartbeatSettingFields } from '@/lib/heartbeat-defaults'
9
+ import { normalizeRuntimeSettingFields } from '@/lib/runtime-loop'
10
+ import type { ExternalAgentRuntime, GatewayProfile, Message } from '@/types'
9
11
  export const UPLOAD_DIR = path.join(DATA_DIR, 'uploads')
10
12
 
11
13
  // --- LRU Cache ---
@@ -135,6 +137,7 @@ const COLLECTIONS = [
135
137
  'tasks',
136
138
  'secrets',
137
139
  'provider_configs',
140
+ 'gateway_profiles',
138
141
  'skills',
139
142
  'connectors',
140
143
  'documents',
@@ -156,8 +159,14 @@ const COLLECTIONS = [
156
159
  'souls',
157
160
  'benchmarks',
158
161
  'approvals',
162
+ 'browser_sessions',
163
+ 'watch_jobs',
164
+ 'delegation_jobs',
165
+ 'external_agents',
159
166
  ] as const
160
167
 
168
+ export type StorageCollection = (typeof COLLECTIONS)[number]
169
+
161
170
  for (const table of COLLECTIONS) {
162
171
  db.exec(`CREATE TABLE IF NOT EXISTS ${table} (id TEXT PRIMARY KEY, data TEXT NOT NULL)`)
163
172
  }
@@ -185,12 +194,34 @@ function getCollectionRawCache(table: string): LRUMap<string, string> {
185
194
  return loaded
186
195
  }
187
196
 
197
+ function normalizeStoredRecord(table: string, value: any): any {
198
+ if (table !== 'sessions') return value
199
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value
200
+
201
+ const session = value as Record<string, any>
202
+ if (session.sessionType !== 'human') session.sessionType = 'human'
203
+ const isLegacyShortcut = (
204
+ (typeof session.id === 'string' && session.id.startsWith('agent-thread-'))
205
+ || (typeof session.name === 'string' && session.name.startsWith('agent-thread:'))
206
+ )
207
+ if (
208
+ isLegacyShortcut
209
+ && typeof session.agentId === 'string'
210
+ && session.agentId.trim()
211
+ && (!session.shortcutForAgentId || session.shortcutForAgentId !== session.agentId)
212
+ ) {
213
+ session.shortcutForAgentId = session.agentId
214
+ }
215
+ if ('mainLoopState' in session) delete session.mainLoopState
216
+ return session
217
+ }
218
+
188
219
  function loadCollection(table: string): Record<string, any> {
189
220
  const raw = getCollectionRawCache(table)
190
221
  const result: Record<string, any> = {}
191
222
  for (const [id, data] of raw.entries()) {
192
223
  try {
193
- result[id] = JSON.parse(data)
224
+ result[id] = normalizeStoredRecord(table, JSON.parse(data))
194
225
  } catch {
195
226
  // Ignore malformed records instead of crashing list endpoints.
196
227
  }
@@ -205,7 +236,8 @@ function saveCollection(table: string, data: Record<string, any>) {
205
236
  const toDelete: string[] = []
206
237
 
207
238
  for (const [id, val] of Object.entries(data)) {
208
- const serialized = JSON.stringify(val)
239
+ const normalized = normalizeStoredRecord(table, val)
240
+ const serialized = JSON.stringify(normalized)
209
241
  if (typeof serialized !== 'string') continue
210
242
  next.set(id, serialized)
211
243
  if (current.get(id) !== serialized) {
@@ -251,7 +283,7 @@ function deleteCollectionItem(table: string, id: string) {
251
283
  * concurrent processes are modifying different items.
252
284
  */
253
285
  function upsertCollectionItem(table: string, id: string, value: any) {
254
- const serialized = JSON.stringify(value)
286
+ const serialized = JSON.stringify(normalizeStoredRecord(table, value))
255
287
  db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (?, ?)`).run(id, serialized)
256
288
  // Update the in-memory cache
257
289
  const cached = collectionCache.get(table)
@@ -260,6 +292,51 @@ function upsertCollectionItem(table: string, id: string, value: any) {
260
292
  }
261
293
  }
262
294
 
295
+ function loadCollectionItem(table: string, id: string): any | null {
296
+ const row = db.prepare(`SELECT data FROM ${table} WHERE id = ?`).get(id) as { data: string } | undefined
297
+ if (!row) return null
298
+ try {
299
+ return normalizeStoredRecord(table, JSON.parse(row.data))
300
+ } catch {
301
+ return null
302
+ }
303
+ }
304
+
305
+ function upsertCollectionItems(table: string, entries: Array<[string, any]>): void {
306
+ if (!entries.length) return
307
+ const prepared = entries
308
+ .map(([id, value]) => [id, JSON.stringify(normalizeStoredRecord(table, value))] as const)
309
+ .filter(([, serialized]) => typeof serialized === 'string')
310
+ if (!prepared.length) return
311
+
312
+ const transaction = db.transaction(() => {
313
+ const upsert = db.prepare(`INSERT OR REPLACE INTO ${table} (id, data) VALUES (?, ?)`)
314
+ for (const [id, serialized] of prepared) {
315
+ upsert.run(id, serialized)
316
+ }
317
+ })
318
+ transaction()
319
+
320
+ const cached = collectionCache.get(table)
321
+ if (cached) {
322
+ for (const [id, serialized] of prepared) {
323
+ cached.set(id, serialized)
324
+ }
325
+ }
326
+ }
327
+
328
+ export function loadStoredItem(table: StorageCollection, id: string): any | null {
329
+ return loadCollectionItem(table, id)
330
+ }
331
+
332
+ export function upsertStoredItem(table: StorageCollection, id: string, value: any): void {
333
+ upsertCollectionItem(table, id, value)
334
+ }
335
+
336
+ export function upsertStoredItems(table: StorageCollection, entries: Array<[string, any]>): void {
337
+ upsertCollectionItems(table, entries)
338
+ }
339
+
263
340
  function loadSingleton(table: string, fallback: any): any {
264
341
  const row = db.prepare(`SELECT data FROM ${table} WHERE id = 1`).get() as { data: string } | undefined
265
342
  return row ? JSON.parse(row.data) : fallback
@@ -279,10 +356,12 @@ const JSON_FILES: Record<string, string> = {
279
356
  tasks: path.join(DATA_DIR, 'tasks.json'),
280
357
  secrets: path.join(DATA_DIR, 'secrets.json'),
281
358
  provider_configs: path.join(DATA_DIR, 'providers.json'),
359
+ gateway_profiles: path.join(DATA_DIR, 'gateways.json'),
282
360
  skills: path.join(DATA_DIR, 'skills.json'),
283
361
  connectors: path.join(DATA_DIR, 'connectors.json'),
284
362
  documents: path.join(DATA_DIR, 'documents.json'),
285
363
  webhooks: path.join(DATA_DIR, 'webhooks.json'),
364
+ external_agents: path.join(DATA_DIR, 'external-agents.json'),
286
365
  }
287
366
 
288
367
  const MIGRATION_FLAG = path.join(DATA_DIR, '.sqlite_migrated')
@@ -392,7 +471,7 @@ if (!IS_BUILD_BOOTSTRAP) {
392
471
 
393
472
  ## Platform
394
473
 
395
- - **Agents** — Create specialized AI agents (Agents tab → "+") with a provider, model, system prompt, and tools. "Generate with AI" scaffolds agents from a description. Toggle "Orchestrator" to let an agent delegate work to others.
474
+ - **Agents** — Create specialized AI agents (Agents tab → "+") with a provider, model, system prompt, and tools. "Generate with AI" scaffolds agents from a description. Enable cross-agent delegation when an agent should assign work to others.
396
475
  - **Providers** — Configure LLM backends in Settings → Providers: Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, OpenClaw, or custom OpenAI-compatible endpoints.
397
476
  - **Tasks** — The Task Board tracks work items. Assign agents and they'll execute autonomously.
398
477
  - **Schedules** — Cron-based recurring jobs that run agents or tasks automatically.
@@ -419,7 +498,7 @@ Use your platform management tools proactively:
419
498
  You have opinions about good agent design. You suggest creative approaches, warn about common pitfalls, and get excited when someone gets something cool working. You're not a manual — you're a collaborator.
420
499
 
421
500
  Be concise but not curt. Warmth doesn't require verbosity. When someone asks "how do I...?", give them the direct steps. Offer to do things rather than just explaining — if someone wants an agent created, create it. Use your tools when actions speak louder than words. If you don't know something, say so honestly.`,
422
- isOrchestrator: false,
501
+ isOrchestrator: true,
423
502
  plugins: defaultStarterTools,
424
503
  heartbeatEnabled: true,
425
504
  platformAssignScope: 'all',
@@ -440,6 +519,15 @@ Be concise but not curt. Warmth doesn't require verbosity. When someone asks "ho
440
519
  existing.plugins = mergedPlugins
441
520
  delete existing.tools
442
521
  existing.updatedAt = Date.now()
522
+ }
523
+ if (existing.platformAssignScope === 'all' || existing.platformAssignScope === 'self') {
524
+ const derivedIsOrchestrator = existing.platformAssignScope === 'all'
525
+ if (existing.isOrchestrator !== derivedIsOrchestrator) {
526
+ existing.isOrchestrator = derivedIsOrchestrator
527
+ existing.updatedAt = Date.now()
528
+ }
529
+ }
530
+ if (JSON.stringify(JSON.parse(row.data)) !== JSON.stringify(existing)) {
443
531
  db.prepare('UPDATE agents SET data = ? WHERE id = ?').run(JSON.stringify(existing), 'default')
444
532
  }
445
533
  } catch {
@@ -685,12 +773,108 @@ export function saveQueue(q: string[]) {
685
773
  }
686
774
 
687
775
  // --- Settings ---
776
+ const APP_SETTINGS_SECRET_FIELDS = [
777
+ 'elevenLabsApiKey',
778
+ 'tavilyApiKey',
779
+ 'braveApiKey',
780
+ ] as const
781
+
782
+ const ENCRYPTED_APP_SETTINGS_KEY = '__encryptedAppSettings'
783
+
784
+ type PersistedSettingsRecord = Record<string, any> & {
785
+ [ENCRYPTED_APP_SETTINGS_KEY]?: Record<string, string>
786
+ }
787
+
788
+ function cloneRecord<T extends Record<string, any>>(value: T): T {
789
+ return JSON.parse(JSON.stringify(value || {})) as T
790
+ }
791
+
792
+ function isPlainRecord(value: unknown): value is Record<string, any> {
793
+ return !!value && typeof value === 'object' && !Array.isArray(value)
794
+ }
795
+
796
+ function getEncryptedAppSettings(settings: PersistedSettingsRecord): Record<string, string> {
797
+ return isPlainRecord(settings[ENCRYPTED_APP_SETTINGS_KEY])
798
+ ? { ...(settings[ENCRYPTED_APP_SETTINGS_KEY] as Record<string, string>) }
799
+ : {}
800
+ }
801
+
802
+ function isClearedSecretValue(value: unknown): boolean {
803
+ return value === null || (typeof value === 'string' && value.trim() === '')
804
+ }
805
+
806
+ function isProvidedSecretValue(value: unknown): value is string {
807
+ return typeof value === 'string' && value.trim().length > 0
808
+ }
809
+
810
+ function buildPersistedSettings(input: Record<string, any>, existing?: PersistedSettingsRecord): PersistedSettingsRecord {
811
+ const next = cloneRecord(input) as PersistedSettingsRecord
812
+ Object.assign(next, normalizeRuntimeSettingFields(next))
813
+ Object.assign(next, normalizeHeartbeatSettingFields(next))
814
+ const encrypted = {
815
+ ...(existing ? getEncryptedAppSettings(existing) : {}),
816
+ ...getEncryptedAppSettings(next),
817
+ }
818
+
819
+ delete next[ENCRYPTED_APP_SETTINGS_KEY]
820
+
821
+ for (const field of APP_SETTINGS_SECRET_FIELDS) {
822
+ const raw = next[field]
823
+ if (isClearedSecretValue(raw)) {
824
+ delete encrypted[field]
825
+ delete next[field]
826
+ continue
827
+ }
828
+ if (isProvidedSecretValue(raw)) {
829
+ encrypted[field] = encryptKey(raw)
830
+ delete next[field]
831
+ }
832
+ }
833
+
834
+ if (Object.keys(encrypted).length > 0) next[ENCRYPTED_APP_SETTINGS_KEY] = encrypted
835
+ return next
836
+ }
837
+
838
+ function resolveSettingsSecrets(settings: PersistedSettingsRecord): Record<string, any> {
839
+ const resolved = cloneRecord(settings)
840
+ delete resolved[ENCRYPTED_APP_SETTINGS_KEY]
841
+
842
+ const encrypted = getEncryptedAppSettings(settings)
843
+ for (const field of APP_SETTINGS_SECRET_FIELDS) {
844
+ if (isProvidedSecretValue(resolved[field])) continue
845
+ const value = encrypted[field]
846
+ if (typeof value !== 'string' || !value) continue
847
+ try {
848
+ resolved[field] = decryptKey(value)
849
+ } catch {
850
+ // Ignore malformed encrypted settings instead of breaking all settings reads.
851
+ }
852
+ }
853
+
854
+ return resolved
855
+ }
856
+
688
857
  export function loadSettings(): Record<string, any> {
689
- return loadSingleton('settings', {})
858
+ const persisted = loadSingleton('settings', {}) as PersistedSettingsRecord
859
+ const normalized = buildPersistedSettings(persisted, persisted)
860
+ if (JSON.stringify(persisted) !== JSON.stringify(normalized)) {
861
+ saveSingleton('settings', normalized)
862
+ }
863
+ return resolveSettingsSecrets(normalized)
690
864
  }
691
865
 
692
866
  export function saveSettings(s: Record<string, any>) {
693
- saveSingleton('settings', s)
867
+ const existing = loadSingleton('settings', {}) as PersistedSettingsRecord
868
+ saveSingleton('settings', buildPersistedSettings(s, existing))
869
+ }
870
+
871
+ export function loadPublicSettings(): Record<string, any> {
872
+ const settings = cloneRecord(loadSettings())
873
+ for (const field of APP_SETTINGS_SECRET_FIELDS) {
874
+ settings[`${field}Configured`] = isProvidedSecretValue(settings[field])
875
+ settings[field] = null
876
+ }
877
+ return settings
694
878
  }
695
879
 
696
880
  // --- Secrets (service keys for orchestrators) ---
@@ -759,6 +943,15 @@ export function saveProviderConfigs(p: Record<string, any>) {
759
943
  saveCollection('provider_configs', p)
760
944
  }
761
945
 
946
+ // --- Gateway Profiles ---
947
+ export function loadGatewayProfiles(): Record<string, any> {
948
+ return loadCollection('gateway_profiles') as Record<string, GatewayProfile>
949
+ }
950
+
951
+ export function saveGatewayProfiles(g: Record<string, GatewayProfile>) {
952
+ saveCollection('gateway_profiles', g)
953
+ }
954
+
762
955
  // --- Model Overrides (user-added models for built-in providers) ---
763
956
  export function loadModelOverrides(): Record<string, string[]> {
764
957
  return loadCollection('model_overrides') as Record<string, string[]>
@@ -788,6 +981,15 @@ export function saveSkills(s: Record<string, any>) {
788
981
  saveCollection('skills', s)
789
982
  }
790
983
 
984
+ // --- External Agent Runtimes ---
985
+ export function loadExternalAgents(): Record<string, ExternalAgentRuntime> {
986
+ return loadCollection('external_agents') as Record<string, ExternalAgentRuntime>
987
+ }
988
+
989
+ export function saveExternalAgents(items: Record<string, ExternalAgentRuntime>) {
990
+ saveCollection('external_agents', items)
991
+ }
992
+
791
993
  // --- Usage ---
792
994
  export function loadUsage(): Record<string, any[]> {
793
995
  const stmt = db.prepare('SELECT session_id, data FROM usage')
@@ -891,11 +1093,11 @@ export function saveIntegrityBaselines(entries: Record<string, any>) {
891
1093
  }
892
1094
 
893
1095
  // --- Webhook Logs ---
894
- export function loadWebhookLogs(): Record<string, any> {
1096
+ export function loadWebhookLogs(): Record<string, unknown> {
895
1097
  return loadCollection('webhook_logs')
896
1098
  }
897
1099
 
898
- export function appendWebhookLog(id: string, entry: any) {
1100
+ export function appendWebhookLog(id: string, entry: unknown) {
899
1101
  upsertCollectionItem('webhook_logs', id, entry)
900
1102
  }
901
1103
 
@@ -1026,6 +1228,49 @@ export function loadApprovals(): Record<string, unknown> {
1026
1228
  return loadCollection('approvals')
1027
1229
  }
1028
1230
 
1231
+ // --- Browser Sessions ---
1232
+ export function loadBrowserSessions(): Record<string, unknown> {
1233
+ return loadCollection('browser_sessions')
1234
+ }
1235
+
1236
+ export function upsertBrowserSession(id: string, data: unknown) {
1237
+ upsertCollectionItem('browser_sessions', id, data)
1238
+ }
1239
+
1240
+ export function deleteBrowserSession(id: string) {
1241
+ deleteCollectionItem('browser_sessions', id)
1242
+ }
1243
+
1244
+ // --- Watch Jobs ---
1245
+ export function loadWatchJobs(): Record<string, unknown> {
1246
+ return loadCollection('watch_jobs')
1247
+ }
1248
+
1249
+ export function upsertWatchJob(id: string, data: unknown) {
1250
+ upsertCollectionItem('watch_jobs', id, data)
1251
+ }
1252
+
1253
+ export function upsertWatchJobs(entries: Array<[string, unknown]>) {
1254
+ upsertCollectionItems('watch_jobs', entries)
1255
+ }
1256
+
1257
+ export function deleteWatchJob(id: string) {
1258
+ deleteCollectionItem('watch_jobs', id)
1259
+ }
1260
+
1261
+ // --- Delegation Jobs ---
1262
+ export function loadDelegationJobs(): Record<string, unknown> {
1263
+ return loadCollection('delegation_jobs')
1264
+ }
1265
+
1266
+ export function upsertDelegationJob(id: string, data: unknown) {
1267
+ upsertCollectionItem('delegation_jobs', id, data)
1268
+ }
1269
+
1270
+ export function deleteDelegationJob(id: string) {
1271
+ deleteCollectionItem('delegation_jobs', id)
1272
+ }
1273
+
1029
1274
  export function upsertApproval(id: string, approval: unknown) {
1030
1275
  upsertCollectionItem('approvals', id, approval)
1031
1276
  }
@@ -0,0 +1,61 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { describe, it } from 'node:test'
5
+ import { buildToolDisciplineLines, looksLikeOpenEndedDeliverableTask } from './stream-agent-chat'
6
+
7
+ const streamAgentChatSource = fs.readFileSync(path.join(path.dirname(new URL(import.meta.url).pathname), 'stream-agent-chat.ts'), 'utf-8')
8
+
9
+ describe('buildToolDisciplineLines', () => {
10
+ it('tells the agent to use direct platform tools when manage_platform is absent', () => {
11
+ const lines = buildToolDisciplineLines(['files', 'manage_schedules'])
12
+
13
+ assert.equal(lines[0], 'Enabled tools in this session: `files`, `manage_schedules`.')
14
+ assert.ok(lines.some((line) => line.includes('Do not substitute `manage_platform`')))
15
+ })
16
+
17
+ it('omits the manage_platform warning when the umbrella tool is enabled', () => {
18
+ const lines = buildToolDisciplineLines(['manage_platform', 'manage_schedules'])
19
+
20
+ assert.ok(lines.every((line) => !line.includes('Do not substitute `manage_platform`')))
21
+ })
22
+
23
+ it('includes concrete files-tool examples for revision work', () => {
24
+ const lines = buildToolDisciplineLines(['files'])
25
+
26
+ assert.ok(lines.some((line) => line.includes('{"action":"read","filePath":"path/to/file.md"}')))
27
+ })
28
+
29
+ it('warns browser tasks to use literal urls and the supported form schema', () => {
30
+ const lines = buildToolDisciplineLines(['browser', 'http_request', 'email', 'ask_human'])
31
+
32
+ assert.ok(lines.some((line) => line.includes('Do not invent placeholder URLs')))
33
+ assert.ok(lines.some((line) => line.includes('A shorthand `form` object keyed by input id/name also works')))
34
+ assert.ok(lines.some((line) => line.includes('Keep JSON request bodies as raw JSON strings')))
35
+ assert.ok(lines.some((line) => line.includes('{"action":"send","to":"user@example.com","subject":"...","body":"..."}')))
36
+ assert.ok(lines.some((line) => line.includes('do not guess or keep re-submitting blank forms')))
37
+ })
38
+
39
+ it('tells the agent that named enabled tools are completion requirements', () => {
40
+ assert.ok(streamAgentChatSource.includes('If a task explicitly names an enabled tool, use that tool before declaring success.'))
41
+ assert.ok(streamAgentChatSource.includes('collect required human input through the tool'))
42
+ assert.ok(streamAgentChatSource.includes('You have not yet completed the required explicit tool step(s):'))
43
+ assert.ok(streamAgentChatSource.includes('[Loop Budget Reached]'))
44
+ })
45
+ })
46
+
47
+ describe('looksLikeOpenEndedDeliverableTask', () => {
48
+ it('detects open-ended deliverable prompts', () => {
49
+ assert.equal(
50
+ looksLikeOpenEndedDeliverableTask('Revise the landing copy and update the proposal draft with a stronger second pass.'),
51
+ true,
52
+ )
53
+ })
54
+
55
+ it('does not misclassify explicit coding tasks', () => {
56
+ assert.equal(
57
+ looksLikeOpenEndedDeliverableTask('Fix the React bug in src/components/chat/chat-area.tsx and run npm run build.'),
58
+ false,
59
+ )
60
+ })
61
+ })