@swarmclawai/swarmclaw 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -1,27 +1,96 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { validateAccessKey, getAccessKey, isFirstTimeSetup, markSetupComplete } from '@/lib/server/storage'
2
+ import { validateAccessKey, isFirstTimeSetup, markSetupComplete } from '@/lib/server/storage'
3
3
  import { ensureDaemonStarted } from '@/lib/server/daemon-state'
4
+ import { AUTH_COOKIE_NAME, getCookieValue } from '@/lib/auth'
4
5
  export const dynamic = 'force-dynamic'
5
6
 
7
+ interface AuthAttemptEntry {
8
+ count: number
9
+ lockedUntil: number
10
+ }
6
11
 
7
- /** GET /api/auth — check if this is a first-time setup (returns key for initial display) */
8
- export async function GET(_req: Request) {
9
- if (isFirstTimeSetup()) {
10
- return NextResponse.json({ firstTime: true, key: getAccessKey() })
12
+ const authRateLimitMap = (
13
+ (globalThis as Record<string, unknown>).__swarmclaw_auth_rate_limit__ ??= new Map()
14
+ ) as Map<string, AuthAttemptEntry>
15
+
16
+ const MAX_ATTEMPTS = 5
17
+ const LOCKOUT_MS = 15 * 60 * 1000
18
+
19
+ function getClientIp(req: Request): string {
20
+ const forwarded = req.headers.get('x-forwarded-for')
21
+ if (forwarded) {
22
+ const first = forwarded.split(',')[0]?.trim()
23
+ if (first) return first
11
24
  }
12
- return NextResponse.json({ firstTime: false })
25
+ const realIp = req.headers.get('x-real-ip')?.trim()
26
+ return realIp || 'unknown'
27
+ }
28
+
29
+ function clearAuthCookie(response: NextResponse): NextResponse {
30
+ response.cookies.set(AUTH_COOKIE_NAME, '', {
31
+ httpOnly: true,
32
+ sameSite: 'lax',
33
+ secure: false,
34
+ path: '/',
35
+ maxAge: 0,
36
+ })
37
+ return response
38
+ }
39
+
40
+ function setAuthCookie(response: NextResponse, req: Request, key: string): NextResponse {
41
+ response.cookies.set(AUTH_COOKIE_NAME, key, {
42
+ httpOnly: true,
43
+ sameSite: 'lax',
44
+ secure: new URL(req.url).protocol === 'https:',
45
+ path: '/',
46
+ maxAge: 60 * 60 * 24 * 30,
47
+ })
48
+ return response
49
+ }
50
+
51
+ /** GET /api/auth — returns setup state and whether the auth cookie is currently valid */
52
+ export async function GET(req: Request) {
53
+ const cookieKey = getCookieValue(req.headers.get('cookie'), AUTH_COOKIE_NAME)
54
+ return NextResponse.json({
55
+ firstTime: isFirstTimeSetup(),
56
+ authenticated: !!cookieKey && validateAccessKey(cookieKey),
57
+ })
13
58
  }
14
59
 
15
60
  /** POST /api/auth — validate an access key */
16
61
  export async function POST(req: Request) {
62
+ const clientIp = getClientIp(req)
63
+ const entry = authRateLimitMap.get(clientIp)
64
+ if (entry && entry.lockedUntil > Date.now()) {
65
+ const retryAfter = Math.ceil((entry.lockedUntil - Date.now()) / 1000)
66
+ return clearAuthCookie(NextResponse.json(
67
+ { error: 'Too many failed attempts. Try again later.', retryAfter },
68
+ { status: 429, headers: { 'Retry-After': String(retryAfter) } },
69
+ ))
70
+ }
71
+
17
72
  const { key } = await req.json()
18
73
  if (!key || !validateAccessKey(key)) {
19
- return NextResponse.json({ error: 'Invalid access key' }, { status: 401 })
74
+ const current = authRateLimitMap.get(clientIp) ?? { count: 0, lockedUntil: 0 }
75
+ current.count += 1
76
+ if (current.count >= MAX_ATTEMPTS) {
77
+ current.lockedUntil = Date.now() + LOCKOUT_MS
78
+ }
79
+ authRateLimitMap.set(clientIp, current)
80
+ return clearAuthCookie(NextResponse.json(
81
+ { error: 'Invalid access key' },
82
+ {
83
+ status: 401,
84
+ headers: { 'X-RateLimit-Remaining': String(Math.max(0, MAX_ATTEMPTS - current.count)) },
85
+ },
86
+ ))
20
87
  }
88
+
89
+ authRateLimitMap.delete(clientIp)
21
90
  // If this was first-time setup, mark it as claimed
22
91
  if (isFirstTimeSetup()) {
23
92
  markSetupComplete()
24
93
  }
25
94
  ensureDaemonStarted('api/auth:post')
26
- return NextResponse.json({ ok: true })
95
+ return setAuthCookie(NextResponse.json({ ok: true }), req, key)
27
96
  }
@@ -8,9 +8,12 @@ import { getProvider } from '@/lib/providers'
8
8
  import {
9
9
  resolveApiKey,
10
10
  parseMentions,
11
+ resolveReplyTargetAgentId,
12
+ resolveAgentApiEndpoint,
11
13
  compactChatroomMessages,
12
14
  buildChatroomSystemPrompt,
13
- buildSyntheticSession,
15
+ ensureSyntheticSession,
16
+ appendSyntheticSessionMessage,
14
17
  buildAgentSystemPromptForChatroom,
15
18
  buildHistoryForAgent,
16
19
  isMuted,
@@ -19,6 +22,7 @@ import { filterHealthyChatroomAgents } from '@/lib/server/chatroom-health'
19
22
  import { evaluateRoutingRules } from '@/lib/server/chatroom-routing'
20
23
  import { markProviderFailure, markProviderSuccess } from '@/lib/server/provider-health'
21
24
  import { applyAgentReactionsFromText } from '@/lib/server/chatroom-orchestration'
25
+ import { resolvePrimaryAgentRoute } from '@/lib/server/agent-runtime-config'
22
26
  import type { Chatroom, ChatroomMessage, Agent } from '@/types'
23
27
 
24
28
  export const dynamic = 'force-dynamic'
@@ -50,7 +54,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
50
54
 
51
55
  // Persist incoming message
52
56
  const senderName = senderId === 'user' ? 'You' : (agents[senderId]?.name || senderId)
53
- let mentions = parseMentions(text, agents, chatroom.agentIds)
57
+ const replyTargetAgentId = resolveReplyTargetAgentId(replyToId, chatroom.messages, chatroom.agentIds)
58
+ let mentions = parseMentions(text, agents, chatroom.agentIds, { replyTargetAgentId })
54
59
  // Routing rules: if no explicit mentions, evaluate keyword/capability rules
55
60
  if (mentions.length === 0 && chatroom.routingRules?.length) {
56
61
  const agentList = chatroom.agentIds.map((aid) => agents[aid]).filter(Boolean)
@@ -147,15 +152,17 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
147
152
  }
148
153
 
149
154
  // Pre-flight: check if the agent's provider is usable before attempting to stream
150
- const providerInfo = getProvider(agent.provider)
151
- const apiKey = resolveApiKey(agent.credentialId)
155
+ const route = resolvePrimaryAgentRoute(agent)
156
+ const providerInfo = getProvider(route?.provider || agent.provider)
157
+ const apiKey = resolveApiKey(route?.credentialId || agent.credentialId)
158
+ const resolvedEndpoint = route?.apiEndpoint || resolveAgentApiEndpoint(agent)
152
159
  if (providerInfo?.requiresApiKey && !apiKey) {
153
160
  writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
154
161
  writeEvent({ t: 'err', text: `${agent.name} has no API credentials configured`, agentId: agent.id, agentName: agent.name })
155
162
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
156
163
  return []
157
164
  }
158
- if (providerInfo?.requiresEndpoint && !agent.apiEndpoint) {
165
+ if (providerInfo?.requiresEndpoint && !resolvedEndpoint) {
159
166
  writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
160
167
  writeEvent({ t: 'err', text: `${agent.name} has no endpoint configured`, agentId: agent.id, agentName: agent.name })
161
168
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
@@ -173,7 +180,13 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
173
180
  notify(`chatroom:${id}`)
174
181
  }
175
182
 
176
- const syntheticSession = buildSyntheticSession(agent, id)
183
+ const syntheticSession = ensureSyntheticSession(agent, id)
184
+ syntheticSession.provider = route?.provider || syntheticSession.provider
185
+ syntheticSession.model = route?.model || syntheticSession.model
186
+ syntheticSession.credentialId = route?.credentialId ?? syntheticSession.credentialId ?? null
187
+ syntheticSession.fallbackCredentialIds = route?.fallbackCredentialIds || syntheticSession.fallbackCredentialIds || []
188
+ syntheticSession.gatewayProfileId = route?.gatewayProfileId ?? syntheticSession.gatewayProfileId ?? null
189
+ syntheticSession.apiEndpoint = resolvedEndpoint
177
190
  const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
178
191
  const chatroomContext = buildChatroomSystemPrompt(freshChatroom, agents, agent.id)
179
192
  const fullSystemPrompt = [agentSystemPrompt, chatroomContext].filter(Boolean).join('\n\n')
@@ -181,6 +194,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
181
194
 
182
195
  // Use enriched context message for chained agents, or reply context + original text
183
196
  const messageForAgent = item.contextMessage || (replyContext + text)
197
+ appendSyntheticSessionMessage(syntheticSession.id, 'user', messageForAgent)
184
198
 
185
199
  let fullText = ''
186
200
  let agentError = ''
@@ -218,12 +232,14 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
218
232
 
219
233
  // Don't persist empty or error-only messages — they pollute chat history
220
234
  if (!responseText.trim() && agentError) {
235
+ appendSyntheticSessionMessage(syntheticSession.id, 'assistant', agentError)
221
236
  markProviderFailure(agent.provider, agentError)
222
237
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
223
238
  return []
224
239
  }
225
240
 
226
241
  if (responseText.trim()) {
242
+ appendSyntheticSessionMessage(syntheticSession.id, 'assistant', responseText)
227
243
  const parsedMentions = parseMentions(responseText, agents, freshChatroom.agentIds)
228
244
  const chainedHealth = filterHealthyChatroomAgents(parsedMentions, agents)
229
245
  const newMentions = chainedHealth.healthyAgentIds
@@ -33,7 +33,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
33
33
  chatroom.updatedAt = Date.now()
34
34
  chatrooms[id] = chatroom
35
35
  saveChatrooms(chatrooms)
36
+ notify('chatrooms')
36
37
  notify(`chatroom:${id}`)
37
38
 
38
- return NextResponse.json({ ok: true, pinnedMessageIds: chatroom.pinnedMessageIds })
39
+ return NextResponse.json(chatroom)
39
40
  }
@@ -36,7 +36,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
36
36
  chatroom.updatedAt = Date.now()
37
37
  chatrooms[id] = chatroom
38
38
  saveChatrooms(chatrooms)
39
+ notify('chatrooms')
39
40
  notify(`chatroom:${id}`)
40
41
 
41
- return NextResponse.json({ ok: true, reactions: message.reactions })
42
+ return NextResponse.json(chatroom)
42
43
  }
@@ -33,6 +33,12 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
33
33
 
34
34
  // Diff agentIds and inject join/leave system messages
35
35
  if (Array.isArray(body.agentIds)) {
36
+ if (body.agentIds.length === 0) {
37
+ return NextResponse.json(
38
+ { error: 'Select at least one chatroom member.' },
39
+ { status: 400 },
40
+ )
41
+ }
36
42
  const agents = loadAgents()
37
43
  const invalidAgentIds = (body.agentIds as string[]).filter((agentId) => !agents[agentId])
38
44
  if (invalidAgentIds.length > 0) {
@@ -1,9 +1,13 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { hasActiveBrowser, cleanupSessionBrowser } from '@/lib/server/session-tools'
3
+ import { loadBrowserSessionRecord } from '@/lib/server/browser-state'
3
4
 
4
5
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
6
  const { id } = await params
6
- return NextResponse.json({ active: hasActiveBrowser(id) })
7
+ return NextResponse.json({
8
+ active: hasActiveBrowser(id),
9
+ state: loadBrowserSessionRecord(id),
10
+ })
7
11
  }
8
12
 
9
13
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -49,7 +49,9 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
49
49
  mode: queueMode,
50
50
  onEvent: (ev) => writeEvent(ev as unknown as Record<string, unknown>),
51
51
  replyToId,
52
- callerSignal: req.signal,
52
+ // Keep user-initiated runs alive even if the SSE transport drops so
53
+ // long-lived tasks can finish and be observed later via polling/history.
54
+ callerSignal: internal ? req.signal : undefined,
53
55
  })
54
56
  abortRun = run.abort
55
57
 
@@ -89,8 +91,10 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
89
91
  })
