@swarmclawai/swarmclaw 1.1.7 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +16 -0
  2. package/next.config.ts +3 -2
  3. package/package.json +1 -1
  4. package/src/app/api/agents/[id]/status/route.ts +28 -0
  5. package/src/app/api/chatrooms/[id]/members/route.ts +9 -1
  6. package/src/app/api/chatrooms/[id]/route.ts +10 -0
  7. package/src/app/api/chatrooms/route.ts +10 -0
  8. package/src/app/api/chats/route.ts +3 -19
  9. package/src/app/api/learned-skills/[id]/route.ts +83 -0
  10. package/src/app/api/skill-review-counts/route.ts +20 -0
  11. package/src/app/autonomy/page.tsx +1 -1
  12. package/src/app/globals.css +8 -0
  13. package/src/app/home/page.tsx +18 -2
  14. package/src/app/usage/page.tsx +1 -1
  15. package/src/cli/index.js +6 -1
  16. package/src/cli/spec.js +12 -1
  17. package/src/components/agents/agent-chat-list.tsx +3 -3
  18. package/src/components/agents/agent-list.tsx +1 -1
  19. package/src/components/chat/chat-area.tsx +34 -15
  20. package/src/components/chatrooms/chatroom-sheet.tsx +2 -1
  21. package/src/components/connectors/connector-inbox.tsx +1 -1
  22. package/src/components/home/cost-trend-chart.tsx +45 -0
  23. package/src/components/layout/daemon-indicator.tsx +1 -1
  24. package/src/components/layout/dashboard-shell.tsx +1 -1
  25. package/src/components/org-chart/delegation-bubble.tsx +102 -0
  26. package/src/components/org-chart/mini-chat-bubble.tsx +22 -4
  27. package/src/components/org-chart/org-chart-activity-feed.tsx +156 -0
  28. package/src/components/org-chart/org-chart-edge.tsx +1 -30
  29. package/src/components/org-chart/org-chart-view.tsx +27 -2
  30. package/src/components/shared/notification-center.tsx +1 -1
  31. package/src/hooks/use-agent-live-status.ts +59 -0
  32. package/src/hooks/use-delegation-edge-state.ts +166 -4
  33. package/src/hooks/use-ws.ts +69 -39
  34. package/src/lib/fetch-dedup.test.ts +8 -4
  35. package/src/lib/fetch-dedup.ts +6 -3
  36. package/src/lib/keyed-queue.ts +18 -2
  37. package/src/lib/provider-sets.ts +2 -2
  38. package/src/lib/providers/claude-cli.ts +23 -54
  39. package/src/lib/providers/cli-utils.ts +301 -0
  40. package/src/lib/providers/codex-cli.ts +42 -72
  41. package/src/lib/providers/error-classification.ts +8 -0
  42. package/src/lib/providers/gemini-cli.ts +177 -0
  43. package/src/lib/providers/index.ts +10 -1
  44. package/src/lib/providers/opencode-cli.ts +39 -57
  45. package/src/lib/server/agents/agent-availability.test.ts +74 -0
  46. package/src/lib/server/agents/agent-availability.ts +16 -0
  47. package/src/lib/server/agents/autonomy-contract.ts +5 -0
  48. package/src/lib/server/agents/delegation-jobs.ts +77 -5
  49. package/src/lib/server/agents/main-agent-loop.ts +39 -13
  50. package/src/lib/server/agents/subagent-runtime.ts +42 -4
  51. package/src/lib/server/agents/subagent-swarm.ts +47 -1
  52. package/src/lib/server/agents/team-resolution.test.ts +169 -0
  53. package/src/lib/server/agents/team-resolution.ts +96 -0
  54. package/src/lib/server/autonomy/supervisor-reflection.ts +132 -1
  55. package/src/lib/server/chat-execution/chat-execution.ts +23 -5
  56. package/src/lib/server/chat-execution/continuation-limits.ts +5 -1
  57. package/src/lib/server/chat-execution/iteration-event-handler.ts +15 -0
  58. package/src/lib/server/chat-execution/post-stream-finalization.ts +6 -4
  59. package/src/lib/server/chat-execution/prompt-builder.ts +30 -2
  60. package/src/lib/server/chat-execution/prompt-sections.ts +80 -0
  61. package/src/lib/server/chat-execution/stream-agent-chat.ts +66 -3
  62. package/src/lib/server/chat-execution/stream-continuation.ts +36 -7
  63. package/src/lib/server/connectors/connector-inbound.ts +1 -15
  64. package/src/lib/server/connectors/connector-lifecycle.ts +1 -1
  65. package/src/lib/server/connectors/manager.test.ts +1 -1
  66. package/src/lib/server/context-manager.ts +31 -0
  67. package/src/lib/server/debug.ts +20 -0
  68. package/src/lib/server/embeddings.ts +36 -16
  69. package/src/lib/server/execution-log.ts +10 -0
  70. package/src/lib/server/extensions.ts +5 -5
  71. package/src/lib/server/openclaw/gateway.ts +4 -6
  72. package/src/lib/server/protocols/protocol-agent-turn.ts +1 -1
  73. package/src/lib/server/protocols/protocol-step-helpers.ts +2 -3
  74. package/src/lib/server/provider-health.ts +23 -12
  75. package/src/lib/server/runtime/daemon-state.ts +40 -2
  76. package/src/lib/server/runtime/heartbeat-service.ts +103 -8
  77. package/src/lib/server/runtime/run-ledger.ts +54 -0
  78. package/src/lib/server/runtime/session-run-manager.ts +112 -7
  79. package/src/lib/server/runtime/system-events.ts +16 -0
  80. package/src/lib/server/runtime/wake-dispatcher.ts +15 -5
  81. package/src/lib/server/session-tools/chatroom.ts +37 -0
  82. package/src/lib/server/session-tools/delegate.ts +29 -20
  83. package/src/lib/server/session-tools/index.ts +4 -0
  84. package/src/lib/server/session-tools/peer-query.test.ts +71 -0
  85. package/src/lib/server/session-tools/peer-query.ts +300 -0
  86. package/src/lib/server/session-tools/team-context.test.ts +41 -0
  87. package/src/lib/server/session-tools/team-context.ts +277 -0
  88. package/src/lib/server/skills/learned-skills.ts +1 -1
  89. package/src/lib/server/storage-normalization.test.ts +26 -25
  90. package/src/lib/server/storage-normalization.ts +36 -6
  91. package/src/lib/server/storage.ts +39 -28
  92. package/src/lib/server/tool-loop-detection.test.ts +44 -1
  93. package/src/lib/server/tool-loop-detection.ts +122 -38
  94. package/src/lib/ws-client.ts +19 -6
  95. package/src/stores/slices/agent-slice.ts +2 -2
  96. package/src/stores/slices/session-slice.ts +2 -2
  97. package/src/stores/use-chat-store.test.ts +7 -0
  98. package/src/stores/use-chat-store.ts +14 -53
  99. package/src/types/index.ts +6 -3
