@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.
- package/README.md +81 -22
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +36 -7
- package/src/app/api/agents/route.ts +12 -1
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +18 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/route.ts +16 -0
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +4 -0
- package/src/cli/index.ts +3 -10
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +207 -16
- package/src/components/agents/inspector-panel.tsx +108 -48
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/chat/chat-area.tsx +29 -13
- package/src/components/chat/chat-card.tsx +4 -20
- package/src/components/chat/chat-header.tsx +255 -353
- package/src/components/chat/chat-list.tsx +7 -9
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +3 -1
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/layout/app-layout.tsx +383 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +245 -46
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +250 -61
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +45 -5
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +946 -110
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/daemon-state.ts +59 -1
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +13 -39
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +27 -967
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +17 -6
- package/src/lib/server/orchestrator.ts +2 -2
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +822 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/queue.ts +3 -20
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +70 -32
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery.ts +22 -4
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +237 -24
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +56 -1
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +150 -7
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +86 -23
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +36 -3
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/subagent.ts +193 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +896 -100
- package/src/lib/server/storage.ts +226 -7
- package/src/lib/server/stream-agent-chat.ts +46 -21
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +44 -7
- package/src/lib/server/tool-capability-policy.ts +6 -0
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +7 -0
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +0 -6
- package/src/stores/use-chat-store.ts +31 -2
- package/src/types/index.ts +287 -44
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState, useCallback } from 'react'
|
|
3
|
+
import { useEffect, useState, useCallback, useMemo } from 'react'
|
|
4
4
|
import {
|
|
5
5
|
LineChart, Line, BarChart, Bar, Cell,
|
|
6
6
|
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
|
|
@@ -158,6 +158,14 @@ export function MetricsDashboard() {
|
|
|
158
158
|
useWs('tasks', loadTaskMetrics, 15_000)
|
|
159
159
|
|
|
160
160
|
const completionRate = computeCompletionRate(tasks)
|
|
161
|
+
const pendingApprovals = useMemo(
|
|
162
|
+
() => Object.values(tasks).filter((task) => !!task.pendingApproval).length,
|
|
163
|
+
[tasks],
|
|
164
|
+
)
|
|
165
|
+
const failedTasks = useMemo(
|
|
166
|
+
() => Object.values(tasks).filter((task) => task.status === 'failed').length,
|
|
167
|
+
[tasks],
|
|
168
|
+
)
|
|
161
169
|
|
|
162
170
|
const timeSeriesFormatted = (data?.timeSeries ?? []).map((pt) => ({
|
|
163
171
|
...pt,
|
|
@@ -202,6 +210,57 @@ export function MetricsDashboard() {
|
|
|
202
210
|
labelStyle: { color: 'var(--color-text-2)' },
|
|
203
211
|
}
|
|
204
212
|
|
|
213
|
+
const insightCards = useMemo(() => {
|
|
214
|
+
const series = data?.timeSeries ?? []
|
|
215
|
+
const latest = series[series.length - 1]
|
|
216
|
+
const previous = series.slice(0, -1)
|
|
217
|
+
const baselineCost = previous.length > 0
|
|
218
|
+
? previous.reduce((sum, point) => sum + point.cost, 0) / previous.length
|
|
219
|
+
: 0
|
|
220
|
+
const costDeltaPct = latest && baselineCost > 0
|
|
221
|
+
? Math.round(((latest.cost - baselineCost) / baselineCost) * 100)
|
|
222
|
+
: null
|
|
223
|
+
|
|
224
|
+
const providerHealthEntries = Object.entries(data?.providerHealth ?? {})
|
|
225
|
+
.filter(([, health]) => health.totalRequests > 0)
|
|
226
|
+
.sort(([, a], [, b]) => b.errorRate - a.errorRate)
|
|
227
|
+
const riskiestProvider = providerHealthEntries[0]
|
|
228
|
+
|
|
229
|
+
const topCostAgent = Object.values(data?.byAgent ?? {})
|
|
230
|
+
.sort((a, b) => b.cost - a.cost)[0]
|
|
231
|
+
|
|
232
|
+
return [
|
|
233
|
+
{
|
|
234
|
+
label: 'Cost Pulse',
|
|
235
|
+
value: latest
|
|
236
|
+
? `${formatCost(latest.cost)}${costDeltaPct !== null ? ` · ${costDeltaPct >= 0 ? '+' : ''}${costDeltaPct}%` : ''}`
|
|
237
|
+
: 'No recent spend',
|
|
238
|
+
hint: latest ? `Latest bucket vs ${previous.length > 0 ? 'range average' : 'current range'}` : 'Waiting for usage data',
|
|
239
|
+
tone: costDeltaPct !== null && costDeltaPct > 40 ? 'text-red-400' : 'text-text',
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
label: 'Provider Risk',
|
|
243
|
+
value: riskiestProvider
|
|
244
|
+
? `${riskiestProvider[0]} · ${(riskiestProvider[1].errorRate * 100).toFixed(1)}%`
|
|
245
|
+
: 'No provider issues',
|
|
246
|
+
hint: riskiestProvider ? `${riskiestProvider[1].totalRequests} requests in range` : 'No provider health records yet',
|
|
247
|
+
tone: riskiestProvider && riskiestProvider[1].errorRate > 0.1 ? 'text-red-400' : 'text-emerald-400',
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
label: 'Top Spend Agent',
|
|
251
|
+
value: topCostAgent ? `${topCostAgent.name} · ${formatCost(topCostAgent.cost)}` : 'No agent activity',
|
|
252
|
+
hint: topCostAgent ? `${formatTokens(topCostAgent.tokens)} tokens` : 'No per-agent usage in range',
|
|
253
|
+
tone: 'text-sky-400',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
label: 'Workflow Friction',
|
|
257
|
+
value: `${pendingApprovals} approvals · ${failedTasks} failed`,
|
|
258
|
+
hint: pendingApprovals + failedTasks > 0 ? 'Operational overhead from live work' : 'No obvious workflow friction',
|
|
259
|
+
tone: pendingApprovals + failedTasks > 0 ? 'text-amber-400' : 'text-emerald-400',
|
|
260
|
+
},
|
|
261
|
+
]
|
|
262
|
+
}, [data, failedTasks, pendingApprovals])
|
|
263
|
+
|
|
205
264
|
return (
|
|
206
265
|
<div className="flex-1 flex flex-col h-full overflow-y-auto">
|
|
207
266
|
<div className="px-8 pt-6 pb-4 shrink-0" style={{ animation: 'fade-up 0.5s var(--ease-spring)' }}>
|
|
@@ -246,6 +305,20 @@ export function MetricsDashboard() {
|
|
|
246
305
|
<StatCard label="Completion Rate" value={`${completionRate}%`} index={3} />
|
|
247
306
|
</div>
|
|
248
307
|
|
|
308
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-4">
|
|
309
|
+
{insightCards.map((card, index) => (
|
|
310
|
+
<div
|
|
311
|
+
key={card.label}
|
|
312
|
+
className="bg-surface-2 rounded-[12px] p-4 border border-white/[0.04] hover:bg-surface transition-all"
|
|
313
|
+
style={{ animation: 'spring-in 0.6s var(--ease-spring) both', animationDelay: `${0.12 + index * 0.04}s` }}
|
|
314
|
+
>
|
|
315
|
+
<p className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/60 mb-2">{card.label}</p>
|
|
316
|
+
<p className={`text-[15px] font-700 leading-tight ${card.tone}`}>{card.value}</p>
|
|
317
|
+
<p className="text-[11px] text-text-3/55 mt-2 leading-relaxed">{card.hint}</p>
|
|
318
|
+
</div>
|
|
319
|
+
))}
|
|
320
|
+
</div>
|
|
321
|
+
|
|
249
322
|
{/* Token usage over time */}
|
|
250
323
|
<div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.2s both' }}>
|
|
251
324
|
<ChartCard title="Token Usage Over Time">
|
|
@@ -70,11 +70,7 @@ export function WalletPanel() {
|
|
|
70
70
|
const data = await api<Record<string, SafeWallet>>('GET', '/wallets')
|
|
71
71
|
setWallets(data)
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
if (walletPanelAgentId) {
|
|
75
|
-
const match = Object.values(data).find((w) => w.agentId === walletPanelAgentId)
|
|
76
|
-
if (match) setSelectedWalletId(match.id)
|
|
77
|
-
} else if (!selectedWalletId && Object.keys(data).length > 0) {
|
|
73
|
+
if (!walletPanelAgentId && !selectedWalletId && Object.keys(data).length > 0) {
|
|
78
74
|
setSelectedWalletId(Object.keys(data)[0])
|
|
79
75
|
}
|
|
80
76
|
} catch { /* ignore */ }
|
|
@@ -85,6 +81,22 @@ export function WalletPanel() {
|
|
|
85
81
|
useEffect(() => { loadWallets() }, [loadWallets])
|
|
86
82
|
useWs('wallets', loadWallets, 15000)
|
|
87
83
|
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!walletPanelAgentId) return
|
|
86
|
+
const match = Object.values(wallets).find((wallet) => wallet.agentId === walletPanelAgentId)
|
|
87
|
+
if (match) {
|
|
88
|
+
setSelectedWalletId(match.id)
|
|
89
|
+
setShowCreateForm(false)
|
|
90
|
+
setCreateError('')
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
if (!agents[walletPanelAgentId]) return
|
|
94
|
+
setSelectedWalletId(null)
|
|
95
|
+
setShowCreateForm(true)
|
|
96
|
+
setCreateAgentId(walletPanelAgentId)
|
|
97
|
+
setCreateError('')
|
|
98
|
+
}, [agents, walletPanelAgentId, wallets])
|
|
99
|
+
|
|
88
100
|
// Load detail when wallet selected
|
|
89
101
|
const selectedWallet = selectedWalletId ? wallets[selectedWalletId] : null
|
|
90
102
|
|
|
@@ -64,8 +64,8 @@ export function WebhookSheet() {
|
|
|
64
64
|
|
|
65
65
|
const editing = editingId ? (webhooks[editingId] as Webhook | undefined) : null
|
|
66
66
|
const endpoint = editing ? webhookUrl(editing.id) : ''
|
|
67
|
-
const
|
|
68
|
-
() => Object.values(agents)
|
|
67
|
+
const eligibleAgents = useMemo(
|
|
68
|
+
() => Object.values(agents),
|
|
69
69
|
[agents]
|
|
70
70
|
)
|
|
71
71
|
|
|
@@ -127,7 +127,7 @@ export function WebhookSheet() {
|
|
|
127
127
|
|
|
128
128
|
const handleSave = async () => {
|
|
129
129
|
if (!agentId) {
|
|
130
|
-
setError('
|
|
130
|
+
setError('Choose an eligible agent to handle this webhook.')
|
|
131
131
|
return
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -185,7 +185,7 @@ export function WebhookSheet() {
|
|
|
185
185
|
<h2 className="font-display text-[24px] font-700 tracking-[-0.02em] mb-1">
|
|
186
186
|
{editing ? 'Edit Webhook' : 'New Webhook'}
|
|
187
187
|
</h2>
|
|
188
|
-
<p className="text-[13px] text-text-3">Create an inbound endpoint that
|
|
188
|
+
<p className="text-[13px] text-text-3">Create an inbound endpoint that launches an agent workflow</p>
|
|
189
189
|
</div>
|
|
190
190
|
|
|
191
191
|
{editing && (
|
|
@@ -294,15 +294,15 @@ export function WebhookSheet() {
|
|
|
294
294
|
</div>
|
|
295
295
|
|
|
296
296
|
<div>
|
|
297
|
-
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Route to
|
|
297
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Route to Agent</label>
|
|
298
298
|
<select
|
|
299
299
|
value={agentId}
|
|
300
300
|
onChange={(e) => setAgentId(e.target.value)}
|
|
301
301
|
className={`${inputClass} appearance-none cursor-pointer`}
|
|
302
302
|
style={{ fontFamily: 'inherit' }}
|
|
303
303
|
>
|
|
304
|
-
<option value="">Select
|
|
305
|
-
{
|
|
304
|
+
<option value="">Select agent...</option>
|
|
305
|
+
{eligibleAgents.map((agent) => (
|
|
306
306
|
<option key={agent.id} value={agent.id}>{agent.name}</option>
|
|
307
307
|
))}
|
|
308
308
|
</select>
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const AUTH_COOKIE_NAME = 'sc_auth'
|
|
2
|
+
|
|
3
|
+
export function getCookieValue(cookieHeader: string | null | undefined, name: string): string {
|
|
4
|
+
if (!cookieHeader) return ''
|
|
5
|
+
const parts = cookieHeader.split(';')
|
|
6
|
+
for (const part of parts) {
|
|
7
|
+
const [rawKey, ...rest] = part.split('=')
|
|
8
|
+
if (!rawKey || rest.length === 0) continue
|
|
9
|
+
if (rawKey.trim() !== name) continue
|
|
10
|
+
try {
|
|
11
|
+
return decodeURIComponent(rest.join('=').trim())
|
|
12
|
+
} catch {
|
|
13
|
+
return rest.join('=').trim()
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import type { Message } from '@/types'
|
|
4
|
+
import {
|
|
5
|
+
mergeCompletedAssistantMessage,
|
|
6
|
+
messagesDiffer,
|
|
7
|
+
pruneStreamingAssistantArtifacts,
|
|
8
|
+
shouldHidePersistedStreamingAssistantMessage,
|
|
9
|
+
upsertStreamingAssistantArtifact,
|
|
10
|
+
} from './chat-streaming-state'
|
|
11
|
+
|
|
12
|
+
describe('chat-streaming-state', () => {
|
|
13
|
+
it('hides persisted streaming assistant artifacts while a local stream bubble is active', () => {
|
|
14
|
+
const message: Message = {
|
|
15
|
+
role: 'assistant',
|
|
16
|
+
text: 'partial',
|
|
17
|
+
time: 1,
|
|
18
|
+
streaming: true,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
assert.equal(
|
|
22
|
+
shouldHidePersistedStreamingAssistantMessage(message, { localStreaming: true, displayText: 'live text' }),
|
|
23
|
+
true,
|
|
24
|
+
)
|
|
25
|
+
assert.equal(
|
|
26
|
+
shouldHidePersistedStreamingAssistantMessage(message, { localStreaming: true, displayText: '' }),
|
|
27
|
+
true,
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('replaces trailing streaming assistant messages with the completed assistant message', () => {
|
|
32
|
+
const messages: Message[] = [
|
|
33
|
+
{ role: 'user', text: 'hello', time: 1 },
|
|
34
|
+
{ role: 'assistant', text: 'partial 1', time: 2, streaming: true },
|
|
35
|
+
{ role: 'assistant', text: 'partial 2', time: 3, streaming: true },
|
|
36
|
+
]
|
|
37
|
+
const completed: Message = { role: 'assistant', text: 'final', time: 4 }
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(mergeCompletedAssistantMessage(messages, completed), [
|
|
40
|
+
{ role: 'user', text: 'hello', time: 1 },
|
|
41
|
+
{ role: 'assistant', text: 'final', time: 4 },
|
|
42
|
+
])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('prunes stale streaming artifacts without touching later system messages', () => {
|
|
46
|
+
const messages: Message[] = [
|
|
47
|
+
{ role: 'user', text: 'hello', time: 1 },
|
|
48
|
+
{ role: 'assistant', text: 'partial', time: 10, streaming: true },
|
|
49
|
+
{ role: 'assistant', text: 'approval card', time: 11, kind: 'system' },
|
|
50
|
+
{ role: 'assistant', text: 'older partial', time: 12, streaming: true },
|
|
51
|
+
{ role: 'assistant', text: 'previous run', time: 2, streaming: true },
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const changed = pruneStreamingAssistantArtifacts(messages, { minIndex: 1, minTime: 10 })
|
|
55
|
+
|
|
56
|
+
assert.equal(changed, true)
|
|
57
|
+
assert.deepEqual(messages, [
|
|
58
|
+
{ role: 'user', text: 'hello', time: 1 },
|
|
59
|
+
{ role: 'assistant', text: 'approval card', time: 11, kind: 'system' },
|
|
60
|
+
{ role: 'assistant', text: 'previous run', time: 2, streaming: true },
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('replaces the current run partial with the latest artifact after system messages', () => {
|
|
65
|
+
const messages: Message[] = [
|
|
66
|
+
{ role: 'user', text: 'hello', time: 1 },
|
|
67
|
+
{ role: 'assistant', text: 'partial', time: 10, streaming: true },
|
|
68
|
+
{ role: 'assistant', text: 'approval card', time: 11, kind: 'system' },
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
upsertStreamingAssistantArtifact(
|
|
72
|
+
messages,
|
|
73
|
+
{ role: 'assistant', text: 'latest partial', time: 12, streaming: true },
|
|
74
|
+
{ minIndex: 1, minTime: 10 },
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
assert.deepEqual(messages, [
|
|
78
|
+
{ role: 'user', text: 'hello', time: 1 },
|
|
79
|
+
{ role: 'assistant', text: 'approval card', time: 11, kind: 'system' },
|
|
80
|
+
{ role: 'assistant', text: 'latest partial', time: 12, streaming: true },
|
|
81
|
+
])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('reuses the previous assistant slot when the server already persisted the same final text', () => {
|
|
85
|
+
const messages: Message[] = [
|
|
86
|
+
{ role: 'user', text: 'hello', time: 1 },
|
|
87
|
+
{ role: 'assistant', text: 'final', time: 2, kind: 'chat' },
|
|
88
|
+
]
|
|
89
|
+
const completed: Message = { role: 'assistant', text: 'final', time: 3, kind: 'chat' }
|
|
90
|
+
|
|
91
|
+
assert.deepEqual(mergeCompletedAssistantMessage(messages, completed), [
|
|
92
|
+
{ role: 'user', text: 'hello', time: 1 },
|
|
93
|
+
{ role: 'assistant', text: 'final', time: 2, kind: 'chat' },
|
|
94
|
+
])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('detects same-length message updates during reconciliation', () => {
|
|
98
|
+
const previous: Message[] = [
|
|
99
|
+
{ role: 'assistant', text: 'partial', time: 1, streaming: true },
|
|
100
|
+
]
|
|
101
|
+
const next: Message[] = [
|
|
102
|
+
{ role: 'assistant', text: 'final', time: 2, kind: 'chat' },
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
assert.equal(messagesDiffer(next, previous), true)
|
|
106
|
+
assert.equal(messagesDiffer(next, next), false)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Message } from '@/types'
|
|
2
|
+
|
|
3
|
+
interface StreamingArtifactWindow {
|
|
4
|
+
minIndex?: number
|
|
5
|
+
minTime?: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isStreamingAssistantMessage(
|
|
9
|
+
message: Message,
|
|
10
|
+
index: number,
|
|
11
|
+
opts: StreamingArtifactWindow,
|
|
12
|
+
): boolean {
|
|
13
|
+
if (message.role !== 'assistant' || message.streaming !== true) return false
|
|
14
|
+
if (typeof opts.minIndex === 'number' && index < opts.minIndex) return false
|
|
15
|
+
if (typeof opts.minTime === 'number') {
|
|
16
|
+
if (typeof message.time !== 'number' || message.time < opts.minTime) return false
|
|
17
|
+
}
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function shouldHidePersistedStreamingAssistantMessage(
|
|
22
|
+
message: Message,
|
|
23
|
+
opts: { localStreaming: boolean; displayText: string },
|
|
24
|
+
): boolean {
|
|
25
|
+
return (
|
|
26
|
+
opts.localStreaming
|
|
27
|
+
&& message.role === 'assistant'
|
|
28
|
+
&& message.streaming === true
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function pruneStreamingAssistantArtifacts(
|
|
33
|
+
messages: Message[],
|
|
34
|
+
opts: StreamingArtifactWindow = {},
|
|
35
|
+
): boolean {
|
|
36
|
+
const kept = messages.filter((message, index) => !isStreamingAssistantMessage(message, index, opts))
|
|
37
|
+
if (kept.length === messages.length) return false
|
|
38
|
+
messages.splice(0, messages.length, ...kept)
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function upsertStreamingAssistantArtifact(
|
|
43
|
+
messages: Message[],
|
|
44
|
+
assistantMessage: Message,
|
|
45
|
+
opts: StreamingArtifactWindow = {},
|
|
46
|
+
): boolean {
|
|
47
|
+
if (assistantMessage.role !== 'assistant' || assistantMessage.streaming !== true) {
|
|
48
|
+
throw new Error('upsertStreamingAssistantArtifact requires an assistant streaming message')
|
|
49
|
+
}
|
|
50
|
+
pruneStreamingAssistantArtifacts(messages, opts)
|
|
51
|
+
messages.push(assistantMessage)
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function mergeCompletedAssistantMessage(messages: Message[], assistantMessage: Message): Message[] {
|
|
56
|
+
let end = messages.length
|
|
57
|
+
while (end > 0) {
|
|
58
|
+
const candidate = messages[end - 1]
|
|
59
|
+
if (candidate.role !== 'assistant' || candidate.streaming !== true) break
|
|
60
|
+
end -= 1
|
|
61
|
+
}
|
|
62
|
+
const base = messages.slice(0, end)
|
|
63
|
+
const last = base[base.length - 1]
|
|
64
|
+
if (
|
|
65
|
+
last
|
|
66
|
+
&& last.role === 'assistant'
|
|
67
|
+
&& (last.kind || 'chat') === (assistantMessage.kind || 'chat')
|
|
68
|
+
&& last.text.trim() === assistantMessage.text.trim()
|
|
69
|
+
) {
|
|
70
|
+
return [
|
|
71
|
+
...base.slice(0, -1),
|
|
72
|
+
{
|
|
73
|
+
...last,
|
|
74
|
+
...assistantMessage,
|
|
75
|
+
time: last.time,
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
return [...base, assistantMessage]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function messageReconciliationKey(message: Message): string {
|
|
83
|
+
return JSON.stringify([
|
|
84
|
+
message.role,
|
|
85
|
+
message.kind || '',
|
|
86
|
+
message.text,
|
|
87
|
+
message.streaming === true,
|
|
88
|
+
message.replyToId || '',
|
|
89
|
+
message.bookmarked === true,
|
|
90
|
+
message.suggestions?.join('\u241f') || '',
|
|
91
|
+
(message.toolEvents || []).map((event) => [
|
|
92
|
+
event.name,
|
|
93
|
+
event.input,
|
|
94
|
+
event.output || '',
|
|
95
|
+
event.error === true,
|
|
96
|
+
]),
|
|
97
|
+
])
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function messagesDiffer(nextMessages: Message[], currentMessages: Message[]): boolean {
|
|
101
|
+
if (nextMessages.length !== currentMessages.length) return true
|
|
102
|
+
for (let i = 0; i < nextMessages.length; i += 1) {
|
|
103
|
+
if (messageReconciliationKey(nextMessages[i]) !== messageReconciliationKey(currentMessages[i])) {
|
|
104
|
+
return true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { buildOpenClawMainSessionKey, normalizeOpenClawAgentId } from './openclaw-agent-id'
|
|
4
|
+
|
|
5
|
+
test('normalizeOpenClawAgentId mirrors gateway-style normalization', () => {
|
|
6
|
+
assert.equal(normalizeOpenClawAgentId('OpenClaw Ops'), 'openclaw-ops')
|
|
7
|
+
assert.equal(normalizeOpenClawAgentId(' Agent / Research '), 'agent-research')
|
|
8
|
+
assert.equal(normalizeOpenClawAgentId('main'), 'main')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('buildOpenClawMainSessionKey uses normalized OpenClaw agent ids', () => {
|
|
12
|
+
assert.equal(buildOpenClawMainSessionKey('OpenClaw Ops'), 'agent:openclaw-ops:main')
|
|
13
|
+
assert.equal(buildOpenClawMainSessionKey(' '), null)
|
|
14
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i
|
|
2
|
+
const INVALID_CHARS_RE = /[^a-z0-9_-]+/g
|
|
3
|
+
const LEADING_DASH_RE = /^-+/
|
|
4
|
+
const TRAILING_DASH_RE = /-+$/
|
|
5
|
+
|
|
6
|
+
export function normalizeOpenClawAgentId(value: string | undefined | null): string {
|
|
7
|
+
const trimmed = (value ?? '').trim()
|
|
8
|
+
if (!trimmed) {
|
|
9
|
+
return 'main'
|
|
10
|
+
}
|
|
11
|
+
if (VALID_ID_RE.test(trimmed)) {
|
|
12
|
+
return trimmed.toLowerCase()
|
|
13
|
+
}
|
|
14
|
+
return (
|
|
15
|
+
trimmed
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(INVALID_CHARS_RE, '-')
|
|
18
|
+
.replace(LEADING_DASH_RE, '')
|
|
19
|
+
.replace(TRAILING_DASH_RE, '')
|
|
20
|
+
.slice(0, 64)
|
|
21
|
+
|| 'main'
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildOpenClawMainSessionKey(agentNameOrId: string | undefined | null): string | null {
|
|
26
|
+
const trimmed = (agentNameOrId ?? '').trim()
|
|
27
|
+
if (!trimmed) {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
return `agent:${normalizeOpenClawAgentId(trimmed)}:main`
|
|
31
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import type { Agent } from '@/types'
|
|
4
|
+
import {
|
|
5
|
+
isDelegationTaskPayload,
|
|
6
|
+
resolveDelegatorAgentId,
|
|
7
|
+
resolveManagedAgentAssignment,
|
|
8
|
+
validateManagedAgentAssignment,
|
|
9
|
+
} from './agent-assignment'
|
|
10
|
+
|
|
11
|
+
const now = Date.now()
|
|
12
|
+
const agents: Record<string, Agent> = {
|
|
13
|
+
molly: {
|
|
14
|
+
id: 'molly',
|
|
15
|
+
name: 'Molly',
|
|
16
|
+
description: '',
|
|
17
|
+
systemPrompt: '',
|
|
18
|
+
provider: 'openai',
|
|
19
|
+
model: 'gpt-4o',
|
|
20
|
+
createdAt: now,
|
|
21
|
+
updatedAt: now,
|
|
22
|
+
},
|
|
23
|
+
writer: {
|
|
24
|
+
id: 'writer',
|
|
25
|
+
name: 'Writer',
|
|
26
|
+
description: '',
|
|
27
|
+
systemPrompt: '',
|
|
28
|
+
provider: 'openai',
|
|
29
|
+
model: 'gpt-4o',
|
|
30
|
+
createdAt: now,
|
|
31
|
+
updatedAt: now,
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('resolveManagedAgentAssignment', () => {
|
|
36
|
+
it('resolves explicit aliases to concrete agent ids', () => {
|
|
37
|
+
const resolved = resolveManagedAgentAssignment({ assignee: 'Writer' }, agents, 'molly')
|
|
38
|
+
assert.equal(resolved.agentId, 'writer')
|
|
39
|
+
assert.equal(resolved.source, 'explicit')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('resolves description-based delegation before scope checks', () => {
|
|
43
|
+
const resolved = resolveManagedAgentAssignment(
|
|
44
|
+
{ description: 'Please delegate this to @Writer and let them handle the draft.' },
|
|
45
|
+
agents,
|
|
46
|
+
'molly',
|
|
47
|
+
)
|
|
48
|
+
assert.equal(resolved.agentId, 'writer')
|
|
49
|
+
assert.equal(resolved.source, 'description')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('validateManagedAgentAssignment', () => {
|
|
54
|
+
it('blocks assigning another agent when scope is self', () => {
|
|
55
|
+
const resolved = resolveManagedAgentAssignment({ assignee: 'writer' }, agents, 'molly')
|
|
56
|
+
const error = validateManagedAgentAssignment({
|
|
57
|
+
resourceLabel: 'tasks',
|
|
58
|
+
agents,
|
|
59
|
+
assignScope: 'self',
|
|
60
|
+
currentAgentId: 'molly',
|
|
61
|
+
targetAgentId: resolved.agentId,
|
|
62
|
+
unresolvedReference: resolved.unresolvedReference,
|
|
63
|
+
})
|
|
64
|
+
assert.match(error || '', /only assign tasks to yourself/i)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('allows self-assignment in self scope', () => {
|
|
68
|
+
const resolved = resolveManagedAgentAssignment({ agentId: 'molly' }, agents, 'molly')
|
|
69
|
+
const error = validateManagedAgentAssignment({
|
|
70
|
+
resourceLabel: 'tasks',
|
|
71
|
+
agents,
|
|
72
|
+
assignScope: 'self',
|
|
73
|
+
currentAgentId: 'molly',
|
|
74
|
+
targetAgentId: resolved.agentId,
|
|
75
|
+
unresolvedReference: resolved.unresolvedReference,
|
|
76
|
+
})
|
|
77
|
+
assert.equal(error, null)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('rejects unknown explicit agent references', () => {
|
|
81
|
+
const resolved = resolveManagedAgentAssignment({ agentId: 'missing-agent' }, agents, 'molly')
|
|
82
|
+
const error = validateManagedAgentAssignment({
|
|
83
|
+
resourceLabel: 'tasks',
|
|
84
|
+
agents,
|
|
85
|
+
assignScope: 'all',
|
|
86
|
+
currentAgentId: 'molly',
|
|
87
|
+
targetAgentId: resolved.agentId,
|
|
88
|
+
unresolvedReference: resolved.unresolvedReference,
|
|
89
|
+
})
|
|
90
|
+
assert.match(error || '', /unknown agent "missing-agent"/i)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('rejects self-delegation using resolved agent ids', () => {
|
|
94
|
+
const payload = {
|
|
95
|
+
agentId: 'molly',
|
|
96
|
+
sourceType: 'delegation',
|
|
97
|
+
delegatedByAgentId: 'Molly',
|
|
98
|
+
}
|
|
99
|
+
const resolved = resolveManagedAgentAssignment(payload, agents, 'molly')
|
|
100
|
+
const error = validateManagedAgentAssignment({
|
|
101
|
+
resourceLabel: 'tasks',
|
|
102
|
+
agents,
|
|
103
|
+
assignScope: 'all',
|
|
104
|
+
currentAgentId: 'molly',
|
|
105
|
+
targetAgentId: resolved.agentId,
|
|
106
|
+
unresolvedReference: resolved.unresolvedReference,
|
|
107
|
+
isDelegation: isDelegationTaskPayload(payload),
|
|
108
|
+
delegatorAgentId: resolveDelegatorAgentId(payload, agents, 'molly'),
|
|
109
|
+
})
|
|
110
|
+
assert.match(error || '', /different agent id/i)
|
|
111
|
+
})
|
|
112
|
+
})
|