@swarmclawai/swarmclaw 1.2.0 → 1.2.1

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 (123) hide show
  1. package/README.md +10 -0
  2. package/package.json +4 -1
  3. package/src/app/api/chats/[id]/deploy/route.ts +11 -6
  4. package/src/app/api/chats/[id]/devserver/route.ts +5 -2
  5. package/src/app/api/chats/[id]/messages/route.ts +7 -1
  6. package/src/app/api/credentials/[id]/route.ts +4 -1
  7. package/src/app/api/extensions/marketplace/route.ts +5 -2
  8. package/src/app/api/memory/maintenance/route.ts +5 -2
  9. package/src/app/api/preview-server/route.ts +14 -11
  10. package/src/app/api/system/status/route.ts +11 -0
  11. package/src/app/api/upload/route.ts +4 -1
  12. package/src/cli/index.js +7 -0
  13. package/src/cli/spec.js +1 -0
  14. package/src/components/agents/agent-files-editor.tsx +44 -32
  15. package/src/components/agents/personality-builder.tsx +13 -7
  16. package/src/components/agents/trash-list.tsx +1 -1
  17. package/src/components/chat/message-bubble.tsx +1 -0
  18. package/src/components/chat/message-list.tsx +25 -39
  19. package/src/components/chat/swarm-status-card.tsx +10 -3
  20. package/src/components/layout/daemon-indicator.tsx +7 -8
  21. package/src/components/layout/update-banner.tsx +8 -13
  22. package/src/components/logs/log-list.tsx +1 -1
  23. package/src/components/memory/memory-card.tsx +3 -1
  24. package/src/components/org-chart/org-chart-view.tsx +4 -0
  25. package/src/components/projects/project-list.tsx +4 -2
  26. package/src/components/projects/tabs/overview-tab.tsx +3 -2
  27. package/src/components/secrets/secret-sheet.tsx +1 -1
  28. package/src/components/secrets/secrets-list.tsx +1 -1
  29. package/src/components/shared/agent-switch-dialog.tsx +12 -6
  30. package/src/components/shared/dir-browser.tsx +22 -18
  31. package/src/components/skills/skill-sheet.tsx +2 -3
  32. package/src/components/tasks/task-list.tsx +1 -1
  33. package/src/components/tasks/task-sheet.tsx +1 -1
  34. package/src/hooks/use-openclaw-gateway.ts +46 -27
  35. package/src/instrumentation.ts +10 -7
  36. package/src/lib/chat/chat.ts +18 -2
  37. package/src/lib/providers/anthropic.ts +6 -3
  38. package/src/lib/providers/claude-cli.ts +9 -3
  39. package/src/lib/providers/cli-utils.ts +15 -0
  40. package/src/lib/providers/codex-cli.ts +9 -3
  41. package/src/lib/providers/gemini-cli.ts +6 -2
  42. package/src/lib/providers/index.ts +4 -1
  43. package/src/lib/providers/ollama.ts +5 -2
  44. package/src/lib/providers/openai.ts +8 -5
  45. package/src/lib/providers/opencode-cli.ts +6 -2
  46. package/src/lib/server/agents/agent-registry.ts +20 -3
  47. package/src/lib/server/agents/main-agent-loop.ts +4 -3
  48. package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
  49. package/src/lib/server/chat-execution/chat-execution.ts +14 -2
  50. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
  51. package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
  52. package/src/lib/server/chat-execution/message-classifier.ts +5 -2
  53. package/src/lib/server/chat-execution/post-stream-finalization.ts +4 -1
  54. package/src/lib/server/chat-execution/prompt-builder.ts +11 -1
  55. package/src/lib/server/chat-execution/prompt-sections.ts +52 -9
  56. package/src/lib/server/chat-execution/response-completeness.ts +5 -2
  57. package/src/lib/server/chat-execution/stream-agent-chat.ts +42 -12
  58. package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
  59. package/src/lib/server/connectors/bluebubbles.ts +7 -4
  60. package/src/lib/server/connectors/connector-inbound.ts +16 -13
  61. package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
  62. package/src/lib/server/connectors/connector-outbound.ts +6 -3
  63. package/src/lib/server/connectors/discord.ts +10 -7
  64. package/src/lib/server/connectors/email.ts +17 -14
  65. package/src/lib/server/connectors/googlechat.ts +7 -4
  66. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
  67. package/src/lib/server/connectors/matrix.ts +6 -3
  68. package/src/lib/server/connectors/openclaw.ts +20 -17
  69. package/src/lib/server/connectors/outbox.ts +4 -1
  70. package/src/lib/server/connectors/runtime-state.ts +19 -0
  71. package/src/lib/server/connectors/session-consolidation.ts +5 -2
  72. package/src/lib/server/connectors/signal.ts +9 -6
  73. package/src/lib/server/connectors/slack.ts +13 -10
  74. package/src/lib/server/connectors/teams.ts +8 -5
  75. package/src/lib/server/connectors/telegram.ts +15 -12
  76. package/src/lib/server/connectors/whatsapp.ts +32 -29
  77. package/src/lib/server/embeddings.ts +4 -1
  78. package/src/lib/server/link-understanding.ts +4 -1
  79. package/src/lib/server/memory/memory-abstract.ts +59 -0
  80. package/src/lib/server/memory/memory-db.ts +40 -14
  81. package/src/lib/server/missions/mission-service.ts +6 -3
  82. package/src/lib/server/openclaw/gateway.ts +8 -5
  83. package/src/lib/server/project-utils.ts +13 -0
  84. package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
  85. package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
  86. package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
  87. package/src/lib/server/provider-health.ts +18 -0
  88. package/src/lib/server/query-expansion.ts +4 -1
  89. package/src/lib/server/runtime/alert-dispatch.ts +7 -6
  90. package/src/lib/server/runtime/daemon-state.ts +189 -50
  91. package/src/lib/server/runtime/heartbeat-service.ts +23 -0
  92. package/src/lib/server/runtime/idle-window.ts +4 -1
  93. package/src/lib/server/runtime/perf.ts +4 -1
  94. package/src/lib/server/runtime/process-manager.ts +7 -4
  95. package/src/lib/server/runtime/queue.ts +31 -28
  96. package/src/lib/server/runtime/scheduler.ts +9 -6
  97. package/src/lib/server/runtime/session-run-manager.ts +3 -0
  98. package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
  99. package/src/lib/server/sandbox/novnc-auth.ts +10 -0
  100. package/src/lib/server/session-tools/context.ts +14 -0
  101. package/src/lib/server/session-tools/discovery.ts +9 -6
  102. package/src/lib/server/session-tools/index.ts +3 -1
  103. package/src/lib/server/session-tools/platform.ts +1 -1
  104. package/src/lib/server/session-tools/subagent.ts +23 -2
  105. package/src/lib/server/session-tools/wallet.ts +4 -1
  106. package/src/lib/server/skills/clawhub-client.ts +4 -1
  107. package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
  108. package/src/lib/server/skills/skill-eligibility.ts +6 -0
  109. package/src/lib/server/solana.ts +6 -0
  110. package/src/lib/server/storage-auth.ts +5 -5
  111. package/src/lib/server/storage-normalization.ts +4 -0
  112. package/src/lib/server/storage.ts +19 -8
  113. package/src/lib/server/tasks/task-followups.ts +4 -1
  114. package/src/lib/server/tool-loop-detection.ts +8 -3
  115. package/src/lib/server/tool-planning.ts +226 -0
  116. package/src/lib/server/tool-retry.ts +4 -3
  117. package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
  118. package/src/lib/server/ws-hub.ts +5 -2
  119. package/src/lib/strip-internal-metadata.test.ts +44 -4
  120. package/src/lib/strip-internal-metadata.ts +20 -6
  121. package/src/stores/use-approval-store.ts +7 -1
  122. package/src/stores/use-chat-store.ts +5 -1
  123. package/src/types/index.ts +6 -0
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { memo, useCallback, useState } from 'react'
3
+ import { memo, useEffect, useState } from 'react'
4
4
  import { AgentAvatar } from '@/components/agents/agent-avatar'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
 
