@swarmclawai/swarmclaw 0.7.3 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) 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 +3 -1
  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/approvals-auto-approve.test.ts +59 -0
  88. package/src/lib/server/build-llm.test.ts +13 -5
  89. package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
  90. package/src/lib/server/chat-execution.ts +159 -71
  91. package/src/lib/server/chatroom-helpers.test.ts +7 -0
  92. package/src/lib/server/chatroom-helpers.ts +99 -6
  93. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  94. package/src/lib/server/connectors/manager.ts +89 -61
  95. package/src/lib/server/connectors/slack.ts +1 -1
  96. package/src/lib/server/daemon-state.ts +3 -2
  97. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  98. package/src/lib/server/eval/agent-regression.ts +1742 -0
  99. package/src/lib/server/eval/runner.ts +11 -1
  100. package/src/lib/server/eval/store.ts +2 -1
  101. package/src/lib/server/heartbeat-service.ts +10 -4
  102. package/src/lib/server/main-agent-loop.ts +13 -6
  103. package/src/lib/server/openclaw-exec-config.ts +4 -2
  104. package/src/lib/server/openclaw-gateway.ts +123 -36
  105. package/src/lib/server/orchestrator-lg.ts +1 -2
  106. package/src/lib/server/orchestrator.ts +3 -2
  107. package/src/lib/server/plugins.test.ts +9 -1
  108. package/src/lib/server/plugins.ts +12 -2
  109. package/src/lib/server/provider-model-discovery.ts +481 -0
  110. package/src/lib/server/queue.ts +1 -1
  111. package/src/lib/server/runtime-settings.test.ts +119 -0
  112. package/src/lib/server/runtime-settings.ts +12 -92
  113. package/src/lib/server/schedule-normalization.ts +187 -0
  114. package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
  115. package/src/lib/server/session-tools/crud.ts +27 -3
  116. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  117. package/src/lib/server/session-tools/discovery.ts +18 -8
  118. package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
  119. package/src/lib/server/session-tools/file.ts +8 -2
  120. package/src/lib/server/session-tools/http.ts +9 -3
  121. package/src/lib/server/session-tools/index.ts +31 -1
  122. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  123. package/src/lib/server/session-tools/monitor.ts +14 -7
  124. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  125. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  126. package/src/lib/server/session-tools/platform.ts +1 -1
  127. package/src/lib/server/session-tools/plugin-creator.ts +9 -2
  128. package/src/lib/server/session-tools/sandbox.ts +51 -92
  129. package/src/lib/server/session-tools/session-info.ts +22 -1
  130. package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
  131. package/src/lib/server/session-tools/shell.ts +2 -2
  132. package/src/lib/server/session-tools/subagent.ts +3 -1
  133. package/src/lib/server/session-tools/web.ts +73 -30
  134. package/src/lib/server/storage.ts +29 -3
  135. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  136. package/src/lib/server/stream-agent-chat.ts +139 -4
  137. package/src/lib/server/structured-extract.ts +1 -1
  138. package/src/lib/server/task-mention.ts +0 -1
  139. package/src/lib/server/tool-aliases.ts +37 -6
  140. package/src/lib/server/tool-capability-policy.ts +1 -1
  141. package/src/lib/setup-defaults.ts +352 -11
  142. package/src/lib/tool-definitions.ts +3 -4
  143. package/src/lib/validation/schemas.ts +55 -1
  144. package/src/stores/use-app-store.ts +43 -1
  145. package/src/stores/use-chatroom-store.ts +153 -26
  146. package/src/types/index.ts +189 -6
  147. package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
1
3
  import { genId } from '@/lib/id'
2
4
  import type { EvalScenario, EvalRun, EvalSuiteResult } from './types'
3
5
  import { getScenario, EVAL_SCENARIOS } from './scenarios'
@@ -5,8 +7,15 @@ import { scoreCriteria } from './scorer'
5
7
  import { saveEvalRun } from './store'
6
8
  import { loadSessions, saveSessions, loadAgents, loadCredentials, decryptKey } from '../storage'
7
9
  import { executeSessionChatTurn } from '../chat-execution'
10
+ import { WORKSPACE_DIR } from '../data-dir'
8
11
  import type { Session } from '@/types'
9
12
 
