@swarmclawai/swarmclaw 1.1.7 → 1.1.9

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 (125) hide show
  1. package/README.md +33 -0
  2. package/next.config.ts +3 -2
  3. package/package.json +1 -1
  4. package/src/app/activity/loading.tsx +5 -0
  5. package/src/app/api/agents/[id]/status/route.ts +28 -0
  6. package/src/app/api/agents/[id]/thread/route.ts +2 -1
  7. package/src/app/api/chatrooms/[id]/members/route.ts +9 -1
  8. package/src/app/api/chatrooms/[id]/route.ts +10 -0
  9. package/src/app/api/chatrooms/route.ts +10 -0
  10. package/src/app/api/chats/route.ts +3 -19
  11. package/src/app/api/learned-skills/[id]/route.ts +83 -0
  12. package/src/app/api/logs/route.ts +32 -5
  13. package/src/app/api/skill-review-counts/route.ts +20 -0
  14. package/src/app/autonomy/page.tsx +1 -1
  15. package/src/app/globals.css +8 -0
  16. package/src/app/home/loading.tsx +5 -0
  17. package/src/app/home/page.tsx +18 -2
  18. package/src/app/logs/loading.tsx +5 -0
  19. package/src/app/memory/loading.tsx +5 -0
  20. package/src/app/tasks/loading.tsx +5 -0
  21. package/src/app/usage/page.tsx +1 -1
  22. package/src/cli/index.js +6 -1
  23. package/src/cli/spec.js +12 -1
  24. package/src/components/agents/agent-chat-list.tsx +3 -3
  25. package/src/components/agents/agent-list.tsx +8 -13
  26. package/src/components/chat/chat-area.tsx +37 -18
  27. package/src/components/chat/chat-list.tsx +13 -2
  28. package/src/components/chatrooms/chatroom-sheet.tsx +2 -1
  29. package/src/components/connectors/connector-inbox.tsx +1 -1
  30. package/src/components/home/cost-trend-chart.tsx +45 -0
  31. package/src/components/layout/daemon-indicator.tsx +1 -1
  32. package/src/components/layout/dashboard-shell.tsx +1 -1
  33. package/src/components/layout/sidebar-rail.tsx +14 -6
  34. package/src/components/memory/memory-graph-view.tsx +120 -68
  35. package/src/components/org-chart/delegation-bubble.tsx +102 -0
  36. package/src/components/org-chart/mini-chat-bubble.tsx +79 -20
  37. package/src/components/org-chart/org-chart-activity-feed.tsx +156 -0
  38. package/src/components/org-chart/org-chart-edge.tsx +1 -30
  39. package/src/components/org-chart/org-chart-view.tsx +27 -2
  40. package/src/components/shared/command-palette.tsx +35 -20
  41. package/src/components/shared/notification-center.tsx +2 -2
  42. package/src/hooks/use-agent-live-status.ts +59 -0
  43. package/src/hooks/use-app-bootstrap.ts +2 -1
  44. package/src/hooks/use-delegation-edge-state.ts +166 -4
  45. package/src/hooks/use-ws.ts +69 -39
  46. package/src/instrumentation.ts +27 -0
  47. package/src/lib/fetch-dedup.test.ts +8 -4
  48. package/src/lib/fetch-dedup.ts +6 -3
  49. package/src/lib/keyed-queue.ts +18 -2
  50. package/src/lib/provider-sets.ts +2 -2
  51. package/src/lib/providers/anthropic.ts +14 -8
  52. package/src/lib/providers/claude-cli.ts +23 -54
  53. package/src/lib/providers/cli-utils.ts +301 -0
  54. package/src/lib/providers/codex-cli.ts +42 -72
  55. package/src/lib/providers/error-classification.ts +8 -0
  56. package/src/lib/providers/gemini-cli.ts +177 -0
  57. package/src/lib/providers/index.ts +10 -1
  58. package/src/lib/providers/openai.ts +3 -3
  59. package/src/lib/providers/opencode-cli.ts +39 -57
  60. package/src/lib/server/agents/agent-availability.test.ts +74 -0
  61. package/src/lib/server/agents/agent-availability.ts +16 -0
  62. package/src/lib/server/agents/agent-thread-session.ts +20 -14
  63. package/src/lib/server/agents/autonomy-contract.ts +5 -0
  64. package/src/lib/server/agents/delegation-jobs.ts +77 -5
  65. package/src/lib/server/agents/main-agent-loop.ts +39 -13
  66. package/src/lib/server/agents/subagent-runtime.ts +42 -4
  67. package/src/lib/server/agents/subagent-swarm.ts +47 -1
  68. package/src/lib/server/agents/team-resolution.test.ts +169 -0
  69. package/src/lib/server/agents/team-resolution.ts +96 -0
  70. package/src/lib/server/autonomy/supervisor-reflection.ts +132 -1
  71. package/src/lib/server/chat-execution/chat-execution.ts +23 -5
  72. package/src/lib/server/chat-execution/continuation-evaluator.ts +2 -2
  73. package/src/lib/server/chat-execution/continuation-limits.ts +5 -1
  74. package/src/lib/server/chat-execution/iteration-event-handler.ts +15 -0
  75. package/src/lib/server/chat-execution/message-classifier.ts +2 -1
  76. package/src/lib/server/chat-execution/post-stream-finalization.ts +6 -4
  77. package/src/lib/server/chat-execution/prompt-builder.ts +30 -2
  78. package/src/lib/server/chat-execution/prompt-sections.ts +80 -0
  79. package/src/lib/server/chat-execution/stream-agent-chat.ts +70 -22
  80. package/src/lib/server/chat-execution/stream-continuation.ts +40 -10
  81. package/src/lib/server/connectors/connector-inbound.ts +1 -15
  82. package/src/lib/server/connectors/connector-lifecycle.ts +1 -1
  83. package/src/lib/server/connectors/manager.test.ts +1 -1
  84. package/src/lib/server/context-manager.ts +31 -0
  85. package/src/lib/server/debug.ts +20 -0
  86. package/src/lib/server/embeddings.ts +36 -16
  87. package/src/lib/server/execution-log.ts +10 -0
  88. package/src/lib/server/extensions.ts +5 -5
  89. package/src/lib/server/llm-response-cache.ts +2 -1
  90. package/src/lib/server/openclaw/gateway.ts +4 -6
  91. package/src/lib/server/playwright-proxy.mjs +25 -6
  92. package/src/lib/server/protocols/protocol-agent-turn.ts +1 -1
  93. package/src/lib/server/protocols/protocol-step-helpers.ts +2 -3
  94. package/src/lib/server/provider-health.ts +23 -12
  95. package/src/lib/server/runtime/daemon-state.ts +40 -2
  96. package/src/lib/server/runtime/heartbeat-service.ts +103 -8
  97. package/src/lib/server/runtime/run-ledger.ts +58 -4
  98. package/src/lib/server/runtime/scheduler.ts +9 -6
  99. package/src/lib/server/runtime/session-run-manager.ts +112 -7
  100. package/src/lib/server/runtime/system-events.ts +16 -0
  101. package/src/lib/server/runtime/wake-dispatcher.ts +15 -5
  102. package/src/lib/server/session-tools/chatroom.ts +37 -0
  103. package/src/lib/server/session-tools/delegate.ts +29 -20
  104. package/src/lib/server/session-tools/index.ts +4 -0
  105. package/src/lib/server/session-tools/peer-query.test.ts +71 -0
  106. package/src/lib/server/session-tools/peer-query.ts +300 -0
  107. package/src/lib/server/session-tools/skill-runtime.ts +10 -1
  108. package/src/lib/server/session-tools/team-context.test.ts +41 -0
  109. package/src/lib/server/session-tools/team-context.ts +277 -0
  110. package/src/lib/server/skills/learned-skills.ts +1 -1
  111. package/src/lib/server/storage-cache.ts +2 -0
  112. package/src/lib/server/storage-normalization.test.ts +26 -25
  113. package/src/lib/server/storage-normalization.ts +36 -6
  114. package/src/lib/server/storage.ts +69 -31
  115. package/src/lib/server/tasks/task-quality-gate.test.ts +1 -1
  116. package/src/lib/server/tasks/task-quality-gate.ts +1 -1
  117. package/src/lib/server/tool-loop-detection.test.ts +44 -1
  118. package/src/lib/server/tool-loop-detection.ts +122 -38
  119. package/src/lib/ws-client.ts +19 -6
  120. package/src/stores/slices/agent-slice.ts +2 -2
  121. package/src/stores/slices/data-slice.ts +11 -0
  122. package/src/stores/slices/session-slice.ts +5 -2
  123. package/src/stores/use-chat-store.test.ts +7 -0
  124. package/src/stores/use-chat-store.ts +14 -53
  125. package/src/types/index.ts +6 -3
