@swarmclawai/swarmclaw 0.7.3 → 0.7.5

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 (152) hide show
  1. package/README.md +47 -40
  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 +17 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +4 -87
  12. package/src/app/api/agents/route.ts +23 -1
  13. package/src/app/api/auth/route.ts +1 -1
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
  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]/route.ts +12 -0
  19. package/src/app/api/chats/heartbeat/route.ts +2 -1
  20. package/src/app/api/chats/route.ts +7 -1
  21. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  22. package/src/app/api/external-agents/[id]/route.ts +31 -0
  23. package/src/app/api/external-agents/register/route.ts +3 -0
  24. package/src/app/api/external-agents/route.ts +66 -0
  25. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  26. package/src/app/api/gateways/[id]/route.ts +79 -0
  27. package/src/app/api/gateways/route.ts +57 -0
  28. package/src/app/api/openclaw/gateway/route.ts +10 -7
  29. package/src/app/api/openclaw/skills/route.ts +1 -1
  30. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  31. package/src/app/api/schedules/[id]/route.ts +38 -9
  32. package/src/app/api/schedules/route.ts +51 -28
  33. package/src/app/api/settings/route.ts +6 -10
  34. package/src/app/api/setup/doctor/route.ts +6 -4
  35. package/src/app/api/tasks/[id]/route.ts +2 -1
  36. package/src/app/api/tasks/bulk/route.ts +2 -2
  37. package/src/app/page.tsx +126 -15
  38. package/src/cli/binary.test.js +142 -0
  39. package/src/cli/index.js +34 -11
  40. package/src/cli/index.test.js +195 -0
  41. package/src/cli/index.ts +20 -4
  42. package/src/cli/server-cmd.test.js +59 -0
  43. package/src/cli/spec.js +20 -2
  44. package/src/components/agents/agent-sheet.tsx +249 -7
  45. package/src/components/agents/inspector-panel.tsx +3 -2
  46. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  47. package/src/components/auth/setup-wizard.tsx +970 -275
  48. package/src/components/chat/chat-area.tsx +41 -14
  49. package/src/components/chat/chat-card.tsx +2 -1
  50. package/src/components/chat/chat-header.tsx +8 -13
  51. package/src/components/chat/chat-list.tsx +58 -20
  52. package/src/components/chat/message-list.tsx +142 -18
  53. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  54. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  55. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  56. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +157 -86
  59. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  60. package/src/components/gateways/gateway-sheet.tsx +567 -0
  61. package/src/components/input/chat-input.tsx +135 -86
  62. package/src/components/layout/app-layout.tsx +2 -0
  63. package/src/components/memory/memory-browser.tsx +71 -6
  64. package/src/components/memory/memory-card.tsx +18 -0
  65. package/src/components/memory/memory-detail.tsx +58 -31
  66. package/src/components/memory/memory-sheet.tsx +32 -4
  67. package/src/components/projects/project-detail.tsx +7 -2
  68. package/src/components/providers/provider-list.tsx +158 -2
  69. package/src/components/providers/provider-sheet.tsx +81 -70
  70. package/src/components/shared/bottom-sheet.tsx +31 -15
  71. package/src/components/shared/confirm-dialog.tsx +45 -30
  72. package/src/components/shared/model-combobox.tsx +90 -8
  73. package/src/components/shared/settings/section-heartbeat.tsx +11 -6
  74. package/src/components/shared/settings/section-orchestrator.tsx +3 -0
  75. package/src/components/shared/settings/settings-page.tsx +5 -3
  76. package/src/components/tasks/approvals-panel.tsx +7 -1
  77. package/src/components/ui/dialog.tsx +2 -2
  78. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  79. package/src/lib/heartbeat-defaults.ts +48 -0
  80. package/src/lib/memory-presentation.ts +59 -0
  81. package/src/lib/provider-model-discovery-client.ts +29 -0
  82. package/src/lib/providers/index.ts +12 -5
  83. package/src/lib/runtime-loop.ts +105 -3
  84. package/src/lib/safe-storage.ts +6 -1
  85. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  86. package/src/lib/server/agent-runtime-config.ts +277 -0
  87. package/src/lib/server/agent-thread-session.test.ts +85 -0
  88. package/src/lib/server/agent-thread-session.ts +123 -0
  89. package/src/lib/server/approvals-auto-approve.test.ts +59 -0
  90. package/src/lib/server/build-llm.test.ts +13 -5
  91. package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
  92. package/src/lib/server/chat-execution.ts +159 -71
  93. package/src/lib/server/chatroom-helpers.test.ts +7 -0
  94. package/src/lib/server/chatroom-helpers.ts +99 -6
  95. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  96. package/src/lib/server/connectors/manager.ts +89 -61
  97. package/src/lib/server/connectors/slack.ts +1 -1
  98. package/src/lib/server/daemon-state.ts +3 -2
  99. package/src/lib/server/data-dir.test.ts +56 -0
  100. package/src/lib/server/data-dir.ts +15 -9
  101. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  102. package/src/lib/server/eval/agent-regression.ts +1742 -0
  103. package/src/lib/server/eval/runner.ts +11 -1
  104. package/src/lib/server/eval/store.ts +2 -1
  105. package/src/lib/server/heartbeat-service.ts +23 -8
  106. package/src/lib/server/heartbeat-wake.ts +6 -2
  107. package/src/lib/server/main-agent-loop.ts +13 -6
  108. package/src/lib/server/openclaw-exec-config.ts +4 -2
  109. package/src/lib/server/openclaw-gateway.ts +123 -36
  110. package/src/lib/server/orchestrator-lg.ts +1 -2
  111. package/src/lib/server/orchestrator.ts +3 -2
  112. package/src/lib/server/plugins.test.ts +9 -1
  113. package/src/lib/server/plugins.ts +12 -2
  114. package/src/lib/server/provider-model-discovery.ts +481 -0
  115. package/src/lib/server/queue.ts +1 -1
  116. package/src/lib/server/runtime-settings.test.ts +119 -0
  117. package/src/lib/server/runtime-settings.ts +12 -92
  118. package/src/lib/server/schedule-normalization.ts +187 -0
  119. package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
  120. package/src/lib/server/session-tools/crud.ts +27 -3
  121. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  122. package/src/lib/server/session-tools/discovery.ts +18 -8
  123. package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
  124. package/src/lib/server/session-tools/file.ts +8 -2
  125. package/src/lib/server/session-tools/http.ts +9 -3
  126. package/src/lib/server/session-tools/index.ts +31 -1
  127. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  128. package/src/lib/server/session-tools/monitor.ts +14 -7
  129. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  130. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  131. package/src/lib/server/session-tools/platform.ts +1 -1
  132. package/src/lib/server/session-tools/plugin-creator.ts +9 -2
  133. package/src/lib/server/session-tools/sandbox.ts +51 -92
  134. package/src/lib/server/session-tools/session-info.ts +22 -1
  135. package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
  136. package/src/lib/server/session-tools/shell.ts +2 -2
  137. package/src/lib/server/session-tools/subagent.ts +3 -1
  138. package/src/lib/server/session-tools/web.ts +73 -30
  139. package/src/lib/server/storage.ts +29 -3
  140. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  141. package/src/lib/server/stream-agent-chat.ts +139 -4
  142. package/src/lib/server/structured-extract.ts +1 -1
  143. package/src/lib/server/task-mention.ts +0 -1
  144. package/src/lib/server/tool-aliases.ts +37 -6
  145. package/src/lib/server/tool-capability-policy.ts +1 -1
  146. package/src/lib/setup-defaults.ts +352 -11
  147. package/src/lib/tool-definitions.ts +3 -4
  148. package/src/lib/validation/schemas.ts +55 -1
  149. package/src/stores/use-app-store.ts +43 -1
  150. package/src/stores/use-chatroom-store.ts +153 -26
  151. package/src/types/index.ts +189 -6
  152. package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