90
92
  },
91
93
  cancel() {
92
- // Client disconnected abort the run so the LLM stream is cancelled.
93
- abortRun?.()
94
+ // Client disconnected. User-facing runs continue in the background so
95
+ // they can persist results even when the transport drops. Explicit stop
96
+ // controls still cancel the run through the session run manager.
97
+ if (internal) abortRun?.()
94
98
  },
95
99
  })
96
100
 
@@ -1,17 +1,26 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadSessions, saveSessions } from '@/lib/server/storage'
2
+ import { active, loadStoredItem, upsertStoredItem } from '@/lib/server/storage'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
+ import { getSessionRunState } from '@/lib/server/session-run-manager'
5
+ import { pruneStreamingAssistantArtifacts } from '@/lib/chat-streaming-state'
4
6
 
5
7
  export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
8
  const { id } = await params
7
- const sessions = loadSessions()
8
- if (!sessions[id]) return notFound()
9
+ const session = loadStoredItem('sessions', id)
10
+ if (!session) return notFound()
11
+ session.messages = Array.isArray(session.messages) ? session.messages : []
12
+
13
+ const run = getSessionRunState(id)
14
+ const hasLiveRun = active.has(id) || !!run.runningRunId
15
+ if (!hasLiveRun && pruneStreamingAssistantArtifacts(session.messages)) {
16
+ upsertStoredItem('sessions', id, session)
17
+ }
9
18
 
