@swarmclawai/swarmclaw 0.7.2 → 0.7.3

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 (197) hide show
  1. package/README.md +81 -22
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +36 -7
  5. package/src/app/api/agents/route.ts +12 -1
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  9. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  10. package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
  11. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  12. package/src/app/api/chats/[id]/route.ts +18 -0
  13. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  14. package/src/app/api/chats/route.ts +16 -0
  15. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  16. package/src/app/api/connectors/doctor/route.ts +13 -0
  17. package/src/app/api/files/open/route.ts +16 -14
  18. package/src/app/api/memory/maintenance/route.ts +11 -1
  19. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  20. package/src/app/api/openclaw/skills/route.ts +11 -3
  21. package/src/app/api/plugins/dependencies/route.ts +24 -0
  22. package/src/app/api/plugins/install/route.ts +15 -92
  23. package/src/app/api/plugins/route.ts +3 -26
  24. package/src/app/api/plugins/settings/route.ts +17 -12
  25. package/src/app/api/plugins/ui/route.ts +1 -0
  26. package/src/app/api/settings/route.ts +49 -7
  27. package/src/app/api/tasks/[id]/route.ts +15 -6
  28. package/src/app/api/tasks/bulk/route.ts +2 -2
  29. package/src/app/api/tasks/route.ts +9 -4
  30. package/src/app/api/webhooks/[id]/route.ts +8 -1
  31. package/src/app/page.tsx +9 -2
  32. package/src/cli/index.js +4 -0
  33. package/src/cli/index.ts +3 -10
  34. package/src/components/agents/agent-card.tsx +15 -12
  35. package/src/components/agents/agent-chat-list.tsx +101 -1
  36. package/src/components/agents/agent-list.tsx +46 -9
  37. package/src/components/agents/agent-sheet.tsx +207 -16
  38. package/src/components/agents/inspector-panel.tsx +108 -48
  39. package/src/components/auth/access-key-gate.tsx +36 -97
  40. package/src/components/chat/chat-area.tsx +29 -13
  41. package/src/components/chat/chat-card.tsx +4 -20
  42. package/src/components/chat/chat-header.tsx +255 -353
  43. package/src/components/chat/chat-list.tsx +7 -9
  44. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  45. package/src/components/chat/message-list.tsx +3 -1
  46. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  47. package/src/components/connectors/connector-list.tsx +265 -127
  48. package/src/components/connectors/connector-sheet.tsx +217 -0
  49. package/src/components/home/home-view.tsx +128 -4
  50. package/src/components/layout/app-layout.tsx +383 -194
  51. package/src/components/layout/mobile-header.tsx +26 -8
  52. package/src/components/plugins/plugin-list.tsx +15 -3
  53. package/src/components/plugins/plugin-sheet.tsx +118 -9
  54. package/src/components/projects/project-detail.tsx +183 -0
  55. package/src/components/shared/agent-picker-list.tsx +2 -2
  56. package/src/components/shared/command-palette.tsx +111 -24
  57. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  58. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  59. package/src/components/shared/settings/section-heartbeat.tsx +77 -0
  60. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  61. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  62. package/src/components/shared/settings/section-secrets.tsx +6 -6
  63. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  64. package/src/components/shared/settings/section-voice.tsx +5 -1
  65. package/src/components/shared/settings/section-web-search.tsx +10 -2
  66. package/src/components/shared/settings/settings-page.tsx +245 -46
  67. package/src/components/tasks/approvals-panel.tsx +205 -18
  68. package/src/components/tasks/task-board.tsx +242 -46
  69. package/src/components/usage/metrics-dashboard.tsx +74 -1
  70. package/src/components/wallets/wallet-panel.tsx +17 -5
  71. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  72. package/src/lib/auth.ts +17 -0
  73. package/src/lib/chat-streaming-state.test.ts +108 -0
  74. package/src/lib/chat-streaming-state.ts +108 -0
  75. package/src/lib/openclaw-agent-id.test.ts +14 -0
  76. package/src/lib/openclaw-agent-id.ts +31 -0
  77. package/src/lib/server/agent-assignment.test.ts +112 -0
  78. package/src/lib/server/agent-assignment.ts +169 -0
  79. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  80. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  81. package/src/lib/server/approvals.ts +483 -75
  82. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  83. package/src/lib/server/browser-state.test.ts +118 -0
  84. package/src/lib/server/browser-state.ts +123 -0
  85. package/src/lib/server/build-llm.test.ts +36 -0
  86. package/src/lib/server/build-llm.ts +11 -4
  87. package/src/lib/server/builtin-plugins.ts +34 -0
  88. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  89. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  90. package/src/lib/server/chat-execution.ts +250 -61
  91. package/src/lib/server/chatroom-health.test.ts +26 -0
  92. package/src/lib/server/chatroom-health.ts +2 -3
  93. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  94. package/src/lib/server/chatroom-helpers.ts +45 -5
  95. package/src/lib/server/connectors/discord.ts +175 -11
  96. package/src/lib/server/connectors/doctor.test.ts +80 -0
  97. package/src/lib/server/connectors/doctor.ts +116 -0
  98. package/src/lib/server/connectors/manager.ts +946 -110
  99. package/src/lib/server/connectors/policy.test.ts +222 -0
  100. package/src/lib/server/connectors/policy.ts +452 -0
  101. package/src/lib/server/connectors/slack.ts +188 -9
  102. package/src/lib/server/connectors/telegram.ts +65 -15
  103. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  104. package/src/lib/server/connectors/thread-context.ts +72 -0
  105. package/src/lib/server/connectors/types.ts +41 -11
  106. package/src/lib/server/daemon-state.ts +59 -1
  107. package/src/lib/server/data-dir.ts +13 -0
  108. package/src/lib/server/delegation-jobs.test.ts +140 -0
  109. package/src/lib/server/delegation-jobs.ts +248 -0
  110. package/src/lib/server/document-utils.test.ts +47 -0
  111. package/src/lib/server/document-utils.ts +397 -0
  112. package/src/lib/server/heartbeat-service.ts +13 -39
  113. package/src/lib/server/heartbeat-source.test.ts +22 -0
  114. package/src/lib/server/heartbeat-source.ts +7 -0
  115. package/src/lib/server/identity-continuity.test.ts +77 -0
  116. package/src/lib/server/identity-continuity.ts +127 -0
  117. package/src/lib/server/mailbox-utils.ts +347 -0
  118. package/src/lib/server/main-agent-loop.ts +27 -967
  119. package/src/lib/server/memory-db.ts +4 -6
  120. package/src/lib/server/memory-tiers.ts +40 -0
  121. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  122. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  123. package/src/lib/server/openclaw-exec-config.ts +5 -6
  124. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  125. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  126. package/src/lib/server/openclaw-sync.ts +3 -2
  127. package/src/lib/server/orchestrator-lg.ts +17 -6
  128. package/src/lib/server/orchestrator.ts +2 -2
  129. package/src/lib/server/playwright-proxy.mjs +27 -3
  130. package/src/lib/server/plugins.test.ts +207 -0
  131. package/src/lib/server/plugins.ts +822 -69
  132. package/src/lib/server/provider-health.ts +33 -3
  133. package/src/lib/server/queue.ts +3 -20
  134. package/src/lib/server/scheduler.ts +2 -0
  135. package/src/lib/server/session-archive-memory.test.ts +85 -0
  136. package/src/lib/server/session-archive-memory.ts +230 -0
  137. package/src/lib/server/session-mailbox.ts +8 -18
  138. package/src/lib/server/session-reset-policy.test.ts +99 -0
  139. package/src/lib/server/session-reset-policy.ts +311 -0
  140. package/src/lib/server/session-run-manager.ts +33 -80
  141. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  142. package/src/lib/server/session-tools/calendar.ts +2 -12
  143. package/src/lib/server/session-tools/connector.ts +109 -8
  144. package/src/lib/server/session-tools/context.ts +14 -2
  145. package/src/lib/server/session-tools/crawl.ts +447 -0
  146. package/src/lib/server/session-tools/crud.ts +70 -32
  147. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  148. package/src/lib/server/session-tools/delegate.ts +406 -20
  149. package/src/lib/server/session-tools/discovery.ts +22 -4
  150. package/src/lib/server/session-tools/document.ts +283 -0
  151. package/src/lib/server/session-tools/email.ts +1 -3
  152. package/src/lib/server/session-tools/extract.ts +137 -0
  153. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  154. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  155. package/src/lib/server/session-tools/file.ts +237 -24
  156. package/src/lib/server/session-tools/human-loop.ts +227 -0
  157. package/src/lib/server/session-tools/image-gen.ts +1 -3
  158. package/src/lib/server/session-tools/index.ts +56 -1
  159. package/src/lib/server/session-tools/mailbox.ts +276 -0
  160. package/src/lib/server/session-tools/memory.ts +35 -3
  161. package/src/lib/server/session-tools/monitor.ts +150 -7
  162. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  163. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  164. package/src/lib/server/session-tools/platform.ts +142 -4
  165. package/src/lib/server/session-tools/plugin-creator.ts +86 -23
  166. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  167. package/src/lib/server/session-tools/replicate.ts +1 -3
  168. package/src/lib/server/session-tools/schedule.ts +20 -10
  169. package/src/lib/server/session-tools/session-info.ts +36 -3
  170. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  171. package/src/lib/server/session-tools/subagent.ts +193 -27
  172. package/src/lib/server/session-tools/table.ts +587 -0
  173. package/src/lib/server/session-tools/wallet.ts +13 -10
  174. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  175. package/src/lib/server/session-tools/web.ts +896 -100
  176. package/src/lib/server/storage.ts +226 -7
  177. package/src/lib/server/stream-agent-chat.ts +46 -21
  178. package/src/lib/server/structured-extract.test.ts +72 -0
  179. package/src/lib/server/structured-extract.ts +373 -0
  180. package/src/lib/server/task-mention.test.ts +16 -2
  181. package/src/lib/server/task-mention.ts +61 -10
  182. package/src/lib/server/tool-aliases.ts +44 -7
  183. package/src/lib/server/tool-capability-policy.ts +6 -0
  184. package/src/lib/server/tool-retry.ts +2 -0
  185. package/src/lib/server/watch-jobs.test.ts +173 -0
  186. package/src/lib/server/watch-jobs.ts +532 -0
  187. package/src/lib/server/ws-hub.ts +5 -3
  188. package/src/lib/validation/schemas.test.ts +26 -0
  189. package/src/lib/validation/schemas.ts +7 -0
  190. package/src/lib/ws-client.ts +14 -12
  191. package/src/proxy.ts +5 -5
  192. package/src/stores/use-app-store.ts +0 -6
  193. package/src/stores/use-chat-store.ts +31 -2
  194. package/src/types/index.ts +287 -44
  195. package/src/components/chat/new-chat-sheet.tsx +0 -253
  196. package/src/lib/server/main-session.ts +0 -17
  197. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -1,6 +1,7 @@