package/README.md CHANGED
@@ -190,6 +190,39 @@ The building blocks are the same: **agents, tools, memory, delegation, schedules
190
190
 
191
191
  ## Release Notes
192
192
 
193
+ ### v1.1.9 Highlights
194
+
195
+ - **Docker build stability**: limit Next.js page data workers to 1 in build mode to prevent `SQLITE_BUSY` contention.
196
+ - **Async file I/O in providers**: Anthropic and OpenAI providers now use `fs.promises` for non-blocking attachment reads.
197
+ - **Anthropic request timeout**: 60s timeout on Anthropic API requests prevents indefinite hangs.
198
+ - **Graceful crash handling**: instrumentation now catches EPIPE and suppresses expected LangGraph unhandled rejections.
199
+ - **Log tail optimization**: `/api/logs` reads only the last 256 KB instead of loading the entire log file.
200
+ - **Thread session fast path**: `ensureAgentThreadSession` uses single-row lookup instead of full table scan when `threadSessionId` is set.
201
+ - **Memory graph performance**: force-directed simulation writes to DOM imperatively instead of re-rendering React state per frame; stops when kinetic energy settles.
202
+ - **Reduced polling frequency**: chat area WS polling intervals relaxed (messages/runs 2s to 10s, browser 5s to 30s) to lower server load.
203
+ - **Chat list indexing**: connector lookup indexed by `agentId` for O(1) instead of O(n) per session filter.
204
+ - **Sidebar skill badges**: skill draft count displayed as a badge on the Skills nav item.
205
+ - **Route loading states**: added `loading.tsx` skeleton pages for activity, home, logs, memory, and tasks routes.
206
+ - **Command palette cleanup**: fixed missing `setOpen` dependencies and removed unused props.
207
+ - **Playwright proxy hardening**: improved stdio pipe handling for dev server restarts.
208
+ - **Scheduler and run ledger fixes**: improved scheduler reliability and run ledger state tracking.
209
+
210
+ ### v1.1.8 Highlights
211
+
212
+ - **Agent live status**: real-time `/agents/:id/status` endpoint exposes goal, progress, and plan steps; org chart detail panel consumes it via `useAgentLiveStatus` hook.
213
+ - **Learned skills lifecycle**: promote, dismiss, and delete learned skills via `/learned-skills/:id`; `/skill-review-counts` provides badge counts for the skills workspace.
214
+ - **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`.
215
+ - **Peer query & team context tools**: new session tools let agents query peers and access team context during conversations.
216
+ - **Team resolution**: dedicated `team-resolution.ts` module resolves agent teams for delegation routing.
217
+ - **Org chart activity feed**: timeline feed component and delegation bubble visualization for the org chart view.
218
+ - **Skills workspace improvements**: expanded skills management UI with review-ready badges.
219
+ - **Cost trend chart**: new dashboard component for cost visualization.
220
+ - **Streaming fix**: text no longer gets stuck on the thinking indicator.
221
+ - **Delegation normalization**: `delegationEnabled` now derived from agent role, removed from starter kit templates.
222
+ - **Chat execution refinements**: improved continuation limits, post-stream finalization, and stream continuation.
223
+ - **Memory and storage improvements**: memory tier management, consolidation enhancements, and storage cache updates.
224
+ - **WebSocket and provider health**: improved WS client handling, delegation edge state, and provider health monitoring.
225
+
193
226
  ### v1.1.7 Highlights
194
227
 
195
228
  - **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.9",
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,5 @@
1
+ import { PageLoader } from '@/components/ui/page-loader'
2
+
3
+ export default function Loading() {
4
+ return <PageLoader label="Loading activity..." />
5
+ }
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agents/agent-availability'
3
3
  import { ensureAgentThreadSession } from '@/lib/server/agents/agent-thread-session'
4
+ import type { Agent } from '@/types'
4
5
  import { loadAgents } from '@/lib/server/storage'
5
6
 
6
7
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -11,7 +12,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
11
12
  if (!agent) {
12
13
  return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
13
14
  }
14
- const session = ensureAgentThreadSession(agentId, user)
15
+ const session = ensureAgentThreadSession(agentId, user, agent as Agent)
15
16
  if (!session) {
16
17
  if (isAgentDisabled(agent)) {
17
18
  return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'start new chats') }, { status: 409 })
@@ -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
+ }
@@ -2,6 +2,9 @@ import { NextResponse } from 'next/server'
2
2
  import fs from 'fs'
3
3
  import { APP_LOG_PATH } from '@/lib/server/data-dir'
4
4
 
5
+ /** Max bytes to read from the tail of the log file (256 KB). */
6
+ const TAIL_BYTES = 256 * 1024
7
+
5
8
  export async function GET(req: Request) {
6
9
  const { searchParams } = new URL(req.url)
7
10
  const lines = parseInt(searchParams.get('lines') || '200', 10)
@@ -13,7 +16,29 @@ export async function GET(req: Request) {
13
16
  return NextResponse.json({ entries: [], total: 0 })
14
17
  }
15
18
 
16
- const content = fs.readFileSync(APP_LOG_PATH, 'utf8')
19
+ const stat = fs.statSync(APP_LOG_PATH)
20
+ const fileSize = stat.size
21
+ if (fileSize === 0) {
22
+ return NextResponse.json({ entries: [], total: 0 })
23
+ }
24
+
25
+ // Read only the tail of the file to avoid loading multi-MB logs into memory
26
+ const readSize = Math.min(fileSize, TAIL_BYTES)
27
+ const buf = Buffer.alloc(readSize)
28
+ const fd = fs.openSync(APP_LOG_PATH, 'r')
29
+ try {
30
+ fs.readSync(fd, buf, 0, readSize, fileSize - readSize)
31
+ } finally {
32
+ fs.closeSync(fd)
33
+ }
34
+
35
+ let content = buf.toString('utf8')
36
+ // If we didn't read from the start, drop the first partial line
37
+ if (readSize < fileSize) {
38
+ const firstNewline = content.indexOf('\n')
39
+ if (firstNewline >= 0) content = content.slice(firstNewline + 1)
40
+ }
41
+
17
42
  let allLines = content.split('\n').filter(Boolean)
18
43
 
19
44
  // Filter by level
@@ -33,8 +58,9 @@ export async function GET(req: Request) {
33
58
  const entries = allLines.slice(-lines).reverse().map(parseLine)
34
59
 
35
60
  return NextResponse.json({ entries, total })
36
- } catch (err: any) {
37
- return NextResponse.json({ error: err.message }, { status: 500 })
61
+ } catch (err: unknown) {
62
+ const message = err instanceof Error ? err.message : String(err)
63
+ return NextResponse.json({ error: message }, { status: 500 })
38
64
  }
39
65
  }
40
66
 
@@ -44,8 +70,9 @@ export async function DELETE() {
44
70
  fs.writeFileSync(APP_LOG_PATH, '')
45
71
  }
46
72
  return NextResponse.json({ ok: true })
47
- } catch (err: any) {
48
- return NextResponse.json({ error: err.message }, { status: 500 })
73
+ } catch (err: unknown) {
74
+ const message = err instanceof Error ? err.message : String(err)
75
+ return NextResponse.json({ error: message }, { status: 500 })
49
76
  }
50
77
  }
51
78
 
@@ -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); }
@@ -0,0 +1,5 @@
1
+ import { PageLoader } from '@/components/ui/page-loader'
2
+
3
+ export default function Loading() {
4
+ return <PageLoader label="Loading home..." />
5
+ }
@@ -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> = {
@@ -0,0 +1,5 @@
1
+ import { PageLoader } from '@/components/ui/page-loader'
2
+
3
+ export default function Loading() {
4
+ return <PageLoader label="Loading logs..." />
5
+ }
@@ -0,0 +1,5 @@
1
+ import { PageLoader } from '@/components/ui/page-loader'
2
+
3
+ export default function Loading() {
4
+ return <PageLoader label="Loading memory..." />
5
+ }
@@ -0,0 +1,5 @@
1
+ import { PageLoader } from '@/components/ui/page-loader'
2
+
3
+ export default function Loading() {
4
+ return <PageLoader label="Loading tasks..." />
5
+ }
@@ -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}