@swarmclawai/swarmclaw 1.2.6 → 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.
Files changed (112) hide show
  1. package/README.md +24 -17
  2. package/next.config.ts +1 -0
  3. package/package.json +3 -2
  4. package/scripts/easy-setup.mjs +1 -1
  5. package/scripts/postinstall.mjs +1 -1
  6. package/skills/swarmclaw.md +115 -0
  7. package/skills/tools/browser.md +131 -0
  8. package/skills/tools/execute.md +98 -0
  9. package/skills/tools/files.md +98 -0
  10. package/skills/tools/memory.md +104 -0
  11. package/skills/tools/platform.md +144 -0
  12. package/skills/tools/skills.md +83 -0
  13. package/src/app/api/chats/[id]/messages/route.ts +23 -19
  14. package/src/app/api/chats/messages-route.test.ts +105 -51
  15. package/src/app/api/mcp-servers/[id]/test/route.ts +3 -2
  16. package/src/app/api/openclaw/deploy/route.ts +2 -0
  17. package/src/app/api/setup/doctor/route.ts +4 -4
  18. package/src/components/agents/agent-chat-list.tsx +23 -1
  19. package/src/components/agents/inspector-panel.tsx +165 -48
  20. package/src/components/chat/chat-area.tsx +38 -9
  21. package/src/components/chat/message-list.tsx +33 -19
  22. package/src/components/gateways/gateway-sheet.tsx +5 -2
  23. package/src/lib/agent-execute-defaults.test.ts +24 -0
  24. package/src/lib/agent-execute-defaults.ts +62 -0
  25. package/src/lib/chat/queued-message-queue.test.ts +134 -1
  26. package/src/lib/chat/queued-message-queue.ts +77 -2
  27. package/src/lib/server/agents/agent-service.ts +5 -0
  28. package/src/lib/server/builtin-extensions.ts +1 -0
  29. package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +1 -1
  30. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +1 -0
  31. package/src/lib/server/chat-execution/chat-execution-utils.ts +2 -2
  32. package/src/lib/server/chat-execution/chat-turn-preparation.ts +79 -42
  33. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -0
  34. package/src/lib/server/chat-execution/continuation-evaluator.ts +8 -0
  35. package/src/lib/server/chat-execution/memory-mutation-tools.ts +1 -1
  36. package/src/lib/server/chat-execution/message-classifier.ts +11 -1
  37. package/src/lib/server/chat-execution/prompt-builder.test.ts +28 -0
  38. package/src/lib/server/chat-execution/prompt-builder.ts +14 -1
  39. package/src/lib/server/chat-execution/prompt-mode.test.ts +24 -0
  40. package/src/lib/server/chat-execution/prompt-mode.ts +5 -1
  41. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +6 -4
  42. package/src/lib/server/chat-execution/stream-agent-chat.ts +45 -16
  43. package/src/lib/server/chatrooms/chatroom-routing.test.ts +4 -0
  44. package/src/lib/server/connectors/discord.ts +2 -2
  45. package/src/lib/server/connectors/matrix.ts +3 -2
  46. package/src/lib/server/connectors/signal.ts +5 -4
  47. package/src/lib/server/connectors/slack.ts +10 -9
  48. package/src/lib/server/connectors/teams.ts +3 -2
  49. package/src/lib/server/connectors/telegram.ts +4 -4
  50. package/src/lib/server/connectors/whatsapp.ts +2 -2
  51. package/src/lib/server/daemon/controller.ts +7 -0
  52. package/src/lib/server/gateways/gateway-profile-service.ts +19 -1
  53. package/src/lib/server/messages/message-repository.test.ts +70 -0
  54. package/src/lib/server/messages/message-repository.ts +11 -6
  55. package/src/lib/server/openclaw/deploy.ts +32 -2
  56. package/src/lib/server/plugins-advanced.test.ts +1 -2
  57. package/src/lib/server/provider-health.ts +1 -1
  58. package/src/lib/server/runtime/process-manager.ts +13 -9
  59. package/src/lib/server/runtime/session-run-manager/queries.ts +15 -0
  60. package/src/lib/server/runtime/session-run-manager.test.ts +58 -0
  61. package/src/lib/server/sandbox/session-runtime.test.ts +18 -1
  62. package/src/lib/server/sandbox/session-runtime.ts +40 -28
  63. package/src/lib/server/session-tools/autonomy-tools.test.ts +7 -9
  64. package/src/lib/server/session-tools/context.ts +1 -1
  65. package/src/lib/server/session-tools/credential-env.ts +109 -0
  66. package/src/lib/server/session-tools/crud.ts +3 -3
  67. package/src/lib/server/session-tools/edit_file.ts +3 -2
  68. package/src/lib/server/session-tools/execute.test.ts +58 -0
  69. package/src/lib/server/session-tools/execute.ts +334 -0
  70. package/src/lib/server/session-tools/files-tool.ts +635 -0
  71. package/src/lib/server/session-tools/index.ts +14 -4
  72. package/src/lib/server/session-tools/memory-tool.ts +242 -0
  73. package/src/lib/server/session-tools/memory.ts +1 -1
  74. package/src/lib/server/session-tools/openclaw-nodes.ts +3 -2
  75. package/src/lib/server/session-tools/openclaw-workspace.ts +3 -2
  76. package/src/lib/server/session-tools/platform-tool.ts +617 -0
  77. package/src/lib/server/session-tools/session-info.ts +3 -2
  78. package/src/lib/server/session-tools/session-tools-wiring.test.ts +3 -4
  79. package/src/lib/server/session-tools/shell.ts +7 -122
  80. package/src/lib/server/session-tools/skills-tool.ts +396 -0
  81. package/src/lib/server/session-tools/web.ts +2 -2
  82. package/src/lib/server/storage-normalization.ts +2 -0
  83. package/src/lib/server/tool-aliases.ts +2 -1
  84. package/src/lib/server/tool-capability-policy-advanced.test.ts +9 -2
  85. package/src/lib/server/tool-capability-policy.test.ts +2 -1
  86. package/src/lib/server/tool-capability-policy.ts +60 -33
  87. package/src/lib/server/tool-planning.ts +11 -0
  88. package/src/lib/setup-defaults.ts +5 -0
  89. package/src/lib/tool-definitions.ts +1 -0
  90. package/src/lib/validation/schemas.test.ts +16 -0
  91. package/src/lib/validation/schemas.ts +16 -0
  92. package/src/stores/use-chat-store.test.ts +231 -0
  93. package/src/stores/use-chat-store.ts +62 -13
  94. package/src/types/agent.ts +348 -0
  95. package/src/types/app-settings.ts +175 -0
  96. package/src/types/approval.ts +27 -0
  97. package/src/types/connector.ts +187 -0
  98. package/src/types/extension.ts +386 -0
  99. package/src/types/index.ts +16 -3555
  100. package/src/types/message.ts +57 -0
  101. package/src/types/misc.ts +739 -0
  102. package/src/types/mission.ts +185 -0
  103. package/src/types/protocol.ts +422 -0
  104. package/src/types/provider.ts +52 -0
  105. package/src/types/run.ts +183 -0
  106. package/src/types/schedule.ts +59 -0
  107. package/src/types/session.ts +265 -0
  108. package/src/types/skill.ts +157 -0
  109. package/src/types/task.ts +140 -0
  110. package/src/types/working-state.ts +211 -0
  111. package/src/views/settings/section-heartbeat.tsx +2 -2
  112. package/src/lib/server/session-tools/sandbox.ts +0 -281
