@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
@@ -28,6 +28,7 @@ import { getPluginManager } from './plugins'
28
28
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
29
29
  import { routeTaskIntent } from './capability-router'
30
30
  import { notify } from './ws-hub'
31
+ import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
31
32
  import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
32
33
  import { pluginIdMatches } from './tool-aliases'
33
34
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
@@ -132,14 +133,10 @@ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
132
133
  const idx = bag.findLastIndex((e) => e.name === (ev.toolName || 'unknown') && !e.output)
133
134
  if (idx === -1) return
134
135
  const output = ev.toolOutput || ''
135
- const isError = /^(Error:|error:)/i.test(output.trim())
136
- || output.includes('ECONNREFUSED')
137
- || output.includes('ETIMEDOUT')
138
- || output.includes('Error:')
139
136
  bag[idx] = {
140
137
  ...bag[idx],
141
138
  output,
142
- error: isError || undefined,
139
+ error: isLikelyToolErrorOutput(output) || undefined,
143
140
  }
144
141
  }
145
142
  }
@@ -191,15 +188,124 @@ function extractDelegateResponse(outputText: string): string | null {
191
188
  }
192
189
  }
193
190
 
191
+ const MANAGE_PLATFORM_RESOURCE_TO_TOOL: Record<string, string> = {
192
+ agent: 'manage_agents',
193
+ agents: 'manage_agents',
194
+ task: 'manage_tasks',
195
+ tasks: 'manage_tasks',
196
+ schedule: 'manage_schedules',
197
+ schedules: 'manage_schedules',
198
+ skill: 'manage_skills',
199
+ skills: 'manage_skills',
200
+ document: 'manage_documents',
201
+ documents: 'manage_documents',
202
+ secret: 'manage_secrets',
203
+ secrets: 'manage_secrets',
204
+ connector: 'manage_connectors',
205
+ connectors: 'manage_connectors',
206
+ session: 'manage_sessions',
207
+ sessions: 'manage_sessions',
208
+ }
209
+
210
+ export function translateRequestedToolInvocation(
211
+ requestedName: string,
212
+ rawArgs: Record<string, unknown>,
213
+ messageFallback: string,
214
+ availableToolNames?: Iterable<string>,
215
+ ): { toolName: string; args: Record<string, unknown> } {
216
+ const available = new Set(availableToolNames || [])
217
+
218
+ if (requestedName === 'web_search') {
219
+ return {
220
+ toolName: 'web',
221
+ args: {
222
+ action: 'search',
223
+ query: typeof rawArgs.query === 'string' ? rawArgs.query : messageFallback.trim(),
224
+ maxResults: typeof rawArgs.maxResults === 'number' ? rawArgs.maxResults : 5,
225
+ },
226
+ }
227
+ }
228
+ if (requestedName === 'web_fetch') {
229
+ return {
230
+ toolName: 'web',
231
+ args: {
232
+ action: 'fetch',
233
+ url: rawArgs.url,
234
+ },
235
+ }
236
+ }
237
+ if (requestedName === 'delegate_to_claude_code') {
238
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
239
+ }
240
+ if (requestedName === 'delegate_to_codex_cli') {
241
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'codex' } }
242
+ }
243
+ if (requestedName === 'delegate_to_opencode_cli') {
244
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'opencode' } }
245
+ }
246
+ if (requestedName === 'delegate_to_gemini_cli') {
247
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
248
+ }
249
+
250
+ const managePrefix = 'manage_'
251
+ if (requestedName === 'manage_platform') {
252
+ const resource = typeof rawArgs.resource === 'string'
253
+ ? rawArgs.resource.trim().toLowerCase()
254
+ : ''
255
+ const specificTool = MANAGE_PLATFORM_RESOURCE_TO_TOOL[resource]
256
+ if (specificTool && available.has(specificTool) && !available.has('manage_platform')) {
257
+ return { toolName: specificTool, args: rawArgs }
258
+ }
259
+ return { toolName: requestedName, args: rawArgs }
260
+ }
261
+
262
+ if (requestedName.startsWith(managePrefix) && requestedName !== 'manage_platform') {
263
+ if (!available.has(requestedName) && available.has('manage_platform')) {
264
+ const resource = requestedName.slice(managePrefix.length)
265
+ if (resource) {
266
+ const { action, id, data, ...rest } = rawArgs
267
+ const nextArgs: Record<string, unknown> = { resource, ...rest }
268
+ if (action !== undefined) nextArgs.action = action
269
+ if (id !== undefined) nextArgs.id = id
270
+ if (data !== undefined) nextArgs.data = data
271
+ return {
272
+ toolName: 'manage_platform',
273
+ args: nextArgs,
274
+ }
275
+ }
276
+ }
277
+ return { toolName: requestedName, args: rawArgs }
278
+ }
279
+
280
+ return { toolName: requestedName, args: rawArgs }
281
+ }
282
+
283
+ export function isLikelyToolErrorOutput(output: string): boolean {
284
+ const trimmed = String(output || '').trim()
285
+ if (!trimmed) return false
286
+ if (/^(Error(?::|\s*\(exit\b[^)]*\):?)|error:)/i.test(trimmed)) return true
287
+ if (/\b(MCP error|ECONNREFUSED|ETIMEDOUT|ERR_CONNECTION_REFUSED|ENOENT|EACCES)\b/i.test(trimmed)) return true
288
+ if (/\binvalid_type\b/i.test(trimmed) && /\b(issue|issues|expected|required|received|zod)\b/i.test(trimmed)) return true
289
+ try {
290
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>
291
+ const status = typeof parsed.status === 'string' ? parsed.status.trim().toLowerCase() : ''
292
+ if (status === 'error' || status === 'failed') return true
293
+ if (typeof parsed.error === 'string' && parsed.error.trim()) return true
294
+ } catch {
295
+ // Ignore non-JSON tool output.
296
+ }
297
+ return false
298
+ }
299
+
194
300
  function normalizeWorkspaceSandboxLinks(text: string, cwd: string): string {
195
- return text.replace(/sandbox:\/workspace\/([^\s)"'\]`]+)/g, (raw, relativePath: string) => {
301
+ return text.replace(/\[([^\]]+)\]\(sandbox:\/workspace\/([^)]+)\)/g, (raw, label: string, relativePath: string) => {
196
302
  const normalized = String(relativePath || '').replace(/^\/+/, '')
197
303
  if (!normalized) return raw
198
304
  const resolvedCwd = path.resolve(cwd)
199
305
  const resolved = path.resolve(resolvedCwd, normalized)
200
306
  if (!resolved.startsWith(resolvedCwd)) return raw
201
307
  if (!fs.existsSync(resolved)) return raw
202
- return `/api/files/serve?path=${encodeURIComponent(resolved)}`
308
+ return `[${label}](/api/files/serve?path=${encodeURIComponent(resolved)})`
203
309
  })
204
310
  }
