@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
@@ -15,6 +15,7 @@ const states: Map<string, ProviderHealthState> =
15
15
  (globalThis as any)[gk] ?? ((globalThis as any)[gk] = new Map<string, ProviderHealthState>())
16
16
 
17
17
  const cliCheckCache = new Map<string, { at: number; ok: boolean }>()
18
+ const delegateReadyCache = new Map<string, { at: number; ok: boolean }>()
18
19
  const CLI_CHECK_TTL_MS = 30_000
19
20
 
20
21
  function commandExists(binary: string): boolean {
@@ -70,6 +71,35 @@ function delegateBinary(delegateTool: DelegateTool): string {
70
71
  return 'opencode'
71
72
  }
72
73
 
74
+ function delegateToolReady(delegateTool: DelegateTool): boolean {
75
+ const now = Date.now()
76
+ const cached = delegateReadyCache.get(delegateTool)
77
+ if (cached && now - cached.at < CLI_CHECK_TTL_MS) return cached.ok
78
+
79
+ const binary = delegateBinary(delegateTool)
80
+ let ok = commandExists(binary)
81
+ if (ok && delegateTool === 'delegate_to_claude_code') {
82
+ const probe = spawnSync(binary, ['auth', 'status'], { encoding: 'utf-8', timeout: 8000 })
83
+ if ((probe.status ?? 1) !== 0) {
84
+ let loggedIn = false
85
+ try {
86
+ const parsed = JSON.parse(probe.stdout || '{}') as { loggedIn?: boolean }
87
+ loggedIn = parsed.loggedIn === true
88
+ } catch {
89
+ loggedIn = false
90
+ }
91
+ ok = loggedIn
92
+ }
93
+ } else if (ok && delegateTool === 'delegate_to_codex_cli') {
94
+ const probe = spawnSync(binary, ['login', 'status'], { encoding: 'utf-8', timeout: 8000 })
95
+ const probeText = `${probe.stdout || ''}\n${probe.stderr || ''}`.toLowerCase()
96
+ ok = (probe.status ?? 1) === 0 && probeText.includes('logged in')
97
+ }
98
+
99
+ delegateReadyCache.set(delegateTool, { at: now, ok })
100
+ return ok
101
+ }
102
+
73
103
  function delegateProviderId(delegateTool: DelegateTool): string {
74
104
  if (delegateTool === 'delegate_to_claude_code') return 'claude-cli'
75
105
  if (delegateTool === 'delegate_to_codex_cli') return 'codex-cli'
@@ -85,9 +115,9 @@ export function rankDelegatesByHealth(order: DelegateTool[]): DelegateTool[] {
85
115
  return true
86
116
  })
87
117
  return deduped.sort((a, b) => {
88
- const aBinOk = commandExists(delegateBinary(a))
89
- const bBinOk = commandExists(delegateBinary(b))
90
- if (aBinOk !== bBinOk) return aBinOk ? -1 : 1
118
+ const aReady = delegateToolReady(a)
119
+ const bReady = delegateToolReady(b)
120
+ if (aReady !== bReady) return aReady ? -1 : 1
91
121
 
92
122
  const aCool = isProviderCoolingDown(delegateProviderId(a))
93
123
  const bCool = isProviderCoolingDown(delegateProviderId(b))
@@ -0,0 +1,481 @@
1
+ import crypto from 'crypto'
2
+ import { getProviderList } from '@/lib/providers'
3
+ import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
4
+ import { decryptKey, loadCredentials } from '@/lib/server/storage'
5
+ import type { ProviderInfo, ProviderModelDiscoveryResult } from '@/types'
6
+
7
+ type DiscoveryStrategy = 'openai-compatible' | 'anthropic' | 'google' | 'ollama' | 'openclaw'
8
+
9
+ interface DiscoveryDescriptor {
10
+ providerId: string
11
+ providerName: string
12
+ strategy: DiscoveryStrategy
13
+ endpoint?: string
14
+ requiresApiKey: boolean
15
+ optionalApiKey: boolean
16
+ supportsDiscovery: boolean
17
+ }
18
+
19
+ interface DiscoverProviderModelsInput {
20
+ providerId: string
21
+ credentialId?: string | null
22
+ endpoint?: string | null
23
+ force?: boolean
24
+ requiresApiKey?: boolean
25
+ }
26
+
27
+ interface DiscoveryCacheEntry {
28
+ expiresAt: number
29
+ value: ProviderModelDiscoveryResult
30
+ }
31
+
32
+ const CLOUD_CACHE_TTL_MS = 15 * 60_000
33
+ const LOCAL_CACHE_TTL_MS = 60_000
34
+ const ERROR_CACHE_TTL_MS = 30_000
35
+ const DISCOVERY_TIMEOUT_MS = 10_000
36
+ const gk = '__swarmclaw_provider_model_discovery__' as const
37
+
38
+ type DiscoveryGlobals = typeof globalThis & {
39
+ [gk]?: {
40
+ cache: Map<string, DiscoveryCacheEntry>
41
+ pending: Map<string, Promise<ProviderModelDiscoveryResult>>
42
+ }
43
+ }
44
+
45
+ const discoveryGlobals = globalThis as DiscoveryGlobals
46
+ const discoveryState = discoveryGlobals[gk] ?? (discoveryGlobals[gk] = {
47
+ cache: new Map<string, DiscoveryCacheEntry>(),
48
+ pending: new Map<string, Promise<ProviderModelDiscoveryResult>>(),
49
+ })
50
+
51
+ function clean(value: string | null | undefined): string {
52
+ return typeof value === 'string' ? value.trim() : ''
53
+ }
54
+
55
+ function normalizeEndpoint(raw: string | null | undefined, fallback = ''): string {
56
+ return (clean(raw) || fallback).replace(/\/+$/, '')
57
+ }
58
+
59
+ function supportsBuiltInModelDiscovery(providerId: string): boolean {
60
+ return !['claude-cli', 'codex-cli', 'opencode-cli', 'fireworks'].includes(providerId)
61
+ }
62
+
63
+ function normalizeGoogleModelsEndpoint(raw: string | null | undefined): string {
64
+ const fallback = 'https://generativelanguage.googleapis.com/v1beta'
65
+ const normalized = normalizeEndpoint(raw, fallback)
66
+ .replace(/\/openai$/i, '')
67
+ .replace(/\/models$/i, '')
68
+ return `${normalized}/models`
69
+ }
70
+
71
+ function resolveProviderInfo(providerId: string): ProviderInfo | null {
72
+ return getProviderList().find((provider) => provider.id === providerId) || null
73
+ }
74
+
75
+ function resolveDescriptor(input: DiscoverProviderModelsInput): DiscoveryDescriptor | null {
76
+ const providerId = clean(input.providerId)
77
+ const provider = resolveProviderInfo(providerId)
78
+ const requiresApiKeyOverride = typeof input.requiresApiKey === 'boolean' ? input.requiresApiKey : undefined
79
+
80
+ if (providerId === 'custom') {
81
+ const endpoint = normalizeEndpoint(input.endpoint)
82
+ if (!endpoint) return null
83
+ return {
84
+ providerId,
85
+ providerName: 'Custom Provider',
86
+ strategy: 'openai-compatible',
87
+ endpoint,
88
+ requiresApiKey: requiresApiKeyOverride ?? true,
89
+ optionalApiKey: false,
90
+ supportsDiscovery: true,
91
+ }
92
+ }
93
+
94
+ if (providerId === 'openclaw') {
95
+ return {
96
+ providerId,
97
+ providerName: 'OpenClaw',
98
+ strategy: 'openclaw',
99
+ endpoint: normalizeEndpoint(input.endpoint, 'http://localhost:18789'),
100
+ requiresApiKey: requiresApiKeyOverride ?? false,
101
+ optionalApiKey: true,
102
+ supportsDiscovery: true,
103
+ }
104
+ }
105
+
106
+ if (!provider) return null
107
+ const supportsDiscovery = provider.supportsModelDiscovery ?? supportsBuiltInModelDiscovery(providerId)
108
+ if (!supportsDiscovery) {
109
+ return {
110
+ providerId,
111
+ providerName: provider.name,
112
+ strategy: 'openai-compatible',
113
+ endpoint: undefined,
114
+ requiresApiKey: provider.requiresApiKey,
115
+ optionalApiKey: Boolean(provider.optionalApiKey),
116
+ supportsDiscovery: false,
117
+ }
118
+ }
119
+
120
+ if (providerId === 'anthropic') {
121
+ return {
122
+ providerId,
123
+ providerName: provider.name,
124
+ strategy: 'anthropic',
125
+ requiresApiKey: requiresApiKeyOverride ?? provider.requiresApiKey,
126
+ optionalApiKey: Boolean(provider.optionalApiKey),
127
+ supportsDiscovery,
128
+ }
129
+ }
130
+
131
+ if (providerId === 'google') {
132
+ return {
133
+ providerId,
134
+ providerName: provider.name,
135
+ strategy: 'google',
136
+ endpoint: normalizeGoogleModelsEndpoint(input.endpoint || provider.defaultEndpoint || ''),
137
+ requiresApiKey: requiresApiKeyOverride ?? provider.requiresApiKey,
138
+ optionalApiKey: Boolean(provider.optionalApiKey),
139
+ supportsDiscovery,
140
+ }
141
+ }
142
+
143
+ if (providerId === 'ollama') {
144
+ return {
145
+ providerId,
146
+ providerName: provider.name,
147
+ strategy: 'ollama',
148
+ endpoint: normalizeEndpoint(input.endpoint, provider.defaultEndpoint || 'http://localhost:11434'),
149
+ requiresApiKey: requiresApiKeyOverride ?? provider.requiresApiKey,
150
+ optionalApiKey: Boolean(provider.optionalApiKey),
151
+ supportsDiscovery,
152
+ }
153
+ }
154
+
155
+ const openAiDefault = OPENAI_COMPATIBLE_DEFAULTS[providerId as keyof typeof OPENAI_COMPATIBLE_DEFAULTS]?.defaultEndpoint
156
+ const endpoint = normalizeEndpoint(input.endpoint, provider.defaultEndpoint || openAiDefault || '')
157
+ return {
158
+ providerId,
159
+ providerName: provider.name,
160
+ strategy: 'openai-compatible',
161
+ endpoint,
162
+ requiresApiKey: requiresApiKeyOverride ?? provider.requiresApiKey,
163
+ optionalApiKey: Boolean(provider.optionalApiKey),
164
+ supportsDiscovery,
165
+ }
166
+ }
167
+
168
+ function parseErrorMessage(text: string, fallback: string): string {
169
+ const body = text.trim()
170
+ if (!body) return fallback
171
+ try {
172
+ const parsed = JSON.parse(body)
173
+ if (typeof parsed?.error?.message === 'string' && parsed.error.message.trim()) return parsed.error.message.trim()
174
+ if (typeof parsed?.error === 'string' && parsed.error.trim()) return parsed.error.trim()
175
+ if (typeof parsed?.message === 'string' && parsed.message.trim()) return parsed.message.trim()
176
+ if (typeof parsed?.detail === 'string' && parsed.detail.trim()) return parsed.detail.trim()
177
+ } catch {
178
+ // Ignore invalid JSON and fall back to the raw text.
179
+ }
180
+ return body.slice(0, 300) || fallback
181
+ }
182
+
183
+ function resolveCredentialApiKey(credentialId: string | null | undefined): string | null {
184
+ const id = clean(credentialId)
185
+ if (!id) return null
186
+ try {
187
+ const credentials = loadCredentials()
188
+ const credential = credentials[id]
189
+ if (!credential?.encryptedKey) return null
190
+ return decryptKey(credential.encryptedKey)
191
+ } catch {
192
+ return null
193
+ }
194
+ }
195
+
196
+ function hashApiKey(apiKey: string | null): string {
197
+ if (!apiKey) return 'anon'
198
+ return crypto.createHash('sha1').update(apiKey).digest('hex').slice(0, 12)
199
+ }
200
+
201
+ function dedupeModels(models: string[]): string[] {
202
+ const seen = new Set<string>()
203
+ const result: string[] = []
204
+ for (const model of models) {
205
+ const trimmed = model.trim()
206
+ if (!trimmed) continue
207
+ if (seen.has(trimmed)) continue
208
+ seen.add(trimmed)
209
+ result.push(trimmed)
210
+ }
211
+ return result
212
+ }
213
+
214
+ function normalizeModelId(modelId: string, strategy: DiscoveryStrategy): string {
215
+ const trimmed = modelId.trim()
216
+ if (!trimmed) return ''
217
+ if (strategy === 'ollama') return trimmed.replace(/:latest$/i, '')
218
+ if (strategy === 'google' && trimmed.startsWith('models/')) return trimmed.slice('models/'.length)
219
+ return trimmed
220
+ }
221
+
222
+ function looksLikeChatModel(providerId: string, modelId: string): boolean {
223
+ const normalized = modelId.toLowerCase()
224
+ if (!normalized) return false
225
+
226
+ const universalExclusions = [
227
+ 'embedding',
228
+ 'rerank',
229
+ 'moderation',
230
+ 'whisper',
231
+ 'transcribe',
232
+ 'transcription',
233
+ 'tts',
234
+ 'speech',
235
+ 'text-to-speech',
236
+ 'stable-diffusion',
237
+ 'sdxl',
238
+ 'flux',
239
+ 'playground-v2',
240
+ 'pix2pix',
241
+ 'clip',
242
+ ]
243
+ if (universalExclusions.some((token) => normalized.includes(token))) return false
244
+
245
+ if (providerId === 'openai') return /^(gpt-|o1($|-)|o3($|-)|o4($|-)|chatgpt-)/.test(normalized)
246
+ if (providerId === 'anthropic') return normalized.startsWith('claude-')
247
+ if (providerId === 'google') return normalized.startsWith('gemini-')
248
+ if (providerId === 'deepseek') return normalized.startsWith('deepseek-')
249
+ if (providerId === 'xai') return normalized.startsWith('grok-')
250
+
251
+ return true
252
+ }
253
+
254
+ function extractCandidateModelIds(payload: unknown, strategy: DiscoveryStrategy): string[] {
255
+ const source = payload as {
256
+ data?: unknown[]
257
+ models?: unknown[]
258
+ }
259
+ const candidates: string[] = []
260
+ const append = (value: unknown) => {
261
+ if (typeof value === 'string' && value.trim()) candidates.push(value.trim())
262
+ }
263
+
264
+ const readCollection = (items: unknown[] | undefined) => {
265
+ if (!Array.isArray(items)) return
266
+ for (const item of items) {
267
+ if (typeof item === 'string') {
268
+ append(item)
269
+ continue
270
+ }
271
+ if (!item || typeof item !== 'object') continue
272
+ const record = item as { id?: unknown; name?: unknown; model?: unknown; baseModelId?: unknown }
273
+ append(record.id)
274
+ append(record.name)
275
+ append(record.model)
276
+ append(record.baseModelId)
277
+ }
278
+ }
279
+
280
+ if (Array.isArray(payload)) readCollection(payload)
281
+ readCollection(source.data)
282
+ readCollection(source.models)
283
+
284
+ const normalized = candidates
285
+ .map((candidate) => normalizeModelId(candidate, strategy))
286
+ .filter(Boolean)
287
+ return dedupeModels(normalized)
288
+ }
289
+
290
+ function extractDiscoveredModels(
291
+ providerId: string,
292
+ strategy: DiscoveryStrategy,
293
+ payload: unknown,
294
+ ): { models: string[]; rawCount: number } {
295
+ const candidates = extractCandidateModelIds(payload, strategy)
296
+ const filtered = strategy === 'ollama'
297
+ ? candidates
298
+ : candidates.filter((candidate) => looksLikeChatModel(providerId, candidate))
299
+ return {
300
+ models: dedupeModels(filtered),
301
+ rawCount: candidates.length,
302
+ }
303
+ }
304
+
305
+ function ttlForDescriptor(descriptor: DiscoveryDescriptor, ok: boolean): number {
306
+ if (!ok) return ERROR_CACHE_TTL_MS
307
+ if (descriptor.strategy === 'ollama' || descriptor.strategy === 'openclaw') return LOCAL_CACHE_TTL_MS
308
+ return CLOUD_CACHE_TTL_MS
309
+ }
310
+
311
+ function buildCacheKey(
312
+ descriptor: DiscoveryDescriptor,
313
+ credentialId: string | null | undefined,
314
+ apiKey: string | null,
315
+ ): string {
316
+ return [
317
+ descriptor.providerId,
318
+ descriptor.strategy,
319
+ descriptor.endpoint || '',
320
+ clean(credentialId),
321
+ hashApiKey(apiKey),
322
+ ].join('::')
323
+ }
324
+
325
+ async function fetchModelsFromProvider(
326
+ descriptor: DiscoveryDescriptor,
327
+ apiKey: string | null,
328
+ ): Promise<{ ok: boolean; models: string[]; message: string }> {
329
+ const headers: Record<string, string> = {}
330
+ let url = descriptor.endpoint || ''
331
+
332
+ if (descriptor.strategy === 'anthropic') {
333
+ url = 'https://api.anthropic.com/v1/models'
334
+ if (apiKey) headers['x-api-key'] = apiKey
335
+ headers['anthropic-version'] = '2023-06-01'
336
+ } else if (descriptor.strategy === 'google') {
337
+ url = descriptor.endpoint || normalizeGoogleModelsEndpoint('')
338
+ if (apiKey) {
339
+ const searchParams = new URLSearchParams({ key: apiKey })
340
+ url = `${url}?${searchParams.toString()}`
341
+ }
342
+ } else if (descriptor.strategy === 'ollama') {
343
+ url = `${descriptor.endpoint}/api/tags`
344
+ } else {
345
+ url = `${descriptor.endpoint}/models`
346
+ if (apiKey) headers.authorization = `Bearer ${apiKey}`
347
+ }
348
+
349
+ const res = await fetch(url, {
350
+ headers,
351
+ signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
352
+ cache: 'no-store',
353
+ })
354
+ if (!res.ok) {
355
+ const text = await res.text().catch(() => '')
356
+ return {
357
+ ok: false,
358
+ models: [],
359
+ message: parseErrorMessage(text, `${descriptor.providerName} returned ${res.status}.`),
360
+ }
361
+ }
362
+
363
+ const payload = await res.json().catch(() => ({}))
364
+ const { models, rawCount } = extractDiscoveredModels(descriptor.providerId, descriptor.strategy, payload)
365
+ if (models.length === 0) {
366
+ return {
367
+ ok: true,
368
+ models: [],
369
+ message: rawCount > 0
370
+ ? `${descriptor.providerName} returned ${rawCount} model(s), but none looked chat-capable.`
371
+ : `${descriptor.providerName} did not report any models.`,
372
+ }
373
+ }
374
+
375
+ const message = rawCount > models.length
376
+ ? `${descriptor.providerName} returned ${rawCount} model(s); showing ${models.length} likely chat models.`
377
+ : `${descriptor.providerName} returned ${models.length} live model(s).`
378
+ return { ok: true, models, message }
379
+ }
380
+
381
+ function buildResult(
382
+ descriptor: DiscoveryDescriptor,
383
+ data: Partial<ProviderModelDiscoveryResult> & Pick<ProviderModelDiscoveryResult, 'ok' | 'models'>,
384
+ ): ProviderModelDiscoveryResult {
385
+ return {
386
+ ok: data.ok,
387
+ providerId: descriptor.providerId,
388
+ providerName: descriptor.providerName,
389
+ models: data.models,
390
+ cached: Boolean(data.cached),
391
+ fetchedAt: data.fetchedAt ?? Date.now(),
392
+ cacheTtlMs: data.cacheTtlMs ?? ttlForDescriptor(descriptor, data.ok),
393
+ supportsDiscovery: descriptor.supportsDiscovery,
394
+ missingCredential: data.missingCredential,
395
+ message: data.message,
396
+ }
397
+ }
398
+
399
+ export async function discoverProviderModels(
400
+ input: DiscoverProviderModelsInput,
401
+ ): Promise<ProviderModelDiscoveryResult> {
402
+ const descriptor = resolveDescriptor(input)
403
+ if (!descriptor) {
404
+ return {
405
+ ok: false,
406
+ providerId: clean(input.providerId) || 'unknown',
407
+ providerName: undefined,
408
+ models: [],
409
+ cached: false,
410
+ fetchedAt: Date.now(),
411
+ cacheTtlMs: ERROR_CACHE_TTL_MS,
412
+ supportsDiscovery: false,
413
+ message: 'Live model discovery is not available for this provider configuration.',
414
+ }
415
+ }
416
+
417
+ if (!descriptor.supportsDiscovery) {
418
+ return buildResult(descriptor, {
419
+ ok: false,
420
+ models: [],
421
+ message: 'This provider does not expose a live model catalog here. You can still type a model name manually.',
422
+ })
423
+ }
424
+
425
+ const apiKey = resolveCredentialApiKey(input.credentialId)
426
+ if (descriptor.requiresApiKey && !apiKey) {
427
+ return buildResult(descriptor, {
428
+ ok: false,
429
+ models: [],
430
+ missingCredential: true,
431
+ message: 'Add an API key to fetch the live model list. Manual model entry still works.',
432
+ })
433
+ }
434
+
435
+ const cacheKey = buildCacheKey(descriptor, input.credentialId, apiKey)
436
+ const now = Date.now()
437
+ if (!input.force) {
438
+ const cached = discoveryState.cache.get(cacheKey)
439
+ if (cached && cached.expiresAt > now) {
440
+ return { ...cached.value, cached: true }
441
+ }
442
+ const pending = discoveryState.pending.get(cacheKey)
443
+ if (pending) return pending
444
+ }
445
+
446
+ const promise = (async () => {
447
+ const fetchedAt = Date.now()
448
+ try {
449
+ const result = await fetchModelsFromProvider(descriptor, apiKey)
450
+ const built = buildResult(descriptor, {
451
+ ok: result.ok,
452
+ models: result.models,
453
+ message: result.message,
454
+ fetchedAt,
455
+ })
456
+ discoveryState.cache.set(cacheKey, {
457
+ expiresAt: fetchedAt + ttlForDescriptor(descriptor, result.ok),
458
+ value: built,
459
+ })
460
+ return built
461
+ } catch (error) {
462
+ const message = error instanceof Error ? error.message : 'Failed to fetch live models.'
463
+ const built = buildResult(descriptor, {
464
+ ok: false,
465
+ models: [],
466
+ message,
467
+ fetchedAt,
468
+ })
469
+ discoveryState.cache.set(cacheKey, {
470
+ expiresAt: fetchedAt + ERROR_CACHE_TTL_MS,
471
+ value: built,
472
+ })
473
+ return built
474
+ } finally {
475
+ discoveryState.pending.delete(cacheKey)
476
+ }
477
+ })()
478
+
479
+ discoveryState.pending.set(cacheKey, promise)
480
+ return promise
481
+ }
@@ -11,7 +11,6 @@ import { pushMainLoopEventToMainSessions } from './main-agent-loop'
11
11
  import { executeSessionChatTurn } from './chat-execution'
12
12
  import { extractTaskResult, formatResultBody } from './task-result'
13
13
  import { getCheckpointSaver } from './langgraph-checkpoint'
14
- import { isMainLoopSession } from './main-session'
15
14
  import { cascadeUnblock } from './dag-validation'
16
15
  import { performGuardianRollback } from './guardian'
17
16
  import type { Agent, BoardTask, Connector, Message } from '@/types'
@@ -282,10 +281,6 @@ function pushQueueUnique(queue: string[], id: string): void {
282
281
  if (!queueContains(queue, id)) queue.push(id)
283
282
  }
284
283
 
285
- function isMainSession(session: SessionLike | null | undefined): boolean {
286
- return isMainLoopSession(session)
287
- }
288
-
289
284
  function resolveTaskOwnerUser(task: ScheduleTaskMeta, sessions: Record<string, SessionLike>): string | null {
290
285
  const direct = typeof task.user === 'string' ? task.user.trim() : ''
291
286
  if (direct) return direct
@@ -555,7 +550,7 @@ function notifyMainChatScheduleResult(task: BoardTask): void {
555
550
  if (task.status !== 'completed' && task.status !== 'failed') return
556
551
 
557
552
  const sessions = loadSessions()
558
- const ownerUser = resolveTaskOwnerUser(scheduleTask, sessions as Record<string, SessionLike>)
553
+ void resolveTaskOwnerUser(scheduleTask, sessions as Record<string, SessionLike>)
559
554
  const scheduleNameRaw = typeof scheduleTask.sourceScheduleName === 'string'
560
555
  ? scheduleTask.sourceScheduleName.trim()
561
556
  : ''
@@ -594,18 +589,7 @@ function notifyMainChatScheduleResult(task: BoardTask): void {
594
589
  return msg
595
590
  }
596
591
 
597
- for (const session of Object.values(sessions) as SessionLike[]) {
598
- if (!isMainSession(session)) continue
599
- if (ownerUser && session?.user && session.user !== ownerUser) continue
600
- const last = Array.isArray(session.messages) ? session.messages.at(-1) : null
601
- if (last?.role === 'assistant' && last?.text === body && typeof last?.time === 'number' && now - last.time < 30_000) continue
602
- if (!Array.isArray(session.messages)) session.messages = []
603
- session.messages.push(buildMsg())
604
- session.lastActiveAt = now
605
- changed = true
606
- }
607
-
608
- // Also push to the agent's persistent thread session
592
+ // Push to the agent's shortcut chat session.
609
593
  try {
610
594
  const agents = loadAgents()
611
595
  const agent = agents[task.agentId]
@@ -830,13 +814,12 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
830
814
  }
831
815
 
832
816
  // Push to delegating agent's active user-facing chat sessions
833
- // so the result is visible in the chat the user is looking at
817
+ // so the result is visible in the chat the user is looking at.
834
818
  if (delegator) {
835
819
  for (const session of Object.values(sessions)) {
836
820
  if (!session || session.agentId !== delegatedBy) continue
837
- // Skip thread sessions and orchestrated/subagent sessions
821
+ // Skip the agent shortcut session itself.
838
822
  if (session.id === delegator.threadSessionId) continue
839
- if (session.sessionType === 'orchestrated') continue
840
823
  // Only push to recently-active sessions (within last 30 minutes)
841
824
  const lastActive = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : 0
842
825
  if (now - lastActive > 30 * 60_000) continue