package/bin/update-cmd.js CHANGED
@@ -1,11 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
+ /* eslint-disable @typescript-eslint/no-require-imports */
3
4
 
4
- const { execSync } = require('node:child_process')
5
+ const { execSync, execFileSync } = require('node:child_process')
5
6
  const path = require('node:path')
7
+ const {
8
+ dependenciesChanged,
9
+ detectPackageManager,
10
+ getGlobalUpdateSpec,
11
+ getInstallCommand,
12
+ } = require('./package-manager.js')
6
13
 
7
14
  const PKG_ROOT = path.resolve(__dirname, '..')
15
+ const PACKAGE_NAME = '@swarmclawai/swarmclaw'
8
16
  const RELEASE_TAG_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/
17
+ const PACKAGE_MANAGER = detectPackageManager(PKG_ROOT)
9
18
 
10
19
  function run(cmd) {
11
20
  return execSync(cmd, { encoding: 'utf-8', cwd: PKG_ROOT, timeout: 60_000 }).trim()
@@ -27,15 +36,37 @@ function getLatestStableTag() {
27
36
  return tags.find((t) => RELEASE_TAG_RE.test(t)) || null
28
37
  }
29
38
 
39
+ function runRegistrySelfUpdate(
40
+ packageManager = PACKAGE_MANAGER,
41
+ execImpl = execFileSync,
42
+ logger = { log, logError },
43
+ ) {
44
+ const update = getGlobalUpdateSpec(packageManager, PACKAGE_NAME)
45
+ logger.log(`No git checkout detected. Updating the global ${PACKAGE_NAME} install via ${packageManager}...`)
46
+ try {
47
+ execImpl(update.command, update.args, {
48
+ cwd: PKG_ROOT,
49
+ stdio: 'inherit',
50
+ timeout: 120_000,
51
+ })
52
+ logger.log(`Global update complete via ${packageManager}.`)
53
+ logger.log('Restart the server to apply changes: swarmclaw server stop && swarmclaw server start')
54
+ return 0
55
+ } catch (err) {
56
+ logger.logError(`Registry update failed: ${err.message}`)
57
+ logger.logError(`Retry manually with: ${update.display}`)
58
+ return 1
59
+ }
60
+ }
61
+
30
62
  function main() {
31
63
  const args = process.argv.slice(3)
32
64
  if (args.includes('-h') || args.includes('--help')) {
33
65
  console.log(`
34
66
  Usage: swarmclaw update
35
67
 
36
- Pull the latest SwarmClaw release via git.
37
- Prefers stable release tags (v*); falls back to origin/main.
38
- Runs npm install if package files changed.
68
+ If running from a git checkout, pull the latest SwarmClaw release tag.
69
+ If running from a registry install, update the global package with ${PACKAGE_MANAGER}.
39
70
  `.trim())
40
71
  process.exit(0)
41
72
  }
@@ -44,8 +75,7 @@ Runs npm install if package files changed.
44
75
  try {
45
76
  run('git rev-parse --git-dir')
46
77
  } catch {
47
- logError('Not a git repository. Cannot update.')
48
- process.exit(1)
78
+ process.exit(runRegistrySelfUpdate(PACKAGE_MANAGER))
49
79
  }
50
80
 
51
81
  const beforeRef = run('git rev-parse HEAD')
@@ -105,9 +135,10 @@ Runs npm install if package files changed.
105
135
  // Install deps if package files changed
106
136
  try {
107
137
  const diff = run(`git diff --name-only ${beforeSha}..HEAD`)
108
- if (diff.includes('package-lock.json') || diff.includes('package.json')) {
109
- log('Package files changed running npm install...')
110
- execSync('npm install --omit=dev', { cwd: PKG_ROOT, stdio: 'inherit', timeout: 120_000 })
138
+ if (dependenciesChanged(diff)) {
139
+ const install = getInstallCommand(PACKAGE_MANAGER, true)
140
+ log(`Package files changed running ${PACKAGE_MANAGER} install...`)
141
+ execFileSync(install.command, install.args, { cwd: PKG_ROOT, stdio: 'inherit', timeout: 120_000 })
111
142
  }
112
143
  } catch {
113
144
  // If diff fails, skip install check
@@ -117,4 +148,11 @@ Runs npm install if package files changed.
117
148
  log('Restart the server to apply changes: swarmclaw server stop && swarmclaw server start')
118
149
  }
119
150
 
120
- main()
151
+ if (require.main === module) {
152
+ main()
153
+ }
154
+
155
+ module.exports = {
156
+ main,
157
+ runRegistrySelfUpdate,
158
+ }
@@ -0,0 +1,55 @@
1
+ 'use strict'
2
+ /* eslint-disable @typescript-eslint/no-require-imports */
3
+
4
+ const test = require('node:test')
5
+ const assert = require('node:assert/strict')
6
+
7
+ const { runRegistrySelfUpdate } = require('./update-cmd.js')
8
+
9
+ test('runRegistrySelfUpdate executes the manager-specific global update command', () => {
10
+ const messages = []
11
+ let captured = null
12
+
13
+ const exitCode = runRegistrySelfUpdate(
14
+ 'pnpm',
15
+ (command, args, options) => {
16
+ captured = { command, args, options }
17
+ },
18
+ {
19
+ log: (message) => messages.push(`log:${message}`),
20
+ logError: (message) => messages.push(`err:${message}`),
21
+ },
22
+ )
23
+
24
+ assert.equal(exitCode, 0)
25
+ assert.deepEqual(captured, {
26
+ command: 'pnpm',
27
+ args: ['add', '-g', '@swarmclawai/swarmclaw@latest'],
28
+ options: {
29
+ cwd: process.cwd(),
30
+ stdio: 'inherit',
31
+ timeout: 120_000,
32
+ },
33
+ })
34
+ assert.match(messages.join('\n'), /updating the global @swarmclawai\/swarmclaw install via pnpm/i)
35
+ assert.match(messages.join('\n'), /global update complete via pnpm/i)
36
+ })
37
+
38
+ test('runRegistrySelfUpdate reports a manual retry command when the registry update fails', () => {
39
+ const messages = []
40
+
41
+ const exitCode = runRegistrySelfUpdate(
42
+ 'bun',
43
+ () => {
44
+ throw new Error('spawn bun ENOENT')
45
+ },
46
+ {
47
+ log: (message) => messages.push(`log:${message}`),
48
+ logError: (message) => messages.push(`err:${message}`),
49
+ },
50
+ )
51
+
52
+ assert.equal(exitCode, 1)
53
+ assert.match(messages.join('\n'), /registry update failed: spawn bun ENOENT/i)
54
+ assert.match(messages.join('\n'), /retry manually with: bun add -g @swarmclawai\/swarmclaw@latest/i)
55
+ })
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
5
5
  "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "registry": "https://registry.npmjs.org/"
9
+ },
6
10
  "repository": {
7
11
  "type": "git",
8
12
  "url": "https://github.com/swarmclawai/swarmclaw.git"
@@ -46,13 +50,14 @@
46
50
  "start": "next start",
47
51
  "start:standalone": "node .next/standalone/server.js",
48
52
  "benchmark:autonomy": "node ./scripts/benchmark-autonomy-harness.mjs",
53
+ "benchmark:agent-regression": "node --import tsx ./scripts/run-agent-regression-suite.ts",
49
54
  "lint": "eslint",
50
55
  "lint:fix": "eslint --fix",
51
56
  "lint:baseline": "node ./scripts/lint-baseline.mjs check",
52
57
  "lint:baseline:update": "node ./scripts/lint-baseline.mjs update",
53
58
  "cli": "node ./bin/swarmclaw.js",
54
- "test:cli": "node --test src/cli/index.test.js",
55
- "test:openclaw": "tsx --test src/lib/server/connectors/openclaw.test.ts src/lib/openclaw-endpoint.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/server/task-validation.test.ts src/lib/server/task-quality-gate.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts",
59
+ "test:cli": "node --test src/cli/*.test.js bin/*.test.js",
60
+ "test:openclaw": "tsx --test src/lib/openclaw-agent-id.test.ts src/lib/openclaw-endpoint.test.ts src/lib/server/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw-agent-resolver.test.ts src/lib/server/openclaw-skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/task-quality-gate.test.ts src/lib/server/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts",
56
61
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
57
62
  "postinstall": "node ./scripts/postinstall.mjs"
58
63
  },
@@ -1,6 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { writeFileSync } from 'node:fs'
3
4
  import { spawnSync } from 'node:child_process'
5
+ const INSTALL_METADATA_FILE = '.swarmclaw-install.json'
6
+
7
+ function detectPackageManagerFromUserAgent(userAgent) {
8
+ const normalized = String(userAgent || '').toLowerCase()
9
+ if (normalized.startsWith('pnpm/')) return 'pnpm'
10
+ if (normalized.startsWith('yarn/')) return 'yarn'
11
+ if (normalized.startsWith('bun/')) return 'bun'
12
+ if (normalized.startsWith('npm/')) return 'npm'
13
+ return null
14
+ }
15
+
16
+ const installedWith = detectPackageManagerFromUserAgent(process.env.npm_config_user_agent) || 'npm'
17
+
18
+ try {
19
+ writeFileSync(
20
+ new URL(`../${INSTALL_METADATA_FILE}`, import.meta.url),
21
+ JSON.stringify({
22
+ packageManager: installedWith,
23
+ installedAt: new Date().toISOString(),
24
+ }, null, 2),
25
+ 'utf8',
26
+ )
27
+ } catch {
28
+ // Ignore metadata write failures for install resilience.
29
+ }
4
30
 
5
31
  const result = spawnSync('npm', ['rebuild', 'better-sqlite3', '--silent'], {
6
32
  stdio: 'ignore',
@@ -23,6 +23,23 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
23
23
  body.apiEndpoint,
24
24
  )
25
25
  }
26
+ if (body.routingTargets !== undefined && Array.isArray(body.routingTargets)) {
27
+ agent.routingTargets = body.routingTargets.map((target: Record<string, unknown>, index: number) => ({
28
+ id: typeof target.id === 'string' && target.id.trim() ? target.id.trim() : `route-${index + 1}`,
29
+ label: typeof target.label === 'string' ? target.label : undefined,
30
+ role: target.role,
31
+ provider: (typeof target.provider === 'string' && target.provider.trim() ? target.provider : agent.provider),
32
+ model: typeof target.model === 'string' ? target.model : '',
33
+ credentialId: target.credentialId ?? null,
34
+ fallbackCredentialIds: Array.isArray(target.fallbackCredentialIds) ? target.fallbackCredentialIds : [],
35
+ apiEndpoint: normalizeProviderEndpoint(
36
+ typeof target.provider === 'string' ? target.provider : agent.provider,
37
+ typeof target.apiEndpoint === 'string' ? target.apiEndpoint : null,
38
+ ),
39
+ gatewayProfileId: target.gatewayProfileId ?? null,
40
+ priority: typeof target.priority === 'number' ? target.priority : index + 1,
41
+ }))
42
+ }
26
43
  delete (agent as Record<string, unknown>).isOrchestrator
27
44
  agent.isOrchestrator = agent.platformAssignScope === 'all'
28
45
  delete (agent as Record<string, unknown>).id
@@ -1,96 +1,13 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { genId } from '@/lib/id'
3
- import { loadAgents, saveAgents, loadSessions, saveSessions } from '@/lib/server/storage'
4
- import { WORKSPACE_DIR } from '@/lib/server/data-dir'
2
+ import { ensureAgentThreadSession } from '@/lib/server/agent-thread-session'
5
3
 
6
4
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
5
  const { id: agentId } = await params
8
- const agents = loadAgents()
9
- const agent = agents[agentId]
10
- if (!agent) {
11
- return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
12
- }
13
-
14
6
  const body = await req.json().catch(() => ({}))
15
7
  const user = body.user || 'default'
16
- const sessions = loadSessions()
17
-
18
- // If the agent already has a shortcut chat session, return it.
19
- if (agent.threadSessionId && 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)
32
- }
33
-
34
- // Legacy fallback for older shortcut sessions that were named using the
35
- // old agent-thread convention before the explicit link was persisted.
36
- const existing = Object.values(sessions).find(
37
- (s: Record<string, unknown>) =>
38
- (
39
- s.shortcutForAgentId === agentId
40
- || s.name === `agent-thread:${agentId}`
41
- )
42
- && s.user === user
43
- )
44
- if (existing) {
45
- agent.threadSessionId = (existing as Record<string, unknown>).id as string
46
- agent.updatedAt = Date.now()
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)
59
- return NextResponse.json(existing)
60
- }
61
-
62
- // Create a new shortcut chat session for this agent.
63
- const sessionId = `agent-chat-${agentId}-${genId()}`
64
- const now = Date.now()
65
- const session = {
66
- id: sessionId,
67
- name: agent.name,
68
- shortcutForAgentId: agentId,
69
- cwd: WORKSPACE_DIR,
70
- user: user,
71
- provider: agent.provider,
72
- model: agent.model,
73
- credentialId: agent.credentialId || null,
74
- fallbackCredentialIds: agent.fallbackCredentialIds || [],
75
- apiEndpoint: agent.apiEndpoint || null,
76
- claudeSessionId: null,
77
- messages: [],
78
- createdAt: now,
79
- lastActiveAt: now,
80
- active: false,
81
- sessionType: 'human' as const,
82
- agentId,
83
- plugins: agent.plugins || agent.tools || [],
84
- heartbeatEnabled: agent.heartbeatEnabled || false,
85
- heartbeatIntervalSec: agent.heartbeatIntervalSec || null,
8
+ const session = ensureAgentThreadSession(agentId, user)
9
+ if (!session) {
10
+ return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
86
11
  }
87
-
88
- sessions[sessionId] = session as Record<string, unknown>
89
- saveSessions(sessions)
90
-
91
- agent.threadSessionId = sessionId
92
- agent.updatedAt = Date.now()
93
- saveAgents(agents)
94
-
95
12
  return NextResponse.json(session)
96
13
  }
