@swarmclawai/swarmclaw 0.5.3 → 0.6.2

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 (224) hide show
  1. package/README.md +53 -9
  2. package/bin/server-cmd.js +1 -0
  3. package/bin/swarmclaw.js +76 -16
  4. package/next.config.ts +11 -1
  5. package/package.json +5 -2
  6. package/scripts/postinstall.mjs +18 -0
  7. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  8. package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
  9. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  10. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  11. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  12. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  13. package/src/app/api/chatrooms/route.ts +50 -0
  14. package/src/app/api/connectors/[id]/route.ts +1 -0
  15. package/src/app/api/connectors/route.ts +2 -1
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/files/open/route.ts +43 -0
  18. package/src/app/api/knowledge/[id]/route.ts +13 -2
  19. package/src/app/api/knowledge/route.ts +8 -1
  20. package/src/app/api/memory/route.ts +8 -0
  21. package/src/app/api/notifications/route.ts +4 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +53 -1
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  29. package/src/app/api/sessions/[id]/route.ts +4 -0
  30. package/src/app/api/sessions/route.ts +3 -3
  31. package/src/app/api/settings/route.ts +9 -0
  32. package/src/app/api/setup/check-provider/route.ts +3 -16
  33. package/src/app/api/skills/[id]/route.ts +6 -0
  34. package/src/app/api/skills/route.ts +6 -0
  35. package/src/app/api/tasks/[id]/route.ts +12 -0
  36. package/src/app/api/tasks/bulk/route.ts +100 -0
  37. package/src/app/api/tasks/metrics/route.ts +101 -0
  38. package/src/app/api/tasks/route.ts +18 -2
  39. package/src/app/api/tts/route.ts +3 -2
  40. package/src/app/api/tts/stream/route.ts +3 -2
  41. package/src/app/api/uploads/[filename]/route.ts +19 -34
  42. package/src/app/api/uploads/route.ts +94 -0
  43. package/src/app/api/webhooks/[id]/route.ts +15 -1
  44. package/src/app/globals.css +63 -15
  45. package/src/app/page.tsx +142 -13
  46. package/src/cli/index.js +40 -1
  47. package/src/cli/index.test.js +30 -0
  48. package/src/cli/spec.js +42 -0
  49. package/src/components/agents/agent-avatar.tsx +57 -10
  50. package/src/components/agents/agent-card.tsx +50 -17
  51. package/src/components/agents/agent-chat-list.tsx +148 -12
  52. package/src/components/agents/agent-list.tsx +50 -19
  53. package/src/components/agents/agent-sheet.tsx +120 -65
  54. package/src/components/agents/inspector-panel.tsx +81 -6
  55. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  56. package/src/components/agents/personality-builder.tsx +42 -14
  57. package/src/components/agents/soul-library-picker.tsx +89 -0
  58. package/src/components/auth/access-key-gate.tsx +10 -3
  59. package/src/components/auth/setup-wizard.tsx +2 -2
  60. package/src/components/auth/user-picker.tsx +31 -3
  61. package/src/components/canvas/canvas-panel.tsx +96 -0
  62. package/src/components/chat/activity-moment.tsx +173 -0
  63. package/src/components/chat/chat-area.tsx +46 -22
  64. package/src/components/chat/chat-header.tsx +457 -286
  65. package/src/components/chat/chat-preview-panel.tsx +1 -2
  66. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  67. package/src/components/chat/delegation-banner.tsx +371 -0
  68. package/src/components/chat/file-path-chip.tsx +146 -0
  69. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  70. package/src/components/chat/markdown-utils.ts +9 -0
  71. package/src/components/chat/message-bubble.tsx +356 -315
  72. package/src/components/chat/message-list.tsx +230 -8
  73. package/src/components/chat/streaming-bubble.tsx +104 -47
  74. package/src/components/chat/suggestions-bar.tsx +1 -1
  75. package/src/components/chat/thinking-indicator.tsx +72 -10
  76. package/src/components/chat/tool-call-bubble.tsx +111 -73
  77. package/src/components/chat/tool-request-banner.tsx +31 -7
  78. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  79. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  80. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  81. package/src/components/chatrooms/chatroom-list.tsx +130 -0
  82. package/src/components/chatrooms/chatroom-message.tsx +432 -0
  83. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  84. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  85. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  86. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  87. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  88. package/src/components/connectors/connector-list.tsx +168 -90
  89. package/src/components/connectors/connector-sheet.tsx +95 -56
  90. package/src/components/home/home-view.tsx +501 -0
  91. package/src/components/input/chat-input.tsx +107 -43
  92. package/src/components/knowledge/knowledge-list.tsx +31 -1
  93. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  94. package/src/components/layout/app-layout.tsx +194 -97
  95. package/src/components/layout/update-banner.tsx +2 -2
  96. package/src/components/logs/log-list.tsx +2 -2
  97. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  98. package/src/components/memory/memory-agent-list.tsx +143 -0
  99. package/src/components/memory/memory-browser.tsx +205 -0
  100. package/src/components/memory/memory-card.tsx +34 -7
  101. package/src/components/memory/memory-detail.tsx +359 -120
  102. package/src/components/memory/memory-sheet.tsx +157 -23
  103. package/src/components/plugins/plugin-list.tsx +1 -1
  104. package/src/components/plugins/plugin-sheet.tsx +1 -1
  105. package/src/components/projects/project-detail.tsx +509 -0
  106. package/src/components/projects/project-list.tsx +195 -59
  107. package/src/components/providers/provider-list.tsx +2 -2
  108. package/src/components/providers/provider-sheet.tsx +3 -3
  109. package/src/components/schedules/schedule-card.tsx +1 -1
  110. package/src/components/schedules/schedule-list.tsx +1 -1
  111. package/src/components/schedules/schedule-sheet.tsx +259 -126
  112. package/src/components/secrets/secret-sheet.tsx +47 -24
  113. package/src/components/secrets/secrets-list.tsx +18 -8
  114. package/src/components/sessions/new-session-sheet.tsx +33 -65
  115. package/src/components/sessions/session-card.tsx +45 -14
  116. package/src/components/sessions/session-list.tsx +35 -18
  117. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  118. package/src/components/shared/agent-picker-list.tsx +90 -0
  119. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  120. package/src/components/shared/attachment-chip.tsx +165 -0
  121. package/src/components/shared/avatar.tsx +10 -1
  122. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  123. package/src/components/shared/check-icon.tsx +12 -0
  124. package/src/components/shared/confirm-dialog.tsx +1 -1
  125. package/src/components/shared/connector-platform-icon.tsx +51 -4
  126. package/src/components/shared/empty-state.tsx +32 -0
  127. package/src/components/shared/file-preview.tsx +34 -0
  128. package/src/components/shared/form-styles.ts +2 -0
  129. package/src/components/shared/icon-button.tsx +16 -2
  130. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  131. package/src/components/shared/notification-center.tsx +44 -6
  132. package/src/components/shared/profile-sheet.tsx +115 -0
  133. package/src/components/shared/reply-quote.tsx +26 -0
  134. package/src/components/shared/search-dialog.tsx +31 -15
  135. package/src/components/shared/section-label.tsx +12 -0
  136. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  137. package/src/components/shared/settings/section-embedding.tsx +48 -13
  138. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  139. package/src/components/shared/settings/section-providers.tsx +1 -1
  140. package/src/components/shared/settings/section-secrets.tsx +1 -1
  141. package/src/components/shared/settings/section-storage.tsx +206 -0
  142. package/src/components/shared/settings/section-theme.tsx +95 -0
  143. package/src/components/shared/settings/section-user-preferences.tsx +57 -0
  144. package/src/components/shared/settings/section-voice.tsx +42 -21
  145. package/src/components/shared/settings/section-web-search.tsx +30 -6
  146. package/src/components/shared/settings/settings-page.tsx +182 -27
  147. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  148. package/src/components/shared/settings/storage-browser.tsx +259 -0
  149. package/src/components/shared/sheet-footer.tsx +33 -0
  150. package/src/components/skills/skill-list.tsx +61 -30
  151. package/src/components/skills/skill-sheet.tsx +81 -2
  152. package/src/components/tasks/task-board.tsx +448 -26
  153. package/src/components/tasks/task-card.tsx +59 -9
  154. package/src/components/tasks/task-column.tsx +62 -3
  155. package/src/components/tasks/task-list.tsx +12 -4
  156. package/src/components/tasks/task-sheet.tsx +416 -74
  157. package/src/components/ui/hover-card.tsx +52 -0
  158. package/src/components/usage/metrics-dashboard.tsx +90 -6
  159. package/src/components/usage/usage-list.tsx +1 -1
  160. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  161. package/src/hooks/use-continuous-speech.ts +10 -4
  162. package/src/hooks/use-view-router.ts +69 -19
  163. package/src/hooks/use-voice-conversation.ts +53 -10
  164. package/src/hooks/use-ws.ts +4 -2
  165. package/src/instrumentation.ts +15 -1
  166. package/src/lib/chat.ts +2 -0
  167. package/src/lib/memory.ts +3 -0
  168. package/src/lib/providers/anthropic.ts +13 -7
  169. package/src/lib/providers/index.ts +1 -0
  170. package/src/lib/providers/openai.ts +13 -7
  171. package/src/lib/server/chat-execution.ts +75 -15
  172. package/src/lib/server/chatroom-helpers.ts +146 -0
  173. package/src/lib/server/connectors/manager.ts +229 -7
  174. package/src/lib/server/context-manager.ts +225 -13
  175. package/src/lib/server/create-notification.ts +14 -2
  176. package/src/lib/server/daemon-state.ts +157 -10
  177. package/src/lib/server/execution-log.ts +1 -0
  178. package/src/lib/server/heartbeat-service.ts +48 -6
  179. package/src/lib/server/heartbeat-wake.ts +110 -0
  180. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  181. package/src/lib/server/main-agent-loop.ts +1 -1
  182. package/src/lib/server/memory-consolidation.ts +105 -0
  183. package/src/lib/server/memory-db.ts +183 -10
  184. package/src/lib/server/mime.ts +51 -0
  185. package/src/lib/server/openclaw-gateway.ts +9 -1
  186. package/src/lib/server/orchestrator-lg.ts +2 -0
  187. package/src/lib/server/orchestrator.ts +5 -2
  188. package/src/lib/server/playwright-proxy.mjs +2 -3
  189. package/src/lib/server/prompt-runtime-context.ts +53 -0
  190. package/src/lib/server/provider-health.ts +125 -0
  191. package/src/lib/server/queue.ts +56 -10
  192. package/src/lib/server/scheduler.ts +8 -0
  193. package/src/lib/server/session-run-manager.ts +4 -0
  194. package/src/lib/server/session-tools/canvas.ts +67 -0
  195. package/src/lib/server/session-tools/chatroom.ts +136 -0
  196. package/src/lib/server/session-tools/connector.ts +83 -9
  197. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  198. package/src/lib/server/session-tools/crud.ts +21 -0
  199. package/src/lib/server/session-tools/delegate.ts +68 -4
  200. package/src/lib/server/session-tools/git.ts +71 -0
  201. package/src/lib/server/session-tools/http.ts +57 -0
  202. package/src/lib/server/session-tools/index.ts +10 -0
  203. package/src/lib/server/session-tools/memory.ts +7 -1
  204. package/src/lib/server/session-tools/search-providers.ts +16 -8
  205. package/src/lib/server/session-tools/subagent.ts +106 -0
  206. package/src/lib/server/session-tools/web.ts +115 -4
  207. package/src/lib/server/storage.ts +53 -29
  208. package/src/lib/server/stream-agent-chat.ts +185 -57
  209. package/src/lib/server/system-events.ts +49 -0
  210. package/src/lib/server/task-mention.ts +41 -0
  211. package/src/lib/server/ws-hub.ts +11 -0
  212. package/src/lib/sessions.ts +10 -0
  213. package/src/lib/soul-library.ts +103 -0
  214. package/src/lib/soul-suggestions.ts +109 -0
  215. package/src/lib/task-dedupe.ts +26 -0
  216. package/src/lib/tasks.ts +4 -1
  217. package/src/lib/tool-definitions.ts +2 -0
  218. package/src/lib/tts.ts +2 -2
  219. package/src/lib/view-routes.ts +36 -1
  220. package/src/lib/ws-client.ts +14 -4
  221. package/src/stores/use-app-store.ts +41 -3
  222. package/src/stores/use-chat-store.ts +113 -5
  223. package/src/stores/use-chatroom-store.ts +276 -0
  224. package/src/types/index.ts +88 -4