package/README.md CHANGED
@@ -190,6 +190,22 @@ The building blocks are the same: **agents, tools, memory, delegation, schedules
190
190
 
191
191
  ## Release Notes
192
192
 
193
+ ### v1.1.8 Highlights
194
+
195
+ - **Agent live status**: real-time `/agents/:id/status` endpoint exposes goal, progress, and plan steps; org chart detail panel consumes it via `useAgentLiveStatus` hook.
196
+ - **Learned skills lifecycle**: promote, dismiss, and delete learned skills via `/learned-skills/:id`; `/skill-review-counts` provides badge counts for the skills workspace.
197
+ - **Gemini CLI provider**: Google Gemini CLI joins the provider roster alongside claude-cli and codex-cli, with shared CLI utilities factored into `cli-utils.ts`.
198
+ - **Peer query & team context tools**: new session tools let agents query peers and access team context during conversations.
199
+ - **Team resolution**: dedicated `team-resolution.ts` module resolves agent teams for delegation routing.
200
+ - **Org chart activity feed**: timeline feed component and delegation bubble visualization for the org chart view.
201
+ - **Skills workspace improvements**: expanded skills management UI with review-ready badges.
202
+ - **Cost trend chart**: new dashboard component for cost visualization.
203
+ - **Streaming fix**: text no longer gets stuck on the thinking indicator.
204
+ - **Delegation normalization**: `delegationEnabled` now derived from agent role, removed from starter kit templates.
205
+ - **Chat execution refinements**: improved continuation limits, post-stream finalization, and stream continuation.
206
+ - **Memory and storage improvements**: memory tier management, consolidation enhancements, and storage cache updates.
207
+ - **WebSocket and provider health**: improved WS client handling, delegation edge state, and provider health monitoring.
208
+
193
209
  ### v1.1.7 Highlights
194
210
 
195
211
  - **Projects page redesign**: tabbed navigation (Overview, Work, Operations, Activity) with health grid, sortable task list, and timeline feed.
package/next.config.ts CHANGED
@@ -58,9 +58,10 @@ const nextConfig: NextConfig = {
58
58
  root: PROJECT_ROOT,
59
59
  },
60
60
  experimental: {
61
- // Disable Turbopack persistent cache — concurrent HMR writes cause
62
- // "Another write batch or compaction is already active" errors
63
61
  turbopackFileSystemCacheForDev: false,
62
+ // Limit build workers to 1 inside Docker to avoid SQLITE_BUSY contention
63
+ // when multiple workers collect page data concurrently.
64
+ ...(process.env.SWARMCLAW_BUILD_MODE ? { cpus: 1 } : {}),
64
65
  },
