@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
@@ -0,0 +1,59 @@
1
+ import type { MemoryEntry } from '@/types'
2
+
3
+ export type MemoryTier = 'working' | 'durable' | 'archive'
4
+ export type MemoryScopeBadge = 'global' | 'agent' | 'shared' | 'session' | 'project'
5
+
6
+ const WORKING_CATEGORIES = new Set(['execution', 'working', 'scratch', 'breadcrumb'])
7
+ const ARCHIVE_CATEGORIES = new Set(['session_archive'])
8
+
9
+ function hasProjectRoot(entry: Pick<MemoryEntry, 'metadata' | 'references' | 'filePaths'>): boolean {
10
+ const metadataRoot = typeof entry.metadata?.projectRoot === 'string' ? entry.metadata.projectRoot.trim() : ''
11
+ if (metadataRoot) return true
12
+
13
+ if (Array.isArray(entry.references)) {
14
+ for (const ref of entry.references) {
15
+ if (typeof ref.projectRoot === 'string' && ref.projectRoot.trim()) return true
16
+ if ((ref.type === 'project' || ref.type === 'folder' || ref.type === 'file') && typeof ref.path === 'string' && ref.path.trim()) {
17
+ return true
18
+ }
19
+ }
20
+ }
21
+
22
+ if (Array.isArray(entry.filePaths)) {
23
+ for (const ref of entry.filePaths) {
24
+ if (typeof ref.projectRoot === 'string' && ref.projectRoot.trim()) return true
25
+ if (typeof ref.path === 'string' && ref.path.trim()) return true
26
+ }
27
+ }
28
+
29
+ return false
30
+ }
31
+
32
+ export function getMemoryTierForCategory(category: unknown): MemoryTier {
33
+ const normalized = typeof category === 'string' ? category.trim().toLowerCase() : ''
34
+ if (ARCHIVE_CATEGORIES.has(normalized)) return 'archive'
35
+ if (WORKING_CATEGORIES.has(normalized)) return 'working'
36
+ return 'durable'
37
+ }
38
+
39
+ export function getMemoryTier(entry: Pick<MemoryEntry, 'category' | 'metadata'>): MemoryTier {
40
+ const metadataTier = typeof entry.metadata?.tier === 'string' ? entry.metadata.tier.trim().toLowerCase() : ''
41
+ if (metadataTier === 'working' || metadataTier === 'durable' || metadataTier === 'archive') {
42
+ return metadataTier
43
+ }
44
+ if (metadataTier === 'session_archive') return 'archive'
45
+ return getMemoryTierForCategory(entry.category)
46
+ }
47
+
48
+ export function deriveMemoryScope(entry: Pick<MemoryEntry, 'agentId' | 'sessionId' | 'sharedWith' | 'metadata' | 'references' | 'filePaths'>): MemoryScopeBadge {
49
+ if (entry.sessionId) return 'session'
50
+ if (hasProjectRoot(entry)) return 'project'
51
+ if (entry.agentId && Array.isArray(entry.sharedWith) && entry.sharedWith.length > 0) return 'shared'
52
+ if (entry.agentId) return 'agent'
53
+ return 'global'
54
+ }
55
+
56
+ export function getMemoryScopeLabel(scope: MemoryScopeBadge): string {
57
+ if (scope === 'agent') return 'private'
58
+ return scope
59
+ }
@@ -0,0 +1,14 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import { buildOpenClawMainSessionKey, normalizeOpenClawAgentId } from './openclaw-agent-id'
4
+
5
+ test('normalizeOpenClawAgentId mirrors gateway-style normalization', () => {
6
+ assert.equal(normalizeOpenClawAgentId('OpenClaw Ops'), 'openclaw-ops')
7
+ assert.equal(normalizeOpenClawAgentId(' Agent / Research '), 'agent-research')
8
+ assert.equal(normalizeOpenClawAgentId('main'), 'main')
9
+ })
10
+
11
+ test('buildOpenClawMainSessionKey uses normalized OpenClaw agent ids', () => {
12
+ assert.equal(buildOpenClawMainSessionKey('OpenClaw Ops'), 'agent:openclaw-ops:main')
13
+ assert.equal(buildOpenClawMainSessionKey(' '), null)
14
+ })
@@ -0,0 +1,31 @@
1
+ const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i
2
+ const INVALID_CHARS_RE = /[^a-z0-9_-]+/g
3
+ const LEADING_DASH_RE = /^-+/
4
+ const TRAILING_DASH_RE = /-+$/
5
+
6
+ export function normalizeOpenClawAgentId(value: string | undefined | null): string {
7
+ const trimmed = (value ?? '').trim()
8
+ if (!trimmed) {
9
+ return 'main'
10
+ }
11
+ if (VALID_ID_RE.test(trimmed)) {
12
+ return trimmed.toLowerCase()
13
+ }
14
+ return (
15
+ trimmed
16
+ .toLowerCase()
17
+ .replace(INVALID_CHARS_RE, '-')
18
+ .replace(LEADING_DASH_RE, '')
19
+ .replace(TRAILING_DASH_RE, '')
20
+ .slice(0, 64)
21
+ || 'main'
22
+ )
23
+ }
24
+
25
+ export function buildOpenClawMainSessionKey(agentNameOrId: string | undefined | null): string | null {
26
+ const trimmed = (agentNameOrId ?? '').trim()
27
+ if (!trimmed) {
28
+ return null
29
+ }
30
+ return `agent:${normalizeOpenClawAgentId(trimmed)}:main`
31
+ }
@@ -0,0 +1,29 @@
1
+ import { api } from './api-client'
2
+ import type { ProviderModelDiscoveryResult } from '@/types'
3
+
4
+ export interface DiscoverProviderModelsParams {
5
+ providerId: string
6
+ credentialId?: string | null
7
+ endpoint?: string | null
8
+ force?: boolean
9
+ requiresApiKey?: boolean
10
+ }
11
+
12
+ export function buildProviderModelDiscoveryPath(params: DiscoverProviderModelsParams): string {
13
+ const searchParams = new URLSearchParams()
14
+ if (params.credentialId) searchParams.set('credentialId', params.credentialId)
15
+ if (params.endpoint?.trim()) searchParams.set('endpoint', params.endpoint.trim())
16
+ if (params.force) searchParams.set('force', '1')
17
+ if (typeof params.requiresApiKey === 'boolean') {
18
+ searchParams.set('requiresApiKey', params.requiresApiKey ? '1' : '0')
19
+ }
20
+ const query = searchParams.toString()
21
+ const encodedProviderId = encodeURIComponent(params.providerId)
22
+ return `/providers/${encodedProviderId}/discover-models${query ? `?${query}` : ''}`
23
+ }
24
+
25
+ export function fetchProviderModelDiscovery(
26
+ params: DiscoverProviderModelsParams,
27
+ ): Promise<ProviderModelDiscoveryResult> {
28
+ return api<ProviderModelDiscoveryResult>('GET', buildProviderModelDiscoveryPath(params))
29
+ }
@@ -256,11 +256,16 @@ export function getProviderList(): ProviderInfo[] {
256
256
  const overrides = getModelOverrides()
257
257
  const builtins = Object.values(PROVIDERS)
258
258
  .filter(({ id }) => id !== 'openclaw')
259
- .map(({ handler, ...info }) => ({
260
- ...info,
261
- models: overrides[info.id] || info.models,
262
- defaultModels: info.models,
263
- }))
259
+ .map((provider) => {
260
+ const { handler, ...info } = provider
261
+ void handler
262
+ return {
263
+ ...info,
264
+ models: overrides[info.id] || info.models,
265
+ defaultModels: info.models,
266
+ supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'fireworks'].includes(info.id),
267
+ }
268
+ })
264
269
 