@@ -56,24 +56,46 @@ export async function POST(req: Request) {
56
56
  id,
57
57
  name: body.name,
58
58
  description: body.description,
59
+ soul: body.soul || undefined,
59
60
  systemPrompt: body.systemPrompt,
60
61
  provider: body.provider,
61
62
  model: body.model,
62
63
  credentialId: body.credentialId,
64
+ fallbackCredentialIds: body.fallbackCredentialIds,
63
65
  apiEndpoint: normalizeProviderEndpoint(body.provider, body.apiEndpoint || null),
66
+ gatewayProfileId: body.gatewayProfileId,
67
+ routingStrategy: body.routingStrategy,
68
+ routingTargets: body.routingTargets?.map((target) => ({
69
+ ...target,
70
+ apiEndpoint: normalizeProviderEndpoint(target.provider, target.apiEndpoint || null),
71
+ })),
64
72
  isOrchestrator: platformAssignScope === 'all',
65
73
  platformAssignScope,
66
74
  subAgentIds: body.subAgentIds,
67
75
  plugins: body.plugins?.length ? body.plugins : (body.tools || []),
76
+ skills: body.skills,
77
+ skillIds: body.skillIds,
78
+ mcpServerIds: body.mcpServerIds,
79
+ mcpDisabledTools: body.mcpDisabledTools?.length ? body.mcpDisabledTools : undefined,
68
80
  capabilities: body.capabilities,
69
81
  thinkingLevel: body.thinkingLevel || undefined,
70
82
  autoRecovery: body.autoRecovery || false,
83
+ heartbeatEnabled: body.heartbeatEnabled || false,
84
+ heartbeatInterval: body.heartbeatInterval,
85
+ heartbeatIntervalSec: body.heartbeatIntervalSec,
86
+ heartbeatModel: body.heartbeatModel,
87
+ heartbeatPrompt: body.heartbeatPrompt,
88
+ elevenLabsVoiceId: body.elevenLabsVoiceId,
71
89
  monthlyBudget: body.monthlyBudget ?? null,
72
90
  dailyBudget: body.dailyBudget ?? null,
73
91
  hourlyBudget: body.hourlyBudget ?? null,
74
92
  budgetAction: body.budgetAction || 'warn',
75
- soul: body.soul || undefined,
76
93
  identityState: body.identityState ?? null,
94
+ memoryScopeMode: body.memoryScopeMode,
95
+ memoryTierPreference: body.memoryTierPreference,
96
+ projectId: body.projectId,
97
+ avatarSeed: body.avatarSeed,
98
+ avatarUrl: body.avatarUrl,
77
99
  sessionResetMode: body.sessionResetMode ?? null,
78
100
  sessionIdleTimeoutSec: body.sessionIdleTimeoutSec ?? null,
79
101
  sessionMaxAgeSec: body.sessionMaxAgeSec ?? null,
@@ -1,5 +1,5 @@
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
4
  import { AUTH_COOKIE_NAME, getCookieValue } from '@/lib/auth'
5
5
  export const dynamic = 'force-dynamic'
@@ -12,7 +12,8 @@ import {
12
12
  resolveAgentApiEndpoint,
13
13
  compactChatroomMessages,
14
14
  buildChatroomSystemPrompt,
15
- buildSyntheticSession,
15
+ ensureSyntheticSession,
16
+ appendSyntheticSessionMessage,
16
17
  buildAgentSystemPromptForChatroom,
17
18
  buildHistoryForAgent,
18
19
  isMuted,
@@ -21,6 +22,7 @@ import { filterHealthyChatroomAgents } from '@/lib/server/chatroom-health'
21
22
  import { evaluateRoutingRules } from '@/lib/server/chatroom-routing'
22
23
  import { markProviderFailure, markProviderSuccess } from '@/lib/server/provider-health'
23
24
  import { applyAgentReactionsFromText } from '@/lib/server/chatroom-orchestration'
25
+ import { resolvePrimaryAgentRoute } from '@/lib/server/agent-runtime-config'
24
26
  import type { Chatroom, ChatroomMessage, Agent } from '@/types'
25
27
 
26
28
  export const dynamic = 'force-dynamic'
@@ -150,9 +152,10 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
150
152
  }
151
153
 
152
154
  // Pre-flight: check if the agent's provider is usable before attempting to stream
153
- const providerInfo = getProvider(agent.provider)
154
- const apiKey = resolveApiKey(agent.credentialId)
155
- const resolvedEndpoint = resolveAgentApiEndpoint(agent)
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)
156
159
  if (providerInfo?.requiresApiKey && !apiKey) {
157
160
  writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
158
161
  writeEvent({ t: 'err', text: `${agent.name} has no API credentials configured`, agentId: agent.id, agentName: agent.name })
@@ -177,7 +180,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
177
180
  notify(`chatroom:${id}`)
178
181
  }
