@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.
- package/README.md +10 -0
- package/package.json +4 -1
- package/src/app/api/chats/[id]/deploy/route.ts +11 -6
- package/src/app/api/chats/[id]/devserver/route.ts +5 -2
- package/src/app/api/chats/[id]/messages/route.ts +7 -1
- package/src/app/api/credentials/[id]/route.ts +4 -1
- package/src/app/api/extensions/marketplace/route.ts +5 -2
- package/src/app/api/memory/maintenance/route.ts +5 -2
- package/src/app/api/preview-server/route.ts +14 -11
- package/src/app/api/system/status/route.ts +11 -0
- package/src/app/api/upload/route.ts +4 -1
- package/src/cli/index.js +7 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-files-editor.tsx +44 -32
- package/src/components/agents/personality-builder.tsx +13 -7
- package/src/components/agents/trash-list.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +1 -0
- package/src/components/chat/message-list.tsx +25 -39
- package/src/components/chat/swarm-status-card.tsx +10 -3
- package/src/components/layout/daemon-indicator.tsx +7 -8
- package/src/components/layout/update-banner.tsx +8 -13
- package/src/components/logs/log-list.tsx +1 -1
- package/src/components/memory/memory-card.tsx +3 -1
- package/src/components/org-chart/org-chart-view.tsx +4 -0
- package/src/components/projects/project-list.tsx +4 -2
- package/src/components/projects/tabs/overview-tab.tsx +3 -2
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +12 -6
- package/src/components/shared/dir-browser.tsx +22 -18
- package/src/components/skills/skill-sheet.tsx +2 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +1 -1
- package/src/hooks/use-openclaw-gateway.ts +46 -27
- package/src/instrumentation.ts +10 -7
- package/src/lib/chat/chat.ts +18 -2
- package/src/lib/providers/anthropic.ts +6 -3
- package/src/lib/providers/claude-cli.ts +9 -3
- package/src/lib/providers/cli-utils.ts +15 -0
- package/src/lib/providers/codex-cli.ts +9 -3
- package/src/lib/providers/gemini-cli.ts +6 -2
- package/src/lib/providers/index.ts +4 -1
- package/src/lib/providers/ollama.ts +5 -2
- package/src/lib/providers/openai.ts +8 -5
- package/src/lib/providers/opencode-cli.ts +6 -2
- package/src/lib/server/agents/agent-registry.ts +20 -3
- package/src/lib/server/agents/main-agent-loop.ts +4 -3
- package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
- package/src/lib/server/chat-execution/chat-execution.ts +14 -2
- package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
- package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
- package/src/lib/server/chat-execution/message-classifier.ts +5 -2
- package/src/lib/server/chat-execution/post-stream-finalization.ts +4 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +11 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +52 -9
- package/src/lib/server/chat-execution/response-completeness.ts +5 -2
- package/src/lib/server/chat-execution/stream-agent-chat.ts +42 -12
- package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
- package/src/lib/server/connectors/bluebubbles.ts +7 -4
- package/src/lib/server/connectors/connector-inbound.ts +16 -13
- package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
- package/src/lib/server/connectors/connector-outbound.ts +6 -3
- package/src/lib/server/connectors/discord.ts +10 -7
- package/src/lib/server/connectors/email.ts +17 -14
- package/src/lib/server/connectors/googlechat.ts +7 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
- package/src/lib/server/connectors/matrix.ts +6 -3
- package/src/lib/server/connectors/openclaw.ts +20 -17
- package/src/lib/server/connectors/outbox.ts +4 -1
- package/src/lib/server/connectors/runtime-state.ts +19 -0
- package/src/lib/server/connectors/session-consolidation.ts +5 -2
- package/src/lib/server/connectors/signal.ts +9 -6
- package/src/lib/server/connectors/slack.ts +13 -10
- package/src/lib/server/connectors/teams.ts +8 -5
- package/src/lib/server/connectors/telegram.ts +15 -12
- package/src/lib/server/connectors/whatsapp.ts +32 -29
- package/src/lib/server/embeddings.ts +4 -1
- package/src/lib/server/link-understanding.ts +4 -1
- package/src/lib/server/memory/memory-abstract.ts +59 -0
- package/src/lib/server/memory/memory-db.ts +40 -14
- package/src/lib/server/missions/mission-service.ts +6 -3
- package/src/lib/server/openclaw/gateway.ts +8 -5
- package/src/lib/server/project-utils.ts +13 -0
- package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
- package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
- package/src/lib/server/provider-health.ts +18 -0
- package/src/lib/server/query-expansion.ts +4 -1
- package/src/lib/server/runtime/alert-dispatch.ts +7 -6
- package/src/lib/server/runtime/daemon-state.ts +189 -50
- package/src/lib/server/runtime/heartbeat-service.ts +23 -0
- package/src/lib/server/runtime/idle-window.ts +4 -1
- package/src/lib/server/runtime/perf.ts +4 -1
- package/src/lib/server/runtime/process-manager.ts +7 -4
- package/src/lib/server/runtime/queue.ts +31 -28
- package/src/lib/server/runtime/scheduler.ts +9 -6
- package/src/lib/server/runtime/session-run-manager.ts +3 -0
- package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
- package/src/lib/server/sandbox/novnc-auth.ts +10 -0
- package/src/lib/server/session-tools/context.ts +14 -0
- package/src/lib/server/session-tools/discovery.ts +9 -6
- package/src/lib/server/session-tools/index.ts +3 -1
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/subagent.ts +23 -2
- package/src/lib/server/session-tools/wallet.ts +4 -1
- package/src/lib/server/skills/clawhub-client.ts +4 -1
- package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
- package/src/lib/server/skills/skill-eligibility.ts +6 -0
- package/src/lib/server/solana.ts +6 -0
- package/src/lib/server/storage-auth.ts +5 -5
- package/src/lib/server/storage-normalization.ts +4 -0
- package/src/lib/server/storage.ts +19 -8
- package/src/lib/server/tasks/task-followups.ts +4 -1
- package/src/lib/server/tool-loop-detection.ts +8 -3
- package/src/lib/server/tool-planning.ts +226 -0
- package/src/lib/server/tool-retry.ts +4 -3
- package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
- package/src/lib/server/ws-hub.ts +5 -2
- package/src/lib/strip-internal-metadata.test.ts +44 -4
- package/src/lib/strip-internal-metadata.ts +20 -6
- package/src/stores/use-approval-store.ts +7 -1
- package/src/stores/use-chat-store.ts +5 -1
- package/src/types/index.ts +6 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { memo,
|
|
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,
|
|
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
|
-
:
|
|
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 =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
}, [
|
|
32
|
+
}, [])
|
|
38
33
|
|
|
39
34
|
const handleUpdate = async () => {
|
|
40
35
|
setUpdateState('updating')
|
|
@@ -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,
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
50
|
-
const
|
|
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(() => {
|
|
@@ -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,
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
16
|
+
// doFetch only uses async callbacks for setState (no synchronous setState)
|
|
17
|
+
const doFetch = useCallback(() => {
|
|
17
18
|
if (!method) return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
}
|
|
50
|
+
}
|
|
36
51
|
|
|
37
|
-
useEffect(() => {
|
|
52
|
+
useEffect(() => { doFetch() }, [doFetch])
|
|
38
53
|
|
|
39
|
-
|
|
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(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
setConnected(
|
|
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])
|
package/src/instrumentation.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+
log.warn(TAG, 'Ignoring EPIPE (expected during dev server restart)')
|
|
58
61
|
return
|
|
59
62
|
}
|
|
60
|
-
|
|
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
|
-
|
|
79
|
+
log.error(TAG, 'Unhandled rejection:', err)
|
|
77
80
|
})
|
|
78
81
|
|
|
79
82
|
shutdownState.registered = true
|
package/src/lib/chat/chat.ts
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|