65
66
  env: {
66
67
  NEXT_PUBLIC_GIT_SHA: getGitSha(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -0,0 +1,28 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadAgents } from '@/lib/server/storage'
3
+ import { getMainLoopStateForSession } from '@/lib/server/agents/main-agent-loop'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params
9
+ const agents = loadAgents()
10
+ const agent = agents[id]
11
+ if (!agent) return NextResponse.json(null, { status: 404 })
12
+
13
+ const sessionId = agent.threadSessionId
14
+ if (!sessionId) return NextResponse.json({ status: 'no_session' }, { status: 200 })
15
+
16
+ const state = getMainLoopStateForSession(sessionId)
17
+ if (!state) return NextResponse.json({ status: 'no_state' }, { status: 200 })
18
+
19
+ return NextResponse.json({
20
+ goal: state.goal,
21
+ status: state.status,
22
+ summary: state.summary,
23
+ nextAction: state.nextAction,
24
+ planSteps: state.planSteps,
25
+ currentPlanStep: state.currentPlanStep,
26
+ updatedAt: state.updatedAt,
27
+ })
28
+ }
@@ -4,6 +4,7 @@ import { notify } from '@/lib/server/ws-hub'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
5
  import { safeParseBody } from '@/lib/server/safe-parse-body'
6
6
  import { genId } from '@/lib/id'
7
+ import { isWorkerOnlyAgent, buildWorkerOnlyAgentMessage } from '@/lib/server/agents/agent-availability'
7
8
 
8
9
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
9
10
  const { id } = await params
@@ -16,11 +17,18 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
16
17
  const agentId = typeof body.agentId === 'string' ? body.agentId : ''
17
18
  if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
18
19
 
20
+ const agents = loadAgents()
21
+ if (isWorkerOnlyAgent(agents[agentId])) {
22
+ return NextResponse.json(
23
+ { error: buildWorkerOnlyAgentMessage(agents[agentId], 'join chatrooms') },
24
+ { status: 400 },
25
+ )
26
+ }
27
+
19
28
  if (!chatroom.agentIds.includes(agentId)) {
20
29
  chatroom.agentIds.push(agentId)
21
30
 
22
31
  // Inject a system event message
23
- const agents = loadAgents()
24
32
  const agentName = agents[agentId]?.name || 'Unknown agent'
25
33
  if (!Array.isArray(chatroom.messages)) chatroom.messages = []
26
34
  chatroom.messages.push({
@@ -4,6 +4,7 @@ import { notify } from '@/lib/server/ws-hub'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
5
  import { safeParseBody } from '@/lib/server/safe-parse-body'
6
6
  import { genId } from '@/lib/id'
7
+ import { isWorkerOnlyAgent } from '@/lib/server/agents/agent-availability'
7
8
 
8
9
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
9
10
  const { id } = await params
@@ -50,6 +51,15 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
50
51
  { status: 400 },
51
52
  )
52
53
  }
54
+ const cliAgentNames = agentIds
55
+ .filter((agentId) => isWorkerOnlyAgent(agents[agentId]))
56
+ .map((agentId) => agents[agentId]?.name || agentId)
57
+ if (cliAgentNames.length > 0) {
58
+ return NextResponse.json(
59
+ { error: `CLI-based agents cannot join chatrooms: ${cliAgentNames.join(', ')}. They can only be used for direct chats and delegation.` },
60
+ { status: 400 },
61
+ )
62
+ }
53
63
 
54
64
  const oldIds = new Set(chatroom.agentIds)
55
65
  const newIds = new Set(agentIds)
@@ -6,6 +6,7 @@ import { ChatroomCreateSchema, formatZodError } from '@/lib/validation/schemas'
6
6
  import { safeParseBody } from '@/lib/server/safe-parse-body'
7
7
  import { z } from 'zod'
8
8
  import type { Chatroom, ChatroomMessage } from '@/types'
9
+ import { isWorkerOnlyAgent } from '@/lib/server/agents/agent-availability'
9
10
 
10
11
  export const dynamic = 'force-dynamic'
11
12
 
@@ -49,6 +50,15 @@ export async function POST(req: Request) {
49
50
  { status: 400 },
50
51
  )
51
52
  }
53
+ const cliAgentNames = requestedAgentIds
54
+ .filter((agentId) => isWorkerOnlyAgent(knownAgents[agentId]))
55
+ .map((agentId) => knownAgents[agentId]?.name || agentId)
56
+ if (cliAgentNames.length > 0) {
57
+ return NextResponse.json(
58
+ { error: `CLI-based agents cannot join chatrooms: ${cliAgentNames.join(', ')}. They can only be used for direct chats and delegation.` },
59
+ { status: 400 },
60
+ )
61
+ }
52
62
  const agentIds: string[] = requestedAgentIds
53
63
  const chatMode = body.chatMode === 'parallel' ? 'parallel' : 'sequential'
54
64
  const autoAddress = Boolean(body.autoAddress)
@@ -3,14 +3,13 @@ import { genId } from '@/lib/id'
3
3
  import os from 'os'
4
4
  import path from 'path'
5
5
  import { perf } from '@/lib/server/runtime/perf'
6
- import { loadSessions, saveSessions, deleteSession, active, loadAgents, upsertStoredItem } from '@/lib/server/storage'
6
+ import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
7
7
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
8
8
  import { notify } from '@/lib/server/ws-hub'
9
9
  import { getSessionQueueSnapshot, getSessionRunState } from '@/lib/server/runtime/session-run-manager'