@@ -31,6 +31,7 @@ export const MessageClassificationSchema = z.object({
31
31
  taskIntent: TaskIntentSchema,
32
32
  isDeliverableTask: z.boolean(),
33
33
  isBroadGoal: z.boolean(),
34
+ isLightweightDirectChat: z.boolean().optional().default(false),
34
35
  walletIntent: z.enum(['none', 'read_only', 'transactional']),
35
36
  hasHumanSignals: z.boolean(),
36
37
  hasSignificantEvent: z.boolean(),
@@ -47,6 +48,7 @@ export interface MessageClassification {
47
48
  taskIntent: MessageTaskIntent
48
49
  isDeliverableTask: boolean
49
50
  isBroadGoal: boolean
51
+ isLightweightDirectChat?: boolean
50
52
  walletIntent: 'none' | 'read_only' | 'transactional'
51
53
  hasHumanSignals: boolean
52
54
  hasSignificantEvent: boolean
@@ -102,6 +104,7 @@ function buildClassificationPrompt(message: string, recentHistory: string): stri
102
104
  '- taskIntent: The primary execution intent. Use exactly one of: "coding", "research", "browsing", "outreach", "scheduling", or "general". Choose "coding" for repo/code/build/debug/edit tasks. Choose "research" for gathering current info or synthesizing sources. Choose "browsing" for page navigation, rendered-page inspection, form work, or literal browser workflows. Choose "outreach" for sending/sharing/delivering updates to an external channel. Choose "scheduling" for reminders, recurring work, monitoring, or follow-up scheduling. Choose "general" when none of the above clearly fits.',
103
105
  '- isDeliverableTask (bool): The user wants a concrete artifact produced — a document, report, plan, proposal, landing page, dashboard, HTML file, markdown file, brief, copy, screenshots, or similar deliverable. NOT simple Q&A, code fixes, or single-command tasks.',
104
106
  '- isBroadGoal (bool): The message describes a broad, multi-step goal (50+ chars, no code blocks, no file paths, no numbered lists). Short questions ending with "?" are NOT broad goals.',
107
+ '- isLightweightDirectChat (bool): This is a low-signal direct chat turn that should get a natural lightweight reply, such as a greeting, acknowledgment, check-in, or simple social/direct question that does NOT require research, file work, planning, delegation, or tool execution.',
105
108
  '- walletIntent: "none" if no crypto/wallet/trading context. "read_only" if mentioning wallet/crypto but only for checking balances, viewing transactions, or research. "transactional" if the user wants to swap, trade, buy, sell, mint, claim, deposit, withdraw, bridge, or execute a transaction.',
106
109
  '- hasHumanSignals (bool): The message contains personal signals — preferences ("I prefer", "call me"), relationships ("my wife", "my partner", "my kid"), life events ("birthday", "wedding", "promotion", "moving", "graduation", "hospital"), or personal disclosures.',
107
110
  '- hasSignificantEvent (bool): The message mentions a notable life/work event or milestone (birthday, anniversary, wedding, graduation, promotion, new job, relocation, illness, funeral, travel, house, deadline, launch).',
@@ -115,13 +118,14 @@ function buildClassificationPrompt(message: string, recentHistory: string): stri
115
118
  '',
116
119
  'Rules:',
117
120
  '- Be conservative. When unsure, default to false/none/empty.',
121
+ '- Mark isLightweightDirectChat true only when a short natural reply is enough and escalating into planning, delegation, or tool execution would be unnecessary.',
118
122
  '- A message can be both a deliverable task AND a broad goal.',
119
123
  '- "walletIntent" should be "transactional" only if the user wants to execute a state-changing action, not just discuss crypto.',
120
124
  '- For "explicitToolRequests", only include tools the user explicitly mentions by name or clear synonym. Do not infer tool needs from the task type.',
121
125
  '- Prefer the most execution-relevant taskIntent. Example: "research this and send me a voice note" is "research", not "outreach".',
122
126
  '',
123
127
  'Output shape:',
124
- '{"taskIntent":"coding|research|browsing|outreach|scheduling|general","isDeliverableTask":bool,"isBroadGoal":bool,"walletIntent":"none|read_only|transactional","hasHumanSignals":bool,"hasSignificantEvent":bool,"isResearchSynthesis":bool,"workType":"coding|research|writing|review|operations|general","wantsScreenshots":bool,"wantsOutboundDelivery":bool,"wantsVoiceDelivery":bool,"explicitToolRequests":[],"confidence":0.0-1.0}',
128
+ '{"taskIntent":"coding|research|browsing|outreach|scheduling|general","isDeliverableTask":bool,"isBroadGoal":bool,"isLightweightDirectChat":bool,"walletIntent":"none|read_only|transactional","hasHumanSignals":bool,"hasSignificantEvent":bool,"isResearchSynthesis":bool,"workType":"coding|research|writing|review|operations|general","wantsScreenshots":bool,"wantsOutboundDelivery":bool,"wantsVoiceDelivery":bool,"explicitToolRequests":[],"confidence":0.0-1.0}',
125
129
  '',
126
130
  recentHistory ? `Recent context:\n${recentHistory}\n` : '',
127
131
  `User message: ${JSON.stringify(message)}`,
@@ -276,6 +280,7 @@ export function toMessageSemanticsSummary(classification: MessageClassification
276
280
  isDeliverableTask: classification.isDeliverableTask,
277
281
  isBroadGoal: classification.isBroadGoal,
278
282
  isResearchSynthesis: classification.isResearchSynthesis,
283
+ isLightweightDirectChat: classification.isLightweightDirectChat === true,
279
284
  hasHumanSignals: classification.hasHumanSignals,
280
285
  hasSignificantEvent: classification.hasSignificantEvent,
281
286
  wantsScreenshots: classification.wantsScreenshots === true,
@@ -324,3 +329,8 @@ export function isResearchSynthesis(classification: MessageClassification | null
324
329
  void routingIntent
325
330
  return classification?.isResearchSynthesis === true
326
331
  }
332
+
333
+ export function isLightweightDirectChat(classification: MessageClassification | null, message?: string): boolean {
334
+ void message
335
+ return classification?.isLightweightDirectChat === true
336
+ }
@@ -26,4 +26,32 @@ describe('buildAgenticExecutionPolicy', () => {
26
26
  assert.ok(prompt.includes('use the concrete tool now'))
27
27
  assert.ok(prompt.includes('prefer the direct `manage_*` tool'))
28
28
  })
29
+
30
+ it('adds lightweight direct-chat guidance when classification marks the turn as lightweight', () => {
31
+ const prompt = buildAgenticExecutionPolicy({
32
+ enabledExtensions: ['memory', 'files', 'delegate'],
33
+ loopMode: 'bounded',
34
+ heartbeatPrompt: 'HEARTBEAT',
35
+ heartbeatIntervalSec: 120,
36
+ userMessage: 'Hello',
37
+ history: [],
38
+ classification: {
39
+ taskIntent: 'general',
40
+ isDeliverableTask: false,
41
+ isBroadGoal: false,
42
+ isLightweightDirectChat: true,
43
+ walletIntent: 'none',
44
+ hasHumanSignals: false,
45
+ hasSignificantEvent: false,
46
+ isResearchSynthesis: false,
47
+ workType: 'general',
48
+ explicitToolRequests: [],
49
+ confidence: 0.98,
50
+ },
51
+ })
52
+
53
+ assert.ok(prompt.includes('## Lightweight Chat'))
54
+ assert.ok(prompt.includes('Reply naturally and briefly.'))
55
+ assert.ok(prompt.includes('prefer 1-3 short sentences'))
56
+ })
29
57
  })
@@ -75,7 +75,6 @@ function buildExtensionCapabilityLines(enabledExtensions: string[], opts?: { del
75
75
 
76
76
  const DISPLAY_TOOL_ALIASES: Record<string, string[]> = {
77
77
  files: ['send_file'],
78
- shell: ['sandbox_exec', 'sandbox_list_runtimes'],
79
78
  }
80
79
 
81
80
  function buildExactToolNameList(enabledExtensions: string[]): string[] {
@@ -158,6 +157,7 @@ export function buildToolDisciplineLines(enabledExtensions: string[]): string[]
158
157
  ...(researchSearchTools.length || researchFetchTools.length ? [...researchSearchTools, ...researchFetchTools] : []),
159
158
  ...httpTools,
160
159
  ...(uniqueTools.includes('shell') ? ['shell'] : []),
160
+ ...(uniqueTools.includes('execute') ? ['execute'] : []),
161
161
  ...(uniqueTools.includes('browser') ? ['browser'] : []),
162
162
  ]))
163
163
  if (alternateResearchTools.length >= 2) {
@@ -330,6 +330,7 @@ export function buildAgenticExecutionPolicy(opts: {
330
330
  const hasManageSessions = opts.enabledExtensions.some((toolId) => (canonicalizeExtensionId(toolId) || toolId) === 'manage_sessions')
331
331
  const hasManageTasks = opts.enabledExtensions.some((toolId) => (canonicalizeExtensionId(toolId) || toolId) === 'manage_tasks')
332
332
  const hasManageSkills = opts.enabledExtensions.some((toolId) => (canonicalizeExtensionId(toolId) || toolId) === 'manage_skills')
333
+ const lightweightDirectChat = opts.classification?.isLightweightDirectChat === true && !opts.isDirectConnectorSession
333
334
  const hasDelegationTools = opts.enabledExtensions.some((toolId) => {
334
335
  const canonical = canonicalizeExtensionId(toolId) || toolId
335
336
  return canonical === 'delegate' || canonical === 'spawn_subagent'
@@ -359,6 +360,15 @@ export function buildAgenticExecutionPolicy(opts: {
359
360
  : 'Loop: BOUNDED — execute multiple steps but finish within recursion budget.',
360
361
  )
361
362
 
363
+ if (lightweightDirectChat) {
364
+ parts.push(
365
+ '## Lightweight Chat',
366
+ 'This turn is a lightweight direct chat. Reply naturally and briefly.',
367
+ 'Do not delegate, create tasks, outline a workflow, or narrate tools unless the user adds a concrete task that actually requires that escalation.',
368
+ 'For greetings, acknowledgements, and simple social questions, a short human-sounding answer is sufficient.',
369
+ )
370
+ }
371
+
362
372
  if (hasTooling) {
363
373
  parts.push(
364
374
  '## Routing Matrix',
@@ -444,6 +454,9 @@ export function buildAgenticExecutionPolicy(opts: {
444
454
  ]),
445
455
  'Keep responses concise. Bullet points over prose. After file operations, confirm the result briefly (path and status) without echoing the full file contents.',
446
456
  'Do not end every reply with a question. Only ask when a specific missing detail blocks progress. When a task is done, state the result and stop.',
457
+ ...(lightweightDirectChat
458
+ ? ['For this turn, prefer 1-3 short sentences over bullets, planning, or process narration.']
459
+ : []),
447
460
  opts.responseStyle === 'concise'
448
461
  ? `IMPORTANT: Be extremely concise.${opts.responseMaxChars ? ` Keep responses under ${opts.responseMaxChars} characters.` : ' Target under 500 characters.'} Lead with the answer, skip preamble.`
449
462
  : opts.responseStyle === 'detailed'
@@ -0,0 +1,24 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { resolvePromptMode } from '@/lib/server/chat-execution/prompt-mode'
5
+
6
+ describe('resolvePromptMode', () => {
7
+ it('returns full for root sessions by default', () => {
8
+ assert.equal(resolvePromptMode({ id: 'root' } as never), 'full')
9
+ })
10
+
11
+ it('prefers minimal mode for lightweight direct-chat turns', () => {
12
+ assert.equal(
13
+ resolvePromptMode({ id: 'root' } as never, { preferMinimalPrompt: true }),
14
+ 'minimal',
15
+ )
16
+ })
17
+
18
+ it('keeps delegated child sessions in minimal mode', () => {
19
+ assert.equal(
20
+ resolvePromptMode({ id: 'child', parentSessionId: 'parent' } as never, { preferMinimalPrompt: false }),
21
+ 'minimal',
22
+ )
23
+ })
24
+ })
@@ -20,7 +20,11 @@ export type PromptMode = 'full' | 'minimal' | 'none'
20
20
  * proactive memory, thinking guidance
21
21
  * - `none` — reserved for bare identity (light heartbeat path)
22
22
  */
23
- export function resolvePromptMode(session: Session): PromptMode {
23
+ export function resolvePromptMode(
24
+ session: Session,
25
+ options?: { preferMinimalPrompt?: boolean },
26
+ ): PromptMode {
24
27
  if (session.parentSessionId) return 'minimal'
28
+ if (options?.preferMinimalPrompt) return 'minimal'
25
29
  return 'full'
26
30
  }
@@ -65,14 +65,13 @@ const streamContinuationSource = _readSibling('stream-continuation.ts')
65
65
  const streamSources = `${streamAgentChatSource}\n${streamContinuationSource}`
66
66
 
67
67
  describe('buildToolDisciplineLines', () => {
68
- it('lists exact callable tool names for extension families like sandbox and browser', () => {
68
+ it('lists exact callable tool names for legacy sandbox aliases and browser', () => {
69
69
  const lines = buildToolAvailabilityLines(['sandbox', 'browser', 'manage_schedules'])
70
70
 
71
71
  assert.equal(lines[0], 'Tool names are case-sensitive. Call tools exactly as listed.')
72
72
  assert.ok(lines.includes('- `browser`'))
73
+ assert.ok(lines.includes('- `execute`'))
73
74
  assert.ok(lines.includes('- `manage_schedules`'))
74
- assert.ok(lines.includes('- `sandbox_exec`'))
75
- assert.ok(lines.includes('- `sandbox_list_runtimes`'))
76
75
  })
77
76
 
78
77
  it('tells the agent to use direct platform tools when manage_platform is absent', () => {
@@ -1033,7 +1032,7 @@ describe('shouldForceDeliverableFollowthrough', () => {
1033
1032
  { name: 'web', input: '{"action":"fetch","url":"https://example.com/topic"}', output: '<html>topic</html>' },
1034
1033
  ],
1035
1034
  history: [
1036
- { role: 'user', text: 'Research 3 topics, take screenshots, write markdown and PDF files, then build a site for each topic.' },
1035
+ { role: 'user', text: 'Research 3 topics, take screenshots, write markdown and PDF files, then build a site for each topic.', time: Date.now() },
1037
1036
  ],
1038
1037
  }),
1039
1038
  true,
@@ -1275,6 +1274,7 @@ describe('parseClassificationResponse', () => {
1275
1274
 
1276
1275
  describe('message classifier adapter functions', () => {
1277
1276
  const deliverableClassification: MessageClassification = {
1277
+ taskIntent: 'general',
1278
1278
  isDeliverableTask: true,
1279
1279
  isBroadGoal: true,
1280
1280
  walletIntent: 'none',
@@ -1286,6 +1286,7 @@ describe('message classifier adapter functions', () => {
1286
1286
  }
1287
1287
 
1288
1288
  const walletClassification: MessageClassification = {
1289
+ taskIntent: 'general',
1289
1290
  isDeliverableTask: false,
1290
1291
  isBroadGoal: false,
1291
1292
  walletIntent: 'transactional',
@@ -1297,6 +1298,7 @@ describe('message classifier adapter functions', () => {
1297
1298
  }
1298
1299
 
1299
1300
  const humanSignalClassification: MessageClassification = {
1301
+ taskIntent: 'general',
1300
1302
  isDeliverableTask: false,
1301
1303
  isBroadGoal: false,
1302
1304
  walletIntent: 'none',
@@ -190,6 +190,7 @@ interface StreamAgentChatOpts {
190
190
  fallbackCredentialIds?: string[]
191
191
  signal?: AbortSignal
192
192
  promptMode?: PromptMode
193
+ classification?: MessageClassification | null
193
194
  /** Run source (e.g. 'heartbeat', 'chat', 'scheduler') — used for heartbeat-specific tuning. */
194
195
  source?: string
195
196
  }
@@ -223,10 +224,23 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
223
224
 
224
225
  async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
225
226
  const startTs = Date.now()
226
- const { session, message, imagePath, imageUrl, attachedFiles, apiKey, systemPrompt, executionBrief, extraSystemContext, write, history, fallbackCredentialIds, signal } = opts
227
+ const {
228
+ session,
229
+ message,
230
+ imagePath,
231
+ imageUrl,
232
+ attachedFiles,
233
+ apiKey,
234
+ systemPrompt,
235
+ executionBrief,
236
+ extraSystemContext,
237
+ write,
238
+ history,
239
+ fallbackCredentialIds,
240
+ signal,
241
+ classification: providedClassification,
242
+ } = opts
227
243
  const isHeartbeat = isHeartbeatSource(opts.source)
228
- const promptMode: PromptMode = opts.promptMode ?? resolvePromptMode(session)
229
- const isMinimalPrompt = promptMode === 'minimal'
230
244
  const isConnectorSession = isDirectConnectorSession(session)
231
245
  const rawExtensions = getEnabledCapabilityIds(session)
232
246
  const hasShellCapability = rawExtensions.some((toolId) => ['shell', 'execute_command'].includes(String(toolId)))
@@ -241,6 +255,23 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
241
255
 
242
256
  const sessionAgent = session.agentId ? getAgent(session.agentId) : null
243
257
 
258
+ const classificationPromise = providedClassification !== undefined
259
+ ? Promise.resolve(providedClassification)
260
+ : classifyMessage({
261
+ sessionId: session.id,
262
+ agentId: session.agentId,
263
+ message,
264
+ history,
265
+ }).catch(() => null as MessageClassification | null)
266
+ const classification = await classificationPromise
267
+ const lightweightDirectChat = classification?.isLightweightDirectChat === true
268
+ && !isConnectorSession
269
+ && !isHeartbeat
270
+ const promptMode: PromptMode = opts.promptMode ?? resolvePromptMode(session, {
271
+ preferMinimalPrompt: lightweightDirectChat,
272
+ })
273
+ const isMinimalPrompt = promptMode === 'minimal'
274
+
244
275
  // Resolve agent's thinking level for provider-native params
245
276
  let agentThinkingLevel: 'minimal' | 'low' | 'medium' | 'high' | undefined
246
277
  if (session.thinkingLevel) {
@@ -248,6 +279,9 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
248
279
  } else if (sessionAgent) {
249
280
  agentThinkingLevel = sessionAgent.thinkingLevel
250
281
  }
282
+ if (lightweightDirectChat) {
283
+ agentThinkingLevel = 'minimal'
284
+ }
251
285
 
252
286
  const llm = buildChatModel({
253
287
  provider: session.provider,
@@ -296,16 +330,6 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
296
330
  return Math.max(0, Math.min(3600, Math.trunc(parsed)))
297
331
  })()
298
332
 
299
- // -------------------------------------------------------------------------
300
- // Start message classification in the background (LLM-based, ~200-800ms)
301
- // -------------------------------------------------------------------------
302
- const classificationPromise = classifyMessage({
303
- sessionId: session.id,
304
- agentId: session.agentId,
305
- message,
306
- history,
307
- }).catch(() => null as MessageClassification | null)
308
-
309
333
  // -------------------------------------------------------------------------
310
334
  // System prompt assembly (stays inline — many async calls + local state)
311
335
  // -------------------------------------------------------------------------
@@ -426,8 +450,6 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
426
450
  const suggestionsBlock = buildSuggestionsSection(settings.suggestionsEnabled, isMinimalPrompt)
427
451
  if (suggestionsBlock) promptParts.push(suggestionsBlock)
428
452
 
429
- // Await classification before building the agentic execution policy
430
- const classification = await classificationPromise
431
453
  const delegationAdvisory = sessionAgent && agentDelegationEnabled
432
454
  ? resolveDelegationAdvisory({
433
455
  currentAgent: sessionAgent,
@@ -1138,7 +1160,14 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
1138
1160
  }
1139
1161
 
1140
1162
  // Async LLM-based incomplete-action check: catches "I'll run the deployment:" with no tool calls
1141
- if (!shouldContinue && outcome && !state.hasToolCalls && state.fullText.trim().length > 0 && state.fullText.trim().length < 500) {
1163
+ if (
1164
+ !shouldContinue
1165
+ && outcome
1166
+ && !state.hasToolCalls
1167
+ && classification?.isLightweightDirectChat !== true
1168
+ && state.fullText.trim().length > 0
1169
+ && state.fullText.trim().length < 500
1170
+ ) {
1142
1171
  const completeness = await evaluateResponseCompleteness({
1143
1172
  sessionId: session.id,
1144
1173
  agentId: session.agentId,
@@ -16,6 +16,8 @@ const agents: Record<string, Agent> = {
16
16
  model: 'gpt-test',
17
17
  systemPrompt: '',
18
18
  capabilities: ['deploy', 'infrastructure'],
19
+ createdAt: Date.now(),
20
+ updatedAt: Date.now(),
19
21
  },
20
22
  design: {
21
23
  id: 'design',
@@ -25,6 +27,8 @@ const agents: Record<string, Agent> = {
25
27
  model: 'gpt-test',
26
28
  systemPrompt: '',
27
29
  capabilities: ['design', 'ui'],
30
+ createdAt: Date.now(),
31
+ updatedAt: Date.now(),
28
32
  },
29
33
  }
30
34
 
@@ -217,8 +217,8 @@ const discord: PlatformConnector = {
217
217
  return String(sent.id || '')
218
218
  },
219
219
  })
220
- } catch (err: any) {
221
- log.error(TAG, 'Error handling message:', err.message)
220
+ } catch (err: unknown) {
221
+ log.error(TAG, 'Error handling message:', errorMessage(err))
222
222
  try {
223
223
  await message.reply('Sorry, I encountered an error processing your message.')
224
224
  } catch { /* ignore */ }
@@ -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: any) {
58
- log.error(TAG, 'Error handling message:', err.message)
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: any) {
111
- throw new Error(`Signal send failed: ${err.message}`)
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: any) {
183
- log.error(TAG, 'Error handling message:', err.message)
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: any) {
149
- const hint = err.code === 'slack_webapi_platform_error'
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.message}${hint}`)
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: any) {
219
- log.warn(TAG, `Media download failed (${f?.name || 'file'}):`, err?.message || String(err))
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: any) {
268
- log.error(TAG, 'Error handling message:', err.message)
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: any) {
326
- log.error(TAG, 'Error handling mention:', err.message)
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: any) {
55
- log.error(TAG, 'Error handling message:', err.message)
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: any) {
131
- log.warn(TAG, `Failed to fetch media ${m.fileId}:`, err?.message || String(err))
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: any) {
181
- log.error(TAG, 'Error handling message:', err.message)
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: any) {
718
- log.error(TAG, `Failed to decode media: ${err?.message || String(err)}`)
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
- if (!gateways[id]) return false
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
+ })