@swarmclawai/swarmclaw 1.5.51 → 1.5.53
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 +14 -0
- package/package.json +1 -1
- package/src/app/api/chats/[id]/execution-log/route.ts +20 -0
- package/src/cli/index.js +1 -0
- package/src/components/agents/inspector-panel.tsx +8 -6
- package/src/components/chat/session-debug-panel.tsx +158 -31
- package/src/components/tasks/task-sheet.tsx +7 -4
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +17 -0
package/README.md
CHANGED
|
@@ -399,6 +399,20 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.5.53 Highlights
|
|
403
|
+
|
|
404
|
+
- **Mission templates library**: the `/missions` page now opens with a curated gallery of starter missions. Each template pre-wires a goal, success criteria, USD / token / turn / wallclock budgets, and a report cadence, so non-technical users can install a working autonomous run in one click. Initial lineup: Daily News Digest, Inbox Triage, Competitor Watch, Weekly Research Report, Social Listener, and Customer Support Triage. Setup notes flag any connector or permission prerequisites before installation. Power-user overrides (budget caps, success criteria, report cadence) live behind a collapsed **Advanced Settings** panel so the default install flow stays one click.
|
|
405
|
+
- **New API routes `GET /api/missions/templates` and `POST /api/missions/templates/:id/instantiate`** with matching CLI commands `swarmclaw missions templates` and `swarmclaw missions instantiate`. Installed missions persist a `templateId` so the origin is traceable for future template-update flows; legacy missions normalize to `templateId: null` on load, no data migration required.
|
|
406
|
+
- **Fix: switching a session's model now sticks in the UI** ([#50](https://github.com/swarmclawai/swarmclaw/pull/50)). The **Switch Model** panel in the agent inspector was reading from `agent.provider` / `agent.model` (the agent's defaults) instead of `session.provider` / `session.model`, so after saving a model switch the collapsed pill still showed the agent default, the combobox reset to the default when reopened, and `selectedProvider` reverted on every save. `ModelSwitcherInline` now uses `session.provider || agent.provider` and `session.model || agent.model` as the source of truth, and its `useEffect` syncs to `session.provider` changes so a successful save updates the panel immediately.
|
|
407
|
+
|
|
408
|
+
Thanks to [@borislavnnikolov](https://github.com/borislavnnikolov) for the contribution.
|
|
409
|
+
|
|
410
|
+
### v1.5.52 Highlights
|
|
411
|
+
|
|
412
|
+
- **Session X-Ray now surfaces the backend execution log** ([#48](https://github.com/swarmclawai/swarmclaw/pull/48), thanks to [@borislavnnikolov](https://github.com/borislavnnikolov)). The debug panel fetches entries from the SQLite execution log on open and merges them with in-memory message events, sorted by time. Expandable entries show provider, model, stream errors, duration, and token counts — the info that was previously invisible when Ollama or other local-model runs failed silently. A new **Tools** filter tab, an `exec` badge for log-sourced entries, an entry count in the stats bar, and a Refresh button round it out. New API route `GET /api/chats/:id/execution-log` with `limit`, `since`, and `category` query params, registered in the CLI manifest as `swarmclaw chats execution-log`.
|
|
413
|
+
- **Execution errors now captured in the log** ([#48](https://github.com/swarmclawai/swarmclaw/pull/48)). `finalizeChatTurn()` writes a structured `error` entry to the execution log on terminal failure, recording provider, model, stream errors, duration, token counts, and whether a response was produced — so the Session X-Ray above actually has something to show.
|
|
414
|
+
- **Fix: blank task-sheet no longer shows `"null"` under *Blocked By*** ([#47](https://github.com/swarmclawai/swarmclaw/pull/47), thanks to [@borislavnnikolov](https://github.com/borislavnnikolov)). A successful task create/update returns `error: null`, and the old `'error' in res` guard treated that as a truthy error and rendered `String(null)` as a red "null" string under the Blocked By field. Now only non-empty string errors trigger the UI, and `depError` is cleared on dialog close so stale state cannot leak across re-opens.
|
|
415
|
+
|
|
402
416
|
### v1.5.51 Highlights
|
|
403
417
|
|
|
404
418
|
- **Desktop app now actually opens and renders on macOS**: packaged builds were broken in v1.5.50 by a stack of independent issues that each masked the next. This release unblocks the cold-boot path end to end. Measured cold-boot time on a populated install: ~1 second to first `/api/healthz` response, down from a hard 60-second timeout.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.53",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
3
|
+
import { getSession } from '@/lib/server/sessions/session-repository'
|
|
4
|
+
import { queryLogs } from '@/lib/server/execution-log'
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
9
|
+
const { id } = await params
|
|
10
|
+
const session = getSession(id)
|
|
11
|
+
if (!session) return notFound()
|
|
12
|
+
|
|
13
|
+
const { searchParams } = new URL(req.url)
|
|
14
|
+
const limit = Math.min(200, Math.max(1, Number(searchParams.get('limit')) || 100))
|
|
15
|
+
const since = searchParams.get('since') ? Number(searchParams.get('since')) : undefined
|
|
16
|
+
const category = searchParams.get('category') as Parameters<typeof queryLogs>[0]['category'] | undefined
|
|
17
|
+
|
|
18
|
+
const entries = queryLogs({ sessionId: id, limit, since, category })
|
|
19
|
+
return NextResponse.json(entries)
|
|
20
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -607,6 +607,7 @@ const COMMAND_GROUPS = [
|
|
|
607
607
|
defaultBody: { action: 'status' },
|
|
608
608
|
}),
|
|
609
609
|
cmd('checkpoints', 'GET', '/chats/:id/checkpoints', 'List checkpoint history for a chat'),
|
|
610
|
+
cmd('execution-log', 'GET', '/chats/:id/execution-log', 'Get execution log entries for a chat'),
|
|
610
611
|
cmd('migrate-messages', 'POST', '/chats/migrate-messages', 'Migrate messages from session blobs to relational table'),
|
|
611
612
|
],
|
|
612
613
|
},
|
|
@@ -73,22 +73,24 @@ function ModelSwitcherInline({ session, agent }: { session: Session; agent: Agen
|
|
|
73
73
|
const refreshSession = useAppStore((s) => s.refreshSession)
|
|
74
74
|
const streaming = useChatStore((s) => s.streaming)
|
|
75
75
|
const [expanded, setExpanded] = useState(false)
|
|
76
|
-
const [selectedProvider, setSelectedProvider] = useState(agent.provider)
|
|
76
|
+
const [selectedProvider, setSelectedProvider] = useState(session.provider || agent.provider)
|
|
77
77
|
const [saving, setSaving] = useState(false)
|
|
78
78
|
|
|
79
79
|
useEffect(() => {
|
|
80
80
|
void loadProviders()
|
|
81
81
|
void loadProviderConfigs()
|
|
82
82
|
}, [loadProviderConfigs, loadProviders])
|
|
83
|
-
|
|
83
|
+
// Sync selectedProvider when the session's provider changes (e.g. after a successful save)
|
|
84
|
+
useEffect(() => { setSelectedProvider(session.provider || agent.provider) }, [session.provider, agent.provider])
|
|
84
85
|
|
|
85
86
|
const agentSelectableProviders = useMemo(
|
|
86
87
|
() => buildAgentSelectableProviders(providers, providerConfigs),
|
|
87
88
|
[providerConfigs, providers],
|
|
88
89
|
)
|
|
89
90
|
const currentProviderInfo = agentSelectableProviders.find((p) => p.id === selectedProvider)
|
|
90
|
-
const
|
|
91
|
-
const
|
|
91
|
+
const activeSessionProvider = agentSelectableProviders.find((p) => p.id === (session.provider || agent.provider))
|
|
92
|
+
const effectiveProvider = session.provider || agent.provider
|
|
93
|
+
const providerLabel = PROVIDER_LABELS[effectiveProvider] || activeSessionProvider?.name || effectiveProvider.replace(/-/g, ' ')
|
|
92
94
|
|
|
93
95
|
const handleModelChange = async (model: string) => {
|
|
94
96
|
if (saving) return
|
|
@@ -117,7 +119,7 @@ function ModelSwitcherInline({ session, agent }: { session: Session; agent: Agen
|
|
|
117
119
|
{providerLabel}
|
|
118
120
|
</span>
|
|
119
121
|
<span className="inline-flex max-w-[180px] items-center rounded-[8px] border border-white/[0.06] bg-white/[0.03] px-2 py-1 text-[10px] font-mono text-text-3/70 truncate group-hover:border-white/[0.1] group-hover:text-text-2 transition-colors">
|
|
120
|
-
{agent.model || 'Default model'}
|
|
122
|
+
{session.model || agent.model || 'Default model'}
|
|
121
123
|
</span>
|
|
122
124
|
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-text-3/30 group-hover:text-text-3/60 transition-colors ml-auto shrink-0">
|
|
123
125
|
<polyline points="6 9 12 15 18 9" />
|
|
@@ -157,7 +159,7 @@ function ModelSwitcherInline({ session, agent }: { session: Session; agent: Agen
|
|
|
157
159
|
{currentProviderInfo && (
|
|
158
160
|
<ModelCombobox
|
|
159
161
|
providerId={currentProviderInfo.id}
|
|
160
|
-
value={agent.model || currentProviderInfo.models[0] || ''}
|
|
162
|
+
value={session.model || agent.model || currentProviderInfo.models[0] || ''}
|
|
161
163
|
onChange={(m) => void handleModelChange(m)}
|
|
162
164
|
models={currentProviderInfo.models}
|
|
163
165
|
defaultModels={currentProviderInfo.defaultModels}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react'
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
4
4
|
import type { Message } from '@/types'
|
|
5
5
|
import { IconButton } from '@/components/shared/icon-button'
|
|
6
6
|
import { CheckpointTimeline } from './checkpoint-timeline'
|
|
@@ -19,7 +19,20 @@ interface DebugEvent {
|
|
|
19
19
|
type: EventType
|
|
20
20
|
label: string
|
|
21
21
|
detail: string
|
|
22
|
+
extraDetail?: Record<string, unknown> | null
|
|
22
23
|
time: number
|
|
24
|
+
source: 'message' | 'execlog'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ExecLogEntry {
|
|
28
|
+
id: string
|
|
29
|
+
sessionId: string
|
|
30
|
+
runId: string | null
|
|
31
|
+
agentId: string | null
|
|
32
|
+
category: string
|
|
33
|
+
summary: string
|
|
34
|
+
detail: Record<string, unknown> | null
|
|
35
|
+
ts: number
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
function classifyMessage(msg: Message): DebugEvent {
|
|
@@ -27,30 +40,66 @@ function classifyMessage(msg: Message): DebugEvent {
|
|
|
27
40
|
|
|
28
41
|
if (msg.role === 'user') {
|
|
29
42
|
if (text.startsWith('[System]')) {
|
|
30
|
-
return { type: 'system', label: 'System', detail: text.replace('[System] ', ''), time: msg.time }
|
|
43
|
+
return { type: 'system', label: 'System', detail: text.replace('[System] ', ''), time: msg.time, source: 'message' }
|
|
31
44
|
}
|
|
32
45
|
if (text.startsWith('[Agent ')) {
|
|
33
46
|
const match = text.match(/\[Agent (.+?) result\]/)
|
|
34
|
-
return { type: 'agent_result', label: `Agent: ${match?.[1] || 'Unknown'}`, detail: text.replace(/\[Agent .+? result\]:?\n?/, ''), time: msg.time }
|
|
47
|
+
return { type: 'agent_result', label: `Agent: ${match?.[1] || 'Unknown'}`, detail: text.replace(/\[Agent .+? result\]:?\n?/, ''), time: msg.time, source: 'message' }
|
|
35
48
|
}
|
|
36
49
|
if (text.startsWith('[Memory search')) {
|
|
37
|
-
return { type: 'system', label: 'Memory Search', detail: text.replace('[Memory search results]:\n', ''), time: msg.time }
|
|
50
|
+
return { type: 'system', label: 'Memory Search', detail: text.replace('[Memory search results]:\n', ''), time: msg.time, source: 'message' }
|
|
38
51
|
}
|
|
39
|
-
return { type: 'user', label: 'User', detail: text, time: msg.time }
|
|
52
|
+
return { type: 'user', label: 'User', detail: text, time: msg.time, source: 'message' }
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
// assistant
|
|
43
56
|
if (text.startsWith('[Delegating to ')) {
|
|
44
57
|
const match = text.match(/\[Delegating to (.+?)\]/)
|
|
45
|
-
return { type: 'delegation', label: `Delegate: ${match?.[1] || 'Unknown'}`, detail: text.replace(/\[Delegating to .+?\]:?\s?/, ''), time: msg.time }
|
|
58
|
+
return { type: 'delegation', label: `Delegate: ${match?.[1] || 'Unknown'}`, detail: text.replace(/\[Delegating to .+?\]:?\s?/, ''), time: msg.time, source: 'message' }
|
|
46
59
|
}
|
|
47
60
|
if (text.startsWith('[Error]')) {
|
|
48
|
-
return { type: 'error', label: 'Error', detail: text.replace('[Error] ', ''), time: msg.time }
|
|
61
|
+
return { type: 'error', label: 'Error', detail: text.replace('[Error] ', ''), time: msg.time, source: 'message' }
|
|
49
62
|
}
|
|
50
63
|
if (text.startsWith('Starting task:')) {
|
|
51
|
-
return { type: 'system', label: 'Task Start', detail: text, time: msg.time }
|
|
64
|
+
return { type: 'system', label: 'Task Start', detail: text, time: msg.time, source: 'message' }
|
|
65
|
+
}
|
|
66
|
+
return { type: 'assistant', label: 'Assistant', detail: text, time: msg.time, source: 'message' }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function classifyExecLogEntry(entry: ExecLogEntry): DebugEvent {
|
|
70
|
+
const catMap: Record<string, EventType> = {
|
|
71
|
+
error: 'error',
|
|
72
|
+
tool_call: 'tool_call',
|
|
73
|
+
tool_result: 'tool_call',
|
|
74
|
+
decision: 'system',
|
|
75
|
+
trigger: 'system',
|
|
76
|
+
loop_detection: 'system',
|
|
77
|
+
delegation_start: 'delegation',
|
|
78
|
+
delegation_complete: 'agent_result',
|
|
79
|
+
delegation_fail: 'error',
|
|
80
|
+
}
|
|
81
|
+
const type: EventType = catMap[entry.category] ?? 'system'
|
|
82
|
+
const labelMap: Record<string, string> = {
|
|
83
|
+
error: 'Error',
|
|
84
|
+
tool_call: 'Tool Call',
|
|
85
|
+
tool_result: 'Tool Result',
|
|
86
|
+
decision: 'Decision',
|
|
87
|
+
trigger: 'Trigger',
|
|
88
|
+
loop_detection: 'Loop Detect',
|
|
89
|
+
delegation_start: 'Delegation',
|
|
90
|
+
delegation_complete: 'Delegation Result',
|
|
91
|
+
delegation_fail: 'Delegation Error',
|
|
92
|
+
heartbeat_failure: 'Heartbeat Fail',
|
|
93
|
+
}
|
|
94
|
+
const label = labelMap[entry.category] ?? entry.category.replace(/_/g, ' ')
|
|
95
|
+
return {
|
|
96
|
+
type,
|
|
97
|
+
label,
|
|
98
|
+
detail: entry.summary,
|
|
99
|
+
extraDetail: entry.detail,
|
|
100
|
+
time: entry.ts,
|
|
101
|
+
source: 'execlog',
|
|
52
102
|
}
|
|
53
|
-
return { type: 'assistant', label: 'Assistant', detail: text, time: msg.time }
|
|
54
103
|
}
|
|
55
104
|
|
|
56
105
|
const TYPE_COLORS: Record<EventType, string> = {
|
|
@@ -67,14 +116,66 @@ function fmtTime(ts: number) {
|
|
|
67
116
|
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
68
117
|
}
|
|
69
118
|
|
|
119
|
+
function ExtraDetail({ data }: { data: Record<string, unknown> }) {
|
|
120
|
+
const entries = Object.entries(data).filter(([, v]) => v !== null && v !== undefined)
|
|
121
|
+
if (entries.length === 0) return null
|
|
122
|
+
return (
|
|
123
|
+
<div className="mt-2 rounded-[8px] bg-black/30 border border-white/[0.06] p-3 text-[11px] font-mono space-y-1">
|
|
124
|
+
{entries.map(([k, v]) => (
|
|
125
|
+
<div key={k} className="flex gap-2 flex-wrap">
|
|
126
|
+
<span className="text-text-3/70 shrink-0">{k}:</span>
|
|
127
|
+
<span className="text-text-2 break-all">
|
|
128
|
+
{Array.isArray(v)
|
|
129
|
+
? v.map(String).join(', ') || '(empty)'
|
|
130
|
+
: typeof v === 'object'
|
|
131
|
+
? JSON.stringify(v)
|
|
132
|
+
: String(v)}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
70
140
|
export function SessionDebugPanel({ messages, open, onClose }: Props) {
|
|
71
141
|
const [tab, setTab] = useState<'log' | 'checkpoints'>('log')
|
|
72
142
|
const [filter, setFilter] = useState<EventType | 'all'>('all')
|
|
73
143
|
const [expandedIdx, setExpandedIdx] = useState<number | null>(null)
|
|
74
|
-
|
|
144
|
+
const [execLogs, setExecLogs] = useState<ExecLogEntry[]>([])
|
|
145
|
+
const [loadingExec, setLoadingExec] = useState(false)
|
|
146
|
+
|
|
75
147
|
const currentSessionId = useAppStore(selectActiveSessionId)
|
|
76
148
|
|
|
77
|
-
const
|
|
149
|
+
const fetchExecLogs = useCallback(async (sessionId: string) => {
|
|
150
|
+
setLoadingExec(true)
|
|
151
|
+
try {
|
|
152
|
+
const res = await fetch(`/api/chats/${sessionId}/execution-log?limit=200`)
|
|
153
|
+
if (res.ok) {
|
|
154
|
+
const data = await res.json() as ExecLogEntry[]
|
|
155
|
+
setExecLogs(Array.isArray(data) ? data : [])
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// non-critical
|
|
159
|
+
} finally {
|
|
160
|
+
setLoadingExec(false)
|
|
161
|
+
}
|
|
162
|
+
}, [])
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (open && currentSessionId) {
|
|
166
|
+
void fetchExecLogs(currentSessionId)
|
|
167
|
+
} else if (!open) {
|
|
168
|
+
setExecLogs([])
|
|
169
|
+
setExpandedIdx(null)
|
|
170
|
+
}
|
|
171
|
+
}, [open, currentSessionId, fetchExecLogs])
|
|
172
|
+
|
|
173
|
+
const msgEvents = messages.map(classifyMessage)
|
|
174
|
+
const execEvents = execLogs.map(classifyExecLogEntry)
|
|
175
|
+
|
|
176
|
+
// Merge and sort by time
|
|
177
|
+
const allEvents = [...msgEvents, ...execEvents].sort((a, b) => a.time - b.time)
|
|
178
|
+
const events = allEvents
|
|
78
179
|
const filtered = filter === 'all' ? events : events.filter((e) => e.type === filter)
|
|
79
180
|
|
|
80
181
|
if (!open) return null
|
|
@@ -85,6 +186,7 @@ export function SessionDebugPanel({ messages, open, onClose }: Props) {
|
|
|
85
186
|
{ id: 'agent_result', label: 'Results' },
|
|
86
187
|
{ id: 'error', label: 'Errors' },
|
|
87
188
|
{ id: 'system', label: 'System' },
|
|
189
|
+
{ id: 'tool_call', label: 'Tools' },
|
|
88
190
|
]
|
|
89
191
|
|
|
90
192
|
return (
|
|
@@ -97,20 +199,20 @@ export function SessionDebugPanel({ messages, open, onClose }: Props) {
|
|
|
97
199
|
<path d="M6 20v-4" />
|
|
98
200
|
</svg>
|
|
99
201
|
<span className="font-display text-[16px] font-600 tracking-[-0.02em] flex-1">Session X-Ray</span>
|
|
100
|
-
|
|
202
|
+
|
|
101
203
|
<div className="flex bg-white/[0.04] p-0.5 rounded-[8px] mr-2">
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
204
|
+
<button
|
|
205
|
+
onClick={() => setTab('log')}
|
|
206
|
+
className={`px-3 py-1 rounded-[6px] text-[11px] font-600 transition-all ${tab === 'log' ? 'bg-white/[0.08] text-text shadow-sm' : 'text-text-3 hover:text-text-2'}`}
|
|
207
|
+
>
|
|
208
|
+
Event Log
|
|
209
|
+
</button>
|
|
210
|
+
<button
|
|
211
|
+
onClick={() => setTab('checkpoints')}
|
|
212
|
+
className={`px-3 py-1 rounded-[6px] text-[11px] font-600 transition-all ${tab === 'checkpoints' ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:text-text-2'}`}
|
|
213
|
+
>
|
|
214
|
+
Checkpoints
|
|
215
|
+
</button>
|
|
114
216
|
</div>
|
|
115
217
|
|
|
116
218
|
<IconButton onClick={onClose} aria-label="Close debug panel">
|
|
@@ -138,6 +240,16 @@ export function SessionDebugPanel({ messages, open, onClose }: Props) {
|
|
|
138
240
|
{f.label}
|
|
139
241
|
</button>
|
|
140
242
|
))}
|
|
243
|
+
{currentSessionId && (
|
|
244
|
+
<button
|
|
245
|
+
onClick={() => void fetchExecLogs(currentSessionId)}
|
|
246
|
+
disabled={loadingExec}
|
|
247
|
+
className="ml-auto px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border bg-surface border-white/[0.06] text-text-3 hover:text-text-2 disabled:opacity-40 whitespace-nowrap"
|
|
248
|
+
style={{ fontFamily: 'inherit' }}
|
|
249
|
+
>
|
|
250
|
+
{loadingExec ? 'Refreshing…' : '↺ Refresh'}
|
|
251
|
+
</button>
|
|
252
|
+
)}
|
|
141
253
|
</div>
|
|
142
254
|
|
|
143
255
|
{/* Event timeline */}
|
|
@@ -162,27 +274,40 @@ export function SessionDebugPanel({ messages, open, onClose }: Props) {
|
|
|
162
274
|
/>
|
|
163
275
|
|
|
164
276
|
{/* Content */}
|
|
165
|
-
<div className="flex items-center gap-2 mb-0.5">
|
|
277
|
+
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
|
|
166
278
|
<span className="text-[11px] font-700 uppercase tracking-wider" style={{ color }}>
|
|
167
279
|
{event.label}
|
|
168
280
|
</span>
|
|
169
281
|
<span className="text-[10px] text-text-3/70 font-mono">{fmtTime(event.time)}</span>
|
|
282
|
+
{event.source === 'execlog' && (
|
|
283
|
+
<span className="text-[9px] text-text-3/40 font-mono uppercase tracking-wider">exec</span>
|
|
284
|
+
)}
|
|
170
285
|
</div>
|
|
171
286
|
|
|
172
287
|
<p className={`text-[12px] text-text-3 leading-[1.5] ${expanded ? 'whitespace-pre-wrap' : 'line-clamp-2'}`}>
|
|
173
288
|
{event.detail}
|
|
174
289
|
</p>
|
|
175
290
|
|
|
291
|
+
{expanded && event.extraDetail && (
|
|
292
|
+
<ExtraDetail data={event.extraDetail} />
|
|
293
|
+
)}
|
|
294
|
+
|
|
176
295
|
{!expanded && event.detail.length > 150 && (
|
|
177
296
|
<span className="text-[11px] text-accent-bright/60 mt-1 inline-block">click to expand</span>
|
|
178
297
|
)}
|
|
298
|
+
{!expanded && event.extraDetail && Object.keys(event.extraDetail).length > 0 && (
|
|
299
|
+
<span className="text-[11px] text-accent-bright/60 mt-1 inline-block ml-2">+ details</span>
|
|
300
|
+
)}
|
|
179
301
|
</button>
|
|
180
302
|
)
|
|
181
303
|
})}
|
|
182
304
|
|
|
183
|
-
{filtered.length === 0 && (
|
|
305
|
+
{filtered.length === 0 && !loadingExec && (
|
|
184
306
|
<p className="text-center text-[13px] text-text-3 py-12">No events matching filter</p>
|
|
185
307
|
)}
|
|
308
|
+
{filtered.length === 0 && loadingExec && (
|
|
309
|
+
<p className="text-center text-[13px] text-text-3 py-12">Loading…</p>
|
|
310
|
+
)}
|
|
186
311
|
</div>
|
|
187
312
|
</div>
|
|
188
313
|
|
|
@@ -198,17 +323,19 @@ export function SessionDebugPanel({ messages, open, onClose }: Props) {
|
|
|
198
323
|
</span>
|
|
199
324
|
)
|
|
200
325
|
})}
|
|
326
|
+
<span className="ml-auto text-[10px] text-text-3/40 font-mono">{execLogs.length} exec log entries</span>
|
|
201
327
|
</div>
|
|
202
328
|
</>
|
|
203
329
|
) : (
|
|
204
330
|
<div className="flex-1 overflow-y-auto">
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
331
|
+
{currentSessionId ? (
|
|
332
|
+
<CheckpointTimeline sessionId={currentSessionId} />
|
|
333
|
+
) : (
|
|
334
|
+
<div className="p-12 text-center text-text-3">No active chat</div>
|
|
335
|
+
)}
|
|
210
336
|
</div>
|
|
211
337
|
)}
|
|
212
338
|
</div>
|
|
213
339
|
)
|
|
214
340
|
}
|
|
341
|
+
|
|
@@ -186,6 +186,7 @@ export function TaskSheet() {
|
|
|
186
186
|
|
|
187
187
|
const onClose = () => {
|
|
188
188
|
formInitRef.current = null
|
|
189
|
+
setDepError(null)
|
|
189
190
|
setOpen(false)
|
|
190
191
|
setEditingId(null)
|
|
191
192
|
}
|
|
@@ -215,14 +216,16 @@ export function TaskSheet() {
|
|
|
215
216
|
try {
|
|
216
217
|
if (editing) {
|
|
217
218
|
const res = await updateTaskMutation.mutateAsync({ id: editing.id, patch: payload })
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
const errMsg = res && typeof res === 'object' ? (res as unknown as Record<string, unknown>).error : undefined
|
|
220
|
+
if (typeof errMsg === 'string' && errMsg.trim()) {
|
|
221
|
+
setDepError(errMsg)
|
|
220
222
|
return
|
|
221
223
|
}
|
|
222
224
|
} else {
|
|
223
225
|
const res = await createTaskMutation.mutateAsync(payload)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
const errMsg = res && typeof res === 'object' ? (res as unknown as Record<string, unknown>).error : undefined
|
|
227
|
+
if (typeof errMsg === 'string' && errMsg.trim()) {
|
|
228
|
+
setDepError(errMsg)
|
|
226
229
|
return
|
|
227
230
|
}
|
|
228
231
|
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { estimateCost } from '@/lib/server/cost'
|
|
34
34
|
import { refreshSessionIdentityState } from '@/lib/server/identity-continuity'
|
|
35
35
|
import { log } from '@/lib/server/logger'
|
|
36
|
+
import { logExecution } from '@/lib/server/execution-log'
|
|
36
37
|
import { syncSessionArchiveMemory } from '@/lib/server/memory/session-archive-memory'
|
|
37
38
|
import { runCapabilityHook, transformCapabilityText } from '@/lib/server/native-capabilities'
|
|
38
39
|
import { isHeartbeatSource } from '@/lib/server/runtime/heartbeat-source'
|
|
@@ -287,6 +288,22 @@ export async function finalizeChatTurn(params: {
|
|
|
287
288
|
inferredError: terminalError,
|
|
288
289
|
})
|
|
289
290
|
}
|
|
291
|
+
logExecution(sessionId, 'error', terminalError, {
|
|
292
|
+
runId,
|
|
293
|
+
agentId: sessionForRun.agentId || null,
|
|
294
|
+
detail: {
|
|
295
|
+
provider: providerType,
|
|
296
|
+
model: sessionForRun.model,
|
|
297
|
+
streamErrors: streamErrors.length > 0 ? streamErrors : undefined,
|
|
298
|
+
source,
|
|
299
|
+
durationMs,
|
|
300
|
+
inputTokens: directUsage.received ? directUsage.inputTokens : null,
|
|
301
|
+
outputTokens: directUsage.received ? directUsage.outputTokens : null,
|
|
302
|
+
tokenUsageReceived: directUsage.received,
|
|
303
|
+
hadResponse: !!(fullResponse || '').trim(),
|
|
304
|
+
toolEventCount: toolEvents.length,
|
|
305
|
+
},
|
|
306
|
+
})
|
|
290
307
|
errorMessage = terminalError
|
|
291
308
|
}
|
|
292
309
|
|