10
19
  const url = new URL(req.url)
11
20
  const limitParam = url.searchParams.get('limit')
12
21
  const beforeParam = url.searchParams.get('before')
13
22
 
14
- const allMessages = sessions[id].messages
23
+ const allMessages = Array.isArray(session.messages) ? session.messages : []
15
24
  const total = allMessages.length
16
25
 
17
26
  // If no limit param, return all messages (backward compatible)
@@ -41,8 +50,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
41
50
  if (body.kind !== 'context-clear') {
42
51
  return NextResponse.json({ error: 'Only context-clear kind is supported' }, { status: 400 })
43
52
  }
44
- const sessions = loadSessions()
45
- const session = sessions[id]
53
+ const session = loadStoredItem('sessions', id)
46
54
  if (!session) return notFound()
47
55
 
48
56
  session.messages.push({
@@ -51,15 +59,14 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
51
59
  kind: 'context-clear',
52
60
  time: Date.now(),
53
61
  })
54
- saveSessions(sessions)
62
+ upsertStoredItem('sessions', id, session)
55
63
  return NextResponse.json({ ok: true })
56
64
  }
57
65
 
58
66
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
59
67
  const { id } = await params
60
68
  const body = await req.json() as { messageIndex: number; bookmarked: boolean }
61
- const sessions = loadSessions()
62
- const session = sessions[id]
69
+ const session = loadStoredItem('sessions', id)
63
70
  if (!session) return notFound()