205
311
 
@@ -520,18 +626,41 @@ function syncSessionFromAgent(sessionId: string): void {
520
626
  if (!agent) return
521
627
 
522
628
  let changed = false
629
+ const route = resolvePrimaryAgentRoute(agent)
523
630
  if (!session.provider && agent.provider) { session.provider = agent.provider; changed = true }
524
631
  if ((session.model === undefined || session.model === null || session.model === '') && agent.model !== undefined) {
525
632
  session.model = agent.model
526
633
  changed = true
527
634
  }
528
- if (session.credentialId === undefined && agent.credentialId !== undefined) {
529
- session.credentialId = agent.credentialId ?? null
530
- changed = true
531
- }
532
- if ((session.apiEndpoint === undefined || session.apiEndpoint === null) && agent.apiEndpoint !== undefined) {
533
- const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
534
- if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
635
+ if (route) {
636
+ const resolved = applyResolvedRoute({ ...session }, route)
637
+ if (session.provider !== resolved.provider) { session.provider = resolved.provider; changed = true }
638
+ if (session.model !== resolved.model) { session.model = resolved.model; changed = true }
639
+ if ((session.credentialId || null) !== (resolved.credentialId || null)) {
640
+ session.credentialId = resolved.credentialId ?? null
641
+ changed = true
642
+ }
643
+ if (JSON.stringify(session.fallbackCredentialIds || []) !== JSON.stringify(resolved.fallbackCredentialIds || [])) {
644
+ session.fallbackCredentialIds = [...resolved.fallbackCredentialIds]
645
+ changed = true
646
+ }
647
+ if ((session.apiEndpoint || null) !== (resolved.apiEndpoint || null)) {
648
+ session.apiEndpoint = resolved.apiEndpoint ?? null
649
+ changed = true
650
+ }
651
+ if ((session.gatewayProfileId || null) !== (resolved.gatewayProfileId || null)) {
652
+ session.gatewayProfileId = resolved.gatewayProfileId ?? null
653
+ changed = true
654
+ }
655
+ } else {
656
+ if (session.credentialId === undefined && agent.credentialId !== undefined) {
657
+ session.credentialId = agent.credentialId ?? null
658
+ changed = true
659
+ }
660
+ if ((session.apiEndpoint === undefined || session.apiEndpoint === null) && agent.apiEndpoint !== undefined) {
661
+ const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
662
+ if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
663
+ }
535
664
  }
536
665
  if (!Array.isArray(session.plugins)) {
537
666
  session.plugins = Array.isArray(agent.plugins) ? [...agent.plugins] : []
@@ -719,6 +848,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
719
848
  let sessionForRun = pluginsForRun === session.plugins
720
849
  ? session
721
850
  : { ...session, plugins: pluginsForRun }
851
+ if (agentForSession) {
852
+ const preferredRoute = resolvePrimaryAgentRoute(agentForSession)
853
+ if (preferredRoute) {
854
+ sessionForRun = applyResolvedRoute({ ...sessionForRun }, preferredRoute)
855
+ }
856
+ }
722
857
  let effectiveMessage = message
723
858
 
724
859
  if (pluginsForRun.length > 0) {
@@ -825,14 +960,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
825
960
  detail: {
826
961
  source,
827
962
  internal,
828
- provider: session.provider,
829
- model: session.model,
963
+ provider: sessionForRun.provider,
964
+ model: sessionForRun.model,
830
965
  messagePreview: effectiveMessage.slice(0, 200),
831
966
  hasImage: !!(imagePath || imageUrl),
832
967
  },
833
968
  })
834
969
 
835
- const providerType = session.provider || 'claude-cli'
970
+ const providerType = sessionForRun.provider || 'claude-cli'
836
971
  const provider = getProvider(providerType)
837
972
  if (!provider) throw new Error(`Unknown provider: ${providerType}`)
838
973
 
@@ -840,7 +975,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
840
975
  throw new Error(`Directory not found: ${session.cwd}`)
841
976
  }
842
977
 
843
- const apiKey = resolveApiKeyForSession(session, provider)
978
+ const apiKey = resolveApiKeyForSession(sessionForRun, provider)
844
979
 
845
980
  if (!internal) {
846
981
  const linkAnalysis = await runLinkUnderstanding(message)
@@ -1105,57 +1240,6 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1105
1240
  : null
1106
1241
  const calledNames = new Set((toolEvents || []).map((t) => t.name))
1107
1242
 
1108
- const translateToolInvocation = (
1109
- requestedName: string,
1110
- rawArgs: Record<string, unknown>,
1111
- ): { toolName: string; args: Record<string, unknown> } => {
1112
- if (requestedName === 'web_search') {
1113
- return {
1114
- toolName: 'web',
1115
- args: {
1116
- action: 'search',
1117
- query: typeof rawArgs.query === 'string' ? rawArgs.query : message.trim(),
1118
- maxResults: typeof rawArgs.maxResults === 'number' ? rawArgs.maxResults : 5,
1119
- },
1120
- }
1121
- }
1122
- if (requestedName === 'web_fetch') {
1123
- return {
1124
- toolName: 'web',
1125
- args: {
1126
- action: 'fetch',
1127
- url: rawArgs.url,
1128
- },
1129
- }
1130
- }
1131
- if (requestedName === 'delegate_to_claude_code') {
1132
- return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
1133
- }
1134
- if (requestedName === 'delegate_to_codex_cli') {
1135
- return { toolName: 'delegate', args: { ...rawArgs, backend: 'codex' } }
1136
- }
1137
- if (requestedName === 'delegate_to_opencode_cli') {
1138
- return { toolName: 'delegate', args: { ...rawArgs, backend: 'opencode' } }
1139
- }
1140
- if (requestedName === 'delegate_to_gemini_cli') {
1141
- return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
1142
- }
1143
-
1144
- const managePrefix = 'manage_'
1145
- if (requestedName.startsWith(managePrefix) && requestedName !== 'manage_platform') {
1146
- const resource = requestedName.slice(managePrefix.length)
1147
- if (resource) {
1148
- const { action, id, data, ...rest } = rawArgs
1149
- return {
1150
- toolName: 'manage_platform',
1151
- args: { resource, action, id, data, ...rest },
1152
- }
1153
- }
1154
- }
1155
-
1156
- return { toolName: requestedName, args: rawArgs }
1157
- }
1158
-
1159
1243
  const invokeSessionTool = async (toolName: string, args: Record<string, unknown>, failurePrefix: string): Promise<boolean> => {
1160
1244
  const blockedReason = resolveConcreteToolPolicyBlock(toolName, toolPolicy, appSettings)
1161
1245
  if (blockedReason) {
@@ -1179,8 +1263,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1179
1263
  mcpDisabledTools: agent?.mcpDisabledTools,
1180
1264
  })
1181
1265
  try {
1182
- const translated = translateToolInvocation(toolName, args)
1183
- const selectedTool = tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
1266
+ const directTool = tools.find((t) => t?.name === toolName) as StructuredToolInterface | undefined
1267
+ const availableToolNames = tools.map((candidate) => candidate?.name).filter(Boolean)
1268
+ const translated = directTool
1269
+ ? { toolName, args }
1270
+ : translateRequestedToolInvocation(toolName, args, message, availableToolNames)
1271
+ const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
1184
1272
  if (!selectedTool?.invoke) return false
1185
1273
  const toolInput = JSON.stringify(translated.args)
1186
1274
  emit({ t: 'tool_call', toolName, toolInput })
@@ -6,6 +6,7 @@ import {
6
6
  compactChatroomMessages,
7
7
  buildHistoryForAgent,
8
8
  buildSyntheticSession,
9
+ resolveChatroomWorkspaceDir,
9
10
  resolveAgentApiEndpoint,
10
11
  resolveReplyTargetAgentId,
11
12
  } from './chatroom-helpers'
@@ -156,4 +157,10 @@ describe('chatroom-helpers', () => {
156
157
  assert.equal(resolveAgentApiEndpoint(agent), 'http://localhost:11434')
157
158
  assert.equal(buildSyntheticSession(agent, 'room-1').apiEndpoint, 'http://localhost:11434')
158
159
  })
160
+
161
+ it('keeps chatroom execution inside the workspace instead of the repo root', () => {
162
+ const cwd = buildSyntheticSession(makeAgents().default, 'room-safe').cwd
163
+ assert.equal(cwd, resolveChatroomWorkspaceDir('room-safe'))
164
+ assert.match(cwd, /chatrooms[\/\\]room-safe$/)
165
+ })
159
166
  })
@@ -1,10 +1,14 @@
1
+ import fs from 'fs'
1
2
  import os from 'os'
2
- import { loadSettings, loadSkills, loadCredentials, decryptKey } from './storage'
3
+ import path from 'path'
4
+ import { loadSettings, loadSkills, loadCredentials, decryptKey, loadSessions, saveSessions } from './storage'
3
5
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
4
6
  import { buildIdentityContinuityContext } from './identity-continuity'
5
7
  import { genId } from '@/lib/id'
6
8
  import { getProvider } from '@/lib/providers'
7
9
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
10
+ import { WORKSPACE_DIR } from './data-dir'
11
+ import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
8
12
  import type { Chatroom, ChatroomMember, Agent, Session, Message, ChatroomMessage } from '@/types'
9
13
 
10
14
  /** Resolve API key from an agent's credentialId */
@@ -210,11 +214,31 @@ export function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<str
210
214
  }