1
1
  import { WebSocketServer, WebSocket } from 'ws'
2
2
  import type { IncomingMessage } from 'http'
3
3
  import { validateAccessKey } from './storage'
4
+ import { AUTH_COOKIE_NAME, getCookieValue } from '@/lib/auth'
4
5
 
5
6
  interface WsClient {
6
7
  ws: WebSocket
@@ -29,9 +30,10 @@ export function initWsServer() {
29
30
  ;(globalThis as any)[GK] = hub
30
31
 
31
32
  wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
32
- // Auth: validate ?key= from upgrade URL
33
- const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
34
- const key = url.searchParams.get('key') || ''
33
+ const headerKey = req.headers['x-access-key']
34
+ const key = (Array.isArray(headerKey) ? headerKey[0] : headerKey)
35
+ || getCookieValue(req.headers.cookie, AUTH_COOKIE_NAME)
36
+ || ''
35
37
  if (!validateAccessKey(key)) {
36
38
  ws.close(4001, 'Unauthorized')
37
39
  return
@@ -0,0 +1,26 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { AgentCreateSchema } from './schemas'
4
+
5
+ describe('AgentCreateSchema', () => {
6
+ it('defaults platformAssignScope to self', () => {
7
+ const parsed = AgentCreateSchema.parse({
8
+ name: 'Solo Agent',
9
+ provider: 'openai',
10
+ })
11
+
12
+ assert.equal(parsed.platformAssignScope, 'self')
13
+ })
14
+
15
+ it('accepts explicit all-scope delegation without relying on legacy orchestrator flags', () => {
16
+ const parsed = AgentCreateSchema.parse({
17
+ name: 'Coordinator',
18
+ provider: 'openai',
19
+ platformAssignScope: 'all',
20
+ isOrchestrator: false,
21
+ })
22
+
23
+ assert.equal(parsed.platformAssignScope, 'all')
24
+ assert.equal(parsed.isOrchestrator, false)
25
+ })
26
+ })
@@ -9,6 +9,7 @@ export const AgentCreateSchema = z.object({
9
9
  credentialId: z.string().nullable().optional().default(null),
10
10
  apiEndpoint: z.string().nullable().optional().default(null),
11
11
  isOrchestrator: z.boolean().optional().default(false),
12
+ platformAssignScope: z.enum(['self', 'all']).optional().default('self'),
12
13
  subAgentIds: z.array(z.string()).optional().default([]),
13
14
  plugins: z.array(z.string()).optional().default([]),
14
15
  /** @deprecated Use plugins */
@@ -16,6 +17,12 @@ export const AgentCreateSchema = z.object({
16
17
  capabilities: z.array(z.string()).optional().default([]),
17
18
  thinkingLevel: z.string().optional(),
18
19
  soul: z.string().optional(),
20
+ identityState: z.record(z.string(), z.unknown()).nullable().optional().default(null),
21
+ sessionResetMode: z.enum(['idle', 'daily']).nullable().optional().default(null),
22
+ sessionIdleTimeoutSec: z.number().int().nonnegative().nullable().optional().default(null),
23
+ sessionMaxAgeSec: z.number().int().nonnegative().nullable().optional().default(null),
24
+ sessionDailyResetAt: z.string().nullable().optional().default(null),
25
+ sessionResetTimezone: z.string().nullable().optional().default(null),
19
26
  autoRecovery: z.boolean().optional().default(false),
20
27
  monthlyBudget: z.number().positive().nullable().optional().default(null),
21
28
  dailyBudget: z.number().positive().nullable().optional().default(null),
@@ -1,15 +1,15 @@
1
1
  type WsCallback = () => void
2
2
 
3
3
  let ws: WebSocket | null = null
4
- let accessKey = ''
4
+ let wsEnabled = false
5
5
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null
6
6
  let reconnectDelay = 1000
7
7
  const MAX_RECONNECT_DELAY = 30_000
8
8
  const listeners = new Map<string, Set<WsCallback>>()
9
9
  let connected = false
10
10
 
11
- function getWsUrl(key: string): string {
12
- if (typeof window === 'undefined') return `ws://localhost:3457/ws?key=${encodeURIComponent(key)}`
11
+ function getWsUrl(): string {
12
+ if (typeof window === 'undefined') return 'ws://localhost:3457/ws'
13
13
 
14
14
  const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
15
15
  const pagePort = window.location.port
@@ -22,7 +22,7 @@ function getWsUrl(key: string): string {
22
22
  const behindProxy = !pagePort || pagePort === '80' || pagePort === '443' || pagePort !== appPort
23
23
  const wsHost = behindProxy ? window.location.host : `${window.location.hostname}:${buildPort}`
24
24
 
25
- return `${protocol}://${wsHost}/ws?key=${encodeURIComponent(key)}`
25
+ return `${protocol}://${wsHost}/ws`
26
26
  }
27
27
 
28
28
  function handleMessage(event: MessageEvent) {
@@ -44,17 +44,18 @@ function scheduleReconnect() {
44
44
  const jitter = Math.random() * 2000
45
45
  reconnectTimer = setTimeout(() => {
46
46
  reconnectTimer = null
47
- if (!accessKey) return
48
- connect(accessKey)
47
+ if (!wsEnabled) return
48
+ connect()
49
49
  }, reconnectDelay + jitter)
50
50
  reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY)
51
51
  }
52
52
 
53
- function connect(key: string) {
53
+ function connect() {
54
+ if (!wsEnabled) return
54
55
  if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return
55
56
 
56
57
  try {
57
- ws = new WebSocket(getWsUrl(key))
58
+ ws = new WebSocket(getWsUrl())
58
59
  } catch {
59
60
  scheduleReconnect()
60
61
  return
@@ -75,7 +76,7 @@ function connect(key: string) {
75
76
  ws.onclose = () => {
76
77
  connected = false
77
78
  ws = null
78
- if (accessKey) scheduleReconnect()
79
+ if (wsEnabled) scheduleReconnect()
79
80
  }
80
81
 
81
82
  ws.onerror = () => {
@@ -84,13 +85,14 @@ function connect(key: string) {
84
85
  }
85
86
 
86
87
  export function connectWs(key: string) {
87
- accessKey = key
88
+ void key
89
+ wsEnabled = true
88
90
  reconnectDelay = 1000
89
- connect(key)
91
+ connect()
90
92
  }
91
93
 
92
94
  export function disconnectWs() {
93
- accessKey = ''
95
+ wsEnabled = false
94
96
  if (reconnectTimer) {
95
97
  clearTimeout(reconnectTimer)
96
98
  reconnectTimer = null
package/src/proxy.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import type { NextRequest } from 'next/server'
3
+ import { AUTH_COOKIE_NAME } from '@/lib/auth'
3
4
 
4
5
  /* ------------------------------------------------------------------ */
5
6
  /* Rate-limit state — HMR-safe via globalThis */
@@ -44,7 +45,7 @@ function getClientIp(request: NextRequest): string {
44
45
  /* ------------------------------------------------------------------ */
45
46
 
46
47
  /** Access key auth proxy with brute-force rate limiting.
47
- * Checks X-Access-Key header or ?key= param on all /api/ routes except /api/auth.
48
+ * Checks X-Access-Key header or auth cookie on all /api/ routes except /api/auth.
48
49
  * The key is validated against the ACCESS_KEY env var.
49
50
  * After 5 failed attempts from a single IP the client is locked out for 15 minutes.
50
51
  */
@@ -55,11 +56,10 @@ export function proxy(request: NextRequest) {
55
56
  const isConnectorWebhook = request.method === 'POST'
56
57
  && /^\/api\/connectors\/[^/]+\/webhook\/?$/.test(pathname)
57
58
 
58
- // Only protect API routes (not auth, uploads served as static assets, or inbound webhooks)
59
+ // Only protect API routes (not auth or inbound webhooks)
59
60
  if (
60
61
  !pathname.startsWith('/api/')
61
62
  || pathname === '/api/auth'
62
- || pathname.startsWith('/api/uploads/')
63
63
  || isWebhookTrigger
64
64
  || isConnectorWebhook
65
65
  ) {
@@ -88,8 +88,8 @@ export function proxy(request: NextRequest) {
88
88
  }
89
89
 
90
90
  const providedKey =
91
- request.headers.get('x-access-key')
92
- || request.nextUrl.searchParams.get('key')
91
+ request.headers.get('x-access-key')?.trim()
92
+ || request.cookies.get(AUTH_COOKIE_NAME)?.value?.trim()
93
93
  || ''
94
94
 
95
95
  if (providedKey !== accessKey) {
@@ -43,9 +43,6 @@ interface AppState {
43
43
  settingsOpen: boolean
44
44
  setSettingsOpen: (open: boolean) => void
45
45
 
46
- newSessionOpen: boolean
47
- setNewSessionOpen: (open: boolean) => void
48
-
49
46
  activeView: AppView
50
47
  setActiveView: (view: AppView) => void
51
48
 
@@ -325,9 +322,6 @@ export const useAppStore = create<AppState>((set, get) => ({
325
322
  settingsOpen: false,
326
323
  setSettingsOpen: (open) => set({ settingsOpen: open }),
327
324
 
328
- newSessionOpen: false,
329
- setNewSessionOpen: (open) => set({ newSessionOpen: open }),
330
-
331
325
  activeView: 'home',
332
326
  setActiveView: (view) => set({ activeView: view }),
333
327
 
@@ -3,6 +3,7 @@
3
3
  import { create } from 'zustand'
4
4
  import type { Message, DevServerStatus, SSEEvent, ChatTraceBlock } from '../types'
5
5
  import { streamChat } from '../lib/chat'
6
+ import { mergeCompletedAssistantMessage } from '../lib/chat-streaming-state'
6
7
  import { speak } from '../lib/tts'
7
8
  import { getStoredAccessKey } from '../lib/api-client'
8
9
  import { useAppStore } from './use-app-store'
@@ -306,6 +307,11 @@ export const useChatStore = create<ChatState>((set, get) => ({
306
307
  } else if (event.t === 'tool_call') {
307
308
  const id = `tc-${++toolCallCounter}`
308
309
  set((s) => ({
310
+ ...(s.toolEvents[s.toolEvents.length - 1]?.name === (event.toolName || 'unknown')
311
+ && s.toolEvents[s.toolEvents.length - 1]?.input === (event.toolInput || '')
312
+ && s.toolEvents[s.toolEvents.length - 1]?.status === 'running'
313
+ ? {}
314
+ : {
309
315
  streamPhase: 'tool' as const,
310
316
  streamToolName: event.toolName || 'unknown',
311
317
  toolEvents: [...s.toolEvents, {
@@ -314,6 +320,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
314
320
  input: event.toolInput || '',
315
321
  status: 'running',
316
322
  }],
323
+ }),
317
324
  }))
318
325
  } else if (event.t === 'tool_result') {
319
326
  const soundOn = get().soundEnabled
@@ -322,6 +329,22 @@ export const useChatStore = create<ChatState>((set, get) => ({
322
329
  const idx = events.findLastIndex(
323
330
  (e) => e.name === event.toolName && e.status === 'running',
324
331
  )
332
+ if (idx === -1) {
333
+ const last = events[events.length - 1]
334
+ const output = event.toolOutput || ''
335
+ const isError = /^(Error:|error:|ECONNREFUSED|ETIMEDOUT|timeout|failed)/i.test(output.trim())
336
+ || output.includes('ECONNREFUSED')
337
+ || output.includes('ETIMEDOUT')
338
+ || output.includes('Error:')
339
+ if (
340
+ last
341
+ && last.name === event.toolName
342
+ && last.output === output
343
+ && last.status === (isError ? 'error' : 'done')
344
+ ) {
345
+ return { toolEvents: events }
346
+ }
347
+ }
325
348
  if (idx !== -1) {
326
349
  const output = event.toolOutput || ''
327
350
  const isError = /^(Error:|error:|ECONNREFUSED|ETIMEDOUT|timeout|failed)/i.test(output.trim())
@@ -348,7 +371,13 @@ export const useChatStore = create<ChatState>((set, get) => ({
348
371
  } else if (event.t === 'status') {
349
372
  try {
350
373
  const parsed = JSON.parse(event.text || '{}')
351
- set({ agentStatus: parsed })
374
+ if (
375
+ parsed
376
+ && typeof parsed === 'object'
377
+ && ['goal', 'status', 'summary', 'nextAction'].some((key) => key in parsed)
378
+ ) {
379
+ set({ agentStatus: parsed })
380
+ }
352
381
  } catch {
353
382
  // ignore malformed status
354
383
  }
@@ -377,7 +406,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
377
406
  suggestions: suggestions || undefined,
378
407
  }
379
408
  set((s) => ({
380
- messages: [...s.messages, assistantMsg],
409
+ messages: mergeCompletedAssistantMessage(s.messages, assistantMsg),
381
410
  streaming: false,
382
411
  streamingSessionId: null,
383
412
  streamText: '',