265
270
  const customs: ProviderInfo[] = Object.values(getCustomProviders())
266
271
  .filter((c) => c.isEnabled)
@@ -269,6 +274,7 @@ export function getProviderList(): ProviderInfo[] {
269
274
  name: c.name,
270
275
  models: c.models,
271
276
  defaultModels: c.models,
277
+ supportsModelDiscovery: !!(c.baseUrl && c.baseUrl.trim()),
272
278
  requiresApiKey: c.requiresApiKey,
273
279
  requiresEndpoint: false as boolean,
274
280
  defaultEndpoint: c.baseUrl,
@@ -283,6 +289,7 @@ export function getProviderList(): ProviderInfo[] {
283
289
  name: String(p.name),
284
290
  models: p.models as string[],
285
291
  defaultModels: p.models as string[],
292
+ supportsModelDiscovery: Boolean(p.supportsModelDiscovery),
286
293
  requiresApiKey: Boolean(p.requiresApiKey),
287
294
  requiresEndpoint: Boolean(p.requiresEndpoint),
288
295
  defaultEndpoint: p.defaultEndpoint as string | undefined,
@@ -3,9 +3,28 @@ import type { LoopMode } from '@/types'
3
3
  export const DEFAULT_LOOP_MODE: LoopMode = 'bounded'
4
4
 
5
5
  // Loop limits
6
- export const DEFAULT_AGENT_LOOP_RECURSION_LIMIT = 30
7
- export const DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT = 40
8
- export const DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS = 10
6
+ export const AGENT_LOOP_RECURSION_LIMIT_MIN = 1
7
+ export const AGENT_LOOP_RECURSION_LIMIT_MAX = 200
8
+ export const ORCHESTRATOR_LOOP_RECURSION_LIMIT_MIN = 1
9
+ export const ORCHESTRATOR_LOOP_RECURSION_LIMIT_MAX = 300
10
+ export const LEGACY_ORCHESTRATOR_MAX_TURNS_MIN = 1
11
+ export const LEGACY_ORCHESTRATOR_MAX_TURNS_MAX = 300
12
+ export const ONGOING_LOOP_MAX_ITERATIONS_MIN = 10
13
+ export const ONGOING_LOOP_MAX_ITERATIONS_MAX = 5000
14
+ export const ONGOING_LOOP_MAX_RUNTIME_MINUTES_MIN = 0
15
+ export const ONGOING_LOOP_MAX_RUNTIME_MINUTES_MAX = 1440
16
+ export const DELEGATION_MAX_DEPTH_MIN = 1
17
+ export const DELEGATION_MAX_DEPTH_MAX = 12
18
+ export const SHELL_COMMAND_TIMEOUT_SEC_MIN = 1
19
+ export const SHELL_COMMAND_TIMEOUT_SEC_MAX = 600
20
+ export const CLAUDE_CODE_TIMEOUT_SEC_MIN = 5
21
+ export const CLAUDE_CODE_TIMEOUT_SEC_MAX = 7200
22
+ export const CLI_PROCESS_TIMEOUT_SEC_MIN = 10
23
+ export const CLI_PROCESS_TIMEOUT_SEC_MAX = 7200
24
+
25
+ export const DEFAULT_AGENT_LOOP_RECURSION_LIMIT = 60
26
+ export const DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT = 80
27
+ export const DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS = 16
9
28
  export const DEFAULT_ONGOING_LOOP_MAX_ITERATIONS = 250
10
29
  export const DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES = 60
11
30
  export const DEFAULT_DELEGATION_MAX_DEPTH = 3
@@ -14,3 +33,86 @@ export const DEFAULT_DELEGATION_MAX_DEPTH = 3
14
33
  export const DEFAULT_SHELL_COMMAND_TIMEOUT_SEC = 30
15
34
  export const DEFAULT_CLAUDE_CODE_TIMEOUT_SEC = 1800
16
35
  export const DEFAULT_CLI_PROCESS_TIMEOUT_SEC = 1800
36
+
37
+ function parseIntSetting(value: unknown, fallback: number, min: number, max: number): number {
38
+ const parsed = typeof value === 'number'
39
+ ? value
40
+ : typeof value === 'string'
41
+ ? Number.parseInt(value, 10)
42
+ : Number.NaN
43
+ if (!Number.isFinite(parsed)) return fallback
44
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
45
+ }
46
+
47
+ export interface NormalizedRuntimeSettingFields {
48
+ loopMode: LoopMode
49
+ agentLoopRecursionLimit: number
50
+ orchestratorLoopRecursionLimit: number
51
+ legacyOrchestratorMaxTurns: number
52
+ delegationMaxDepth: number
53
+ ongoingLoopMaxIterations: number
54
+ ongoingLoopMaxRuntimeMinutes: number
55
+ shellCommandTimeoutSec: number
56
+ claudeCodeTimeoutSec: number
57
+ cliProcessTimeoutSec: number
58
+ }
59
+
60
+ export function normalizeRuntimeSettingFields(settings: Record<string, unknown>): NormalizedRuntimeSettingFields {
61
+ return {
62
+ loopMode: settings.loopMode === 'ongoing' ? 'ongoing' : DEFAULT_LOOP_MODE,
63
+ agentLoopRecursionLimit: parseIntSetting(
64
+ settings.agentLoopRecursionLimit,
65
+ DEFAULT_AGENT_LOOP_RECURSION_LIMIT,
66
+ AGENT_LOOP_RECURSION_LIMIT_MIN,
67
+ AGENT_LOOP_RECURSION_LIMIT_MAX,
68
+ ),
69
+ orchestratorLoopRecursionLimit: parseIntSetting(
70
+ settings.orchestratorLoopRecursionLimit,
71
+ DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT,
72
+ ORCHESTRATOR_LOOP_RECURSION_LIMIT_MIN,
73
+ ORCHESTRATOR_LOOP_RECURSION_LIMIT_MAX,
74
+ ),
75
+ legacyOrchestratorMaxTurns: parseIntSetting(
76
+ settings.legacyOrchestratorMaxTurns,
77
+ DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS,
78
+ LEGACY_ORCHESTRATOR_MAX_TURNS_MIN,
79
+ LEGACY_ORCHESTRATOR_MAX_TURNS_MAX,
80
+ ),
81
+ delegationMaxDepth: parseIntSetting(
82
+ settings.delegationMaxDepth,
83
+ DEFAULT_DELEGATION_MAX_DEPTH,
84
+ DELEGATION_MAX_DEPTH_MIN,
85
+ DELEGATION_MAX_DEPTH_MAX,
86
+ ),
87
+ ongoingLoopMaxIterations: parseIntSetting(
88
+ settings.ongoingLoopMaxIterations,
89
+ DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
90
+ ONGOING_LOOP_MAX_ITERATIONS_MIN,
91
+ ONGOING_LOOP_MAX_ITERATIONS_MAX,
92
+ ),
93
+ ongoingLoopMaxRuntimeMinutes: parseIntSetting(
94
+ settings.ongoingLoopMaxRuntimeMinutes,
95
+ DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES,
96
+ ONGOING_LOOP_MAX_RUNTIME_MINUTES_MIN,
97
+ ONGOING_LOOP_MAX_RUNTIME_MINUTES_MAX,
98
+ ),
99
+ shellCommandTimeoutSec: parseIntSetting(
100
+ settings.shellCommandTimeoutSec,
101
+ DEFAULT_SHELL_COMMAND_TIMEOUT_SEC,
102
+ SHELL_COMMAND_TIMEOUT_SEC_MIN,
103
+ SHELL_COMMAND_TIMEOUT_SEC_MAX,
104
+ ),
105
+ claudeCodeTimeoutSec: parseIntSetting(
106
+ settings.claudeCodeTimeoutSec,
107
+ DEFAULT_CLAUDE_CODE_TIMEOUT_SEC,
108
+ CLAUDE_CODE_TIMEOUT_SEC_MIN,
109
+ CLAUDE_CODE_TIMEOUT_SEC_MAX,
110
+ ),
111
+ cliProcessTimeoutSec: parseIntSetting(
112
+ settings.cliProcessTimeoutSec,
113
+ DEFAULT_CLI_PROCESS_TIMEOUT_SEC,
114
+ CLI_PROCESS_TIMEOUT_SEC_MIN,
115
+ CLI_PROCESS_TIMEOUT_SEC_MAX,
116
+ ),
117
+ }
118
+ }
@@ -1,5 +1,10 @@
1
1
  function canUseLocalStorage(): boolean {
2
- return typeof window !== 'undefined' && !!window.localStorage
2
+ if (typeof window === 'undefined') return false
3
+ try {
4
+ return !!window.localStorage
5
+ } catch {
6
+ return false
7
+ }
3
8
  }
4
9
 
5
10
  export function safeStorageGet(key: string): string | null {
@@ -0,0 +1,112 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import type { Agent } from '@/types'
4
+ import {
5
+ isDelegationTaskPayload,
6
+ resolveDelegatorAgentId,
7
+ resolveManagedAgentAssignment,
8
+ validateManagedAgentAssignment,
9
+ } from './agent-assignment'
10
+
11
+ const now = Date.now()
12
+ const agents: Record<string, Agent> = {
13
+ molly: {
14
+ id: 'molly',
15
+ name: 'Molly',
16
+ description: '',
17
+ systemPrompt: '',
18
+ provider: 'openai',
19
+ model: 'gpt-4o',
20
+ createdAt: now,
21
+ updatedAt: now,
22
+ },
23
+ writer: {
24
+ id: 'writer',
25
+ name: 'Writer',
26
+ description: '',
27
+ systemPrompt: '',
28
+ provider: 'openai',
29
+ model: 'gpt-4o',
30
+ createdAt: now,
31
+ updatedAt: now,
32
+ },
33
+ }
34
+
35
+ describe('resolveManagedAgentAssignment', () => {
36
+ it('resolves explicit aliases to concrete agent ids', () => {
37
+ const resolved = resolveManagedAgentAssignment({ assignee: 'Writer' }, agents, 'molly')
38
+ assert.equal(resolved.agentId, 'writer')
39
+ assert.equal(resolved.source, 'explicit')
40
+ })
41
+
42
+ it('resolves description-based delegation before scope checks', () => {
43
+ const resolved = resolveManagedAgentAssignment(
44
+ { description: 'Please delegate this to @Writer and let them handle the draft.' },
45
+ agents,
46
+ 'molly',
47
+ )
48
+ assert.equal(resolved.agentId, 'writer')
49
+ assert.equal(resolved.source, 'description')
50
+ })
51
+ })
52
+
53
+ describe('validateManagedAgentAssignment', () => {
54
+ it('blocks assigning another agent when scope is self', () => {
55
+ const resolved = resolveManagedAgentAssignment({ assignee: 'writer' }, agents, 'molly')
56
+ const error = validateManagedAgentAssignment({
57
+ resourceLabel: 'tasks',
58
+ agents,
59
+ assignScope: 'self',
60
+ currentAgentId: 'molly',
61
+ targetAgentId: resolved.agentId,
62
+ unresolvedReference: resolved.unresolvedReference,
63
+ })
64
+ assert.match(error || '', /only assign tasks to yourself/i)
65
+ })
66
+
67
+ it('allows self-assignment in self scope', () => {
68
+ const resolved = resolveManagedAgentAssignment({ agentId: 'molly' }, agents, 'molly')
69
+ const error = validateManagedAgentAssignment({
70
+ resourceLabel: 'tasks',
71
+ agents,
72
+ assignScope: 'self',
73
+ currentAgentId: 'molly',
74
+ targetAgentId: resolved.agentId,
75
+ unresolvedReference: resolved.unresolvedReference,
76
+ })
77
+ assert.equal(error, null)
78
+ })
79
+
80
+ it('rejects unknown explicit agent references', () => {
81
+ const resolved = resolveManagedAgentAssignment({ agentId: 'missing-agent' }, agents, 'molly')
82
+ const error = validateManagedAgentAssignment({
83
+ resourceLabel: 'tasks',
84
+ agents,
85
+ assignScope: 'all',
86
+ currentAgentId: 'molly',
87
+ targetAgentId: resolved.agentId,
88
+ unresolvedReference: resolved.unresolvedReference,
89
+ })
90
+ assert.match(error || '', /unknown agent "missing-agent"/i)
91
+ })
92
+
93
+ it('rejects self-delegation using resolved agent ids', () => {
94
+ const payload = {
95
+ agentId: 'molly',
96
+ sourceType: 'delegation',
97
+ delegatedByAgentId: 'Molly',
98
+ }
99
+ const resolved = resolveManagedAgentAssignment(payload, agents, 'molly')
100
+ const error = validateManagedAgentAssignment({
101
+ resourceLabel: 'tasks',
102
+ agents,
103
+ assignScope: 'all',
104
+ currentAgentId: 'molly',
105
+ targetAgentId: resolved.agentId,
106
+ unresolvedReference: resolved.unresolvedReference,
107
+ isDelegation: isDelegationTaskPayload(payload),
108
+ delegatorAgentId: resolveDelegatorAgentId(payload, agents, 'molly'),
109
+ })
110
+ assert.match(error || '', /different agent id/i)
111
+ })
112
+ })
@@ -0,0 +1,169 @@
1
+ import type { Agent } from '@/types'
2
+ import { resolveAgentReference, resolveTaskAgentFromDescription } from './task-mention'
3
+
4
+ export const MANAGED_AGENT_REFERENCE_KEYS = [
5
+ 'agentId',
6
+ 'agent_id',
7
+ 'assignedAgentId',
8
+ 'assigned_agent_id',
9
+ 'assignedToAgentId',
10
+ 'assigned_to_agent_id',
11
+ 'assigneeId',
12
+ 'assignee_id',
13
+ 'assignedAgent',
14
+ 'assigned_agent',
15
+ 'assignedTo',
16
+ 'assigned_to',
17
+ 'assignee',
18
+ 'agent',
19
+ 'owner',
20
+ ] as const
21
+
22
+ type AssignmentSource = 'explicit' | 'description' | 'fallback' | 'none'
23
+
24
+ export interface ManagedAgentAssignmentResolution {
25
+ agentId: string | null
26
+ explicitReference: string | null
27
+ unresolvedReference: string | null
28
+ source: AssignmentSource
29
+ hadExplicitInput: boolean
30
+ }
31
+
32
+ function firstNonEmptyString(
33
+ parsed: Record<string, unknown>,
34
+ keys: readonly string[],
35
+ ): string | null {
36
+ for (const key of keys) {
37
+ const raw = parsed[key]
38
+ if (typeof raw !== 'string') continue
39
+ const trimmed = raw.trim()
40
+ if (trimmed) return trimmed
41
+ }
42
+ return null
43
+ }
44
+
45
+ export function hasManagedAgentAssignmentInput(
46
+ parsed: Record<string, unknown>,
47
+ keys: readonly string[] = MANAGED_AGENT_REFERENCE_KEYS,
48
+ ): boolean {
49
+ return keys.some((key) => Object.prototype.hasOwnProperty.call(parsed, key))
50
+ }
51
+
52
+ export function resolveManagedAgentAssignment(
53
+ parsed: Record<string, unknown>,
54
+ agents: Record<string, Agent>,
55
+ fallbackAgentId?: string | null,
56
+ opts?: {
57
+ allowDescription?: boolean
58
+ keys?: readonly string[]
59
+ },
60
+ ): ManagedAgentAssignmentResolution {
61
+ const keys = opts?.keys ?? MANAGED_AGENT_REFERENCE_KEYS
62
+ const explicitReference = firstNonEmptyString(parsed, keys)
63
+ const hadExplicitInput = hasManagedAgentAssignmentInput(parsed, keys)
64
+ if (explicitReference) {
65
+ const resolved = resolveAgentReference(explicitReference, agents)
66
+ return {
67
+ agentId: resolved,
68
+ explicitReference,
69
+ unresolvedReference: resolved ? null : explicitReference,
70
+ source: 'explicit',
71
+ hadExplicitInput,
72
+ }
73
+ }
74
+
75
+ if (opts?.allowDescription !== false) {
76
+ const description = typeof parsed.description === 'string' ? parsed.description.trim() : ''
77
+ if (description) {
78
+ const resolvedFromDescription = resolveTaskAgentFromDescription(description, '', agents).trim()
79
+ if (resolvedFromDescription) {
80
+ return {
81
+ agentId: resolvedFromDescription,
82
+ explicitReference: null,
83
+ unresolvedReference: null,
84
+ source: 'description',
85
+ hadExplicitInput,
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ const fallback = typeof fallbackAgentId === 'string' ? fallbackAgentId.trim() : ''
92
+ if (fallback) {
93
+ return {
94
+ agentId: fallback,
95
+ explicitReference: null,
96
+ unresolvedReference: null,
97
+ source: 'fallback',
98
+ hadExplicitInput,
99
+ }
100
+ }
101
+
102
+ return {
103
+ agentId: null,
104
+ explicitReference: null,
105
+ unresolvedReference: null,
106
+ source: 'none',
107
+ hadExplicitInput,
108
+ }
109
+ }
110
+
111
+ export function resolveDelegatorAgentId(
112
+ parsed: Record<string, unknown>,
113
+ agents: Record<string, Agent>,
114
+ fallbackAgentId?: string | null,
115
+ ): string | null {
116
+ const explicitDelegator = typeof parsed.delegatedByAgentId === 'string'
117
+ ? parsed.delegatedByAgentId.trim()
118
+ : ''
119
+ if (explicitDelegator) {
120
+ return resolveAgentReference(explicitDelegator, agents) || explicitDelegator
121
+ }
122
+ const fallback = typeof fallbackAgentId === 'string' ? fallbackAgentId.trim() : ''
123
+ return fallback || null
124
+ }
125
+
126
+ export function isDelegationTaskPayload(parsed: Record<string, unknown>): boolean {
127
+ const sourceType = typeof parsed.sourceType === 'string' ? parsed.sourceType.trim().toLowerCase() : ''
128
+ if (sourceType === 'delegation') return true
129
+ if (typeof parsed.delegatedFromTaskId === 'string' && parsed.delegatedFromTaskId.trim()) return true
130
+ if (typeof parsed.delegatedByAgentId === 'string' && parsed.delegatedByAgentId.trim()) return true
131
+ return false
132
+ }
133
+
134
+ export function validateManagedAgentAssignment(params: {
135
+ resourceLabel: string
136
+ agents: Record<string, Agent>
137
+ assignScope: 'self' | 'all'
138
+ currentAgentId?: string | null
139
+ targetAgentId?: string | null
140
+ unresolvedReference?: string | null
141
+ isDelegation?: boolean
142
+ delegatorAgentId?: string | null
143
+ }): string | null {
144
+ const currentAgentId = typeof params.currentAgentId === 'string' ? params.currentAgentId.trim() : ''
145
+ const targetAgentId = typeof params.targetAgentId === 'string' ? params.targetAgentId.trim() : ''
146
+ const unresolvedReference = typeof params.unresolvedReference === 'string' ? params.unresolvedReference.trim() : ''
147
+ const delegatorAgentId = typeof params.delegatorAgentId === 'string' ? params.delegatorAgentId.trim() : ''
148
+
149
+ if (unresolvedReference) {
150
+ return `Error: Unknown agent "${unresolvedReference}". Use an existing agent ID or exact agent name.`
151
+ }
152
+
153
+ if (targetAgentId && !params.agents[targetAgentId]) {
154
+ return `Error: Unknown agent "${targetAgentId}". Use an existing agent ID or exact agent name.`
155
+ }
156
+
157
+ if (params.assignScope === 'self' && currentAgentId && targetAgentId && targetAgentId !== currentAgentId) {
158
+ return `Error: You can only assign ${params.resourceLabel} to yourself ("${currentAgentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
159
+ }
160
+
161
+ if (params.isDelegation && targetAgentId) {
162
+ const comparisonId = delegatorAgentId || currentAgentId
163
+ if (comparisonId && targetAgentId === comparisonId) {
164
+ return 'Error: Delegation target must be a different agent ID. Create a normal self-task instead of delegating to yourself.'
165
+ }
166
+ }
167
+
168
+ return null
169
+ }