211
215
 
212
216
  /** Build a synthetic session object for an agent in a chatroom */
213
- export function buildSyntheticSession(agent: Agent, chatroomId: string): Session {
217
+ export function resolveChatroomWorkspaceDir(chatroomId: string): string {
218
+ return path.join(WORKSPACE_DIR, 'chatrooms', chatroomId)
219
+ }
220
+
221
+ export function resolveSyntheticSessionId(chatroomId: string, agentId: string): string {
222
+ return `chatroom-${chatroomId}-${agentId}`
223
+ }
224
+
225
+ function buildEmptyDelegateResumeIds(): NonNullable<Session['delegateResumeIds']> {
214
226
  return {
215
- id: `chatroom-${chatroomId}-${agent.id}`,
227
+ claudeCode: null,
228
+ codex: null,
229
+ opencode: null,
230
+ gemini: null,
231
+ }
232
+ }
233
+
234
+ export function buildSyntheticSession(agent: Agent, chatroomId: string): Session {
235
+ const roomWorkspace = resolveChatroomWorkspaceDir(chatroomId)
236
+ fs.mkdirSync(roomWorkspace, { recursive: true })
237
+ const now = Date.now()
238
+ return applyResolvedRoute({
239
+ id: resolveSyntheticSessionId(chatroomId, agent.id),
216
240
  name: `Chatroom session for ${agent.name}`,
217
- cwd: process.cwd(),
241
+ cwd: roomWorkspace,
218
242
  user: 'chatroom',
219
243
  provider: agent.provider,
220
244
  model: agent.model,
@@ -222,12 +246,81 @@ export function buildSyntheticSession(agent: Agent, chatroomId: string): Session
222
246
  fallbackCredentialIds: agent.fallbackCredentialIds,
223
247
  apiEndpoint: resolveAgentApiEndpoint(agent),
224
248
  claudeSessionId: null,
249
+ codexThreadId: null,
250
+ opencodeSessionId: null,
251
+ delegateResumeIds: buildEmptyDelegateResumeIds(),
225
252
  messages: [],
226
- createdAt: Date.now(),
227
- lastActiveAt: Date.now(),
253
+ createdAt: now,
254
+ lastActiveAt: now,
255
+ sessionType: 'human',
228
256
  plugins: agent.plugins || agent.tools || [],
229
257
  agentId: agent.id,
258
+ }, resolvePrimaryAgentRoute(agent))
259
+ }
260
+
261
+ export function ensureSyntheticSession(agent: Agent, chatroomId: string): Session {
262
+ const roomWorkspace = resolveChatroomWorkspaceDir(chatroomId)
263
+ fs.mkdirSync(roomWorkspace, { recursive: true })
264
+ const sessionId = resolveSyntheticSessionId(chatroomId, agent.id)
265
+ const sessions = loadSessions()
266
+ const now = Date.now()
267
+ const existing = sessions[sessionId]
268
+ const session: Session = existing
269
+ ? applyResolvedRoute({
270
+ ...existing,
271
+ id: sessionId,
272
+ name: `Chatroom session for ${agent.name}`,
273
+ cwd: roomWorkspace,
274
+ user: 'chatroom',
275
+ provider: agent.provider,
276
+ model: agent.model,
277
+ credentialId: agent.credentialId ?? null,
278
+ fallbackCredentialIds: Array.isArray(agent.fallbackCredentialIds) ? [...agent.fallbackCredentialIds] : [],
279
+ apiEndpoint: resolveAgentApiEndpoint(agent),
280
+ sessionType: existing.sessionType || 'human',
281
+ agentId: agent.id,
282
+ plugins: agent.plugins || agent.tools || [],
283
+ tools: agent.plugins || agent.tools || [],
284
+ createdAt: existing.createdAt || now,
285
+ lastActiveAt: now,
286
+ }, resolvePrimaryAgentRoute(agent))
287
+ : applyResolvedRoute({
288
+ ...buildSyntheticSession(agent, chatroomId),
289
+ fallbackCredentialIds: Array.isArray(agent.fallbackCredentialIds) ? [...agent.fallbackCredentialIds] : [],
290
+ lastActiveAt: now,
291
+ tools: agent.plugins || agent.tools || [],
292
+ }, resolvePrimaryAgentRoute(agent))
293
+
294
+ if (!Array.isArray(session.messages)) session.messages = []
295
+ if (!session.delegateResumeIds || typeof session.delegateResumeIds !== 'object') {
296
+ session.delegateResumeIds = buildEmptyDelegateResumeIds()
230
297
  }
298
+ if (session.codexThreadId === undefined) session.codexThreadId = null
299
+ if (session.opencodeSessionId === undefined) session.opencodeSessionId = null
300
+ sessions[sessionId] = session
301
+ saveSessions(sessions)
302
+ return session
303
+ }
304
+
305
+ export function appendSyntheticSessionMessage(
306
+ sessionId: string,
307
+ role: 'user' | 'assistant',
308
+ text: string,
309
+ ): void {
310
+ const trimmed = String(text || '').trim()
311
+ if (!trimmed) return
312
+ const sessions = loadSessions()
313
+ const session = sessions[sessionId]
314
+ if (!session) return
315
+ if (!Array.isArray(session.messages)) session.messages = []
316
+ session.messages.push({
317
+ role,
318
+ text: trimmed,
319
+ time: Date.now(),
320
+ })
321
+ session.lastActiveAt = Date.now()
322
+ sessions[sessionId] = session
323
+ saveSessions(sessions)
231
324
  }
232
325
 
233
326
  /** Build agent's system prompt including skills and identity context */
@@ -0,0 +1,87 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { describe, it } from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chatroom-session-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ DATA_DIR: path.join(tempDir, 'data'),
18
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
+ },
20
+ encoding: 'utf-8',
21
+ })
22
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
23
+ const lines = (result.stdout || '')
24
+ .trim()
25
+ .split('\n')
26
+ .map((line) => line.trim())
27
+ .filter(Boolean)
28
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
29
+ return JSON.parse(jsonLine || '{}')
30
+ } finally {
31
+ fs.rmSync(tempDir, { recursive: true, force: true })
32
+ }
33
+ }
34
+
35
+ describe('chatroom synthetic session persistence', () => {
36
+ it('reuses stored synthetic sessions and preserves delegate resume state', () => {
37
+ const output = runWithTempDataDir(`
38
+ const helpersMod = await import('./src/lib/server/chatroom-helpers.ts')
39
+ const helpers = helpersMod.default || helpersMod
40
+ const storageMod = await import('./src/lib/server/storage.ts')
41
+ const storage = storageMod.default || storageMod
42
+ const now = Date.now()
43
+ const agent = {
44
+ id: 'default',
45
+ name: 'Molly',
46
+ description: '',
47
+ systemPrompt: '',
48
+ provider: 'openai',
49
+ model: 'gpt-4o',
50
+ createdAt: now,
51
+ updatedAt: now,
52
+ plugins: ['delegate'],
53
+ }
54
+
55
+ const first = helpers.ensureSyntheticSession(agent, 'room-1')
56
+ helpers.appendSyntheticSessionMessage(first.id, 'user', 'first prompt')
57
+
58
+ const sessions = storage.loadSessions()
59
+ sessions[first.id].delegateResumeIds = {
60
+ claudeCode: null,
61
+ codex: 'resume-123',
62
+ opencode: null,
63
+ gemini: null,
64
+ }
65
+ storage.saveSessions(sessions)
66
+
67
+ const second = helpers.ensureSyntheticSession({ ...agent, model: 'gpt-4.1' }, 'room-1')
68
+ console.log(JSON.stringify({
69
+ sessionId: second.id,
70
+ cwd: second.cwd,
71
+ model: second.model,
72
+ messageCount: second.messages.length,
73
+ firstMessage: second.messages[0]?.text || '',
74
+ delegateResumeIds: second.delegateResumeIds,
75
+ plugins: second.plugins || [],
76
+ }))
77
+ `)
78
+
79
+ assert.equal(output.sessionId, 'chatroom-room-1-default')
80
+ assert.match(String(output.cwd), /chatrooms[\/\\]room-1$/)
81
+ assert.equal(output.model, 'gpt-4.1')
82
+ assert.equal(output.messageCount, 1)
83
+ assert.equal(output.firstMessage, 'first prompt')
84
+ assert.equal(output.delegateResumeIds?.codex, 'resume-123')
85
+ assert.deepEqual(output.plugins, ['delegate'])
86
+ })
87
+ })