@@ -115,7 +115,7 @@ const SwarmMemberCard = memo(function SwarmMemberCard({
115
115
  agents,
116
116
  }: {
117
117
  member: SwarmMemberData
118
- agents: Record<string, any>
118
+ agents: Record<string, { avatarSeed?: string; avatarUrl?: string | null; name?: string }>
119
119
  }) {
120
120
  const [expanded, setExpanded] = useState(false)
121
121
  const cfg = MEMBER_STATUS_CONFIG[member.status]
@@ -206,6 +206,13 @@ const SwarmMemberCard = memo(function SwarmMemberCard({
206
206
  function SwarmSummaryBar({ data }: { data: SwarmStatusData }) {
207
207
  const cfg = SWARM_STATUS_CONFIG[data.status]
208
208
  const isTerminal = data.status === 'completed' || data.status === 'partial' || data.status === 'failed'
209
+ const [now, setNow] = useState(data.completedAt ?? data.createdAt)
210
+
211
+ useEffect(() => {
212
+ if (isTerminal) return
213
+ const id = setInterval(() => setNow(Date.now()), 1000)
214
+ return () => clearInterval(id)
215
+ }, [isTerminal])
209
216
 
210
217
  const formatDuration = (ms: number) => {
211
218
  if (ms < 1000) return `${ms}ms`
@@ -215,7 +222,7 @@ function SwarmSummaryBar({ data }: { data: SwarmStatusData }) {
215
222
 
216
223
  const durationMs = data.completedAt
217
224
  ? data.completedAt - data.createdAt
218
- : Date.now() - data.createdAt
225
+ : now - data.createdAt
219
226
 
220
227
  return (
221
228
  <div
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState } from 'react'
3
+ import { useCallback, useEffect, useState } from 'react'
4
4
  import { api } from '@/lib/app/api-client'
5
5
  import { useWs } from '@/hooks/use-ws'
6
6
  import { StatusDot } from '@/components/ui/status-dot'
@@ -16,14 +16,13 @@ interface DaemonStatus {
16
16
  export function DaemonIndicator() {
17
17
  const [status, setStatus] = useState<DaemonStatus | null>(null)
18
18
 
19
- const fetchStatus = async () => {
20
- try {
21
- const data = await api<DaemonStatus>('GET', '/daemon')
22
- setStatus(data)
23
- } catch { /* ignore */ }
24
- }
19
+ const fetchStatus = useCallback(() => {
20
+ api<DaemonStatus>('GET', '/daemon')
21
+ .then(setStatus)
22
+ .catch(() => {})
23
+ }, [])
25
24
 
26
- useEffect(() => { fetchStatus() }, [])
25
+ useEffect(() => { fetchStatus() }, [fetchStatus])
27
26
  useWs('daemon', fetchStatus, 60_000)
28
27
 
29
28
  const toggle = async () => {
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState, useCallback } from 'react'
3
+ import { useEffect, useState } from 'react'
4
4
 
5
5
  const CHECK_INTERVAL = 5 * 60_000 // 5 minutes
6
6
 
@@ -19,22 +19,17 @@ export function UpdateBanner() {
19
19
  const [dismissed, setDismissed] = useState<string | null>(null)
20
20
  const [errorMsg, setErrorMsg] = useState('')
21
21
 
22
- const checkVersion = useCallback(async () => {
23
- try {
24
- const res = await fetch('/api/version')
25
- if (!res.ok) return
26
- const data: VersionInfo = await res.json()
27
- setVersion(data)
28
- } catch {
29
- // silently fail — no network or server issue
30
- }
31
- }, [])
32
-
33
22
  useEffect(() => {
23
+ const checkVersion = () => {
24
+ fetch('/api/version')
25
+ .then((res) => res.ok ? res.json() as Promise<VersionInfo> : null)
26
+ .then((data) => { if (data) setVersion(data) })
27
+ .catch(() => {})
28
+ }
34
29
  checkVersion()
35
30
  const id = setInterval(checkVersion, CHECK_INTERVAL)
36
31
  return () => clearInterval(id)
37
- }, [checkVersion])
32
+ }, [])
38
33
 
39
34
  const handleUpdate = async () => {
40
35
  setUpdateState('updating')
@@ -67,7 +67,7 @@ export function LogList() {
67
67
  useEffect(() => {
68
68
  fetchLogs()
69
69
  loadAgents()
70
- }, [fetchLogs])
70
+ }, [fetchLogs, loadAgents])
71
71
 
72
72
  useWs('logs', fetchLogs, autoRefresh ? 3000 : undefined)
73
73
 
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { useState } from 'react'
3
4
  import type { MemoryEntry } from '@/types'
4
5
  import { AgentAvatar } from '@/components/agents/agent-avatar'
5
6
  import { deriveMemoryScope, getMemoryScopeLabel, getMemoryTier } from '@/lib/memory-presentation'
@@ -15,6 +16,7 @@ interface Props {
15
16
  }
16
17
 
17
18
  export function MemoryCard({ entry, active, agentName, agentAvatarSeed, agentAvatarUrl, onClick }: Props) {
19
+ const [now] = useState(() => Date.now())
18
20
  const scope = deriveMemoryScope(entry)
19
21
  const tier = getMemoryTier(entry)
20
22
 
@@ -41,7 +43,7 @@ export function MemoryCard({ entry, active, agentName, agentAvatarSeed, agentAva
41
43
  )}
42
44
  <span className="font-display text-[13px] font-600 truncate flex-1 tracking-[-0.01em]">{entry.title}</span>
43
45
  <span className="text-[10px] text-text-3/60 shrink-0 tabular-nums font-mono">
44
- {timeAgoShort(entry.updatedAt || entry.createdAt, Date.now())}
46
+ {timeAgoShort(entry.updatedAt || entry.createdAt, now)}
45
47
  </span>
46
48
  </div>
47
49
  <div className="text-[12px] text-text-2/40 mt-1 line-clamp-3 leading-relaxed">
@@ -21,6 +21,7 @@ import type { ContextAction } from './org-chart-context-menu'
21
21
  import { useOrgChartPanZoom } from './use-org-chart-pan-zoom'
22
22
  import { useOrgChartDrag } from './use-org-chart-drag'
23
23
  import { useNavigate } from '@/lib/app/navigation'
24
+ import { useWs } from '@/hooks/use-ws'
24
25
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
25
26
 
26
27
  const NODE_W = 200
@@ -57,6 +58,9 @@ export function OrgChartView() {
57
58
  // eslint-disable-next-line react-hooks/exhaustive-deps
58
59
  useEffect(() => { loadAgents(); loadSessions() }, [])
59
60
 
61
+ useWs('agents', loadAgents, 60_000)
62
+ useWs('sessions', loadSessions, 30_000)
63
+
60
64
  // Running agents — derived from sessions
61
65
  const runningAgentIds = useMemo(() => {
62
66
  const ids = new Set<string>()
@@ -58,8 +58,10 @@ export function ProjectList() {
58
58
  }
59
59
  for (const t of Object.values(tasks)) {
60
60
  if (t.projectId && map[t.projectId]) {
61
- map[t.projectId].tasks++
62
- if (t.status === 'completed') map[t.projectId].completedTasks++
61
+ if (t.status !== 'cancelled' && t.status !== 'archived') {
62
+ map[t.projectId].tasks++
63
+ if (t.status === 'completed') map[t.projectId].completedTasks++
64
+ }
63
65
  if (t.updatedAt && t.updatedAt > map[t.projectId].lastActivity) {
64
66
  map[t.projectId].lastActivity = t.updatedAt
65
67
  }
@@ -46,8 +46,9 @@ export function OverviewTab({ project, missions }: OverviewTabProps) {
46
46
  [now, projectTasks],
47
47
  )
48
48
 
49
- const completedTasks = projectTasks.filter((t) => t.status === 'completed').length
50
- const totalTasks = projectTasks.length
49
+ const actionableTasks = projectTasks.filter((t) => t.status !== 'cancelled' && t.status !== 'archived')
50
+ const completedTasks = actionableTasks.filter((t) => t.status === 'completed').length
51
+ const totalTasks = actionableTasks.length
51
52
  const progressPct = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0
52
53
 
53
54
  const tasksByStatus = useMemo(() => {
@@ -34,7 +34,7 @@ export function SecretSheet() {
34
34
 
35
35
  useEffect(() => {
36
36
  if (open) loadAgents()
37
- }, [open])
37
+ }, [open, loadAgents])
38
38
 
39
39
  useEffect(() => {
40
40
  if (editing) {
@@ -18,7 +18,7 @@ export function SecretsList({ inSidebar }: Props) {
18
18
 
19
19
  useEffect(() => {
20
20
  loadSecrets()
21
- }, [])
21
+ }, [loadSecrets])
22
22
 
23
23
  const handleDelete = async (e: React.MouseEvent, id: string) => {
24
24
  e.stopPropagation()
@@ -31,13 +31,19 @@ export function AgentSwitchDialog() {
31
31
  return () => window.removeEventListener('keydown', handler)
32
32
  }, [])
33
33
 
34
- // Reset on open
34
+ // Reset on open (render-time state adjustment)
35
+ const [prevOpen, setPrevOpen] = useState(false)
36
+ if (open && !prevOpen) {
37
+ setQuery('')
38
+ setSelectedIdx(0)
39
+ }
40
+ if (open !== prevOpen) {
41
+ setPrevOpen(open)
42
+ }
43
+
44
+ // Focus input after opening
35
45
  useEffect(() => {
36
- if (open) {
37
- setQuery('')
38
- setSelectedIdx(0)
39
- setTimeout(() => inputRef.current?.focus(), 50)
40
- }
46
+ if (open) setTimeout(() => inputRef.current?.focus(), 50)
41
47
  }, [open])
42
48
 
43
49
  const filtered = useMemo(() => {
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState, useCallback, useMemo } from 'react'
3
+ import { useEffect, useState, useMemo } from 'react'
4
4
  import { api } from '@/lib/app/api-client'
5
5
  import { SearchInput } from '@/components/ui/search-input'
6
6
 
@@ -37,26 +37,30 @@ export function DirBrowser({ value, file, onChange, onClear }: DirBrowserProps)
37
37
  const [pathInput, setPathInput] = useState('')
38
38
  const [search, setSearch] = useState('')
39
39
 
40
- const fetchDirs = useCallback(async (dirPath: string) => {
41
- setLoading(true)
42
- try {
43
- const data = await api<DirApiResponse>('GET', `/dirs?path=${encodeURIComponent(dirPath)}`)
44
- setDirs(data.dirs || [])
45
- setCurrentPath(data.currentPath || dirPath)
46
- setParentPath(data.parentPath || null)
47
- setPathInput(data.currentPath || dirPath)
48
- } catch {
49
- setDirs([])
50
- }
51
- setLoading(false)
52
- }, [])
53
-
54
- useEffect(() => {
40
+ // Reset search and mark loading when navigating in browse mode
41
+ const [prevBrowsePath, setPrevBrowsePath] = useState(browsePath)
42
+ const [prevMode, setPrevMode] = useState(mode)
43
+ if (browsePath !== prevBrowsePath || mode !== prevMode) {
44
+ setPrevBrowsePath(browsePath)
45
+ setPrevMode(mode)
55
46
  if (mode === 'browse') {
56
- fetchDirs(browsePath)
57
47
  setSearch('')
48
+ setLoading(true)
58
49
  }
59
- }, [browsePath, mode, fetchDirs])
50
+ }
51
+
52
+ useEffect(() => {
53
+ if (mode !== 'browse') return
54
+ api<DirApiResponse>('GET', `/dirs?path=${encodeURIComponent(browsePath)}`)
55
+ .then((data) => {
56
+ setDirs(data.dirs || [])
57
+ setCurrentPath(data.currentPath || browsePath)
58
+ setParentPath(data.parentPath || null)
59
+ setPathInput(data.currentPath || browsePath)
60
+ })
61
+ .catch(() => { setDirs([]) })
62
+ .finally(() => setLoading(false))
63
+ }, [browsePath, mode])
60
64
 
61
65
  const filteredDirs = useMemo(() => {
62
66
  if (!search) return dirs
@@ -64,8 +64,7 @@ export function SkillSheet() {
64
64
 
65
65
  useEffect(() => {
66
66
  if (open) loadAgents()
67
- // eslint-disable-next-line react-hooks/exhaustive-deps
68
- }, [open])
67
+ }, [open, loadAgents])
69
68
 
70
69
  useEffect(() => {
71
70
  if (open) {
@@ -91,7 +90,7 @@ export function SkillSheet() {
91
90
  setMetadataPreview(null)
92
91
  }
93
92
  }
94
- }, [open, editingId])
93
+ }, [open, editingId, editing])
95
94
 
96
95
  const onClose = () => {
97
96
  setConfirmDelete(false)
@@ -29,7 +29,7 @@ export function TaskList({ inSidebar }: { inSidebar?: boolean }) {
29
29
  const [search, setSearch] = useState('')
30
30
  const [clearing, setClearing] = useState(false)
31
31
 
32
- useEffect(() => { loadTasks() }, [])
32
+ useEffect(() => { loadTasks() }, [loadTasks])
33
33
  useWs('tasks', loadTasks, 5000)
34
34
 
35
35
  const sorted = useMemo(() =>
@@ -151,7 +151,7 @@ export function TaskSheet() {
151
151
  if (open && !editing && !agentId && agentList.length) {
152
152
  setAgentId(agentList[0].id)
153
153
  }
154
- }, [open, editing, agentId, agentList.length, agents])
154
+ }, [open, editing, agentId, agentList])
155
155
 
156
156
  useEffect(() => {
157
157
  if (!editing?.id || !open) {
@@ -8,35 +8,57 @@ import { useWs } from './use-ws'
8
8
  /** Call an OpenClaw gateway RPC method via the proxy route. */
9
9
  export function useOpenClawRpc<T = unknown>(method: string | null, params?: unknown) {
10
10
  const [data, setData] = useState<T | null>(null)
11
- const [loading, setLoading] = useState(false)
11
+ const [loading, setLoading] = useState(!!method)
12
12
  const [error, setError] = useState<string | null>(null)
13
13
  const paramsRef = useRef(params)
14
- paramsRef.current = params
14
+ useEffect(() => { paramsRef.current = params })
15
15
 
16
- const fetch = useCallback(async () => {
16
+ // doFetch only uses async callbacks for setState (no synchronous setState)
17
+ const doFetch = useCallback(() => {
17
18
  if (!method) return
18
- setLoading(true)
19
- setError(null)
20
- try {
21
- const res = await api<{ ok: boolean; result: T; error?: string }>('POST', '/openclaw/gateway', {
22
- method,
23
- params: paramsRef.current,
19
+ api<{ ok: boolean; result: T; error?: string }>('POST', '/openclaw/gateway', {
20
+ method,
21
+ params: paramsRef.current,
22
+ })
23
+ .then((res) => {
24
+ if (res.error) {
25
+ setError(res.error)
26
+ } else {
27
+ setData(res.result)
28
+ }
29
+ })
30
+ .catch((err: unknown) => {
31
+ setError(errorMessage(err))
24
32
  })
25
- if (res.error) {
26
- setError(res.error)
27
- } else {
28
- setData(res.result)
29
- }
30
- } catch (err: unknown) {
31
- setError(errorMessage(err))
32
- } finally {
33
+ .finally(() => {
34
+ setLoading(false)
35
+ })
36
+ }, [method])
37
+
38
+ // Reset loading/error when method changes (render-time state adjustment)
39
+ const [prevMethod, setPrevMethod] = useState(method)
40
+ if (method !== prevMethod) {
41
+ setPrevMethod(method)
42
+ if (method) {
43
+ setLoading(true)
44
+ setError(null)
45
+ } else {
33
46
  setLoading(false)
47
+ setError(null)
48
+ setData(null)
34
49
  }
35
- }, [method])
50
+ }
36
51
 
37
- useEffect(() => { fetch() }, [fetch])
52
+ useEffect(() => { doFetch() }, [doFetch])
38
53
 
39
- return { data, loading, error, refetch: fetch }
54
+ // refetch wraps doFetch with loading/error reset (called from event handlers)
55
+ const refetch = useCallback(() => {
56
+ setLoading(true)
57
+ setError(null)
58
+ doFetch()
59
+ }, [doFetch])
60
+
61
+ return { data, loading, error, refetch }
40
62
  }
41
63
 
42
64
  /** Subscribe to an OpenClaw event topic via the WS hub. */
@@ -48,13 +70,10 @@ export function useOpenClawEvent(topic: string, handler: () => void) {
48
70
  export function useOpenClawConnected() {
49
71
  const [connected, setConnected] = useState(false)
50
72
 
51
- const check = useCallback(async () => {
52
- try {
53
- const res = await api<{ connected: boolean }>('GET', '/openclaw/gateway')
54
- setConnected(res.connected)
55
- } catch {
56
- setConnected(false)
57
- }
73
+ const check = useCallback(() => {
74
+ api<{ connected: boolean }>('GET', '/openclaw/gateway')
75
+ .then((res) => setConnected(res.connected))
76
+ .catch(() => setConnected(false))
58
77
  }, [])
59
78
 
60
79
  useEffect(() => { check() }, [check])
@@ -1,4 +1,7 @@
1
1
  import { hmrSingleton } from '@/lib/shared-utils'
2
+ import { log } from '@/lib/server/logger'
3
+
4
+ const TAG = 'instrumentation'
2
5
 
3
6
  export async function register() {
4
7
  if (process.env.NEXT_RUNTIME === 'nodejs') {
@@ -12,12 +15,12 @@ export async function register() {
12
15
  backfillAllKnownPeerIds()
13
16
  pruneThreadConnectorMirrors()
14
17
  } catch (err) {
15
- console.error('[instrumentation] connector session consolidation failed:', err)
18
+ log.error(TAG, 'connector session consolidation failed:', err)
16
19
  }
17
20
 
18
21
  // In worker-only mode, we FORCE the daemon to start, but skip the WebSocket listener
19
22
  if (isWorkerOnly) {
20
- console.log('[instrumentation] Booting in WORKER ONLY mode')
23
+ log.info(TAG, 'Booting in WORKER ONLY mode')
21
24
  ensureDaemonStarted('worker-boot')
22
25
  } else {
23
26
  // In normal mode, we start the WS server, and conditionally start the daemon if autostart allows
@@ -34,12 +37,12 @@ export async function register() {
34
37
  const shutdown = async (signal: string) => {
35
38
  if (shutdownState.shuttingDown) return
36
39
  shutdownState.shuttingDown = true
37
- console.log(`[server] ${signal} received, shutting down gracefully...`)
40
+ log.info(TAG, `${signal} received, shutting down gracefully...`)
38
41
  try {
39
42
  const { stopDaemon } = await import('@/lib/server/runtime/daemon-state')
40
43
  await stopDaemon({ source: signal })
41
44
  } catch (err) {
42
- console.error('[instrumentation] Failed to stop daemon during shutdown:', err)
45
+ log.error(TAG, 'Failed to stop daemon during shutdown:', err)
43
46
  }
44
47
  if (!isWorkerOnly) {
45
48
  await closeWsServer()
@@ -54,10 +57,10 @@ export async function register() {
54
57
  // that occur during dev server restarts when stdio pipes break
55
58
  process.on('uncaughtException', (err: NodeJS.ErrnoException) => {
56
59
  if (err.code === 'EPIPE') {
57
- console.warn('[instrumentation] Ignoring EPIPE (expected during dev server restart)')
60
+ log.warn(TAG, 'Ignoring EPIPE (expected during dev server restart)')
58
61
  return
59
62
  }
60
- console.error('[instrumentation] Uncaught exception:', err)
63
+ log.error(TAG, 'Uncaught exception:', err)
61
64
  process.exit(1)
62
65
  })
63
66
 
@@ -73,7 +76,7 @@ export async function register() {
73
76
  ) {
74
77
  return
75
78
  }
76
- console.error('[instrumentation] Unhandled rejection:', err)
79
+ log.error(TAG, 'Unhandled rejection:', err)
77
80
  })
78
81
 
79
82
  shutdownState.registered = true
@@ -53,10 +53,26 @@ export async function streamChat(
53
53
  const reader = res.body.getReader()
54
54
  const decoder = new TextDecoder()
55
55
  let buf = ''
56
+ const STREAM_IDLE_TIMEOUT_MS = 300_000
56
57
 
57
58
  while (true) {
58
- const { done, value } = await reader.read()
59
- if (done) break
59
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
60
+ let timedOut = false
61
+ const idleAbort = new Promise<{ done: true; value: undefined }>((resolve) => {
62
+ timeoutId = setTimeout(() => {
63
+ timedOut = true
64
+ resolve({ done: true, value: undefined })
65
+ }, STREAM_IDLE_TIMEOUT_MS)
66
+ })
67
+ const { done, value } = await Promise.race([reader.read(), idleAbort])
68
+ clearTimeout(timeoutId)
69
+ if (done) {
70
+ if (timedOut) {
71
+ onEvent?.({ t: 'err', text: 'Stream timed out (no data for 5 minutes)' })
72
+ reader.cancel().catch(() => {})
73
+ }
74
+ break
75
+ }
60
76
  buf += decoder.decode(value, { stream: true })
61
77
  const lines = buf.split('\n')
62
78
  buf = lines.pop() || ''
@@ -2,8 +2,11 @@ import fs from 'fs'
2
2
  import https from 'https'
3
3
  import type { StreamChatOptions } from './index'
4
4
  import { PROVIDER_DEFAULTS, IMAGE_EXTS, TEXT_EXTS, ANTHROPIC_MAX_TOKENS, MAX_HISTORY_MESSAGES, writeSSE } from './provider-defaults'
5
+ import { log } from '@/lib/server/logger'
5
6
  import { resolveImagePath } from '@/lib/server/resolve-image'
6
7
 
8
+ const TAG = 'provider-anthropic'
9
+
7
10
  async function fileToContentBlocks(filePath: string): Promise<Array<Record<string, unknown>>> {
8
11
  if (!filePath || !fs.existsSync(filePath)) return []
9
12
  if (IMAGE_EXTS.test(filePath)) {
@@ -71,7 +74,7 @@ export function streamAnthropicChat({ session, message, imagePath, apiKey, syste
71
74
  apiRes.on('data', (c: Buffer) => errBody += c)
72
75
  apiRes.on('end', () => {
73
76
  const msg = `Anthropic error ${apiRes.statusCode}: ${errBody.slice(0, 200)}`
74
- console.error(`[${session.id}] ${msg}`)
77
+ log.error(TAG, `[${session.id}] ${msg}`)
75
78
  let errMsg = `Anthropic API error (${apiRes.statusCode})`
76
79
  try {
77
80
  const parsed = JSON.parse(errBody)
@@ -124,12 +127,12 @@ export function streamAnthropicChat({ session, message, imagePath, apiKey, syste
124
127
  active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
125
128
 
126
129
  apiReq.on('timeout', () => {
127
- console.error(`[${session.id}] anthropic request timed out after 60s`)
130
+ log.error(TAG, `[${session.id}] anthropic request timed out after 60s`)
128
131
  apiReq.destroy(new Error('Request timed out after 60s'))
129
132
  })
130
133
 
131
134
  apiReq.on('error', (e) => {
132
- console.error(`[${session.id}] anthropic request error:`, e.message)
135
+ log.error(TAG, `[${session.id}] anthropic request error:`, e.message)
133
136
  writeSSE(write, 'err', e.message)
134
137
  active.delete(session.id)
135
138
  reject(e)
@@ -6,7 +6,9 @@ import type { StreamChatOptions } from './index'
6
6
  import { log } from '../server/logger'
7
7
  import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
8
8
  import { getEnabledToolIds } from '@/lib/capability-selection'
9
- import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler } from './cli-utils'
9
+ import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
10
+
11
+ const TAG = 'provider-claude-cli'
10
12
 
11
13
  export function streamClaudeCliChat({ session, message, imagePath, systemPrompt, write, active, signal }: StreamChatOptions): Promise<string> {
12
14
  const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
@@ -152,8 +154,12 @@ export function streamClaudeCliChat({ session, message, imagePath, systemPrompt,
152
154
  const text = chunk.toString()
153
155
  stderrText += text
154
156
  if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
155
- log.warn('claude-cli', `stderr [${session.id}]`, text.slice(0, 500))
156
- console.error(`[${session.id}] stderr:`, text.slice(0, 200))
157
+ if (isStderrNoise(text)) {
158
+ log.debug('claude-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
159
+ } else {
160
+ log.warn('claude-cli', `stderr [${session.id}]`, text.slice(0, 500))
161
+ log.error(TAG, `[${session.id}] stderr:`, text.slice(0, 200))
162
+ }
157
163
  })
158
164
 
159
165
  return new Promise((resolve) => {
@@ -106,6 +106,8 @@ export function buildCliEnv(opts?: {
106
106
  }
107
107
  }
108
108
 
109
+ delete (env as Record<string, unknown>).MallocStackLogging
110
+
109
111
  if (opts?.inject) {
110
112
  for (const [key, value] of Object.entries(opts.inject)) {
111
113
  env[key] = value
@@ -115,6 +117,19 @@ export function buildCliEnv(opts?: {
115
117
  return env
116
118
  }
117
119
 
120
+ // ---------------------------------------------------------------------------
121
+ // Stderr Noise Filter
122
+ // ---------------------------------------------------------------------------
123
+
124
+ const STDERR_NOISE_PATTERNS: RegExp[] = [
125
+ /MallocStackLogging/,
126
+ /^\s*$/,
127
+ ]
128
+
129
+ export function isStderrNoise(text: string): boolean {
130
+ return STDERR_NOISE_PATTERNS.some((re) => re.test(text))
131
+ }
132
+
118
133
  // ---------------------------------------------------------------------------
119
134
  // Auth Probing
120
135
  // ---------------------------------------------------------------------------
@@ -5,7 +5,9 @@ import { spawn } from 'child_process'
5
5
  import type { StreamChatOptions } from './index'
6
6
  import { log } from '../server/logger'
7
7
  import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
8
- import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, symlinkConfigFiles } from './cli-utils'
8
+ import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, symlinkConfigFiles, isStderrNoise } from './cli-utils'
9
+
10
+ const TAG = 'provider-codex'
9
11
 
10
12
  function codexModelRequiresReasoningDowngrade(model: string | null | undefined): boolean {
11
13
  const value = String(model || '').trim().toLowerCase()
@@ -196,8 +198,12 @@ export function streamCodexCliChat({ session, message, imagePath, systemPrompt,
196
198
  const text = chunk.toString()
197
199
  stderrText += text
198
200
  if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
199
- log.warn('codex-cli', `stderr [${session.id}]`, text.slice(0, 500))
200
- console.error(`[${session.id}] codex stderr:`, text.slice(0, 200))
201
+ if (isStderrNoise(text)) {
202
+ log.debug('codex-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
203
+ } else {
204
+ log.warn('codex-cli', `stderr [${session.id}]`, text.slice(0, 500))
205
+ log.error(TAG, `[${session.id}] codex stderr:`, text.slice(0, 200))
206
+ }
201
207
  })
202
208
 
203
209
  return new Promise((resolve) => {
@@ -2,7 +2,7 @@ import { spawn } from 'child_process'
2
2
  import type { StreamChatOptions } from './index'
3
3
  import { log } from '../server/logger'
4
4
  import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
5
- import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler } from './cli-utils'
5
+ import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
6
6
 
7
7
  /**
8
8
  * Gemini CLI provider — spawns `gemini --prompt <message> --output-format stream-json --yolo`.
@@ -151,7 +151,11 @@ export function streamGeminiCliChat({ session, message, imagePath, systemPrompt,
151
151
  const text = chunk.toString()
152
152
  stderrText += text
153
153
  if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
154
- log.warn('gemini-cli', `stderr [${session.id}]`, text.slice(0, 500))
154
+ if (isStderrNoise(text)) {
155
+ log.debug('gemini-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
156
+ } else {
157
+ log.warn('gemini-cli', `stderr [${session.id}]`, text.slice(0, 500))
158
+ }
155
159
  })
156
160
 
157
161
  return new Promise((resolve) => {