@@ -43,9 +43,9 @@ async function fileToContentParts(filePath: string): Promise<any[]> {
43
43
  return [{ type: 'text', text: `[Attached file: ${name}]` }]
44
44
  }
45
45
 
46
- export function streamOpenAiChat({ session, message, imagePath, apiKey, systemPrompt, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
46
+ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
47
47
  return new Promise(async (resolve) => {
48
- const messages = await buildMessages(session, message, imagePath, systemPrompt, loadHistory)
48
+ const messages = await buildMessages(session, message, imagePath, systemPrompt, loadHistory, imageUrl)
49
49
  const model = session.model || 'gpt-4o'
50
50
 
51
51
  const payload = JSON.stringify({
@@ -163,7 +163,11 @@ export function streamOpenAiChat({ session, message, imagePath, apiKey, systemPr
163
163
  })
164
164
  }
165
165
 
166
- async function buildMessages(session: any, message: string, imagePath: string | undefined, systemPrompt: string | undefined, loadHistory: (id: string) => any[]) {
166
+ function urlToImagePart(url: string): { type: string; image_url: { url: string; detail: string } } {
167
+ return { type: 'image_url', image_url: { url, detail: 'auto' } }
168
+ }
169
+
170
+ async function buildMessages(session: any, message: string, imagePath: string | undefined, systemPrompt: string | undefined, loadHistory: (id: string) => any[], imageUrl?: string) {
167
171
  const msgs: Array<{ role: string; content: any }> = []
168
172
 
169
173
  if (systemPrompt) {
@@ -173,8 +177,9 @@ async function buildMessages(session: any, message: string, imagePath: string |
173
177
  if (loadHistory) {
174
178
  const history = loadHistory(session.id).slice(-40)
175
179
  for (const m of history) {
176
- if (m.role === 'user' && m.imagePath) {
177
- const parts = await fileToContentParts(m.imagePath)
180
+ if (m.role === 'user' && (m.imagePath || m.imageUrl)) {
181
+ const parts = m.imagePath ? await fileToContentParts(m.imagePath) : []
182
+ if (m.imageUrl) parts.push(urlToImagePart(m.imageUrl))
178
183
  msgs.push({ role: 'user', content: [...parts, { type: 'text', text: m.text }] })
179
184
  } else {
180
185
  msgs.push({ role: m.role, content: m.text })
@@ -183,8 +188,9 @@ async function buildMessages(session: any, message: string, imagePath: string |
183
188
  }
184
189
 
185
190
  // Current message with optional attachment
186
- if (imagePath) {
187
- const parts = await fileToContentParts(imagePath)
191
+ if (imagePath || imageUrl) {
192
+ const parts = imagePath ? await fileToContentParts(imagePath) : []
193
+ if (imageUrl) parts.push(urlToImagePart(imageUrl))
188
194
  msgs.push({ role: 'user', content: [...parts, { type: 'text', text: message }] })
189
195
  } else {
190
196
  msgs.push({ role: 'user', content: message })
@@ -24,11 +24,20 @@ import { getMemoryDb } from './memory-db'
24
24
  import { routeTaskIntent } from './capability-router'
25
25
  import { notify } from './ws-hub'
26
26
  import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
27
- import type { MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
27
+ import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
28
+ import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
28
29
  import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
29
30
  import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
30
31
  type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'
31
32
 
33
+ /** Slice history from the most recent context-clear marker forward */
34
+ function applyContextClearBoundary(messages: Message[]): Message[] {
35
+ for (let i = messages.length - 1; i >= 0; i--) {
36
+ if (messages[i].kind === 'context-clear') return messages.slice(i + 1)
37
+ }
38
+ return messages
39
+ }
40
+
32
41
  interface SessionWithTools {
33
42
  tools?: string[] | null
34
43
  }
@@ -55,6 +64,7 @@ export interface ExecuteChatTurnInput {
55
64
  onEvent?: (event: SSEEvent) => void
56
65
  modelOverride?: string
57
66
  heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
67
+ replyToId?: string
58
68
  }
59
69
 
60
70
  export interface ExecuteChatTurnResult {
@@ -306,11 +316,11 @@ function buildAgentSystemPrompt(session: any): string | undefined {
306
316
  if (!session.agentId) return undefined
307
317
  const agents = loadAgents()
308
318
  const agent = agents[session.agentId]
309
- if (!agent?.systemPrompt && !agent?.soul) return undefined
310
319
 
311
320
  const settings = loadSettings()
312
321
  const parts: string[] = []
313
322
  if (settings.userPrompt) parts.push(settings.userPrompt)
323
+ parts.push(buildCurrentDateTimePromptContext())
314
324
  if (agent.soul) parts.push(agent.soul)
315
325
  if (agent.systemPrompt) parts.push(agent.systemPrompt)
316
326
  if (agent.skillIds?.length) {
@@ -320,6 +330,7 @@ function buildAgentSystemPrompt(session: any): string | undefined {
320
330
  if (skill?.content) parts.push(`## Skill: ${skill.name}\n${skill.content}`)
321
331
  }
322
332
  }
333
+ if (!parts.length) return undefined
323
334
  return parts.join('\n\n')
324
335
  }
325
336
 
@@ -341,13 +352,26 @@ function resolveApiKeyForSession(session: SessionWithCredentials, provider: Prov
341
352
  return null
342
353
  }
343
354
 
344
- function classifyHeartbeatResponse(text: string, ackMaxChars: number): 'suppress' | 'strip' | 'keep' {
345
- const trimmed = text.trim()
346
- if (trimmed === 'HEARTBEAT_OK' || trimmed === 'NO_MESSAGE') return 'suppress'
347
- const stripped = trimmed.replace(/HEARTBEAT_OK/gi, '').replace(/NO_MESSAGE/gi, '').trim()
355
+ function stripMarkupForHeartbeat(text: string): string {
356
+ return text
357
+ .replace(/<[^>]*>/g, ' ') // strip HTML tags
358
+ .replace(/&nbsp;/gi, ' ') // decode nbsp
359
+ .replace(/^[*`~_]+/, '') // strip leading markdown
360
+ .replace(/[*`~_]+$/, '') // strip trailing markdown
361
+ .trim()
362
+ }
363
+
364
+ const HEARTBEAT_OK_RE = /HEARTBEAT_OK[^\w]{0,4}$/
365
+ const NO_MESSAGE_RE = /NO_MESSAGE[^\w]{0,4}$/
366
+
367
+ function classifyHeartbeatResponse(text: string, ackMaxChars: number, hadToolCalls: boolean): 'suppress' | 'strip' | 'keep' {
368
+ const cleaned = stripMarkupForHeartbeat(text)
369
+ if (cleaned === 'HEARTBEAT_OK' || cleaned === 'NO_MESSAGE') return 'suppress'
370
+ if (HEARTBEAT_OK_RE.test(cleaned) || NO_MESSAGE_RE.test(cleaned)) return 'suppress'
371
+ const stripped = cleaned.replace(/HEARTBEAT_OK/gi, '').replace(/NO_MESSAGE/gi, '').trim()
348
372
  if (!stripped) return 'suppress'
349
- if (stripped.length <= ackMaxChars) return 'suppress'
350
- return stripped.length < trimmed.length ? 'strip' : 'keep'
373
+ if (!hadToolCalls && stripped.length <= ackMaxChars) return 'suppress'
374
+ return stripped.length < cleaned.length ? 'strip' : 'keep'
351
375
  }
352
376
 
353
377
  function estimateConversationTone(text: string): string {
@@ -542,6 +566,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
542
566
  imagePath: imagePath || undefined,
543
567
  imageUrl: imageUrl || undefined,
544
568
  attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
569
+ replyToId: input.replyToId || undefined,
545
570
  })
546
571
  session.lastActiveAt = Date.now()
547
572
  saveSessions(sessions)
@@ -551,6 +576,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
551
576
  const toolEvents: MessageToolEvent[] = []
552
577
  const streamErrors: string[] = []
553
578
 
579
+ let thinkingText = ''
554
580
  const emit = (ev: SSEEvent) => {
555
581
  if (ev.t === 'err' && typeof ev.text === 'string') {
556
582
  const trimmed = ev.text.trim()
@@ -559,6 +585,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
559
585
  if (streamErrors.length > 8) streamErrors.shift()
560
586
  }
561
587
  }
588
+ if (ev.t === 'thinking' && ev.text) {
589
+ thinkingText += ev.text
590
+ }
562
591
  collectToolEvent(ev, toolEvents)
563
592
  onEvent?.(ev)
564
593
  }
@@ -593,9 +622,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
593
622
  const hasTools = !!sessionForRun.tools?.length && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
594
623
 
595
624
  try {
596
- // Heartbeat runs are self-contained skip conversation history to avoid
597
- // blowing past the context window on long-lived sessions.
598
- const heartbeatHistory = isAutoRunNoHistory ? [] : undefined
625
+ // Heartbeat runs get a small tail of recent messages so the agent can see
626
+ // prior findings and avoid repeating the same searches. Full history is
627
+ // skipped to avoid blowing the context window on long-lived sessions.
628
+ const heartbeatHistory = isAutoRunNoHistory
629
+ ? getSessionMessages(sessionId).slice(-6)
630
+ : undefined
599
631
 
600
632
  console.log(`[chat-execution] provider=${providerType}, hasTools=${hasTools}, imagePath=${imagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, tools=${(sessionForRun.tools || []).length}`)
601
633
 
@@ -608,7 +640,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
608
640
  apiKey,
609
641
  systemPrompt,
610
642
  write: (raw) => parseAndEmit(raw),
611
- history: heartbeatHistory ?? getSessionMessages(sessionId),
643
+ history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
612
644
  signal: abortController.signal,
613
645
  })).fullText
614
646
  : await provider.handler.streamChat({
@@ -619,7 +651,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
619
651
  systemPrompt,
620
652
  write: (raw: string) => parseAndEmit(raw),
621
653
  active,
622
- loadHistory: isAutoRunNoHistory ? () => [] : getSessionMessages,
654
+ loadHistory: isAutoRunNoHistory ? () => getSessionMessages(sessionId).slice(-6) : (sid: string) => applyContextClearBoundary(getSessionMessages(sid)),
623
655
  onUsage: (u) => { directUsage.inputTokens = u.inputTokens; directUsage.outputTokens = u.outputTokens; directUsage.received = true },
624
656
  })
625
657
  } catch (err: any) {
@@ -708,7 +740,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
708
740
  const toolOutput = await selectedTool.invoke(args)
709
741
  const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
710
742
  emit({ t: 'tool_result', toolName, toolOutput: outputText })
711
- if (outputText?.trim()) fullResponse = outputText.trim()
743
+ // Don't overwrite fullResponse with raw tool output — it's already captured
744
+ // in toolEvents. Only set a brief notice when the LLM produced no text,
745
+ // so the message bubble isn't empty.
746
+ if (!fullResponse.trim() && outputText?.trim()) {
747
+ const label = toolName.replace(/_/g, ' ')
748
+ fullResponse = `Used **${label}** — see tool output above for details.`
749
+ }
712
750
  calledNames.add(toolName)
713
751
  return true
714
752
  } catch (forceErr: any) {
@@ -854,7 +892,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
854
892
  const heartbeatConfig = input.heartbeatConfig
855
893
  let heartbeatClassification: 'suppress' | 'strip' | 'keep' | null = null
856
894
  if (isHeartbeatRun && textForPersistence.length > 0) {
857
- heartbeatClassification = classifyHeartbeatResponse(textForPersistence, heartbeatConfig?.ackMaxChars ?? 300)
895
+ heartbeatClassification = classifyHeartbeatResponse(textForPersistence, heartbeatConfig?.ackMaxChars ?? 300, toolEvents.length > 0)
896
+ }
897
+
898
+ // Emit WS notification for every heartbeat completion so UI can show pulse
899
+ if (isHeartbeatRun && session.agentId) {
900
+ notify(`heartbeat:agent:${session.agentId}`)
858
901
  }
859
902
 
860
903
  const shouldPersistAssistant = textForPersistence.length > 0
@@ -904,6 +947,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
904
947
  role: 'assistant',
905
948
  text: persistedText,
906
949
  time: Date.now(),
950
+ thinking: thinkingText || undefined,
907
951
  toolEvents: toolEvents.length ? toolEvents : undefined,
908
952
  kind: persistedKind,
909
953
  })
@@ -944,6 +988,22 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
944
988
  // Best effort — connector manager may not be loaded
945
989
  }
946
990
  }
991
+
992
+ // Auto-discover connectors linked to this agent when no explicit target is set
993
+ if (isHeartbeatRun && !heartbeatConfig?.target && heartbeatConfig?.showAlerts !== false && session.agentId) {
994
+ try {
995
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
996
+ const { listRunningConnectors: listRunning, sendConnectorMessage: sendMsg } = require('./connectors/manager')
997
+ const agentConnectors = listRunning().filter((c: { agentId: string | null; recentChannelId: string | null; supportsSend: boolean }) =>
998
+ c.agentId === session.agentId && c.recentChannelId && c.supportsSend
999
+ )
1000
+ for (const conn of agentConnectors) {
1001
+ sendMsg({ connectorId: conn.id, channelId: conn.recentChannelId, text: persistedText }).catch(() => {})
1002
+ }
1003
+ } catch {
1004
+ // Best effort — connector manager may not be loaded
1005
+ }
1006
+ }
947
1007
  }
948
1008
 
949
1009
  const autoMemoryEligible = shouldStoreAutoMemoryNote({
@@ -0,0 +1,146 @@
1
+ import { loadSettings, loadSkills, loadCredentials, decryptKey } from './storage'
2
+ import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
3
+ import type { Chatroom, Agent, Session, Message } from '@/types'
4
+
5
+ /** Resolve API key from an agent's credentialId */
6
+ export function resolveApiKey(credentialId: string | null | undefined): string | null {
7
+ if (!credentialId) return null
8
+ const creds = loadCredentials()
9
+ const cred = creds[credentialId]
10
+ if (!cred?.encryptedKey) return null
11
+ try { return decryptKey(cred.encryptedKey) } catch { return null }
12
+ }
13
+
14
+ /** Parse @mentions from message text, returns matching agentIds */
15
+ export function parseMentions(text: string, agents: Record<string, Agent>, memberIds: string[]): string[] {
16
+ if (/@all\b/i.test(text)) return [...memberIds]
17
+ const mentionPattern = /@(\S+)/g
18
+ const mentioned: string[] = []
19
+ let match: RegExpExecArray | null
20
+ while ((match = mentionPattern.exec(text)) !== null) {
21
+ const name = match[1].toLowerCase()
22
+ for (const id of memberIds) {
23
+ const agent = agents[id]
24
+ if (agent && agent.name.toLowerCase().replace(/\s+/g, '') === name) {
25
+ if (!mentioned.includes(id)) mentioned.push(id)
26
+ }
27
+ }
28
+ }
29
+ return mentioned
30
+ }
31
+
32
+ /** Build chatroom context as a system prompt addendum with agent profiles and collaboration guidelines */
33
+ export function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<string, Agent>, agentId: string): string {
34
+ const selfAgent = agents[agentId]
35
+ const selfName = selfAgent?.name || agentId
36
+
37
+ // Build team profiles with capabilities
38
+ const teamProfiles = chatroom.agentIds
39
+ .filter((id) => id !== agentId)
40
+ .map((id) => {
41
+ const a = agents[id]
42
+ if (!a) return null
43
+ const tools = a.tools?.length ? `Tools: ${a.tools.join(', ')}` : 'No specialized tools'
44
+ const desc = a.description || a.soul || 'No description'
45
+ return `- **${a.name}**: ${desc}\n ${tools}`
46
+ })
47
+ .filter(Boolean)
48
+ .join('\n')
49
+
50
+ const recentMessages = chatroom.messages.slice(-30).map((m) => {
51
+ return `[${m.senderName}]: ${m.text}`
52
+ }).join('\n')
53
+
54
+ const memberCount = chatroom.agentIds.length
55
+ const otherNames = chatroom.agentIds
56
+ .filter((id) => id !== agentId)
57
+ .map((id) => agents[id]?.name)
58
+ .filter(Boolean)
59
+
60
+ return [
61
+ `## Chatroom Context`,
62
+ `You are **${selfName}** in a group chatroom called "${chatroom.name}" with ${memberCount} participants (you, ${otherNames.join(', ') || 'others'}, and the user).`,
63
+ selfAgent?.description ? `Your role: ${selfAgent.description}` : '',
64
+ selfAgent?.tools?.length ? `Your available tools: ${selfAgent.tools.join(', ')}` : '',
65
+ '',
66
+ '## Team Members',
67
+ teamProfiles || '(no other agents)',
68
+ '',
69
+ '## How to Behave in This Chatroom',
70
+ '- **You are in a group chat.** Talk like you are in a real-time conversation with teammates — be direct, casual, and concise.',
71
+ '- **Be yourself.** Respond with personality. Don\'t give generic "let me know if you need anything" responses. Actually engage with what was said.',
72
+ '- **Answer the question or react to the message.** If someone says "how are you doing?" just answer naturally. If someone asks a question you can help with, help directly.',
73
+ '- **Keep responses short** unless depth is needed. A few sentences is usually enough. This is a chat, not an essay.',
74
+ '- **@mention teammates** only when you genuinely need their specific expertise. Don\'t tag people just to be polite.',
75
+ '- **Don\'t narrate your capabilities** unless asked. Just demonstrate them by doing things.',
76
+ '- **Read the room.** Look at recent messages to understand context. Don\'t repeat what others already said.',
77
+ '',
78
+ '## Recent Messages',
79
+ recentMessages || '(no messages yet)',
80
+ ].filter((line) => line !== undefined).join('\n')
81
+ }
82
+
83
+ /** Build a synthetic session object for an agent in a chatroom */
84
+ export function buildSyntheticSession(agent: Agent, chatroomId: string): Session {
85
+ return {
86
+ id: `chatroom-${chatroomId}-${agent.id}`,
87
+ name: `Chatroom session for ${agent.name}`,
88
+ cwd: process.cwd(),
89
+ user: 'chatroom',
90
+ provider: agent.provider,
91
+ model: agent.model,
92
+ credentialId: agent.credentialId ?? null,
93
+ fallbackCredentialIds: agent.fallbackCredentialIds,
94
+ apiEndpoint: agent.apiEndpoint ?? null,
95
+ claudeSessionId: null,
96
+ messages: [],
97
+ createdAt: Date.now(),
98
+ lastActiveAt: Date.now(),
99
+ tools: agent.tools || [],
100
+ agentId: agent.id,
101
+ }
102
+ }
103
+
104
+ /** Build agent's system prompt including skills */
105
+ export function buildAgentSystemPromptForChatroom(agent: Agent): string {
106
+ const settings = loadSettings()
107
+ const parts: string[] = []
108
+ if (settings.userPrompt) parts.push(settings.userPrompt)
109
+ parts.push(buildCurrentDateTimePromptContext())
110
+ if (agent.soul) parts.push(agent.soul)
111
+ if (agent.systemPrompt) parts.push(agent.systemPrompt)
112
+ if (agent.skillIds?.length) {
113
+ const allSkills = loadSkills()
114
+ for (const skillId of agent.skillIds) {
115
+ const skill = allSkills[skillId]
116
+ if (skill?.content) parts.push(`## Skill: ${skill.name}\n${skill.content}`)
117
+ }
118
+ }
119
+ return parts.join('\n\n')
120
+ }
121
+
122
+ /** Convert chatroom messages to Message history format for LLM */
123
+ export function buildHistoryForAgent(chatroom: Chatroom, agentId: string, imagePath?: string, attachedFiles?: string[]): Message[] {
124
+ const history = chatroom.messages.slice(-50).map((m) => {
125
+ let msgText = `[${m.senderName}]: ${m.text}`
126
+ // Include attachment info in history
127
+ if (m.attachedFiles?.length) {
128
+ const names = m.attachedFiles.map((f) => f.split('/').pop()).join(', ')
129
+ msgText += `\n[Attached: ${names}]`
130
+ }
131
+ return {
132
+ role: m.senderId === agentId ? 'assistant' as const : 'user' as const,
133
+ text: msgText,
134
+ time: m.time,
135
+ ...(m.imagePath ? { imagePath: m.imagePath } : {}),
136
+ ...(m.attachedFiles ? { attachedFiles: m.attachedFiles } : {}),
137
+ }
138
+ })
139
+ // Pass through imagePath/attachedFiles from the current message to the last history entry
140
+ if (history.length > 0 && (imagePath || attachedFiles)) {
141
+ const last = history[history.length - 1]
142
+ if (imagePath && !last.imagePath) last.imagePath = imagePath
143
+ if (attachedFiles && !last.attachedFiles) last.attachedFiles = attachedFiles
144
+ }
145
+ return history
146
+ }