@swarmclawai/swarmclaw 1.2.5 → 1.2.8
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.
- package/README.md +24 -17
- package/next.config.ts +1 -0
- package/package.json +3 -2
- package/scripts/easy-setup.mjs +1 -1
- package/scripts/postinstall.mjs +1 -1
- package/skills/swarmclaw.md +115 -0
- package/skills/tools/browser.md +131 -0
- package/skills/tools/execute.md +98 -0
- package/skills/tools/files.md +98 -0
- package/skills/tools/memory.md +104 -0
- package/skills/tools/platform.md +144 -0
- package/skills/tools/skills.md +83 -0
- package/src/app/api/chats/[id]/messages/route.ts +23 -19
- package/src/app/api/chats/messages-route.test.ts +105 -51
- package/src/app/api/mcp-servers/[id]/test/route.ts +3 -2
- package/src/app/api/openclaw/deploy/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.ts +10 -2
- package/src/app/api/setup/doctor/route.ts +4 -4
- package/src/components/agents/agent-chat-list.tsx +23 -1
- package/src/components/agents/inspector-panel.tsx +165 -48
- package/src/components/chat/chat-area.tsx +38 -9
- package/src/components/chat/message-list.tsx +33 -19
- package/src/components/gateways/gateway-sheet.tsx +5 -2
- package/src/lib/agent-execute-defaults.test.ts +24 -0
- package/src/lib/agent-execute-defaults.ts +62 -0
- package/src/lib/chat/queued-message-queue.test.ts +134 -1
- package/src/lib/chat/queued-message-queue.ts +77 -2
- package/src/lib/providers/index.test.ts +108 -0
- package/src/lib/providers/index.ts +38 -15
- package/src/lib/server/agents/agent-service.ts +5 -0
- package/src/lib/server/builtin-extensions.ts +1 -0
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +1 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +2 -2
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +79 -42
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -0
- package/src/lib/server/chat-execution/continuation-evaluator.ts +8 -0
- package/src/lib/server/chat-execution/memory-mutation-tools.ts +1 -1
- package/src/lib/server/chat-execution/message-classifier.ts +11 -1
- package/src/lib/server/chat-execution/prompt-builder.test.ts +28 -0
- package/src/lib/server/chat-execution/prompt-builder.ts +14 -1
- package/src/lib/server/chat-execution/prompt-mode.test.ts +24 -0
- package/src/lib/server/chat-execution/prompt-mode.ts +5 -1
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +6 -4
- package/src/lib/server/chat-execution/stream-agent-chat.ts +45 -16
- package/src/lib/server/chatrooms/chatroom-routing.test.ts +4 -0
- package/src/lib/server/connectors/discord.ts +2 -2
- package/src/lib/server/connectors/matrix.ts +3 -2
- package/src/lib/server/connectors/signal.ts +5 -4
- package/src/lib/server/connectors/slack.ts +10 -9
- package/src/lib/server/connectors/teams.ts +3 -2
- package/src/lib/server/connectors/telegram.ts +4 -4
- package/src/lib/server/connectors/whatsapp.ts +2 -2
- package/src/lib/server/daemon/controller.ts +7 -0
- package/src/lib/server/gateways/gateway-profile-service.ts +19 -1
- package/src/lib/server/messages/message-repository.test.ts +70 -0
- package/src/lib/server/messages/message-repository.ts +11 -6
- package/src/lib/server/openclaw/deploy.ts +32 -2
- package/src/lib/server/plugins-advanced.test.ts +1 -2
- package/src/lib/server/provider-health.ts +1 -1
- package/src/lib/server/runtime/process-manager.ts +13 -9
- package/src/lib/server/runtime/session-run-manager/queries.ts +15 -0
- package/src/lib/server/runtime/session-run-manager.test.ts +58 -0
- package/src/lib/server/sandbox/session-runtime.test.ts +18 -1
- package/src/lib/server/sandbox/session-runtime.ts +40 -28
- package/src/lib/server/session-tools/autonomy-tools.test.ts +7 -9
- package/src/lib/server/session-tools/context.ts +1 -1
- package/src/lib/server/session-tools/credential-env.ts +109 -0
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/edit_file.ts +3 -2
- package/src/lib/server/session-tools/execute.test.ts +58 -0
- package/src/lib/server/session-tools/execute.ts +334 -0
- package/src/lib/server/session-tools/files-tool.ts +635 -0
- package/src/lib/server/session-tools/index.ts +14 -4
- package/src/lib/server/session-tools/memory-tool.ts +242 -0
- package/src/lib/server/session-tools/memory.ts +1 -1
- package/src/lib/server/session-tools/openclaw-nodes.ts +3 -2
- package/src/lib/server/session-tools/openclaw-workspace.ts +3 -2
- package/src/lib/server/session-tools/platform-tool.ts +617 -0
- package/src/lib/server/session-tools/session-info.ts +3 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +3 -4
- package/src/lib/server/session-tools/shell.ts +7 -122
- package/src/lib/server/session-tools/skills-tool.ts +396 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/storage-normalization.ts +2 -0
- package/src/lib/server/tool-aliases.ts +2 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +9 -2
- package/src/lib/server/tool-capability-policy.test.ts +2 -1
- package/src/lib/server/tool-capability-policy.ts +60 -33
- package/src/lib/server/tool-planning.ts +11 -0
- package/src/lib/setup-defaults.ts +5 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.test.ts +16 -0
- package/src/lib/validation/schemas.ts +16 -0
- package/src/stores/use-chat-store.test.ts +231 -0
- package/src/stores/use-chat-store.ts +62 -13
- package/src/types/agent.ts +348 -0
- package/src/types/app-settings.ts +175 -0
- package/src/types/approval.ts +27 -0
- package/src/types/connector.ts +187 -0
- package/src/types/extension.ts +386 -0
- package/src/types/index.ts +16 -3555
- package/src/types/message.ts +57 -0
- package/src/types/misc.ts +739 -0
- package/src/types/mission.ts +185 -0
- package/src/types/protocol.ts +422 -0
- package/src/types/provider.ts +52 -0
- package/src/types/run.ts +183 -0
- package/src/types/schedule.ts +59 -0
- package/src/types/session.ts +265 -0
- package/src/types/skill.ts +157 -0
- package/src/types/task.ts +140 -0
- package/src/types/working-state.ts +211 -0
- package/src/views/settings/section-heartbeat.tsx +2 -2
- package/src/lib/server/session-tools/sandbox.ts +0 -281
|
@@ -4,6 +4,7 @@ import path from 'path'
|
|
|
4
4
|
import { DATA_DIR } from '../data-dir'
|
|
5
5
|
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
6
6
|
import { resolveConnectorIngressReply } from './ingress-delivery'
|
|
7
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
7
8
|
|
|
8
9
|
const TAG = 'matrix'
|
|
9
10
|
|
|
@@ -54,8 +55,8 @@ const matrix: PlatformConnector = {
|
|
|
54
55
|
const reply = await resolveConnectorIngressReply(onMessage, inbound)
|
|
55
56
|
if (!reply) return
|
|
56
57
|
await client.sendText(roomId, reply.visibleText)
|
|
57
|
-
} catch (err:
|
|
58
|
-
log.error(TAG, 'Error handling message:', err
|
|
58
|
+
} catch (err: unknown) {
|
|
59
|
+
log.error(TAG, 'Error handling message:', errorMessage(err))
|
|
59
60
|
try {
|
|
60
61
|
await client.sendText(roomId, 'Sorry, I encountered an error processing your message.')
|
|
61
62
|
} catch { /* ignore */ }
|
|
@@ -4,6 +4,7 @@ import type { ChildProcess } from 'child_process'
|
|
|
4
4
|
import type { Connector } from '@/types'
|
|
5
5
|
import type { PlatformConnector, ConnectorInstance, InboundMessage, ConnectorIngressResult } from './types'
|
|
6
6
|
import { resolveConnectorIngressReply } from './ingress-delivery'
|
|
7
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
7
8
|
|
|
8
9
|
const TAG = 'signal'
|
|
9
10
|
|
|
@@ -107,8 +108,8 @@ const signal: PlatformConnector = {
|
|
|
107
108
|
`${cliPath} -u ${phoneNumber} send -m ${JSON.stringify(text)} ${channelId}`,
|
|
108
109
|
{ timeout: 15_000 },
|
|
109
110
|
)
|
|
110
|
-
} catch (err:
|
|
111
|
-
throw new Error(`Signal send failed: ${err
|
|
111
|
+
} catch (err: unknown) {
|
|
112
|
+
throw new Error(`Signal send failed: ${errorMessage(err)}`)
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
},
|
|
@@ -179,8 +180,8 @@ export async function handleSignalEvent(
|
|
|
179
180
|
{ timeout: 15_000 },
|
|
180
181
|
)
|
|
181
182
|
}
|
|
182
|
-
} catch (err:
|
|
183
|
-
log.error(TAG, 'Error handling message:', err
|
|
183
|
+
} catch (err: unknown) {
|
|
184
|
+
log.error(TAG, 'Error handling message:', errorMessage(err))
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
|
|
@@ -145,11 +145,12 @@ const slack: PlatformConnector = {
|
|
|
145
145
|
}
|
|
146
146
|
botUserId = auth.user_id as string
|
|
147
147
|
log.info(TAG, `Authenticated as @${auth.user} in workspace "${auth.team}"`)
|
|
148
|
-
} catch (err:
|
|
149
|
-
const hint = err.code
|
|
148
|
+
} catch (err: unknown) {
|
|
149
|
+
const hint = (err instanceof Error && 'code' in err) ? (err as { code: string }).code : undefined
|
|
150
|
+
const suffix = hint === 'slack_webapi_platform_error'
|
|
150
151
|
? '. Check that your Bot Token (xoxb-...) is correct and the app is installed to the workspace.'
|
|
151
152
|
: ''
|
|
152
|
-
throw new Error(`Slack auth failed: ${err
|
|
153
|
+
throw new Error(`Slack auth failed: ${errorMessage(err)}${suffix}`)
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
const app = new App({
|
|
@@ -215,8 +216,8 @@ const slack: PlatformConnector = {
|
|
|
215
216
|
media.push(stored)
|
|
216
217
|
continue
|
|
217
218
|
}
|
|
218
|
-
} catch (err:
|
|
219
|
-
log.warn(TAG, `Media download failed (${f?.name || 'file'}):`,
|
|
219
|
+
} catch (err: unknown) {
|
|
220
|
+
log.warn(TAG, `Media download failed (${f?.name || 'file'}):`, errorMessage(err))
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
media.push({
|
|
@@ -264,8 +265,8 @@ const slack: PlatformConnector = {
|
|
|
264
265
|
return sent.ts || undefined
|
|
265
266
|
},
|
|
266
267
|
})
|
|
267
|
-
} catch (err:
|
|
268
|
-
log.error(TAG, 'Error handling message:', err
|
|
268
|
+
} catch (err: unknown) {
|
|
269
|
+
log.error(TAG, 'Error handling message:', errorMessage(err))
|
|
269
270
|
try {
|
|
270
271
|
await say('Sorry, I encountered an error processing your message.')
|
|
271
272
|
} catch { /* ignore */ }
|
|
@@ -322,8 +323,8 @@ const slack: PlatformConnector = {
|
|
|
322
323
|
return sent.ts || undefined
|
|
323
324
|
},
|
|
324
325
|
})
|
|
325
|
-
} catch (err:
|
|
326
|
-
log.error(TAG, 'Error handling mention:', err
|
|
326
|
+
} catch (err: unknown) {
|
|
327
|
+
log.error(TAG, 'Error handling mention:', errorMessage(err))
|
|
327
328
|
}
|
|
328
329
|
})
|
|
329
330
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { log } from '@/lib/server/logger'
|
|
2
2
|
import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
|
|
3
3
|
import { resolveConnectorIngressReply } from './ingress-delivery'
|
|
4
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
4
5
|
|
|
5
6
|
const TAG = 'teams'
|
|
6
7
|
|
|
@@ -51,8 +52,8 @@ const teams: PlatformConnector = {
|
|
|
51
52
|
const reply = await resolveConnectorIngressReply(onMessage, inbound)
|
|
52
53
|
if (!reply) return
|
|
53
54
|
await context.sendActivity(reply.visibleText)
|
|
54
|
-
} catch (err:
|
|
55
|
-
log.error(TAG, 'Error handling message:', err
|
|
55
|
+
} catch (err: unknown) {
|
|
56
|
+
log.error(TAG, 'Error handling message:', errorMessage(err))
|
|
56
57
|
try {
|
|
57
58
|
await context.sendActivity('Sorry, I encountered an error processing your message.')
|
|
58
59
|
} catch { /* ignore */ }
|
|
@@ -127,8 +127,8 @@ const telegram: PlatformConnector = {
|
|
|
127
127
|
mimeType: m.mimeType,
|
|
128
128
|
})
|
|
129
129
|
if (stored) media.push(stored)
|
|
130
|
-
} catch (err:
|
|
131
|
-
log.warn(TAG, `Failed to fetch media ${m.fileId}:`,
|
|
130
|
+
} catch (err: unknown) {
|
|
131
|
+
log.warn(TAG, `Failed to fetch media ${m.fileId}:`, errorMessage(err))
|
|
132
132
|
media.push({
|
|
133
133
|
type: m.type,
|
|
134
134
|
fileName: m.fileName,
|
|
@@ -177,8 +177,8 @@ const telegram: PlatformConnector = {
|
|
|
177
177
|
return String(sent.message_id)
|
|
178
178
|
},
|
|
179
179
|
})
|
|
180
|
-
} catch (err:
|
|
181
|
-
log.error(TAG, 'Error handling message:', err
|
|
180
|
+
} catch (err: unknown) {
|
|
181
|
+
log.error(TAG, 'Error handling message:', errorMessage(err))
|
|
182
182
|
try {
|
|
183
183
|
await ctx.reply('Sorry, I encountered an error processing your message.')
|
|
184
184
|
} catch { /* ignore */ }
|
|
@@ -714,8 +714,8 @@ const whatsapp: PlatformConnector = {
|
|
|
714
714
|
fileName: mediaCandidate.payload?.fileName || undefined,
|
|
715
715
|
})
|
|
716
716
|
media.push(saved)
|
|
717
|
-
} catch (err:
|
|
718
|
-
log.error(TAG, `Failed to decode media: ${
|
|
717
|
+
} catch (err: unknown) {
|
|
718
|
+
log.error(TAG, `Failed to decode media: ${errorMessage(err)}`)
|
|
719
719
|
media.push({
|
|
720
720
|
type: mediaCandidate.kind,
|
|
721
721
|
fileName: mediaCandidate.payload?.fileName || undefined,
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
} from '@/lib/server/daemon/types'
|
|
26
26
|
import { DATA_DIR } from '@/lib/server/data-dir'
|
|
27
27
|
import { loadEstopState } from '@/lib/server/runtime/estop'
|
|
28
|
+
import { getDaemonStatus } from '@/lib/server/runtime/daemon-state/core'
|
|
28
29
|
import { daemonAutostartEnvEnabled } from '@/lib/server/runtime/daemon-policy'
|
|
29
30
|
import {
|
|
30
31
|
releaseRuntimeLock,
|
|
@@ -344,6 +345,12 @@ export async function ensureDaemonProcessRunning(
|
|
|
344
345
|
source: string,
|
|
345
346
|
opts?: { manualStart?: boolean },
|
|
346
347
|
): Promise<boolean> {
|
|
348
|
+
// In dev mode, the daemon may already be running in-process (same Next.js server)
|
|
349
|
+
// without a daemon-admin.json file. Check in-process state first to avoid spawning
|
|
350
|
+
// a subprocess that fails to acquire the already-held lease.
|
|
351
|
+
const inProcessStatus = getDaemonStatus()
|
|
352
|
+
if (inProcessStatus.running) return false
|
|
353
|
+
|
|
347
354
|
const manualStart = opts?.manualStart === true
|
|
348
355
|
const record = loadDaemonStatusRecord()
|
|
349
356
|
if (loadEstopState().level !== 'none') return false
|
|
@@ -4,6 +4,7 @@ import { genId } from '@/lib/id'
|
|
|
4
4
|
import { normalizeOpenClawEndpoint } from '@/lib/openclaw/openclaw-endpoint'
|
|
5
5
|
import { listAgents, saveAgentMany } from '@/lib/server/agents/agent-repository'
|
|
6
6
|
import { getGatewayProfiles } from '@/lib/server/agents/agent-runtime-config'
|
|
7
|
+
import { deleteCredentialRecord } from '@/lib/server/credentials/credential-service'
|
|
7
8
|
import {
|
|
8
9
|
loadGatewayProfile,
|
|
9
10
|
loadGatewayProfiles,
|
|
@@ -161,7 +162,9 @@ export function updateGatewayProfile(id: string, input: Record<string, unknown>)
|
|
|
161
162
|
|
|
162
163
|
export function deleteGatewayProfileAndDetachAgents(id: string): boolean {
|
|
163
164
|
const gateways = loadGatewayProfiles()
|
|
164
|
-
|
|
165
|
+
const deleted = gateways[id]
|
|
166
|
+
if (!deleted) return false
|
|
167
|
+
const orphanCredentialId = deleted.credentialId || null
|
|
165
168
|
delete gateways[id]
|
|
166
169
|
saveGatewayProfiles(gateways)
|
|
167
170
|
|
|
@@ -195,6 +198,21 @@ export function deleteGatewayProfileAndDetachAgents(id: string): boolean {
|
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
if (changed.length > 0) saveAgentMany(changed)
|
|
201
|
+
|
|
202
|
+
// Clean up orphaned credential if no other gateway or agent references it
|
|
203
|
+
if (orphanCredentialId) {
|
|
204
|
+
const stillReferencedByGateway = Object.values(gateways).some(
|
|
205
|
+
(gw) => gw && gw.credentialId === orphanCredentialId,
|
|
206
|
+
)
|
|
207
|
+
const stillReferencedByAgent = !stillReferencedByGateway && Object.values(agents).some(
|
|
208
|
+
(a) => a.credentialId === orphanCredentialId
|
|
209
|
+
|| (Array.isArray(a.fallbackCredentialIds) && a.fallbackCredentialIds.includes(orphanCredentialId)),
|
|
210
|
+
)
|
|
211
|
+
if (!stillReferencedByGateway && !stillReferencedByAgent) {
|
|
212
|
+
deleteCredentialRecord(orphanCredentialId)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
198
216
|
notify('gateways')
|
|
199
217
|
return true
|
|
200
218
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
4
|
+
|
|
5
|
+
test('appendMessage notifies both generic and per-session message topics', () => {
|
|
6
|
+
const output = runWithTempDataDir<{
|
|
7
|
+
genericTopics: string[]
|
|
8
|
+
sessionTopics: string[]
|
|
9
|
+
}>(`
|
|
10
|
+
const { WebSocket } = await import('ws')
|
|
11
|
+
const storageMod = await import('@/lib/server/storage')
|
|
12
|
+
const repoMod = await import('@/lib/server/messages/message-repository')
|
|
13
|
+
const storage = storageMod.default || storageMod
|
|
14
|
+
const repo = repoMod.default || repoMod
|
|
15
|
+
|
|
16
|
+
storage.saveSessions({
|
|
17
|
+
'sess-notify': {
|
|
18
|
+
id: 'sess-notify',
|
|
19
|
+
name: 'Notify Session',
|
|
20
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
21
|
+
user: 'tester',
|
|
22
|
+
provider: 'openai',
|
|
23
|
+
model: 'gpt-5',
|
|
24
|
+
claudeSessionId: null,
|
|
25
|
+
codexThreadId: null,
|
|
26
|
+
opencodeSessionId: null,
|
|
27
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
28
|
+
messages: [],
|
|
29
|
+
createdAt: Date.now(),
|
|
30
|
+
lastActiveAt: Date.now(),
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const genericPayloads = []
|
|
35
|
+
const sessionPayloads = []
|
|
36
|
+
globalThis.__swarmclaw_ws__ = {
|
|
37
|
+
wss: null,
|
|
38
|
+
clients: new Set([
|
|
39
|
+
{
|
|
40
|
+
ws: {
|
|
41
|
+
readyState: WebSocket.OPEN,
|
|
42
|
+
send(payload) { genericPayloads.push(JSON.parse(payload)) },
|
|
43
|
+
},
|
|
44
|
+
topics: new Set(['messages']),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
ws: {
|
|
48
|
+
readyState: WebSocket.OPEN,
|
|
49
|
+
send(payload) { sessionPayloads.push(JSON.parse(payload)) },
|
|
50
|
+
},
|
|
51
|
+
topics: new Set(['messages:sess-notify']),
|
|
52
|
+
},
|
|
53
|
+
]),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
repo.appendMessage('sess-notify', {
|
|
57
|
+
role: 'user',
|
|
58
|
+
text: 'hello',
|
|
59
|
+
time: 1,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
console.log(JSON.stringify({
|
|
63
|
+
genericTopics: genericPayloads.map((payload) => payload.topic),
|
|
64
|
+
sessionTopics: sessionPayloads.map((payload) => payload.topic),
|
|
65
|
+
}))
|
|
66
|
+
`, { prefix: 'swarmclaw-message-repo-notify-' })
|
|
67
|
+
|
|
68
|
+
assert.deepEqual(output.genericTopics, ['messages'])
|
|
69
|
+
assert.deepEqual(output.sessionTopics, ['messages:sess-notify'])
|
|
70
|
+
})
|
|
@@ -107,6 +107,11 @@ function syncSessionMeta(sessionId: string): void {
|
|
|
107
107
|
})
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
function notifyMessageTopics(sessionId: string, action: string): void {
|
|
111
|
+
notify('messages', action, sessionId)
|
|
112
|
+
notify(`messages:${sessionId}`, action)
|
|
113
|
+
}
|
|
114
|
+
|
|
110
115
|
// ---------------------------------------------------------------------------
|
|
111
116
|
// Lazy migration — copies blob messages → table on first access
|
|
112
117
|
// ---------------------------------------------------------------------------
|
|
@@ -229,7 +234,7 @@ export function appendMessage(sessionId: string, message: Message): number {
|
|
|
229
234
|
const seq = nextSeq(sessionId)
|
|
230
235
|
stmts().insert.run(sessionId, seq, JSON.stringify(message))
|
|
231
236
|
syncSessionMeta(sessionId)
|
|
232
|
-
|
|
237
|
+
notifyMessageTopics(sessionId, 'append')
|
|
233
238
|
return seq
|
|
234
239
|
}, { sessionId })
|
|
235
240
|
}
|
|
@@ -247,7 +252,7 @@ export function appendMessages(sessionId: string, messages: Message[]): void {
|
|
|
247
252
|
}
|
|
248
253
|
})
|
|
249
254
|
syncSessionMeta(sessionId)
|
|
250
|
-
|
|
255
|
+
notifyMessageTopics(sessionId, 'append')
|
|
251
256
|
}, { sessionId, count: messages.length })
|
|
252
257
|
}
|
|
253
258
|
|
|
@@ -256,7 +261,7 @@ export function replaceMessageAt(sessionId: string, seq: number, message: Messag
|
|
|
256
261
|
perf.measureSync('message-repo', 'replaceMessageAt', () => {
|
|
257
262
|
stmts().update.run(JSON.stringify(message), sessionId, seq)
|
|
258
263
|
syncSessionMeta(sessionId)
|
|
259
|
-
|
|
264
|
+
notifyMessageTopics(sessionId, 'update')
|
|
260
265
|
}, { sessionId, seq })
|
|
261
266
|
}
|
|
262
267
|
|
|
@@ -265,7 +270,7 @@ export function truncateAfter(sessionId: string, seq: number): void {
|
|
|
265
270
|
perf.measureSync('message-repo', 'truncateAfter', () => {
|
|
266
271
|
stmts().deleteAfter.run(sessionId, seq)
|
|
267
272
|
syncSessionMeta(sessionId)
|
|
268
|
-
|
|
273
|
+
notifyMessageTopics(sessionId, 'truncate')
|
|
269
274
|
}, { sessionId, seq })
|
|
270
275
|
}
|
|
271
276
|
|
|
@@ -274,7 +279,7 @@ export function clearMessages(sessionId: string): void {
|
|
|
274
279
|
perf.measureSync('message-repo', 'clearMessages', () => {
|
|
275
280
|
stmts().deleteAll.run(sessionId)
|
|
276
281
|
syncSessionMeta(sessionId)
|
|
277
|
-
|
|
282
|
+
notifyMessageTopics(sessionId, 'clear')
|
|
278
283
|
}, { sessionId })
|
|
279
284
|
}
|
|
280
285
|
|
|
@@ -289,7 +294,7 @@ export function replaceAllMessages(sessionId: string, messages: Message[]): void
|
|
|
289
294
|
}
|
|
290
295
|
})
|
|
291
296
|
syncSessionMeta(sessionId)
|
|
292
|
-
|
|
297
|
+
notifyMessageTopics(sessionId, 'replace')
|
|
293
298
|
}, { sessionId, count: messages.length })
|
|
294
299
|
}
|
|
295
300
|
|
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
type ProcessStatus,
|
|
13
13
|
} from '@/lib/server/runtime/process-manager'
|
|
14
14
|
import { normalizeOpenClawEndpoint, deriveOpenClawWsUrl } from '@/lib/openclaw/openclaw-endpoint'
|
|
15
|
+
import { createCredentialRecord } from '@/lib/server/credentials/credential-service'
|
|
16
|
+
import { createGatewayProfile } from '@/lib/server/gateways/gateway-profile-service'
|
|
17
|
+
import { loadGatewayProfiles } from '@/lib/server/gateways/gateway-profile-repository'
|
|
15
18
|
import { probeOpenClawHealth, type OpenClawHealthResult } from './health'
|
|
16
19
|
import { DATA_DIR } from '../data-dir'
|
|
17
20
|
|
|
@@ -1046,7 +1049,7 @@ export async function startOpenClawLocalDeploy(input?: {
|
|
|
1046
1049
|
port?: number
|
|
1047
1050
|
token?: string | null
|
|
1048
1051
|
makePrimary?: boolean
|
|
1049
|
-
}): Promise<{ local: OpenClawLocalDeployStatus; locals: OpenClawLocalDeployStatus[]; token: string }> {
|
|
1052
|
+
}): Promise<{ local: OpenClawLocalDeployStatus; locals: OpenClawLocalDeployStatus[]; token: string; gatewayProfileId: string | null }> {
|
|
1050
1053
|
const state = getRuntimeState()
|
|
1051
1054
|
const port = sanitizeLocalPort(input?.port, DEFAULT_LOCAL_PORT)
|
|
1052
1055
|
const requestedLocalId = typeof input?.localId === 'string' && input.localId.trim()
|
|
@@ -1145,11 +1148,38 @@ export async function startOpenClawLocalDeploy(input?: {
|
|
|
1145
1148
|
|
|
1146
1149
|
await waitForLocalRuntime(result.processId)
|
|
1147
1150
|
|
|
1151
|
+
// Auto-provision gateway profile so agents can connect immediately
|
|
1152
|
+
const existingGateways = loadGatewayProfiles()
|
|
1153
|
+
const existingProfile = Object.values(existingGateways).find(
|
|
1154
|
+
(gw) => gw && gw.endpoint === endpoint,
|
|
1155
|
+
)
|
|
1156
|
+
let gatewayProfileId: string | null = null
|
|
1157
|
+
|
|
1158
|
+
if (existingProfile) {
|
|
1159
|
+
gatewayProfileId = existingProfile.id
|
|
1160
|
+
} else {
|
|
1161
|
+
const deployName = current.name || 'Local OpenClaw'
|
|
1162
|
+
const cred = createCredentialRecord({
|
|
1163
|
+
provider: 'openclaw',
|
|
1164
|
+
name: `${deployName} token`,
|
|
1165
|
+
apiKey: token,
|
|
1166
|
+
})
|
|
1167
|
+
const profile = createGatewayProfile({
|
|
1168
|
+
name: deployName,
|
|
1169
|
+
endpoint,
|
|
1170
|
+
credentialId: cred.id,
|
|
1171
|
+
isDefault: Object.keys(existingGateways).length === 0,
|
|
1172
|
+
deployment: { method: 'local', port, localId },
|
|
1173
|
+
})
|
|
1174
|
+
gatewayProfileId = profile.id
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1148
1177
|
const local = getOpenClawLocalDeployStatus(localId)
|
|
1149
1178
|
return {
|
|
1150
1179
|
local,
|
|
1151
1180
|
locals: getOpenClawLocalDeployStatuses(),
|
|
1152
1181
|
token,
|
|
1182
|
+
gatewayProfileId,
|
|
1153
1183
|
}
|
|
1154
1184
|
}
|
|
1155
1185
|
|
|
@@ -1190,7 +1220,7 @@ export async function restartOpenClawLocalDeploy(input?: {
|
|
|
1190
1220
|
port?: number
|
|
1191
1221
|
token?: string | null
|
|
1192
1222
|
makePrimary?: boolean
|
|
1193
|
-
}): Promise<{ local: OpenClawLocalDeployStatus; locals: OpenClawLocalDeployStatus[]; token: string }> {
|
|
1223
|
+
}): Promise<{ local: OpenClawLocalDeployStatus; locals: OpenClawLocalDeployStatus[]; token: string; gatewayProfileId: string | null }> {
|
|
1194
1224
|
const state = getRuntimeState()
|
|
1195
1225
|
const current = findLocalRuntimeState(state, {
|
|
1196
1226
|
localId: input?.localId ?? null,
|
|
@@ -340,8 +340,7 @@ describe('complex expansion scenarios', () => {
|
|
|
340
340
|
it('sandbox aliases expand', () => {
|
|
341
341
|
const result = expandExtensionIds(['sandbox'])
|
|
342
342
|
assert.ok(result.includes('sandbox'))
|
|
343
|
-
assert.ok(result.includes('
|
|
344
|
-
assert.ok(result.includes('sandbox_list_runtimes'))
|
|
343
|
+
assert.ok(result.includes('execute'))
|
|
345
344
|
})
|
|
346
345
|
|
|
347
346
|
it('files expands to include read_file, write_file, etc.', () => {
|
|
@@ -42,7 +42,7 @@ function commandExists(binary: string): boolean {
|
|
|
42
42
|
pruneTTLCache(cliCheckCache, CLI_CHECK_TTL_MS, CLI_CHECK_CACHE_MAX)
|
|
43
43
|
const probe = isWindows
|
|
44
44
|
? spawnSync('where', [binary], { timeout: 2000, stdio: 'pipe' })
|
|
45
|
-
: spawnSync('/bin/
|
|
45
|
+
: spawnSync(process.env.SHELL || '/bin/bash', ['-lc', `command -v ${binary} >/dev/null 2>&1`], { timeout: 2000 })
|
|
46
46
|
const ok = (probe.status ?? 1) === 0
|
|
47
47
|
cliCheckCache.set(binary, { at: now, ok })
|
|
48
48
|
return ok
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { genId } from '@/lib/id'
|
|
2
|
-
import { hmrSingleton, sleep } from '@/lib/shared-utils'
|
|
2
|
+
import { hmrSingleton, sleep, errorMessage } from '@/lib/shared-utils'
|
|
3
3
|
import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
|
|
4
4
|
import { detectDocker } from '@/lib/server/sandbox/docker-detect'
|
|
5
5
|
import { log } from '@/lib/server/logger'
|
|
@@ -169,7 +169,8 @@ export function buildDockerExecArgs(params: {
|
|
|
169
169
|
|
|
170
170
|
export function getShellCommand(command: string, processId?: string, sandbox?: SandboxOptions): { shell: string; args: string[]; containerName?: string } {
|
|
171
171
|
if (!sandbox) {
|
|
172
|
-
|
|
172
|
+
const shell = process.env.SHELL || '/bin/bash'
|
|
173
|
+
return { shell, args: ['-lc', command] }
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
if (sandbox.kind === 'persistent') {
|
|
@@ -247,9 +248,12 @@ export async function startManagedProcess(opts: StartProcessOptions): Promise<St
|
|
|
247
248
|
state.records.set(id, record)
|
|
248
249
|
|
|
249
250
|
const { shell, args } = getShellCommand(opts.command, id, opts.sandbox)
|
|
250
|
-
// Strip
|
|
251
|
-
//
|
|
252
|
-
|
|
251
|
+
// Strip env vars that should not leak into child processes:
|
|
252
|
+
// - PORT, ACCESS_KEY, NEXTAUTH_SECRET: SwarmClaw-specific bindings
|
|
253
|
+
// - npm_config_prefix: injected by npm when running scripts; conflicts with nvm
|
|
254
|
+
// in login shell subprocesses (nvm refuses to initialize when this is set,
|
|
255
|
+
// breaking node PATH resolution for all nvm users)
|
|
256
|
+
const stripKeys = new Set(['PORT', 'ACCESS_KEY', 'NEXTAUTH_SECRET', 'npm_config_prefix'])
|
|
253
257
|
const cleanEnv = Object.fromEntries(Object.entries(process.env).filter(([k]) => !stripKeys.has(k))) as NodeJS.ProcessEnv
|
|
254
258
|
const child = spawn(shell, args, {
|
|
255
259
|
cwd: opts.sandbox ? undefined : opts.cwd,
|
|
@@ -405,8 +409,8 @@ export function writeManagedProcessStdin(processId: string, data: string, eof?:
|
|
|
405
409
|
if (data) child.stdin.write(data)
|
|
406
410
|
if (eof) child.stdin.end()
|
|
407
411
|
return { ok: true }
|
|
408
|
-
} catch (err:
|
|
409
|
-
return { ok: false, error: err
|
|
412
|
+
} catch (err: unknown) {
|
|
413
|
+
return { ok: false, error: errorMessage(err) || String(err) }
|
|
410
414
|
}
|
|
411
415
|
}
|
|
412
416
|
|
|
@@ -418,8 +422,8 @@ export function killManagedProcess(processId: string, signal: NodeJS.Signals = '
|
|
|
418
422
|
rec.status = 'killed'
|
|
419
423
|
child.kill(signal)
|
|
420
424
|
return { ok: true }
|
|
421
|
-
} catch (err:
|
|
422
|
-
return { ok: false, error: err
|
|
425
|
+
} catch (err: unknown) {
|
|
426
|
+
return { ok: false, error: errorMessage(err) || String(err) }
|
|
423
427
|
}
|
|
424
428
|
}
|
|
425
429
|
|
|
@@ -49,12 +49,27 @@ function toQueuedTurn(entry: SessionRunQueueEntry, index: number): SessionQueued
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function toActiveTurn(entry: SessionRunQueueEntry): SessionQueuedTurn {
|
|
53
|
+
return {
|
|
54
|
+
...toQueuedTurn(entry, 0),
|
|
55
|
+
position: 0,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function visibleActiveTurnForSession(sessionId: string): SessionQueuedTurn | null {
|
|
60
|
+
const running = Array.from(state.runningByExecution.values())
|
|
61
|
+
.find((entry) => entry.run.sessionId === sessionId && entry.run.status === 'running')
|
|
62
|
+
if (!running || running.run.internal === true) return null
|
|
63
|
+
return toActiveTurn(running)
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
export function getSessionQueueSnapshot(sessionId: string): SessionQueueSnapshot {
|
|
53
67
|
const execution = getSessionExecutionState(sessionId)
|
|
54
68
|
const visibleQueued = visibleQueuedEntriesForSession(sessionId)
|
|
55
69
|
return {
|
|
56
70
|
sessionId,
|
|
57
71
|
activeRunId: execution.runningRunId || null,
|
|
72
|
+
activeTurn: visibleActiveTurnForSession(sessionId),
|
|
58
73
|
queueLength: visibleQueued.length,
|
|
59
74
|
items: visibleQueued.map((entry, index) => toQueuedTurn(entry, index)),
|
|
60
75
|
}
|
|
@@ -521,6 +521,37 @@ describe('session-run-manager', () => {
|
|
|
521
521
|
})
|
|
522
522
|
})
|
|
523
523
|
|
|
524
|
+
it('exposes the active user-visible turn separately from queued follow-ups', () => {
|
|
525
|
+
const running = enqueue({
|
|
526
|
+
sessionId: 'sess-active-turn',
|
|
527
|
+
message: 'running now',
|
|
528
|
+
source: 'chat',
|
|
529
|
+
})
|
|
530
|
+
const queued = enqueue({
|
|
531
|
+
sessionId: 'sess-active-turn',
|
|
532
|
+
message: 'queued next',
|
|
533
|
+
source: 'chat',
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
const snapshot = mgr.getSessionQueueSnapshot('sess-active-turn')
|
|
537
|
+
assert.equal(snapshot.activeRunId, running.runId)
|
|
538
|
+
assert.deepEqual(snapshot.activeTurn, {
|
|
539
|
+
runId: running.runId,
|
|
540
|
+
sessionId: 'sess-active-turn',
|
|
541
|
+
missionId: null,
|
|
542
|
+
text: 'running now',
|
|
543
|
+
queuedAt: snapshot.activeTurn?.queuedAt,
|
|
544
|
+
position: 0,
|
|
545
|
+
imagePath: undefined,
|
|
546
|
+
imageUrl: undefined,
|
|
547
|
+
attachedFiles: undefined,
|
|
548
|
+
replyToId: undefined,
|
|
549
|
+
source: 'chat',
|
|
550
|
+
})
|
|
551
|
+
assert.deepEqual(snapshot.items.map((item) => item.runId), [queued.runId])
|
|
552
|
+
assert.equal(snapshot.queueLength, 1)
|
|
553
|
+
})
|
|
554
|
+
|
|
524
555
|
it('hides internal queued runs from the user-visible queue snapshot', () => {
|
|
525
556
|
enqueue({ sessionId: 'sess-visible-queue', message: 'running' })
|
|
526
557
|
enqueue({
|
|
@@ -538,9 +569,36 @@ describe('session-run-manager', () => {
|
|
|
538
569
|
|
|
539
570
|
const snapshot = mgr.getSessionQueueSnapshot('sess-visible-queue')
|
|
540
571
|
assert.equal(snapshot.queueLength, 1)
|
|
572
|
+
assert.equal(snapshot.activeTurn?.runId, snapshot.activeRunId)
|
|
541
573
|
assert.deepEqual(snapshot.items.map((item) => item.runId), [visible.runId])
|
|
542
574
|
})
|
|
543
575
|
|
|
576
|
+
it('hides internal active runs from the active-turn snapshot field', () => {
|
|
577
|
+
const { entry, promise } = makeManualQueuedEntry({
|
|
578
|
+
sessionId: 'sess-hidden-active',
|
|
579
|
+
runId: 'run-hidden',
|
|
580
|
+
message: 'heartbeat hidden',
|
|
581
|
+
internal: true,
|
|
582
|
+
source: 'heartbeat',
|
|
583
|
+
})
|
|
584
|
+
entry.run.status = 'running'
|
|
585
|
+
entry.run.startedAt = Date.now()
|
|
586
|
+
const state = getRuntimeState()
|
|
587
|
+
state.runningByExecution.set(entry.executionKey, entry as unknown)
|
|
588
|
+
state.runs.set(entry.run.id, entry.run)
|
|
589
|
+
state.promises.set(entry.run.id, promise)
|
|
590
|
+
pendingPromises.push(promise.catch(() => {}))
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const snapshot = mgr.getSessionQueueSnapshot('sess-hidden-active')
|
|
594
|
+
assert.equal(snapshot.activeRunId, 'run-hidden')
|
|
595
|
+
assert.equal(snapshot.activeTurn, null)
|
|
596
|
+
assert.equal(snapshot.queueLength, 0)
|
|
597
|
+
} finally {
|
|
598
|
+
entry.resolve(undefined)
|
|
599
|
+
}
|
|
600
|
+
})
|
|
601
|
+
|
|
544
602
|
it('reports heartbeat vs non-heartbeat queued runs', () => {
|
|
545
603
|
enqueue({ sessionId: 'sess-hb-state', message: 'occupier' })
|
|
546
604
|
enqueue({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import test from 'node:test'
|
|
3
|
-
import { resolveSandboxRuntimeStatus, resolveSandboxWorkdir } from '@/lib/server/sandbox/session-runtime'
|
|
3
|
+
import { resolveSandboxRuntimeStatus, resolveSandboxSessionContext, resolveSandboxWorkdir } from '@/lib/server/sandbox/session-runtime'
|
|
4
4
|
import type { Session } from '@/types'
|
|
5
5
|
|
|
6
6
|
test('resolveSandboxRuntimeStatus defaults enabled sandboxes to all sessions', () => {
|
|
@@ -49,6 +49,23 @@ test('resolveSandboxRuntimeStatus sandboxes child sessions in non-main mode', ()
|
|
|
49
49
|
assert.equal(status.scopeKey, 'agent:agent-1')
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
+
test('resolveSandboxSessionContext resolves browser-compatible workspace context without starting containers', () => {
|
|
53
|
+
const context = resolveSandboxSessionContext({
|
|
54
|
+
config: { enabled: true, scope: 'agent', workdir: '/workspace' },
|
|
55
|
+
session: {
|
|
56
|
+
id: 'child-session',
|
|
57
|
+
agentId: 'agent-1',
|
|
58
|
+
parentSessionId: 'main-session',
|
|
59
|
+
} as Session,
|
|
60
|
+
workspaceDir: '/tmp/project',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
assert.ok(context)
|
|
64
|
+
assert.equal(context?.scopeKey, 'agent:agent-1')
|
|
65
|
+
assert.equal(context?.workspaceDir, '/tmp/project')
|
|
66
|
+
assert.equal(context?.containerWorkdir, '/workspace')
|
|
67
|
+
})
|
|
68
|
+
|
|
52
69
|
test('resolveSandboxWorkdir maps nested host paths into the container workspace', () => {
|
|
53
70
|
const resolved = resolveSandboxWorkdir({
|
|
54
71
|
workspaceDir: '/tmp/project',
|