64
71
 
65
72
  const { messageIndex, bookmarked } = body
@@ -68,15 +75,14 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
68
75
  }
69
76
 
70
77
  session.messages[messageIndex].bookmarked = bookmarked
71
- saveSessions(sessions)
78
+ upsertStoredItem('sessions', id, session)
72
79
  return NextResponse.json(session.messages[messageIndex])
73
80
  }
74
81
 
75
82
  export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
76
83
  const { id } = await params
77
84
  const body = await req.json() as { messageIndex: number }
78
- const sessions = loadSessions()
79
- const session = sessions[id]
85
+ const session = loadStoredItem('sessions', id)
80
86
  if (!session) return notFound()
81
87
 
82
88
  const { messageIndex } = body
@@ -90,6 +96,6 @@ export async function DELETE(req: Request, { params }: { params: Promise<{ id: s
90
96
  }
91
97
 
92
98
  session.messages.splice(messageIndex, 1)
93
- saveSessions(sessions)
99
+ upsertStoredItem('sessions', id, session)
94
100
  return NextResponse.json({ ok: true })
95
101
  }
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
5
+ import { resolvePrimaryAgentRoute } from '@/lib/server/agent-runtime-config'
5
6
 
6
7
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
8
  const { id } = await params
@@ -17,6 +18,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
17
18
  }