179
182
 
180
- 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
181
189
  syntheticSession.apiEndpoint = resolvedEndpoint
182
190
  const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
183
191
  const chatroomContext = buildChatroomSystemPrompt(freshChatroom, agents, agent.id)
@@ -186,6 +194,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
186
194
 
187
195
  // Use enriched context message for chained agents, or reply context + original text
188
196
  const messageForAgent = item.contextMessage || (replyContext + text)
197
+ appendSyntheticSessionMessage(syntheticSession.id, 'user', messageForAgent)
189
198
 
190
199
  let fullText = ''
191
200
  let agentError = ''
@@ -223,12 +232,14 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
223
232
 
224
233
  // Don't persist empty or error-only messages — they pollute chat history
225
234
  if (!responseText.trim() && agentError) {
235
+ appendSyntheticSessionMessage(syntheticSession.id, 'assistant', agentError)
226
236
  markProviderFailure(agent.provider, agentError)
227
237
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
228
238
  return []
229
239
  }
230
240
 
231
241
  if (responseText.trim()) {
242
+ appendSyntheticSessionMessage(syntheticSession.id, 'assistant', responseText)
232
243
  const parsedMentions = parseMentions(responseText, agents, freshChatroom.agentIds)
233
244
  const chainedHealth = filterHealthyChatroomAgents(parsedMentions, agents)
234
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) {
@@ -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,
@@ -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,
@@ -113,6 +116,9 @@ export async function POST(req: Request) {
113
116
  identityState: body.identityState ?? agent?.identityState ?? null,
114
117
  sessionArchiveState: body.sessionArchiveState ?? null,
115
118
  }
119
+ sessions[id] = (body.provider || body.model || body.credentialId || body.apiEndpoint)
120
+ ? nextSession
121
+ : applyResolvedRoute(nextSession, resolvedRoute)
116
122
  saveSessions(sessions)
117
123
  notify('sessions')
118
124
  return NextResponse.json(sessions[id])
@@ -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
+ }