@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -15,28 +15,57 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
15
15
  const user = body.user || 'default'
16
16
  const sessions = loadSessions()
17
17
 
18
- // If agent already has a thread session that exists, return it
18
+ // If the agent already has a shortcut chat session, return it.
19
19
  if (agent.threadSessionId && sessions[agent.threadSessionId]) {
20
- return NextResponse.json(sessions[agent.threadSessionId])
20
+ const existing = sessions[agent.threadSessionId] as Record<string, unknown>
21
+ let changed = false
22
+ if (existing.shortcutForAgentId !== agentId) {
23
+ existing.shortcutForAgentId = agentId
24
+ changed = true
25
+ }
26
+ if (existing.name !== agent.name) {
27
+ existing.name = agent.name
28
+ changed = true
29
+ }
30
+ if (changed) saveSessions(sessions)
31
+ return NextResponse.json(existing)
21
32
  }
22
33
 
23
- // Check if an existing session is already linked to this agent as a thread
34
+ // Legacy fallback for older shortcut sessions that were named using the
35
+ // old agent-thread convention before the explicit link was persisted.
24
36
  const existing = Object.values(sessions).find(
25
- (s: Record<string, unknown>) => s.name === `agent-thread:${agentId}` && s.user === user
37
+ (s: Record<string, unknown>) =>
38
+ (
39
+ s.shortcutForAgentId === agentId
40
+ || s.name === `agent-thread:${agentId}`
41
+ )
42
+ && s.user === user
26
43
  )
27
44
  if (existing) {
28
45
  agent.threadSessionId = (existing as Record<string, unknown>).id as string
29
46
  agent.updatedAt = Date.now()
30
47
  saveAgents(agents)
48
+ let changed = false
49
+ const existingRecord = existing as Record<string, unknown>
50
+ if (existingRecord.shortcutForAgentId !== agentId) {
51
+ existingRecord.shortcutForAgentId = agentId
52
+ changed = true
53
+ }
54
+ if (existingRecord.name !== agent.name) {
55
+ existingRecord.name = agent.name
56
+ changed = true
57
+ }
58
+ if (changed) saveSessions(sessions)
31
59
  return NextResponse.json(existing)
32
60
  }
33
61
 
34
- // Create a new thread session
35
- const sessionId = `agent-thread-${agentId}-${genId()}`
62
+ // Create a new shortcut chat session for this agent.
63
+ const sessionId = `agent-chat-${agentId}-${genId()}`
36
64
  const now = Date.now()
37
65
  const session = {
38
66
  id: sessionId,
39
- name: `agent-thread:${agentId}`,
67
+ name: agent.name,
68
+ shortcutForAgentId: agentId,
40
69
  cwd: WORKSPACE_DIR,
41
70
  user: user,
42
71
  provider: agent.provider,
@@ -49,10 +78,9 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
49
78
  createdAt: now,
50
79
  lastActiveAt: now,
51
80
  active: false,
52
- mainSession: true,
53
81
  sessionType: 'human' as const,
54
82
  agentId,
55
- tools: agent.tools || [],
83
+ plugins: agent.plugins || agent.tools || [],
56
84
  heartbeatEnabled: agent.heartbeatEnabled || false,
57
85
  heartbeatIntervalSec: agent.heartbeatIntervalSec || null,
58
86
  }