10
10
  import { normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
11
11
  import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
12
12
  import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agents/agent-availability'
13
- import { materializeStreamingAssistantArtifacts } from '@/lib/chat/chat-streaming-state'
14
13
  import { buildSessionListSummary } from '@/lib/chat/session-summary'
15
14
  import { normalizeCapabilitySelection } from '@/lib/capability-selection'
16
15
  import { enrichSessionWithMissionSummary } from '@/lib/server/missions/mission-service'
@@ -24,30 +23,15 @@ async function ensureDaemonIfNeeded(source: string) {
24
23
 
25
24
  export async function GET(req: Request) {
26
25
  const endPerf = perf.start('api', 'GET /api/chats')
27
- try {
28
- const { pruneThreadConnectorMirrors } = await import('@/lib/server/connectors/session-consolidation')
29
- pruneThreadConnectorMirrors()
30
- } catch (err) {
31
- console.error('[api/chats] pruneThreadConnectorMirrors failed:', err)
32
- }
26
+ // Note: pruneThreadConnectorMirrors and materializeStreamingAssistantArtifacts
27
+ // are handled by the daemon periodic health check, not on every list fetch.
33
28
  const sessions = loadSessions()
34
- const changedSessionIds: string[] = []
35
29
  for (const id of Object.keys(sessions)) {
36
30
  const run = getSessionRunState(id)
37
31
  const queue = getSessionQueueSnapshot(id)
38
32
  sessions[id].active = active.has(id) || !!run.runningRunId
39
33
  sessions[id].queuedCount = queue.queueLength
40
34
  sessions[id].currentRunId = run.runningRunId || null
41
- if (!sessions[id].active && Array.isArray(sessions[id].messages)) {
42
- if (materializeStreamingAssistantArtifacts(sessions[id].messages)) changedSessionIds.push(id)
43
- }
44
- }
45
- for (const id of changedSessionIds) {
46
- const persisted = { ...sessions[id] } as Record<string, unknown>
47
- delete persisted.active
48
- delete persisted.queuedCount
49
- delete persisted.currentRunId
50
- upsertStoredItem('sessions', id, persisted)
51
35
  }
52
36
 
53
37
  const summarized = Object.fromEntries(
@@ -0,0 +1,83 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ import { loadLearnedSkill, upsertLearnedSkill, deleteLearnedSkill } from '@/lib/server/storage'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
9
+ const { id } = await params
10
+ const url = new URL(req.url)
11
+ const action = url.searchParams.get('action')
12
+
13
+ const skill = loadLearnedSkill(id)
14
+ if (!skill) {
15
+ return NextResponse.json({ error: 'Learned skill not found' }, { status: 404 })
16
+ }
17
+
18
+ if (action === 'promote') {
19
+ if (skill.lifecycle !== 'review_ready') {
20
+ return NextResponse.json(
21
+ { error: 'Only review_ready skills can be promoted' },
22
+ { status: 400 },
23
+ )
24
+ }
25
+
26
+ // Demote parent first — if this fails, child stays unpromoted (safe)
27
+ if (skill.parentSkillId) {
28
+ const parent = loadLearnedSkill(skill.parentSkillId)
29
+ if (parent && parent.lifecycle === 'active') {
30
+ parent.lifecycle = 'demoted'
31
+ parent.demotedAt = Date.now()
32
+ parent.demotionReason = 'Replaced by promoted revision'
33
+ parent.updatedAt = Date.now()
34
+ upsertLearnedSkill(parent.id, parent)
35
+ }
36
+ }
37
+
38
+ skill.lifecycle = 'active'
39
+ skill.activationCount = (skill.activationCount ?? 0) + 1
40
+ skill.reviewReadyAt = null
41
+ skill.updatedAt = Date.now()
42
+ upsertLearnedSkill(id, skill)
43
+
44
+ notify('learned_skills')
45
+ return NextResponse.json(skill)
46
+ }
47
+
48
+ if (action === 'dismiss') {
49
+ let reason: string | null = null
50
+ try {
51
+ const body = await req.json()
52
+ if (body && typeof body.reason === 'string') {
53
+ reason = body.reason
54
+ }
55
+ } catch {
56
+ // no body or invalid JSON — reason stays null
57
+ }
58
+
59
+ skill.lifecycle = 'demoted'
60
+ skill.demotedAt = Date.now()
61
+ skill.demotionReason = reason ?? 'Dismissed by user'
62
+ skill.updatedAt = Date.now()
63
+ upsertLearnedSkill(id, skill)
64
+
65
+ notify('learned_skills')
66
+ return NextResponse.json(skill)
67
+ }
68
+
69
+ return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 })
70
+ }
71
+
72
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
73
+ const { id } = await params
74
+
75
+ const skill = loadLearnedSkill(id)
76
+ if (!skill) {
77
+ return NextResponse.json({ error: 'Learned skill not found' }, { status: 404 })
78
+ }
79
+
80
+ deleteLearnedSkill(id)
81
+ notify('learned_skills')
82
+ return NextResponse.json({ ok: true })
83
+ }
@@ -0,0 +1,20 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ import { listLearnedSkills } from '@/lib/server/skills/learned-skills'
4
+ import { listSkillSuggestions } from '@/lib/server/skills/skill-suggestions'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export async function GET() {
9
+ const suggestions = listSkillSuggestions()
10
+ const learned = listLearnedSkills()
11
+
12
+ const draftSuggestions = suggestions.filter((s) => s.status === 'draft').length
13
+ const reviewReadyLearned = learned.filter((s) => s.lifecycle === 'review_ready').length
14
+
15
+ return NextResponse.json({
16
+ draftSuggestions,
17
+ reviewReadyLearned,
18
+ total: draftSuggestions + reviewReadyLearned,
19
+ })
20
+ }
@@ -266,7 +266,7 @@ export default function AutonomyPage() {
266
266
  setAgents(agentMap ? Object.values(agentMap) : [])
267
267
  } catch { /* swallow — load() will surface errors */ }