18
19
 
19
20
  const linkedAgent = nextAgentId ? loadAgents()[nextAgentId] : null
21
+ const linkedRoute = linkedAgent ? resolvePrimaryAgentRoute(linkedAgent) : null
20
22
 
21
23
  if (updates.name !== undefined) sessions[id].name = updates.name
22
24
  if (updates.cwd !== undefined) sessions[id].cwd = updates.cwd
@@ -24,11 +26,19 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
24
26
  else if (agentIdUpdateProvided && linkedAgent?.provider) sessions[id].provider = linkedAgent.provider
25
27
 
26
28
  if (updates.model !== undefined) sessions[id].model = updates.model
29
+ else if (agentIdUpdateProvided && linkedRoute?.model) sessions[id].model = linkedRoute.model
27
30
  else if (agentIdUpdateProvided && linkedAgent?.model !== undefined) sessions[id].model = linkedAgent.model
28
31
 
29
32
  if (updates.credentialId !== undefined) sessions[id].credentialId = updates.credentialId
33
+ else if (agentIdUpdateProvided && linkedRoute) sessions[id].credentialId = linkedRoute.credentialId ?? null
30
34
  else if (agentIdUpdateProvided && linkedAgent) sessions[id].credentialId = linkedAgent.credentialId ?? null
31
35
 
36
+ if (updates.fallbackCredentialIds !== undefined) sessions[id].fallbackCredentialIds = updates.fallbackCredentialIds
37
+ else if (agentIdUpdateProvided && linkedRoute) sessions[id].fallbackCredentialIds = [...linkedRoute.fallbackCredentialIds]
38
+
39
+ if (updates.gatewayProfileId !== undefined) sessions[id].gatewayProfileId = updates.gatewayProfileId
40
+ else if (agentIdUpdateProvided && linkedRoute) sessions[id].gatewayProfileId = linkedRoute.gatewayProfileId ?? null
41
+
32
42
  if (updates.plugins !== undefined) sessions[id].plugins = updates.plugins
33
43
  else if (agentIdUpdateProvided && linkedAgent) sessions[id].plugins = Array.isArray(linkedAgent.plugins) ? linkedAgent.plugins : []
34
44
 
@@ -37,6 +47,8 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
37
47
  updates.provider || sessions[id].provider,
38
48
  updates.apiEndpoint,
39
49
  )