@@ -13,6 +13,9 @@ export async function GET(req: Request) {
13
13
  const agents = loadAgents()
14
14
  const sessions = loadSessions()
15
15
  const usage = loadUsage()
16
+ for (const agent of Object.values(agents)) {
17
+ agent.isOrchestrator = agent.platformAssignScope === 'all'
18
+ }
16
19
  // Enrich agents that have spend limits with current spend windows
17
20
  for (const agent of Object.values(agents)) {
18
21
  if (
@@ -48,6 +51,7 @@ export async function POST(req: Request) {
48
51
  const id = genId()
49
52
  const now = Date.now()
50
53
  const agents = loadAgents()
54
+ const platformAssignScope = body.platformAssignScope
51
55
  agents[id] = {
52
56
  id,
53
57
  name: body.name,
@@ -57,9 +61,10 @@ export async function POST(req: Request) {
57
61
  model: body.model,
58
62
  credentialId: body.credentialId,
59
63
  apiEndpoint: normalizeProviderEndpoint(body.provider, body.apiEndpoint || null),
60
- isOrchestrator: body.isOrchestrator,
64
+ isOrchestrator: platformAssignScope === 'all',
65
+ platformAssignScope,
61
66
  subAgentIds: body.subAgentIds,
62
- tools: body.tools,
67
+ plugins: body.plugins?.length ? body.plugins : (body.tools || []),
63
68
  capabilities: body.capabilities,
64
69
  thinkingLevel: body.thinkingLevel || undefined,
65
70
  autoRecovery: body.autoRecovery || false,
@@ -68,6 +73,12 @@ export async function POST(req: Request) {
68
73
  hourlyBudget: body.hourlyBudget ?? null,
69
74
  budgetAction: body.budgetAction || 'warn',
70
75
  soul: body.soul || undefined,
76
+ identityState: body.identityState ?? null,
77
+ sessionResetMode: body.sessionResetMode ?? null,
78
+ sessionIdleTimeoutSec: body.sessionIdleTimeoutSec ?? null,
79
+ sessionMaxAgeSec: body.sessionMaxAgeSec ?? null,
80
+ sessionDailyResetAt: body.sessionDailyResetAt ?? null,
81
+ sessionResetTimezone: body.sessionResetTimezone ?? null,
71
82
  createdAt: now,
72
83
  updatedAt: now,
73
84
  }
@@ -1,27 +1,96 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { validateAccessKey, getAccessKey, 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,6 +8,8 @@ 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
15
  buildSyntheticSession,
@@ -50,7 +52,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
50
52
 
51
53
  // Persist incoming message
52
54
  const senderName = senderId === 'user' ? 'You' : (agents[senderId]?.name || senderId)
53
- let mentions = parseMentions(text, agents, chatroom.agentIds)
55
+ const replyTargetAgentId = resolveReplyTargetAgentId(replyToId, chatroom.messages, chatroom.agentIds)
56
+ let mentions = parseMentions(text, agents, chatroom.agentIds, { replyTargetAgentId })
54
57
  // Routing rules: if no explicit mentions, evaluate keyword/capability rules
55
58
  if (mentions.length === 0 && chatroom.routingRules?.length) {
56
59
  const agentList = chatroom.agentIds.map((aid) => agents[aid]).filter(Boolean)
@@ -149,13 +152,14 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
149
152
  // Pre-flight: check if the agent's provider is usable before attempting to stream
150
153
  const providerInfo = getProvider(agent.provider)
151
154
  const apiKey = resolveApiKey(agent.credentialId)
155
+ const resolvedEndpoint = resolveAgentApiEndpoint(agent)
152
156
  if (providerInfo?.requiresApiKey && !apiKey) {
153
157
  writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
154
158
  writeEvent({ t: 'err', text: `${agent.name} has no API credentials configured`, agentId: agent.id, agentName: agent.name })
155
159
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
156
160
  return []
157
161
  }
158
- if (providerInfo?.requiresEndpoint && !agent.apiEndpoint) {
162
+ if (providerInfo?.requiresEndpoint && !resolvedEndpoint) {
159
163
  writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
160
164
  writeEvent({ t: 'err', text: `${agent.name} has no endpoint configured`, agentId: agent.id, agentName: agent.name })
161
165
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
@@ -174,6 +178,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
174
178
  }
175
179
 
176
180
  const syntheticSession = buildSyntheticSession(agent, id)
181
+ syntheticSession.apiEndpoint = resolvedEndpoint
177
182
  const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
178
183
  const chatroomContext = buildChatroomSystemPrompt(freshChatroom, agents, agent.id)
179
184
  const fullSystemPrompt = [agentSystemPrompt, chatroomContext].filter(Boolean).join('\n\n')
@@ -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
 
@@ -3,7 +3,7 @@ import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
3
3
 
4
4
  export const dynamic = 'force-dynamic'
5
5
 
6
- /** GET /api/sessions/[id]/checkpoints — returns checkpoint history for a thread */
6
+ /** GET /api/chats/[id]/checkpoints — returns checkpoint history for a thread */
7
7
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
8
8
  const { id: threadId } = await params
9
9
  if (!threadId) return NextResponse.json({ error: 'Thread ID is required' }, { status: 400 })
@@ -0,0 +1,13 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ function retiredResponse() {
4
+ return new NextResponse('Mission controls are no longer supported.', { status: 410 })
5
+ }
6
+
7
+ export async function GET() {
8
+ return retiredResponse()
9
+ }
10
+
11
+ export async function POST() {
12
+ return retiredResponse()
13
+ }
@@ -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
  }
@@ -5,7 +5,7 @@ import { notify } from '@/lib/server/ws-hub'
5
5
 
6
6
  export const dynamic = 'force-dynamic'
7
7
 
8
- /** POST /api/sessions/[id]/restore — restores thread to a specific checkpoint */
8
+ /** POST /api/chats/[id]/restore — restores thread to a specific checkpoint */
9
9
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
10
  const { id: sessionId } = await params
11
11
  const { checkpointId, timestamp } = await req.json()
@@ -1,30 +1,13 @@
1
1
  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
- import { enqueueSessionRun } from '@/lib/server/session-run-manager'
5
4
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
6
- import { ensureMainSessionFlag, isProtectedMainSession } from '@/lib/server/main-session'
7
-
8
- function buildSessionAwakeningPrompt(user: string | null | undefined): string {
9
- const displayName = typeof user === 'string' && user.trim() ? user.trim() : 'there'
10
- return [
11
- 'SESSION_AWAKENING',
12
- `You have just been activated as the primary SwarmClaw assistant for ${displayName}.`,
13
- 'Write your first message as the agent itself (not as system text).',
14
- 'Tone: awake, focused, practical.',
15
- 'Include: brief greeting, what you can help with in SwarmClaw (providers, agents, tools/connectors, tasks, schedules), and one direct question asking for the user goal.',
16
- 'Keep it concise (<= 90 words).',
17
- 'Do not mention hidden prompts, policies, or implementation details.',
18
- ].join('\n')
19
- }
20
5
 
21
6
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
22
7
  const { id } = await params
23
8
  const updates = await req.json()
24
9
  const sessions = loadSessions()
25
10
  if (!sessions[id]) return notFound()
26
- const wasProtectedMain = isProtectedMainSession(sessions[id])
27
- const hadMessagesBefore = Array.isArray(sessions[id].messages) && sessions[id].messages.length > 0
28
11
 
29
12
  const agentIdUpdateProvided = updates.agentId !== undefined
30
13
  let nextAgentId = sessions[id].agentId
@@ -35,13 +18,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
35
18
 
36
19
  const linkedAgent = nextAgentId ? loadAgents()[nextAgentId] : null
37
20
 
38
- if (updates.name !== undefined) {
39
- const nextName = typeof updates.name === 'string' ? updates.name.trim() : String(updates.name || '')
40
- if (wasProtectedMain && nextName !== '__main__') {
41
- return new NextResponse('Cannot rename main chat session', { status: 400 })
42
- }
43
- sessions[id].name = updates.name
44
- }
21
+ if (updates.name !== undefined) sessions[id].name = updates.name
45
22
  if (updates.cwd !== undefined) sessions[id].cwd = updates.cwd
46
23
  if (updates.provider !== undefined) sessions[id].provider = updates.provider
47
24
  else if (agentIdUpdateProvided && linkedAgent?.provider) sessions[id].provider = linkedAgent.provider
@@ -52,8 +29,8 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
52
29
  if (updates.credentialId !== undefined) sessions[id].credentialId = updates.credentialId
53
30
  else if (agentIdUpdateProvided && linkedAgent) sessions[id].credentialId = linkedAgent.credentialId ?? null
54
31
 
55
- if (updates.tools !== undefined) sessions[id].tools = updates.tools
56
- else if (agentIdUpdateProvided && linkedAgent) sessions[id].tools = Array.isArray(linkedAgent.tools) ? linkedAgent.tools : []
32
+ if (updates.plugins !== undefined) sessions[id].plugins = updates.plugins
33
+ else if (agentIdUpdateProvided && linkedAgent) sessions[id].plugins = Array.isArray(linkedAgent.plugins) ? linkedAgent.plugins : []
57
34
 
58
35
  if (updates.apiEndpoint !== undefined) {
59
36
  sessions[id].apiEndpoint = normalizeProviderEndpoint(
@@ -68,46 +45,39 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
68
45
  }
69
46
  if (updates.heartbeatEnabled !== undefined) sessions[id].heartbeatEnabled = updates.heartbeatEnabled
70
47
  if (updates.heartbeatIntervalSec !== undefined) sessions[id].heartbeatIntervalSec = updates.heartbeatIntervalSec
48
+ if (updates.sessionResetMode !== undefined) sessions[id].sessionResetMode = updates.sessionResetMode
49
+ if (updates.sessionIdleTimeoutSec !== undefined) sessions[id].sessionIdleTimeoutSec = updates.sessionIdleTimeoutSec
50
+ if (updates.sessionMaxAgeSec !== undefined) sessions[id].sessionMaxAgeSec = updates.sessionMaxAgeSec
51
+ if (updates.sessionDailyResetAt !== undefined) sessions[id].sessionDailyResetAt = updates.sessionDailyResetAt
52
+ if (updates.sessionResetTimezone !== undefined) sessions[id].sessionResetTimezone = updates.sessionResetTimezone
53
+ if (updates.thinkingLevel !== undefined) sessions[id].thinkingLevel = updates.thinkingLevel
54
+ if (updates.connectorThinkLevel !== undefined) sessions[id].connectorThinkLevel = updates.connectorThinkLevel
55
+ if (updates.connectorSessionScope !== undefined) sessions[id].connectorSessionScope = updates.connectorSessionScope
56
+ if (updates.connectorReplyMode !== undefined) sessions[id].connectorReplyMode = updates.connectorReplyMode
57
+ if (updates.connectorThreadBinding !== undefined) sessions[id].connectorThreadBinding = updates.connectorThreadBinding
58
+ if (updates.connectorGroupPolicy !== undefined) sessions[id].connectorGroupPolicy = updates.connectorGroupPolicy
59
+ if (updates.connectorIdleTimeoutSec !== undefined) sessions[id].connectorIdleTimeoutSec = updates.connectorIdleTimeoutSec
60
+ if (updates.connectorMaxAgeSec !== undefined) sessions[id].connectorMaxAgeSec = updates.connectorMaxAgeSec
61
+ if (updates.connectorContext !== undefined) sessions[id].connectorContext = updates.connectorContext
62
+ if (updates.identityState !== undefined) sessions[id].identityState = updates.identityState
63
+ if (updates.sessionArchiveState !== undefined) sessions[id].sessionArchiveState = updates.sessionArchiveState
64
+ if (updates.lastSessionResetAt !== undefined) sessions[id].lastSessionResetAt = updates.lastSessionResetAt
65
+ if (updates.lastSessionResetReason !== undefined) sessions[id].lastSessionResetReason = updates.lastSessionResetReason
71
66
  if (updates.pinned !== undefined) sessions[id].pinned = !!updates.pinned
72
67
  if (updates.claudeSessionId !== undefined) sessions[id].claudeSessionId = updates.claudeSessionId
73
68
  if (updates.codexThreadId !== undefined) sessions[id].codexThreadId = updates.codexThreadId
74
69
  if (updates.opencodeSessionId !== undefined) sessions[id].opencodeSessionId = updates.opencodeSessionId
75
70
  if (updates.delegateResumeIds !== undefined) sessions[id].delegateResumeIds = updates.delegateResumeIds
76
71
  if (!Array.isArray(sessions[id].messages)) sessions[id].messages = []
77
- ensureMainSessionFlag(sessions[id])
78
-
79
- const shouldKickoffAwakening = isProtectedMainSession(sessions[id])
80
- && agentIdUpdateProvided
81
- && !!sessions[id].agentId
82
- && !hadMessagesBefore
83
- && sessions[id].messages.length === 0
84
72
 
85
73
  saveSessions(sessions)
86
-
87
- if (shouldKickoffAwakening) {
88
- try {
89
- enqueueSessionRun({
90
- sessionId: id,
91
- message: buildSessionAwakeningPrompt(sessions[id].user),
92
- internal: true,
93
- source: 'session-awakening',
94
- mode: 'steer',
95
- dedupeKey: `session-awakening:${id}`,
96
- })
97
- } catch {
98
- // Best-effort kickoff only.
99
- }
100
- }
101
-
102
74
  return NextResponse.json(sessions[id])
103
75
  }
104
76
 
105
77
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
106
78
  const { id } = await params
107
79
  const sessions = loadSessions()
108
- if (isProtectedMainSession(sessions[id])) {
109
- return new NextResponse('Cannot delete main chat session', { status: 403 })
110
- }
80
+ if (!sessions[id]) return notFound()
111
81
  if (active.has(id)) {
112
82
  try { active.get(id).kill() } catch {}
113
83
  active.delete(id)
@@ -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)
@@ -7,7 +7,6 @@ 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 { ensureMainSessionFlag, isProtectedMainSession } from '@/lib/server/main-session'
11
10
  export const dynamic = 'force-dynamic'
12
11
 
13
12
 
@@ -39,7 +38,7 @@ export async function DELETE(req: Request) {
39
38
  const sessions = loadSessions()
40
39
  let deleted = 0
41
40
  for (const id of ids) {
42
- if (isProtectedMainSession(sessions[id])) continue
41
+ if (!sessions[id]) continue
43
42
  if (active.has(id)) {
44
43
  try { active.get(id).kill() } catch {}
45
44
  active.delete(id)
@@ -61,15 +60,15 @@ export async function POST(req: Request) {
61
60
  const id = body.id || genId()
62
61
  const sessions = loadSessions()
63
62
  const agent = body.agentId ? loadAgents()[body.agentId] : null
64
- const requestedTools = Array.isArray(body.tools) ? body.tools : null
65
- const resolvedTools = requestedTools ?? (Array.isArray(agent?.tools) ? agent.tools : [])
63
+ const requestedPlugins = Array.isArray(body.plugins) ? body.plugins : (Array.isArray(body.tools) ? body.tools : null)
64
+ const resolvedPlugins = requestedPlugins ?? (Array.isArray(agent?.plugins) ? agent.plugins : (Array.isArray(agent?.tools) ? agent.tools : []))
66
65
 
67
66
  // If session with this ID already exists, return it as-is
68
67
  if (body.id && sessions[id]) {
69
68
  return NextResponse.json(sessions[id])
70
69
  }
71
70
 
72
- const sessionName = body.name || 'New Session'
71
+ const sessionName = body.name || 'New Chat'
73
72
 
74
73
  sessions[id] = {
75
74
  id, name: sessionName, cwd,
@@ -94,11 +93,26 @@ export async function POST(req: Request) {
94
93
  sessionType: body.sessionType || 'human',
95
94
  agentId: body.agentId || null,
96
95
  parentSessionId: body.parentSessionId || null,
97
- tools: resolvedTools,
96
+ plugins: resolvedPlugins,
98
97
  heartbeatEnabled: body.heartbeatEnabled ?? null,
99
98
  heartbeatIntervalSec: body.heartbeatIntervalSec ?? null,
99
+ sessionResetMode: body.sessionResetMode ?? agent?.sessionResetMode ?? null,
100
+ sessionIdleTimeoutSec: body.sessionIdleTimeoutSec ?? agent?.sessionIdleTimeoutSec ?? null,
101
+ sessionMaxAgeSec: body.sessionMaxAgeSec ?? agent?.sessionMaxAgeSec ?? null,
102
+ sessionDailyResetAt: body.sessionDailyResetAt ?? agent?.sessionDailyResetAt ?? null,
103
+ sessionResetTimezone: body.sessionResetTimezone ?? agent?.sessionResetTimezone ?? null,
104
+ thinkingLevel: body.thinkingLevel ?? null,
105
+ connectorThinkLevel: body.connectorThinkLevel ?? null,
106
+ connectorSessionScope: body.connectorSessionScope ?? null,
107
+ connectorReplyMode: body.connectorReplyMode ?? null,
108
+ connectorThreadBinding: body.connectorThreadBinding ?? null,
109
+ connectorGroupPolicy: body.connectorGroupPolicy ?? null,
110
+ connectorIdleTimeoutSec: body.connectorIdleTimeoutSec ?? null,
111
+ connectorMaxAgeSec: body.connectorMaxAgeSec ?? null,
112
+ connectorContext: body.connectorContext ?? null,
113
+ identityState: body.identityState ?? agent?.identityState ?? null,
114
+ sessionArchiveState: body.sessionArchiveState ?? null,
100
115
  }
101
- ensureMainSessionFlag(sessions[id])
102
116
  saveSessions(sessions)
103
117
  notify('sessions')
104
118
  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
+ }