13
+ export function resolveEvalSessionCwd(runId: string): string {
14
+ const dir = path.join(WORKSPACE_DIR, 'evals', runId)
15
+ fs.mkdirSync(dir, { recursive: true })
16
+ return dir
17
+ }
18
+
10
19
  export async function runEvalScenario(scenarioId: string, agentId: string): Promise<EvalRun> {
11
20
  const scenario = getScenario(scenarioId)
12
21
  if (!scenario) throw new Error(`Unknown eval scenario: ${scenarioId}`)
@@ -18,6 +27,7 @@ export async function runEvalScenario(scenarioId: string, agentId: string): Prom
18
27
  const runId = genId()
19
28
  const sessionId = `eval-${runId}`
20
29
  const now = Date.now()
30
+ const sessionCwd = resolveEvalSessionCwd(runId)
21
31
 
22
32
  const run: EvalRun = {
23
33
  id: runId,
@@ -36,7 +46,7 @@ export async function runEvalScenario(scenarioId: string, agentId: string): Prom
36
46
  const evalSession: Session = {
37
47
  id: sessionId,
38
48
  name: `Eval: ${scenario.name}`,
39
- cwd: process.cwd(),
49
+ cwd: sessionCwd,
40
50
  user: 'eval-runner',
41
51
  provider: (agent.provider as Session['provider']) ?? 'anthropic',
42
52
  model: (agent.model as string) ?? '',
@@ -1,8 +1,9 @@
1
1
  import Database from 'better-sqlite3'
2
2
  import path from 'path'
3
3
  import type { EvalRun } from './types'
4
+ import { DATA_DIR } from '../data-dir'
4
5
 
5
- const DB_PATH = path.join(process.cwd(), 'data', 'eval-runs.db')
6
+ const DB_PATH = path.join(DATA_DIR, 'eval-runs.db')
6
7
 
7
8
  let db: Database.Database | null = null
8
9
 
@@ -1,5 +1,11 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
+ import {
4
+ DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
5
+ DEFAULT_HEARTBEAT_INTERVAL_SEC,
6
+ DEFAULT_HEARTBEAT_SHOW_ALERTS,
7
+ DEFAULT_HEARTBEAT_SHOW_OK,
8
+ } from '@/lib/heartbeat-defaults'
3
9
  import { loadAgents, loadSessions, loadSettings } from './storage'
4
10
  import { enqueueSessionRun, getSessionRunState } from './session-run-manager'
5
11
  import { log } from './logger'
@@ -275,7 +281,7 @@ function resolveNum(obj: Record<string, any>, key: string, current: number): num
275
281
 
276
282
  function heartbeatConfigForSession(session: any, settings: Record<string, any>, agents: Record<string, any>): HeartbeatConfig {
277
283
  // Global defaults — 30 min interval (was 120s)
278
- let intervalSec = resolveInterval(settings, 1800)
284
+ let intervalSec = resolveInterval(settings, DEFAULT_HEARTBEAT_INTERVAL_SEC)
279
285
  const globalPrompt = (typeof settings.heartbeatPrompt === 'string' && settings.heartbeatPrompt.trim())
280
286
  ? settings.heartbeatPrompt.trim()
281
287
  : DEFAULT_HEARTBEAT_PROMPT
@@ -283,9 +289,9 @@ function heartbeatConfigForSession(session: any, settings: Record<string, any>,
283
289
  let enabled = intervalSec > 0
284
290
  let prompt = globalPrompt
285
291
  let model: string | null = resolveStr(settings, 'heartbeatModel', null)
286
- let ackMaxChars = resolveNum(settings, 'heartbeatAckMaxChars', 300)
287
- let showOk = resolveBool(settings, 'heartbeatShowOk', false)
288
- let showAlerts = resolveBool(settings, 'heartbeatShowAlerts', true)
292
+ let ackMaxChars = resolveNum(settings, 'heartbeatAckMaxChars', DEFAULT_HEARTBEAT_ACK_MAX_CHARS)
293
+ let showOk = resolveBool(settings, 'heartbeatShowOk', DEFAULT_HEARTBEAT_SHOW_OK)
294
+ let showAlerts = resolveBool(settings, 'heartbeatShowAlerts', DEFAULT_HEARTBEAT_SHOW_ALERTS)
289
295
  let target: string | null = resolveStr(settings, 'heartbeatTarget', null)
290
296
 
291
297
  // Agent layer overrides
@@ -66,11 +66,13 @@ export interface HandleMainLoopRunResultInput {
66
66
  estimatedCost?: number
67
67
  }
68
68
 
69
- export function isMainSession(_session: unknown): boolean {
69
+ export function isMainSession(session: unknown): boolean {
70
+ void session
70
71
  return false
71
72
  }
72
73
 
73
- export function buildMainLoopHeartbeatPrompt(_session: unknown, fallbackPrompt: string): string {
74
+ export function buildMainLoopHeartbeatPrompt(session: unknown, fallbackPrompt: string): string {
75
+ void session
74
76
  return fallbackPrompt
75
77
  }
76
78
 
@@ -82,18 +84,23 @@ export function stripMainLoopMetaForPersistence(text: string): string {
82
84
  .trim()
83
85
  }
84
86
 
85
- export function getMainLoopStateForSession(_sessionId: string): MainLoopState | null {
87
+ export function getMainLoopStateForSession(sessionId: string): MainLoopState | null {
88
+ void sessionId
86
89
  return null
87
90
  }
88
91
 
89
- export function setMainLoopStateForSession(_sessionId: string, _patch: Partial<MainLoopState>): MainLoopState | null {
92
+ export function setMainLoopStateForSession(sessionId: string, patch: Partial<MainLoopState>): MainLoopState | null {
93
+ void sessionId
94
+ void patch
90
95
  return null
91
96
  }
92
97
 
93
- export function pushMainLoopEventToMainSessions(_input: PushMainLoopEventInput): number {
98
+ export function pushMainLoopEventToMainSessions(input: PushMainLoopEventInput): number {
99
+ void input
94
100
  return 0
95
101
  }
96
102
 
97
- export function handleMainLoopRunResult(_input: HandleMainLoopRunResultInput): MainLoopFollowupRequest | null {
103
+ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): MainLoopFollowupRequest | null {
104
+ void input
98
105
  return null
99
106
  }
@@ -8,7 +8,8 @@ const DEFAULT_CONFIG: ExecApprovalConfig = {
8
8
  }
9
9
 
10
10
  /** Fetch the gateway's global exec approval config. */
11
- export async function getExecConfig(_agentId?: string): Promise<ExecApprovalSnapshot> {
11
+ export async function getExecConfig(agentId?: string): Promise<ExecApprovalSnapshot> {
12
+ void agentId
12
13
  const gw = await ensureGatewayConnected()
13
14
  if (!gw) throw new Error('Gateway not connected')
14
15
 
@@ -21,10 +22,11 @@ export async function getExecConfig(_agentId?: string): Promise<ExecApprovalSnap
21
22
 
22
23
  /** Save exec approval config with hash-based conflict retry (up to 3 attempts) */
23
24
  export async function setExecConfig(
24
- _agentId: string,
25
+ agentId: string,
25
26
  config: ExecApprovalConfig,
26
27
  baseHash: string,
27
28
  ): Promise<{ ok: boolean; hash: string }> {
29
+ void agentId
28
30
  const gw = await ensureGatewayConnected()
29
31
  if (!gw) throw new Error('Gateway not connected')
30
32
 
@@ -3,6 +3,7 @@ import { randomUUID } from 'crypto'
3
3
  import { wsConnect, buildOpenClawConnectParams } from '../providers/openclaw'
4
4
  import { loadAgents, loadCredentials, decryptKey } from './storage'
5
5
  import { notify, notifyWithPayload } from './ws-hub'
6
+ import { getGatewayProfile, getGatewayProfiles, resolvePrimaryAgentRoute } from './agent-runtime-config'
6
7
 
7
8
  // --- Types ---
8
9
 
@@ -19,19 +20,22 @@ type EventHandler = (payload: unknown) => void
19
20
  const GK = '__swarmclaw_ocgateway__' as const
20
21
 
21
22
  interface GatewayState {
22
- instance: OpenClawGateway | null
23
+ instances: Map<string, OpenClawGateway>
24
+ activeKey: string | null
23
25
  }
24
26
 
25
27
  function getState(): GatewayState {
26
28
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
29
  const g = globalThis as any
28
- if (!g[GK]) g[GK] = { instance: null }
30
+ if (!g[GK]) g[GK] = { instances: new Map<string, OpenClawGateway>(), activeKey: null }
29
31
  return g[GK] as GatewayState
30
32
  }
31
33
 
32
34
  // --- Helper: resolve gateway config from first OpenClaw agent ---
33
35
 
34
36
  interface GatewayConfig {
37
+ key: string
38
+ profileId?: string | null
35
39
  wsUrl: string
36
40
  token: string | undefined
37
41
  }
@@ -43,27 +47,79 @@ function normalizeWsUrl(raw: string): string {
43
47
  return url.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:')
44
48
  }
45
49
 
46
- export function resolveGatewayConfig(): GatewayConfig | null {
47
- const agents = loadAgents({ includeTrashed: true })
50
+ function resolveTokenForCredential(credentialId?: string | null): string | undefined {
51
+ const id = typeof credentialId === 'string' && credentialId.trim() ? credentialId.trim() : ''
52
+ if (!id) return undefined
48
53
  const creds = loadCredentials()
54
+ const cred = creds[id]
55
+ if (!cred?.encryptedKey) return undefined
56
+ try {
57
+ return decryptKey(cred.encryptedKey)
58
+ } catch {
59
+ return undefined
60
+ }
61
+ }
62
+
63
+ export function resolveGatewayConfig(target?: {
64
+ profileId?: string | null
65
+ agentId?: string | null
66
+ }): GatewayConfig | null {
67
+ const profileId = typeof target?.profileId === 'string' ? target.profileId.trim() : ''
68
+ if (profileId) {
69
+ const profile = getGatewayProfile(profileId)
70
+ if (!profile) return null
71
+ return {
72
+ key: `profile:${profile.id}`,
73
+ profileId: profile.id,
74
+ wsUrl: profile.wsUrl ? normalizeWsUrl(profile.wsUrl) : normalizeWsUrl(profile.endpoint),
75
+ token: resolveTokenForCredential(profile.credentialId),
76
+ }
77
+ }
78
+
79
+ const agentId = typeof target?.agentId === 'string' ? target.agentId.trim() : ''
80
+ if (agentId) {
81
+ const agents = loadAgents({ includeTrashed: true })
82
+ const agent = agents[agentId]
83
+ const route = resolvePrimaryAgentRoute(agent)
84
+ if (route?.provider === 'openclaw') {
85
+ return {
86
+ key: route.gatewayProfileId ? `profile:${route.gatewayProfileId}` : `agent:${agentId}`,
87
+ profileId: route.gatewayProfileId ?? null,
88
+ wsUrl: normalizeWsUrl(route.apiEndpoint || 'ws://127.0.0.1:18789'),
89
+ token: resolveTokenForCredential(route.credentialId),
90
+ }
91
+ }
92
+ }
93
+
94
+ const gatewayProfiles = getGatewayProfiles('openclaw')
95
+ if (gatewayProfiles[0]) {
96
+ const profile = gatewayProfiles[0]
97
+ return {
98
+ key: `profile:${profile.id}`,
99
+ profileId: profile.id,
100
+ wsUrl: profile.wsUrl ? normalizeWsUrl(profile.wsUrl) : normalizeWsUrl(profile.endpoint),
101
+ token: resolveTokenForCredential(profile.credentialId),
102
+ }
103
+ }
104
+
105
+ const agents = loadAgents({ includeTrashed: true })
49
106
  for (const agent of Object.values(agents)) {
50
107
  if (agent?.provider !== 'openclaw') continue
51
108
  const wsUrl = agent.apiEndpoint
52
109
  ? normalizeWsUrl(agent.apiEndpoint)
53
110
  : 'ws://127.0.0.1:18789'
54
- let token: string | undefined
55
- if (agent.credentialId) {
56
- const cred = creds[agent.credentialId]
57
- if (cred?.encryptedKey) {
58
- try { token = decryptKey(cred.encryptedKey) } catch { /* ignore */ }
59
- }
111
+ return {
112
+ key: `agent:${agent.id}`,
113
+ profileId: agent.gatewayProfileId ?? null,
114
+ wsUrl,
115
+ token: resolveTokenForCredential(agent.credentialId),
60
116
  }
61
- return { wsUrl, token }
62
117
  }
63
118
  return null
64
119
  }
65
120
 
66
121
  export function hasOpenClawAgents(): boolean {
122
+ if (getGatewayProfiles('openclaw').length > 0) return true
67
123
  const agents = loadAgents({ includeTrashed: true })
68
124
  return Object.values(agents).some((a) => a?.provider === 'openclaw' && !a.trashedAt)
69
125
  }
@@ -253,47 +309,78 @@ export class OpenClawGateway {
253
309
 
254
310
  // --- Singleton access ---
255
311
 
256
- export function getGateway(): OpenClawGateway | null {
257
- return getState().instance
312
+ export function getGateway(profileId?: string | null): OpenClawGateway | null {
313
+ const state = getState()
314
+ const key = typeof profileId === 'string' && profileId.trim() ? `profile:${profileId.trim()}` : null
315
+ if (key) {
316
+ return state.instances.get(key) || null
317
+ }
318
+ if (state.activeKey) {
319
+ return state.instances.get(state.activeKey) || null
320
+ }
321
+ for (const instance of state.instances.values()) {
322
+ if (instance.connected) return instance
323
+ }
324
+ return null
258
325
  }
259
326
 
260
- export async function ensureGatewayConnected(): Promise<OpenClawGateway | null> {
327
+ export async function ensureGatewayConnected(target?: {
328
+ profileId?: string | null
329
+ agentId?: string | null
330
+ }): Promise<OpenClawGateway | null> {
261
331
  const state = getState()
262
- if (state.instance?.connected) return state.instance
263
-
264
- const config = resolveGatewayConfig()
332
+ const config = resolveGatewayConfig(target)
265
333
  if (!config) return null
266
-
267
- if (!state.instance) {
268
- state.instance = new OpenClawGateway()
334
+ const existing = state.instances.get(config.key)
335
+ if (existing?.connected) {
336
+ state.activeKey = config.key
337
+ return existing
269
338
  }
270
339
 
271
- const ok = await state.instance.connect(config.wsUrl, config.token)
272
- return ok ? state.instance : null
340
+ const instance = existing || new OpenClawGateway()
341
+ state.instances.set(config.key, instance)
342
+ const ok = await instance.connect(config.wsUrl, config.token)
343
+ if (ok) {
344
+ state.activeKey = config.key
345
+ return instance
346
+ }
347
+ return null
273
348
  }
274
349
 
275
- export function disconnectGateway() {
350
+ export function disconnectGateway(profileId?: string | null) {
276
351
  const state = getState()
277
- if (state.instance) {
278
- state.instance.disconnect()
279
- state.instance = null
352
+ const key = typeof profileId === 'string' && profileId.trim() ? `profile:${profileId.trim()}` : null
353
+ if (key) {
354
+ const instance = state.instances.get(key)
355
+ if (instance) {
356
+ instance.disconnect()
357
+ state.instances.delete(key)
358
+ if (state.activeKey === key) state.activeKey = null
359
+ }
360
+ return
361
+ }
362
+ for (const [instanceKey, instance] of state.instances.entries()) {
363
+ instance.disconnect()
364
+ state.instances.delete(instanceKey)
280
365
  }
366
+ state.activeKey = null
281
367
  }
282
368
 
283
369
  /** Manual connect with explicit URL/token (used by gateway connection panel) */
284
- export async function manualConnect(url?: string, token?: string): Promise<boolean> {
370
+ export async function manualConnect(url?: string, token?: string, profileId?: string | null): Promise<boolean> {
285
371
  const state = getState()
286
- if (state.instance?.connected) {
287
- state.instance.disconnect()
372
+ const config = resolveGatewayConfig({ profileId: profileId || null })
373
+ const key = profileId ? `profile:${profileId}` : '__manual__'
374
+ const instance = state.instances.get(key) || new OpenClawGateway()
375
+ if (instance.connected) {
376
+ instance.disconnect()
288
377
  }
289
-
290
- const config = resolveGatewayConfig()
378
+ state.instances.set(key, instance)
291
379
  const wsUrl = url ? normalizeWsUrl(url) : config?.wsUrl ?? 'ws://127.0.0.1:18789'
292
380
  const resolvedToken = token ?? config?.token
293
-
294
- if (!state.instance) {
295
- state.instance = new OpenClawGateway()
381
+ const ok = await instance.connect(wsUrl, resolvedToken)
382
+ if (ok) {
383
+ state.activeKey = key
296
384
  }
297
-
298
- return state.instance.connect(wsUrl, resolvedToken)
385
+ return ok
299
386
  }
@@ -612,8 +612,7 @@ export async function executeLangGraphOrchestrator(
612
612
  const completeMatch = finalResult.match(/ORCHESTRATION_COMPLETE:\s*([\s\S]+)/)
613
613
  const summary = completeMatch ? completeMatch[1].trim() : finalResult
614
614
 
615
- // Append meta so the main-loop knows we are done
616
- return `${summary}\n\n[MAIN_LOOP_META] {"status":"ok", "follow_up":false, "summary":${JSON.stringify(summary.slice(0, 300))}, "mission_task_id":${JSON.stringify(taskId || null)}}`
615
+ return summary
617
616
  }
618
617
 
619
618
  /**
@@ -86,6 +86,7 @@ async function executeOrchestratorLegacy(
86
86
  sessionId: string,
87
87
  taskId?: string,
88
88
  ): Promise<string> {
89
+ void taskId
89
90
  const allAgents = loadAgents()
90
91
  const sessions = loadSessions()
91
92
  const session = sessions[sessionId]
@@ -241,7 +242,7 @@ async function executeOrchestratorLegacy(
241
242
 
242
243
  if (cmd.done) {
243
244
  result = cmd.summary || fullText
244
- return `${result}\n\n[MAIN_LOOP_META] {"status":"ok", "follow_up":false, "summary":${JSON.stringify(result.slice(0, 300))}, "mission_task_id":${JSON.stringify(taskId || null)}}`
245
+ return result
245
246
  }
246
247
  }
247
248
 
@@ -256,7 +257,7 @@ async function executeOrchestratorLegacy(
256
257
  result = `Loop stopped after reaching max turns (${maxTurns}).`
257
258
  }
258
259
 
259
- return `${result}\n\n[MAIN_LOOP_META] {"status":"ok", "follow_up":false, "summary":${JSON.stringify(result.slice(0, 300))}, "mission_task_id":${JSON.stringify(taskId || null)}}`
260
+ return result
260
261
  }
261
262
 
262
263
  async function executeSubTask(
@@ -3,7 +3,7 @@ import { describe, it } from 'node:test'
3
3
  import fs from 'node:fs'
4
4
  import path from 'node:path'
5
5
  import { getPluginManager, normalizeMarketplacePluginUrl, sanitizePluginFilename } from './plugins'
6
- import { canonicalizePluginId, expandPluginIds } from './tool-aliases'
6
+ import { canonicalizePluginId, expandPluginIds, pluginIdMatches } from './tool-aliases'
7
7
  import { DATA_DIR } from './data-dir'
8
8
 
9
9
  let testPluginSeq = 0
@@ -33,6 +33,14 @@ describe('plugin id canonicalization', () => {
33
33
  assert.equal(expanded.includes('ask_human'), true)
34
34
  assert.equal(expanded.includes('human_loop'), true)
35
35
  })
36
+
37
+ it('does not expand a specific platform tool back into manage_platform', () => {
38
+ const expanded = expandPluginIds(['manage_schedules'])
39
+ assert.equal(expanded.includes('manage_schedules'), true)
40
+ assert.equal(expanded.includes('manage_platform'), false)
41
+ assert.equal(pluginIdMatches(['manage_platform'], 'manage_schedules'), true)
42
+ assert.equal(pluginIdMatches(['manage_schedules'], 'manage_platform'), false)
43
+ })
36
44
  })
37
45
 
38
46
  describe('plugin install helpers', () => {
@@ -510,12 +510,22 @@ class PluginManager {
510
510
  if (this.watcher) return
511
511
  try {
512
512
  this.ensurePluginDirs()
513
- this.watcher = fs.watch(PLUGINS_DIR, (_eventType, filename) => {
513
+ const watcher = fs.watch(PLUGINS_DIR, (_eventType, filename) => {
514
514
  if (!filename || (!filename.endsWith('.js') && !filename.endsWith('.mjs'))) return
515
515
  this.loaded = false
516
516
  notify('plugins')
517
517
  })
518
- this.watcher.unref?.()
518
+ watcher.on('error', (err: unknown) => {
519
+ log.warn('plugins', 'Plugin watcher disabled after runtime watch failure', {
520
+ error: err instanceof Error ? err.message : String(err),
521
+ })
522
+ if (this.watcher === watcher) {
523
+ try { watcher.close() } catch { /* ignore */ }
524
+ this.watcher = null
525
+ }
526
+ })
527
+ watcher.unref?.()
528
+ this.watcher = watcher
519
529
  } catch (err: unknown) {
520
530
  log.warn('plugins', 'Failed to watch plugins directory', {
521
531
  error: err instanceof Error ? err.message : String(err),