268
268
  }, [])
269
- useWs('agents', loadAgents, 30_000)
269
+ useWs('agents', loadAgents, 60_000)
270
270
 
271
271
  async function toggleOrchestrator(agent: Agent) {
272
272
  setPendingAction('orchestrator-toggle')
@@ -324,6 +324,14 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
324
324
  100% { opacity: 0; }
325
325
  }
326
326
 
327
+ /* Delegation bubble above org-chart nodes: pop-in, hold, fade-out over 5s */
328
+ @keyframes delegationBubbleFade {
329
+ 0% { opacity: 0; transform: translateY(6px) scale(0.95); }
330
+ 6% { opacity: 1; transform: translateY(0) scale(1); }
331
+ 80% { opacity: 1; }
332
+ 100% { opacity: 0; transform: translateY(-2px); }
333
+ }
334
+
327
335
  /* ===== SwarmClaw Loader Keyframes ===== */
328
336
  @keyframes sc-orbit {
329
337
  from { transform: rotate(0deg); }
@@ -13,7 +13,7 @@ import { isLocalhostBrowser, isVisibleSessionForViewer } from '@/lib/observabili
13
13
  import { getSessionLastMessage } from '@/lib/chat/session-summary'
14
14
  import { getNotificationActivityAt, getNotificationOccurrenceCount } from '@/lib/notifications/notification-utils'
15
15
  import { timeAgo, timeUntil } from '@/lib/time-format'
16
- import type { Agent, Session, ActivityEntry, BoardTask, AppNotification } from '@/types'
16
+ import type { Agent, Session, BoardTask, AppNotification, ActivityEntry } from '@/types'
17
17
  import { getEnabledCapabilityIds } from '@/lib/capability-selection'
18
18
  import { HintTip } from '@/components/shared/hint-tip'
19
19
  import { MainContent } from '@/components/layout/main-content'
@@ -34,9 +34,17 @@ const ACTIVITY_ICONS: Record<ActivityEntry['action'], string> = {
34
34
  failed: 'M18 6L6 18M6 6l12 12',
35
35
  approved: 'M22 11.08V12a10 10 0 1 1-5.93-9.14',
36
36
  rejected: 'M10 15l5-5m0 5l-5-5',
37
+ delegated: 'M7 17l9.2-9.2M17 17V7H7',
38
+ queried: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z',
39
+ spawned: 'M12 2v4m0 12v4m10-10h-4M6 12H2m15.07-5.07l-2.83 2.83M9.76 14.24l-2.83 2.83m11.14 0l-2.83-2.83M9.76 9.76L6.93 6.93',
40
+ timeout: 'M12 6v6l4 2M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20',
41
+ cancelled: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20m5 5L7 17',
42
+ incident: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4m0 4h.01',
43
+ running: 'M12 2v4m0 12v4m10-10h-4M6 12H2',
44
+ claimed: 'M9 12l2 2 4-4m6 2a10 10 0 1 1-20 0 10 10 0 0 1 20 0z',
37
45
  }
38
46
 
39
- const ACTIVITY_COLORS: Record<ActivityEntry['action'], string> = {
47
+ const ACTIVITY_COLORS: Record<string, string> = {
40
48
  created: 'text-emerald-400',
41
49
  updated: 'text-sky-400',
42
50
  deleted: 'text-red-400',
@@ -49,6 +57,14 @@ const ACTIVITY_COLORS: Record<ActivityEntry['action'], string> = {
49
57
  failed: 'text-red-400',
50
58
  approved: 'text-emerald-400',
51
59
  rejected: 'text-red-400',
60
+ delegated: 'text-purple-400',
61
+ queried: 'text-sky-400',
62
+ spawned: 'text-purple-400',
63
+ timeout: 'text-amber-400',
64
+ cancelled: 'text-gray-400',
65
+ incident: 'text-red-400',
66
+ running: 'text-blue-400',
67
+ claimed: 'text-emerald-400',
52
68
  }
53
69
 
54
70
  const PLATFORM_LABELS: Record<string, string> = {
@@ -141,7 +141,7 @@ export default function UsagePage() {
141
141
 
142
142
  useEffect(() => { loadTaskMetrics() }, [loadTaskMetrics])
143
143
 
144
- useWs('usage', loadData, 30_000)
144
+ useWs('usage', loadData, 60_000)
145
145
  useWs('tasks', loadTaskMetrics, 15_000)
146
146
 
147
147
  const completionRate = computeCompletionRate(tasks)
package/src/cli/index.js CHANGED
@@ -25,6 +25,7 @@ const COMMAND_GROUPS = [
25
25
  cmd('thread', 'POST', '/agents/:id/thread', 'Get or create agent thread session'),
26
26
  cmd('clone', 'POST', '/agents/:id/clone', 'Clone an agent'),
27
27
  cmd('bulk-update', 'PATCH', '/agents/bulk', 'Bulk update agents', { expectsJsonBody: true }),
28
+ cmd('status', 'GET', '/agents/:id/status', 'Get live status for an agent'),
28
29
  ],
29
30
  },
30
31
  {
@@ -619,6 +620,10 @@ const COMMAND_GROUPS = [
619
620
  description: 'Inspect agent-scoped learned skills',
620
621
  commands: [
621
622
  cmd('list', 'GET', '/learned-skills', 'List learned skills'),
623
+ cmd('promote', 'POST', '/learned-skills/:id?action=promote', 'Promote a review-ready skill to active'),
624
+ cmd('dismiss', 'POST', '/learned-skills/:id?action=dismiss', 'Dismiss a learned skill'),
625
+ cmd('delete', 'DELETE', '/learned-skills/:id', 'Delete a learned skill'),
626
+ cmd('review-counts', 'GET', '/skill-review-counts', 'Show pending review counts'),
622
627
  ],
623
628
  },
624
629
  {
@@ -1566,7 +1571,7 @@ function extractById(payload, id) {
1566
1571
  function getApiCoveragePairs() {
1567
1572
  return COMMANDS
1568
1573
  .filter((command) => !command.virtual)
1569
- .map((command) => `${command.method} ${command.route}`)
1574
+ .map((command) => `${command.method} ${command.route.split('?')[0]}`)
1570
1575
  }
1571
1576
 
1572
1577
  module.exports = {
package/src/cli/spec.js CHANGED
@@ -11,6 +11,7 @@ const COMMAND_GROUPS = {
11
11
  trash: { description: 'List trashed agents', method: 'GET', path: '/agents/trash' },
12
12
  restore: { description: 'Restore a trashed agent', method: 'POST', path: '/agents/trash' },
13
13
  purge: { description: 'Permanently delete a trashed agent', method: 'DELETE', path: '/agents/trash' },
14
+ status: { description: 'Get live status for an agent', method: 'GET', path: '/agents/:id/status', params: ['id'] },
14
15
  },
15
16
  },
16
17
  activity: {
@@ -460,6 +461,16 @@ const COMMAND_GROUPS = {
460
461
  doctor: { description: 'Run local setup diagnostics', method: 'GET', path: '/setup/doctor' },
461
462
  },
462
463
  },
464
+ 'learned-skills': {
465
+ description: 'Inspect agent-scoped learned skills',
466
+ commands: {
467
+ list: { description: 'List learned skills', method: 'GET', path: '/learned-skills' },
468
+ promote: { description: 'Promote a review-ready skill to active', method: 'POST', path: '/learned-skills/:id?action=promote', params: ['id'] },
469
+ dismiss: { description: 'Dismiss a learned skill', method: 'POST', path: '/learned-skills/:id?action=dismiss', params: ['id'] },
470
+ delete: { description: 'Delete a learned skill', method: 'DELETE', path: '/learned-skills/:id', params: ['id'] },
471
+ 'review-counts': { description: 'Show pending review counts', method: 'GET', path: '/skill-review-counts' },
472
+ },
473
+ },
463
474
  skills: {
464
475
  description: 'SwarmClaw and Claude skills',
465
476
  commands: {
@@ -544,7 +555,7 @@ function listCoveredRoutes() {
544
555
  for (const action of Object.keys(commands)) {
545
556
  const cmd = commands[action]
546
557
  if (cmd.method && cmd.path) {
547
- routes.push(`${cmd.method.toUpperCase()} ${cmd.path}`)
558
+ routes.push(`${cmd.method.toUpperCase()} ${cmd.path.split('?')[0]}`)
548
559
  }
549
560
  }
550
561
  }
@@ -94,7 +94,7 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
94
94
  }, [])
95
95
 
96
96
  useEffect(() => { loadAgents() }, [loadAgents])
97
- useWs('agents', loadAgents, 30_000)
97
+ useWs('agents', loadAgents, 60_000)
98
98
  useWs('sessions', loadSessions, 15_000)
99
99
  useWs('runs', loadSessions, 5_000)
100
100
 
@@ -450,7 +450,7 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
450
450
  }`} />
451
451
  </div>
452
452
  <div className="flex flex-col flex-1 min-w-0">
453
- <div className="flex items-center gap-2">
453
+ <div className="flex items-center gap-2 min-w-0">
454
454
  <span className="font-display text-[13.5px] font-600 truncate flex-1 tracking-[-0.01em]">
455
455
  {agent.name}
456
456
  </span>
@@ -464,7 +464,7 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
464
464
  Default
465
465
  </span>
466
466
  )}
467
- <span className="text-[10px] text-text-3/60 font-mono shrink-0">
467
+ <span className="text-[10px] text-text-3/60 font-mono shrink-0 max-w-[30%] truncate">
468
468
  {(threadSession?.model || agent.model)
469
469
  ? (threadSession?.model || agent.model)!.split('/').pop()?.split(':')[0]
470
470
  : agent.provider}
@@ -49,7 +49,7 @@ export function AgentList({ inSidebar }: Props) {
49
49
 
50
50
  const [loaded, setLoaded] = useState(Object.keys(agents).length > 0)
51
51
  useEffect(() => { loadAgents().then(() => setLoaded(true)) }, [loadAgents])
52
- useWs('agents', loadAgents, 30_000)
52
+ useWs('agents', loadAgents, 60_000)
53
53
 
54
54
  // Compute which agents are "running" (have active sessions)
55
55
  const runningAgentIds = useMemo(() => {
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useCallback, useState, useRef, useMemo } from 'react'
4
+ import dynamic from 'next/dynamic'
4
5
  import { useAppStore } from '@/stores/use-app-store'
5
6
  import { selectActiveSessionId } from '@/stores/slices/session-slice'
6
7
  import { useWs } from '@/hooks/use-ws'
@@ -12,13 +13,15 @@ import { useMediaQuery } from '@/hooks/use-media-query'
12
13
  import { ChatHeader } from './chat-header'
13
14
  import { DevServerBar } from './dev-server-bar'
14
15
  import { MessageList } from './message-list'
15
- import { SessionDebugPanel } from './session-debug-panel'
16
16
  import { VoiceOverlay } from './voice-overlay'
17
17
  import { useVoiceConversation } from '@/hooks/use-voice-conversation'
18
18
  import { ChatInput } from '@/components/input/chat-input'
19
- import { ChatPreviewPanel } from './chat-preview-panel'
20
- import { InspectorPanel } from '@/components/agents/inspector-panel'
21
- import { HeartbeatHistoryPanel } from './heartbeat-history-panel'
19
+
20
+ // Lazy-load conditional panels only bundled when actually rendered
21
+ const SessionDebugPanel = dynamic(() => import('./session-debug-panel').then((m) => m.SessionDebugPanel), { ssr: false })
22
+ const ChatPreviewPanel = dynamic(() => import('./chat-preview-panel').then((m) => m.ChatPreviewPanel), { ssr: false })
23
+ const InspectorPanel = dynamic(() => import('@/components/agents/inspector-panel').then((m) => m.InspectorPanel), { ssr: false })
24
+ const HeartbeatHistoryPanel = dynamic(() => import('./heartbeat-history-panel').then((m) => m.HeartbeatHistoryPanel), { ssr: false })
22
25
  import { Dropdown, DropdownItem } from '@/components/shared/dropdown'
23
26
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
24
27
  import { speak } from '@/lib/tts'
@@ -68,7 +71,10 @@ export function ChatArea() {
68
71
  const setPreviewContent = useChatStore((s) => s.setPreviewContent)
69
72
  const isDesktop = useMediaQuery('(min-width: 768px)')
70
73
 
71
- const agents = useAppStore((s) => s.agents)
74
+ const currentAgent = useAppStore((s) => {
75
+ const agentId = session?.agentId
76
+ return agentId ? s.agents[agentId] ?? null : null
77
+ })
72
78
  const loadAgents = useAppStore((s) => s.loadAgents)
73
79
  const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
74
80
  const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
@@ -76,7 +82,6 @@ export function ChatArea() {
76
82
  const inspectorOpen = useAppStore((s) => s.inspectorOpen)
77
83
  const sidebarOpen = useAppStore((s) => s.sidebarOpen)
78
84
  const setSidebarOpen = useAppStore((s) => s.setSidebarOpen)
79
- const currentAgent = session?.agentId ? agents[session.agentId] ?? null : null
80
85
  const queuedCount = session?.queuedCount ?? 0
81
86
  const promptSuggestions = useMemo(
82
87
  () => (currentAgent ? AGENT_PROMPT_SUGGESTIONS : DIRECT_PROMPT_SUGGESTIONS),
@@ -146,7 +151,7 @@ export function ChatArea() {
146
151
  if (!preserveLocalStream) setMessages([])
147
152
  setMessagesLoading(true)
148
153
  if (!preserveLocalStream) {
149
- useChatStore.setState({ streaming: false, streamingSessionId: null, streamText: '', assistantRenderId: null, toolEvents: [] })
154
+ useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', assistantRenderId: null, toolEvents: [] })
150
155
  }
151
156
  fetchMessagesPaginated(requestedSessionId, 100).then((data) => {
152
157
  if (cancelled || selectActiveSessionId(useAppStore.getState()) !== requestedSessionId) return
@@ -174,7 +179,7 @@ export function ChatArea() {
174
179
 
175
180
  const sessionAtLoad = useAppStore.getState().sessions[requestedSessionId]
176
181
  if (sessionAtLoad?.active) {
177
- useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamText: '' })
182
+ useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamSource: 'server', streamText: '' })
178
183
  }
179
184
 
180
185
  return () => {
@@ -191,7 +196,7 @@ export function ChatArea() {
191
196
  if (cancelled || selectActiveSessionId(useAppStore.getState()) !== requestedSessionId) return
192
197
  const refreshed = useAppStore.getState().sessions[requestedSessionId]
193
198
  if (refreshed?.active) {
194
- useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamText: '' })
199
+ useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamSource: 'server', streamText: '' })
195
200
  }
196
201
  }).catch((err) => console.error('Failed to refresh session:', err))
197
202
 
@@ -248,11 +253,11 @@ export function ChatArea() {
248
253
  // Fetching messages here would replace the array with new objects on every
249
254
  // WS notification, causing the full MessageList to re-render and flash.
250
255
  const chatState = useChatStore.getState()
251
- if (chatState.streaming && chatState.streamingSessionId === sessionId) return
256
+ if (chatState.streaming && chatState.streamingSessionId === sessionId && chatState.streamSource === 'local') return
252
257
  try {
253
258
  const msgs = await fetchMessages(sessionId)
254
259
  const currentChatState = useChatStore.getState()
255
- if (currentChatState.streaming && currentChatState.streamingSessionId === sessionId) return
260
+ if (currentChatState.streaming && currentChatState.streamingSessionId === sessionId && currentChatState.streamSource === 'local') return
256
261
  const previous = messagesRef.current
257
262
  if (messagesDiffer(msgs, previous)) {
258
263
  const newMsgs = msgs.length > previous.length ? msgs.slice(previous.length) : []
@@ -290,6 +295,7 @@ export function ChatArea() {
290
295
  useChatStore.setState({
291
296
  streaming: true,
292
297
  streamingSessionId: sessionId,
298
+ streamSource: 'server',
293
299
  streamPhase: 'thinking',
294
300
  streamText: '',
295
301
  thinkingStartTime: Date.now(),
@@ -313,13 +319,26 @@ export function ChatArea() {
313
319
  sessionId && (isServerActive || queuedCount > 0) ? 2000 : undefined,
314
320
  )
315
321
 
322
+ // Listen for stream-end signal from the server — clears streaming state
323
+ // when a server-only run finishes without a local SSE stream driving the UI.
324
+ const handleStreamEnd = useCallback(() => {
325
+ if (!sessionId) return
326
+ const state = useChatStore.getState()
327
+ if (state.streamSource === 'server' && state.streamingSessionId === sessionId) {
328
+ useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', displayText: '', streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
329
+ void refreshMessages()
330
+ void refreshSession(sessionId)
331
+ }
332
+ }, [sessionId, refreshMessages, refreshSession])
333
+ useWs(sessionId ? `stream-end:${sessionId}` : '', handleStreamEnd)
334
+
316
335
  // Keep the local typing indicator aligned with the server's active state
317
336
  useEffect(() => {
318
337
  if (!sessionId) return
319
338
  const state = useChatStore.getState()
320
339
  if (isServerActive) {
321
340
  if (!state.streaming && !state.streamText) {
322
- useChatStore.setState({ streaming: true, streamingSessionId: sessionId, streamText: '' })
341
+ useChatStore.setState({ streaming: true, streamingSessionId: sessionId, streamSource: 'server', streamText: '' })
323
342
  }
324
343
  return
325
344
  }
@@ -330,7 +349,7 @@ export function ChatArea() {
330
349
  ) {
331
350
  // Server finished — clear all streaming state and fetch final messages
332
351
  fetchMessages(sessionId).then(setMessages).catch(() => {})
333
- useChatStore.setState({ streaming: false, streamingSessionId: null, streamText: '', displayText: '', streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
352
+ useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', displayText: '', streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
334
353
  }
335
354
  }, [isServerActive, sessionId, setMessages])
336
355
 
@@ -606,11 +625,11 @@ export function ChatArea() {
606
625
  onConfirm={handleDelete}
607
626
  onCancel={() => setConfirmDelete(false)}
608
627
  />
609
- {session.agentId && agents[session.agentId] && (
628
+ {session.agentId && currentAgent && (
610
629
  <ConfirmDialog
611
630
  open={confirmDeleteAgent}
612
631
  title="Delete Agent"
613
- message={`Delete agent "${agents[session.agentId].name}"? This cannot be undone.`}
632
+ message={`Delete agent "${currentAgent.name}"? This cannot be undone.`}
614
633
  confirmLabel="Delete"
615
634
  danger
616
635
  onConfirm={async () => {
@@ -9,6 +9,7 @@ import { toast } from 'sonner'
9
9
  import { AgentAvatar } from '@/components/agents/agent-avatar'
10
10
  import type { Agent, ChatroomRoutingRule } from '@/types'
11
11
  import { CheckIcon } from '@/components/shared/check-icon'
12
+ import { WORKER_ONLY_PROVIDER_IDS } from '@/lib/provider-sets'
12
13
 
13
14
  function genRuleId(): string {
14
15
  return `rule-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
@@ -285,7 +286,7 @@ export function ChatroomSheet() {
285
286
  }
286
287
 
287
288
  const agentList = Object.values(agents).filter(
288
- (a: Agent) => !a.trashedAt
289
+ (a: Agent) => !a.trashedAt && !WORKER_ONLY_PROVIDER_IDS.has(a.provider)
289
290
  ) as Agent[]
290
291
 
291
292
  const memberAgents = agentList.filter((a) => selectedAgentIds.includes(a.id))