50
+ } else if (agentIdUpdateProvided && linkedRoute) {
51
+ sessions[id].apiEndpoint = linkedRoute.apiEndpoint ?? null
40
52
  } else if (agentIdUpdateProvided && linkedAgent) {
41
53
  sessions[id].apiEndpoint = normalizeProviderEndpoint(
42
54
  linkedAgent.provider,
@@ -45,6 +57,24 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
45
57
  }
46
58
  if (updates.heartbeatEnabled !== undefined) sessions[id].heartbeatEnabled = updates.heartbeatEnabled
47
59
  if (updates.heartbeatIntervalSec !== undefined) sessions[id].heartbeatIntervalSec = updates.heartbeatIntervalSec
60
+ if (updates.sessionResetMode !== undefined) sessions[id].sessionResetMode = updates.sessionResetMode
61
+ if (updates.sessionIdleTimeoutSec !== undefined) sessions[id].sessionIdleTimeoutSec = updates.sessionIdleTimeoutSec
62
+ if (updates.sessionMaxAgeSec !== undefined) sessions[id].sessionMaxAgeSec = updates.sessionMaxAgeSec
63
+ if (updates.sessionDailyResetAt !== undefined) sessions[id].sessionDailyResetAt = updates.sessionDailyResetAt
64
+ if (updates.sessionResetTimezone !== undefined) sessions[id].sessionResetTimezone = updates.sessionResetTimezone
65
+ if (updates.thinkingLevel !== undefined) sessions[id].thinkingLevel = updates.thinkingLevel
66
+ if (updates.connectorThinkLevel !== undefined) sessions[id].connectorThinkLevel = updates.connectorThinkLevel
67
+ if (updates.connectorSessionScope !== undefined) sessions[id].connectorSessionScope = updates.connectorSessionScope
68
+ if (updates.connectorReplyMode !== undefined) sessions[id].connectorReplyMode = updates.connectorReplyMode
69
+ if (updates.connectorThreadBinding !== undefined) sessions[id].connectorThreadBinding = updates.connectorThreadBinding
70
+ if (updates.connectorGroupPolicy !== undefined) sessions[id].connectorGroupPolicy = updates.connectorGroupPolicy
71
+ if (updates.connectorIdleTimeoutSec !== undefined) sessions[id].connectorIdleTimeoutSec = updates.connectorIdleTimeoutSec
72
+ if (updates.connectorMaxAgeSec !== undefined) sessions[id].connectorMaxAgeSec = updates.connectorMaxAgeSec
73
+ if (updates.connectorContext !== undefined) sessions[id].connectorContext = updates.connectorContext
74
+ if (updates.identityState !== undefined) sessions[id].identityState = updates.identityState
75
+ if (updates.sessionArchiveState !== undefined) sessions[id].sessionArchiveState = updates.sessionArchiveState
76
+ if (updates.lastSessionResetAt !== undefined) sessions[id].lastSessionResetAt = updates.lastSessionResetAt
77
+ if (updates.lastSessionResetReason !== undefined) sessions[id].lastSessionResetReason = updates.lastSessionResetReason
48
78
  if (updates.pinned !== undefined) sessions[id].pinned = !!updates.pinned
49
79
  if (updates.claudeSessionId !== undefined) sessions[id].claudeSessionId = updates.claudeSessionId
50
80
  if (updates.codexThreadId !== undefined) sessions[id].codexThreadId = updates.codexThreadId
@@ -1,10 +1,15 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { active } from '@/lib/server/storage'
2
+ import { pruneStreamingAssistantArtifacts } from '@/lib/chat-streaming-state'
3
+ import { active, loadStoredItem, upsertStoredItem } from '@/lib/server/storage'
3
4
  import { cancelSessionRuns } from '@/lib/server/session-run-manager'
4
5
 
5
6
  export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
7
  const { id } = await params
7
8
  const cancel = cancelSessionRuns(id, 'Stopped by user')
9
+ const session = loadStoredItem('sessions', id)
10
+ if (session && Array.isArray(session.messages) && pruneStreamingAssistantArtifacts(session.messages)) {
11
+ upsertStoredItem('sessions', id, session)
12
+ }
8
13
  if (active.has(id)) {
9
14
  try { active.get(id).kill() } catch {}
10
15
  active.delete(id)
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
2
3
  import { disableAllSessionHeartbeats, loadSettings, saveSettings } from '@/lib/server/storage'
3
4
  import { cancelAllHeartbeatRuns } from '@/lib/server/session-run-manager'
4
5
 
@@ -11,7 +12,7 @@ export async function POST(req: Request) {
11
12
 
12
13
  const updatedSessions = disableAllSessionHeartbeats()
13
14
  const settings = loadSettings()
14
- if ((settings.heartbeatIntervalSec ?? 120) !== 0) {
15
+ if ((settings.heartbeatIntervalSec ?? DEFAULT_HEARTBEAT_INTERVAL_SEC) !== 0) {
15
16
  settings.heartbeatIntervalSec = 0
16
17
  saveSettings(settings)
17
18
  }
@@ -7,6 +7,7 @@ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
7
7
  import { notify } from '@/lib/server/ws-hub'
8
8
  import { getSessionRunState } from '@/lib/server/session-run-manager'
9
9
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
10
+ import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agent-runtime-config'
10
11
  export const dynamic = 'force-dynamic'
11
12
 
12
13
 
@@ -60,6 +61,7 @@ export async function POST(req: Request) {
60
61
  const id = body.id || genId()
61
62
  const sessions = loadSessions()
62
63
  const agent = body.agentId ? loadAgents()[body.agentId] : null
64
+ const resolvedRoute = agent ? resolvePrimaryAgentRoute(agent) : null
63
65
  const requestedPlugins = Array.isArray(body.plugins) ? body.plugins : (Array.isArray(body.tools) ? body.tools : null)
64
66
  const resolvedPlugins = requestedPlugins ?? (Array.isArray(agent?.plugins) ? agent.plugins : (Array.isArray(agent?.tools) ? agent.tools : []))
65
67
 
@@ -70,12 +72,13 @@ export async function POST(req: Request) {
70
72
 
71
73
  const sessionName = body.name || 'New Chat'
72
74
 
73
- sessions[id] = {
75
+ const nextSession = {
74
76
  id, name: sessionName, cwd,
75
77
  user: body.user || 'user',
76
78
  provider: body.provider || agent?.provider || 'claude-cli',
77
79
  model: body.model || agent?.model || '',
78
80
  credentialId: body.credentialId || agent?.credentialId || null,
81
+ fallbackCredentialIds: body.fallbackCredentialIds || agent?.fallbackCredentialIds || [],
79
82
  apiEndpoint: normalizeProviderEndpoint(
80
83
  body.provider || agent?.provider || 'claude-cli',
81
84
  body.apiEndpoint || agent?.apiEndpoint || null,
@@ -96,7 +99,26 @@ export async function POST(req: Request) {
96
99
  plugins: resolvedPlugins,
97
100
  heartbeatEnabled: body.heartbeatEnabled ?? null,
98
101
  heartbeatIntervalSec: body.heartbeatIntervalSec ?? null,
102
+ sessionResetMode: body.sessionResetMode ?? agent?.sessionResetMode ?? null,
103
+ sessionIdleTimeoutSec: body.sessionIdleTimeoutSec ?? agent?.sessionIdleTimeoutSec ?? null,
104
+ sessionMaxAgeSec: body.sessionMaxAgeSec ?? agent?.sessionMaxAgeSec ?? null,
105
+ sessionDailyResetAt: body.sessionDailyResetAt ?? agent?.sessionDailyResetAt ?? null,
106
+ sessionResetTimezone: body.sessionResetTimezone ?? agent?.sessionResetTimezone ?? null,
107
+ thinkingLevel: body.thinkingLevel ?? null,
108
+ connectorThinkLevel: body.connectorThinkLevel ?? null,
109
+ connectorSessionScope: body.connectorSessionScope ?? null,
110
+ connectorReplyMode: body.connectorReplyMode ?? null,
111
+ connectorThreadBinding: body.connectorThreadBinding ?? null,
112
+ connectorGroupPolicy: body.connectorGroupPolicy ?? null,
113
+ connectorIdleTimeoutSec: body.connectorIdleTimeoutSec ?? null,
114
+ connectorMaxAgeSec: body.connectorMaxAgeSec ?? null,
115
+ connectorContext: body.connectorContext ?? null,
116
+ identityState: body.identityState ?? agent?.identityState ?? null,
117
+ sessionArchiveState: body.sessionArchiveState ?? null,
99
118
  }
119
+ sessions[id] = (body.provider || body.model || body.credentialId || body.apiEndpoint)
120
+ ? nextSession
121
+ : applyResolvedRoute(nextSession, resolvedRoute)
100
122
  saveSessions(sessions)
101
123
  notify('sessions')
102
124
  return NextResponse.json(sessions[id])
@@ -0,0 +1,26 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadConnectors } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+ import { buildConnectorDoctorPreview, buildConnectorDoctorReport, type ConnectorDoctorPreviewInput } from '@/lib/server/connectors/doctor'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
9
+ const { id } = await params
10
+ const connectors = loadConnectors()
11
+ const connector = connectors[id]
12
+ if (!connector) return notFound()
13
+
14
+ return NextResponse.json(buildConnectorDoctorReport(connector, null, { baseConnector: connector }))
15
+ }
16
+
17
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
18
+ const { id } = await params
19
+ const connectors = loadConnectors()
20
+ const baseConnector = connectors[id]
21
+ if (!baseConnector) return notFound()
22
+
23
+ const body = await req.json().catch(() => ({})) as ConnectorDoctorPreviewInput
24
+ const connector = buildConnectorDoctorPreview({ baseConnector, input: body, fallbackId: id })
25
+ return NextResponse.json(buildConnectorDoctorReport(connector, body.sampleMsg, { baseConnector }))
26
+ }
@@ -0,0 +1,13 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { buildConnectorDoctorPreview, buildConnectorDoctorReport, type ConnectorDoctorPreviewInput } from '@/lib/server/connectors/doctor'
3
+ import { loadConnectors } from '@/lib/server/storage'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function POST(req: Request) {
8
+ const body = await req.json().catch(() => ({})) as ConnectorDoctorPreviewInput
9
+ const connectors = loadConnectors()
10
+ const baseConnector = typeof body.id === 'string' ? connectors[body.id] : null
11
+ const connector = buildConnectorDoctorPreview({ baseConnector, input: body })
12
+ return NextResponse.json(buildConnectorDoctorReport(connector, body.sampleMsg, { baseConnector }))
13
+ }
@@ -0,0 +1,33 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadExternalAgents, saveExternalAgents } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params
9
+ const body = await req.json().catch(() => ({}))
10
+ const items = loadExternalAgents()
11
+ const runtime = items[id]
12
+ if (!runtime) return notFound()
13
+ const now = Date.now()
14
+ runtime.lastHeartbeatAt = now
15
+ runtime.lastSeenAt = now
16
+ runtime.updatedAt = now
17
+ runtime.status = body.status || 'online'
18
+ if (body.tokenStats && typeof body.tokenStats === 'object') {
19
+ runtime.tokenStats = {
20
+ ...(runtime.tokenStats || {}),
21
+ ...body.tokenStats,
22
+ }
23
+ }
24
+ if (body.metadata && typeof body.metadata === 'object') {
25
+ runtime.metadata = {
26
+ ...(runtime.metadata || {}),
27
+ ...body.metadata,
28
+ }
29
+ }
30
+ saveExternalAgents(items)
31
+ notify('external_agents')
32
+ return NextResponse.json({ ok: true, id, lastHeartbeatAt: now })
33
+ }
@@ -0,0 +1,31 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadExternalAgents, saveExternalAgents } from '@/lib/server/storage'
3
+ import { mutateItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ const ops: CollectionOps<any> = { load: loadExternalAgents, save: saveExternalAgents, topic: 'external_agents' }
9
+
10
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
11
+ const { id } = await params
12
+ const body = await req.json().catch(() => ({}))
13
+ const result = mutateItem(ops, id, (runtime) => ({
14
+ ...runtime,
15
+ ...body,
16
+ id,
17
+ updatedAt: Date.now(),
18
+ }))
19
+ if (!result) return notFound()
20
+ return NextResponse.json(result)
21
+ }
22
+
23
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
24
+ const { id } = await params
25
+ const items = loadExternalAgents()
26
+ if (!items[id]) return notFound()
27
+ delete items[id]
28
+ saveExternalAgents(items)
29
+ notify('external_agents')
30
+ return NextResponse.json({ ok: true })
31
+ }
@@ -0,0 +1,3 @@
1
+ import { POST } from '../route'
2
+ export const dynamic = 'force-dynamic'
3
+ export { POST }