@swarmclawai/swarmclaw 0.6.4 → 0.6.7

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 (143) hide show
  1. package/README.md +62 -30
  2. package/package.json +10 -1
  3. package/src/app/api/agents/[id]/clone/route.ts +40 -0
  4. package/src/app/api/agents/route.ts +39 -14
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
  6. package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
  7. package/src/app/api/chatrooms/[id]/route.ts +34 -2
  8. package/src/app/api/chatrooms/route.ts +26 -3
  9. package/src/app/api/connectors/[id]/health/route.ts +64 -0
  10. package/src/app/api/connectors/route.ts +17 -2
  11. package/src/app/api/knowledge/route.ts +6 -1
  12. package/src/app/api/openclaw/doctor/route.ts +17 -0
  13. package/src/app/api/schedules/[id]/run/route.ts +3 -0
  14. package/src/app/api/sessions/[id]/chat/route.ts +5 -1
  15. package/src/app/api/sessions/route.ts +11 -2
  16. package/src/app/api/tasks/[id]/route.ts +18 -13
  17. package/src/app/api/tasks/route.ts +44 -1
  18. package/src/app/api/usage/route.ts +16 -7
  19. package/src/app/api/wallets/[id]/approve/route.ts +62 -0
  20. package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
  21. package/src/app/api/wallets/[id]/route.ts +118 -0
  22. package/src/app/api/wallets/[id]/send/route.ts +118 -0
  23. package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
  24. package/src/app/api/wallets/route.ts +74 -0
  25. package/src/app/globals.css +8 -0
  26. package/src/cli/index.js +20 -0
  27. package/src/cli/index.ts +223 -39
  28. package/src/cli/spec.js +14 -0
  29. package/src/components/agents/agent-avatar.tsx +15 -1
  30. package/src/components/agents/agent-card.tsx +38 -6
  31. package/src/components/agents/agent-chat-list.tsx +79 -3
  32. package/src/components/agents/agent-sheet.tsx +191 -26
  33. package/src/components/auth/setup-wizard.tsx +268 -353
  34. package/src/components/chat/chat-area.tsx +24 -9
  35. package/src/components/chat/chat-header.tsx +48 -19
  36. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  37. package/src/components/chat/delegation-banner.test.ts +27 -0
  38. package/src/components/chat/delegation-banner.tsx +109 -23
  39. package/src/components/chat/message-bubble.tsx +17 -16
  40. package/src/components/chat/message-list.tsx +6 -5
  41. package/src/components/chat/streaming-bubble.tsx +3 -2
  42. package/src/components/chat/thinking-indicator.tsx +3 -2
  43. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  44. package/src/components/chatrooms/agent-hover-card.tsx +1 -1
  45. package/src/components/chatrooms/chatroom-input.tsx +1 -1
  46. package/src/components/chatrooms/chatroom-message.tsx +165 -23
  47. package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
  48. package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
  49. package/src/components/chatrooms/chatroom-view.tsx +62 -17
  50. package/src/components/connectors/connector-health.tsx +120 -0
  51. package/src/components/connectors/connector-list.tsx +1 -1
  52. package/src/components/connectors/connector-sheet.tsx +9 -0
  53. package/src/components/home/home-view.tsx +25 -3
  54. package/src/components/input/chat-input.tsx +8 -1
  55. package/src/components/knowledge/knowledge-list.tsx +1 -1
  56. package/src/components/knowledge/knowledge-sheet.tsx +1 -1
  57. package/src/components/layout/app-layout.tsx +35 -4
  58. package/src/components/memory/memory-agent-list.tsx +1 -1
  59. package/src/components/memory/memory-browser.tsx +1 -0
  60. package/src/components/memory/memory-card.tsx +3 -2
  61. package/src/components/memory/memory-detail.tsx +3 -3
  62. package/src/components/memory/memory-sheet.tsx +2 -2
  63. package/src/components/projects/project-detail.tsx +4 -4
  64. package/src/components/schedules/schedule-list.tsx +55 -9
  65. package/src/components/schedules/schedule-sheet.tsx +134 -23
  66. package/src/components/secrets/secret-sheet.tsx +1 -1
  67. package/src/components/secrets/secrets-list.tsx +1 -1
  68. package/src/components/sessions/session-card.tsx +1 -1
  69. package/src/components/shared/agent-picker-list.tsx +1 -1
  70. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  71. package/src/components/shared/command-palette.tsx +237 -0
  72. package/src/components/shared/connector-platform-icon.tsx +1 -0
  73. package/src/components/shared/settings/section-user-preferences.tsx +4 -4
  74. package/src/components/skills/skill-list.tsx +1 -1
  75. package/src/components/skills/skill-sheet.tsx +1 -1
  76. package/src/components/tasks/task-board.tsx +3 -3
  77. package/src/components/tasks/task-card.tsx +22 -2
  78. package/src/components/tasks/task-sheet.tsx +112 -17
  79. package/src/components/usage/metrics-dashboard.tsx +13 -25
  80. package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
  81. package/src/components/wallets/wallet-panel.tsx +616 -0
  82. package/src/components/wallets/wallet-section.tsx +100 -0
  83. package/src/hooks/use-swipe.ts +49 -0
  84. package/src/lib/providers/anthropic.ts +16 -2
  85. package/src/lib/providers/claude-cli.ts +7 -1
  86. package/src/lib/providers/index.ts +7 -0
  87. package/src/lib/providers/ollama.ts +16 -2
  88. package/src/lib/providers/openai.ts +7 -2
  89. package/src/lib/providers/openclaw.ts +6 -1
  90. package/src/lib/providers/provider-defaults.ts +7 -0
  91. package/src/lib/schedule-templates.ts +115 -0
  92. package/src/lib/server/agent-registry.ts +2 -2
  93. package/src/lib/server/alert-dispatch.ts +64 -0
  94. package/src/lib/server/chat-execution.ts +76 -4
  95. package/src/lib/server/chatroom-health.ts +60 -0
  96. package/src/lib/server/chatroom-helpers.test.ts +94 -0
  97. package/src/lib/server/chatroom-helpers.ts +86 -12
  98. package/src/lib/server/chatroom-routing.ts +65 -0
  99. package/src/lib/server/connectors/discord.ts +3 -0
  100. package/src/lib/server/connectors/email.ts +267 -0
  101. package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
  102. package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
  103. package/src/lib/server/connectors/manager.ts +239 -5
  104. package/src/lib/server/connectors/openclaw.ts +3 -0
  105. package/src/lib/server/connectors/slack.ts +6 -0
  106. package/src/lib/server/connectors/telegram.ts +18 -0
  107. package/src/lib/server/connectors/types.ts +2 -0
  108. package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
  109. package/src/lib/server/connectors/whatsapp-text.ts +26 -0
  110. package/src/lib/server/connectors/whatsapp.ts +17 -5
  111. package/src/lib/server/cost.ts +70 -0
  112. package/src/lib/server/create-notification.ts +2 -0
  113. package/src/lib/server/daemon-state.ts +124 -0
  114. package/src/lib/server/dag-validation.ts +115 -0
  115. package/src/lib/server/memory-db.ts +12 -7
  116. package/src/lib/server/openclaw-doctor.ts +48 -0
  117. package/src/lib/server/orchestrator-lg.ts +12 -2
  118. package/src/lib/server/orchestrator.ts +6 -1
  119. package/src/lib/server/queue-followups.test.ts +224 -0
  120. package/src/lib/server/queue.ts +238 -24
  121. package/src/lib/server/scheduler.ts +3 -0
  122. package/src/lib/server/session-run-manager.ts +22 -1
  123. package/src/lib/server/session-tools/chatroom.ts +11 -2
  124. package/src/lib/server/session-tools/context-mgmt.ts +2 -2
  125. package/src/lib/server/session-tools/index.ts +8 -2
  126. package/src/lib/server/session-tools/memory.ts +23 -4
  127. package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
  128. package/src/lib/server/session-tools/shell.ts +1 -1
  129. package/src/lib/server/session-tools/wallet.ts +124 -0
  130. package/src/lib/server/session-tools/web.ts +2 -2
  131. package/src/lib/server/solana.ts +122 -0
  132. package/src/lib/server/storage.ts +158 -6
  133. package/src/lib/server/stream-agent-chat.ts +126 -63
  134. package/src/lib/server/task-mention.test.ts +41 -0
  135. package/src/lib/server/task-mention.ts +3 -2
  136. package/src/lib/setup-defaults.ts +277 -0
  137. package/src/lib/tool-definitions.ts +1 -0
  138. package/src/lib/validation/schemas.ts +69 -0
  139. package/src/lib/view-routes.ts +1 -0
  140. package/src/stores/use-app-store.ts +15 -3
  141. package/src/stores/use-chatroom-store.ts +52 -2
  142. package/src/types/index.ts +98 -2
  143. package/tsconfig.json +2 -1
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
2
+ import { loadChatrooms, saveChatrooms, loadAgents, loadConnectors, saveConnectors } from '@/lib/server/storage'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
5
  import { genId } from '@/lib/id'
@@ -21,16 +21,33 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
21
21
 
22
22
  if (body.name !== undefined) chatroom.name = body.name
23
23
  if (body.description !== undefined) chatroom.description = body.description
24
+ if (body.chatMode !== undefined) {
25
+ chatroom.chatMode = body.chatMode === 'parallel' ? 'parallel' : 'sequential'
26
+ }
27
+ if (body.autoAddress !== undefined) {
28
+ chatroom.autoAddress = Boolean(body.autoAddress)
29
+ }
30
+ if (body.routingRules !== undefined) {
31
+ chatroom.routingRules = Array.isArray(body.routingRules) ? body.routingRules : undefined
32
+ }
24
33
 
25
34
  // Diff agentIds and inject join/leave system messages
26
35
  if (Array.isArray(body.agentIds)) {
36
+ const agents = loadAgents()
37
+ const invalidAgentIds = (body.agentIds as string[]).filter((agentId) => !agents[agentId])
38
+ if (invalidAgentIds.length > 0) {
39
+ return NextResponse.json(
40
+ { error: `Unknown chatroom member(s): ${invalidAgentIds.join(', ')}` },
41
+ { status: 400 },
42
+ )
43
+ }
44
+
27
45
  const oldIds = new Set(chatroom.agentIds)
28
46
  const newIds = new Set(body.agentIds as string[])
29
47
  const added = (body.agentIds as string[]).filter((aid: string) => !oldIds.has(aid))
30
48
  const removed = chatroom.agentIds.filter((aid: string) => !newIds.has(aid))
31
49
 
32
50
  if (added.length > 0 || removed.length > 0) {
33
- const agents = loadAgents()
34
51
  if (!Array.isArray(chatroom.messages)) chatroom.messages = []
35
52
  const now = Date.now()
36
53
  let offset = 0
@@ -77,6 +94,21 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
77
94
  const chatrooms = loadChatrooms()
78
95
  if (!chatrooms[id]) return notFound()
79
96
 
97
+ // Cascade: null out chatroomId on any connectors that reference this chatroom
98
+ const connectors = loadConnectors()
99
+ let connectorsDirty = false
100
+ for (const connector of Object.values(connectors)) {
101
+ if (connector.chatroomId === id) {
102
+ connector.chatroomId = null
103
+ connector.updatedAt = Date.now()
104
+ connectorsDirty = true
105
+ }
106
+ }
107
+ if (connectorsDirty) {
108
+ saveConnectors(connectors)
109
+ notify('connectors')
110
+ }
111
+
80
112
  delete chatrooms[id]
81
113
  saveChatrooms(chatrooms)
82
114
  notify('chatrooms')
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
3
  import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
4
4
  import { notify } from '@/lib/server/ws-hub'
5
+ import { ChatroomCreateSchema, formatZodError } from '@/lib/validation/schemas'
6
+ import { z } from 'zod'
5
7
  import type { Chatroom, ChatroomMessage } from '@/types'
6
8
 
7
9
  export const dynamic = 'force-dynamic'
@@ -12,15 +14,31 @@ export async function GET() {
12
14
  }
13
15
 
14
16
  export async function POST(req: Request) {
15
- const body = await req.json()
17
+ const raw = await req.json()
18
+ const parsed = ChatroomCreateSchema.safeParse(raw)
19
+ if (!parsed.success) {
20
+ return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
21
+ }
22
+ const body = parsed.data
16
23
  const chatrooms = loadChatrooms()
17
24
  const id = genId()
18
25
 
19
- const agentIds: string[] = Array.isArray(body.agentIds) ? body.agentIds : []
26
+ const requestedAgentIds: string[] = body.agentIds
27
+ const knownAgents = loadAgents()
28
+ const invalidAgentIds = requestedAgentIds.filter((agentId) => !knownAgents[agentId])
29
+ if (invalidAgentIds.length > 0) {
30
+ return NextResponse.json(
31
+ { error: `Unknown chatroom member(s): ${invalidAgentIds.join(', ')}` },
32
+ { status: 400 },
33
+ )
34
+ }
35
+ const agentIds: string[] = requestedAgentIds
36
+ const chatMode = body.chatMode === 'parallel' ? 'parallel' : 'sequential'
37
+ const autoAddress = Boolean(body.autoAddress)
20
38
  const now = Date.now()
21
39
 
22
40
  // Generate join messages for initial agents
23
- const agents = agentIds.length > 0 ? loadAgents() : {}
41
+ const agents = agentIds.length > 0 ? knownAgents : {}
24
42
  const joinMessages: ChatroomMessage[] = agentIds.map((agentId: string, i: number) => ({
25
43
  id: genId(),
26
44
  senderId: 'system',
@@ -38,6 +56,11 @@ export async function POST(req: Request) {
38
56
  description: body.description || '',
39
57
  agentIds,
40
58
  messages: joinMessages,
59
+ chatMode,
60
+ autoAddress,
61
+ ...(Array.isArray(body.routingRules) && body.routingRules.length > 0
62
+ ? { routingRules: body.routingRules }
63
+ : {}),
41
64
  createdAt: now,
42
65
  updatedAt: now,
43
66
  }
@@ -0,0 +1,64 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadConnectors, loadConnectorHealth } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+ import type { ConnectorHealthEvent } from '@/types'
5
+
6
+ export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
+ const { id } = await params
8
+ const connectors = loadConnectors()
9
+ if (!connectors[id]) return notFound()
10
+
11
+ const url = new URL(req.url)
12
+ const since = url.searchParams.get('since')
13
+
14
+ const allHealth = loadConnectorHealth()
15
+ const events: ConnectorHealthEvent[] = []
16
+
17
+ for (const raw of Object.values(allHealth)) {
18
+ const entry = raw as ConnectorHealthEvent
19
+ if (entry.connectorId !== id) continue
20
+ if (since && entry.timestamp < since) continue
21
+ events.push(entry)
22
+ }
23
+
24
+ // Sort by timestamp ascending
25
+ events.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
26
+
27
+ // Compute uptime percentage
28
+ const uptimePercent = computeUptime(events)
29
+
30
+ return NextResponse.json({ events, uptimePercent })
31
+ }
32
+
33
+ function computeUptime(events: ConnectorHealthEvent[]): number {
34
+ if (events.length === 0) return 0
35
+
36
+ const firstTime = new Date(events[0].timestamp).getTime()
37
+ const now = Date.now()
38
+ const totalMs = now - firstTime
39
+ if (totalMs <= 0) return 100
40
+
41
+ let uptimeMs = 0
42
+ let lastUpAt: number | null = null
43
+
44
+ for (const ev of events) {
45
+ const t = new Date(ev.timestamp).getTime()
46
+ if (ev.event === 'started' || ev.event === 'reconnected') {
47
+ if (lastUpAt === null) {
48
+ lastUpAt = t
49
+ }
50
+ } else if (ev.event === 'stopped' || ev.event === 'error' || ev.event === 'disconnected') {
51
+ if (lastUpAt !== null) {
52
+ uptimeMs += t - lastUpAt
53
+ lastUpAt = null
54
+ }
55
+ }
56
+ }
57
+
58
+ // If still up, count time until now
59
+ if (lastUpAt !== null) {
60
+ uptimeMs += now - lastUpAt
61
+ }
62
+
63
+ return Math.round((uptimeMs / totalMs) * 10000) / 100
64
+ }
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
3
  import { loadConnectors, saveConnectors } from '@/lib/server/storage'
4
4
  import { notify } from '@/lib/server/ws-hub'
5
+ import { ConnectorCreateSchema, formatZodError } from '@/lib/validation/schemas'
6
+ import { z } from 'zod'
5
7
  import type { Connector } from '@/types'
6
8
  export const dynamic = 'force-dynamic'
7
9
 
@@ -10,7 +12,7 @@ export async function GET(_req: Request) {
10
12
  const connectors = loadConnectors()
11
13
  // Merge runtime status from manager
12
14
  try {
13
- const { getConnectorStatus, isConnectorAuthenticated, hasConnectorCredentials, getConnectorQR } = await import('@/lib/server/connectors/manager')
15
+ const { getConnectorStatus, isConnectorAuthenticated, hasConnectorCredentials, getConnectorQR, getReconnectState } = await import('@/lib/server/connectors/manager')
14
16
  for (const c of Object.values(connectors) as Connector[]) {
15
17
  c.status = getConnectorStatus(c.id)
16
18
  if (c.platform === 'whatsapp') {
@@ -19,13 +21,26 @@ export async function GET(_req: Request) {
19
21
  const qr = getConnectorQR(c.id)
20
22
  if (qr) c.qrDataUrl = qr
21
23
  }
24
+ // Surface reconnect state if connector is in a recovery cycle
25
+ const rState = getReconnectState(c.id)
26
+ if (rState) {
27
+ const ext = c as unknown as Record<string, unknown>
28
+ ext.reconnectAttempts = rState.attempts
29
+ ext.nextRetryAt = rState.nextRetryAt
30
+ ext.reconnectError = rState.error
31
+ }
22
32
  }
23
33
  } catch { /* manager not loaded yet */ }
24
34
  return NextResponse.json(connectors)
25
35
  }
26
36
 
27
37
  export async function POST(req: Request) {
28
- const body = await req.json()
38
+ const raw = await req.json()
39
+ const parsed = ConnectorCreateSchema.safeParse(raw)
40
+ if (!parsed.success) {
41
+ return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
42
+ }
43
+ const body = parsed.data
29
44
  const connectors = loadConnectors()
30
45
  const id = genId()
31
46
 
@@ -25,7 +25,7 @@ export async function POST(req: Request) {
25
25
  return NextResponse.json({ error: 'Invalid JSON body.' }, { status: 400 })
26
26
  }
27
27
 
28
- const { title, content, tags, scope, agentIds } = body as Record<string, unknown>
28
+ const { title, content, tags, scope, agentIds, source, sourceUrl } = body as Record<string, unknown>
29
29
 
30
30
  if (typeof title !== 'string' || !title.trim()) {
31
31
  return NextResponse.json({ error: 'title is required.' }, { status: 400 })
@@ -43,12 +43,17 @@ export async function POST(req: Request) {
43
43
  ? (agentIds as unknown[]).filter((id): id is string => typeof id === 'string')
44
44
  : []
45
45
 
46
+ const normalizedSource = typeof source === 'string' && source.trim() ? source.trim() : undefined
47
+ const normalizedSourceUrl = typeof sourceUrl === 'string' && sourceUrl.trim() ? sourceUrl.trim() : undefined
48
+
46
49
  const entry = addKnowledge({
47
50
  title: title.trim(),
48
51
  content,
49
52
  tags: normalizedTags,
50
53
  scope: normalizedScope,
51
54
  agentIds: normalizedAgentIds,
55
+ source: normalizedSource,
56
+ sourceUrl: normalizedSourceUrl,
52
57
  })
53
58
 
54
59
  return NextResponse.json(entry)
@@ -0,0 +1,17 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { runOpenClawDoctor } from '@/lib/server/openclaw-doctor'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET() {
7
+ const result = await runOpenClawDoctor()
8
+ return NextResponse.json(result)
9
+ }
10
+
11
+ export async function POST(req: Request) {
12
+ const body = await req.json().catch(() => ({}))
13
+ const fix = typeof body.fix === 'boolean' ? body.fix : false
14
+ const workspace = typeof body.workspace === 'string' ? body.workspace : undefined
15
+ const result = await runOpenClawDoctor({ fix, workspace })
16
+ return NextResponse.json(result)
17
+ }
@@ -53,7 +53,10 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
53
53
  existingTask.title = `[Sched] ${schedule.name} (run #${schedule.runNumber})`
54
54
  existingTask.result = null
55
55
  existingTask.error = null
56
+ existingTask.outputFiles = []
57
+ existingTask.artifacts = []
56
58
  existingTask.sessionId = null
59
+ existingTask.completionReportPath = null
57
60
  existingTask.updatedAt = now
58
61
  existingTask.queuedAt = null
59
62
  existingTask.startedAt = null
@@ -25,6 +25,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
25
25
  }
26
26
 
27
27
  const encoder = new TextEncoder()
28
+ let abortRun: (() => void) | null = null
28
29
  const stream = new ReadableStream({
29
30
  start(controller) {
30
31
  let closed = false
@@ -48,7 +49,9 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
48
49
  mode: queueMode,
49
50
  onEvent: (ev) => writeEvent(ev as unknown as Record<string, unknown>),
50
51
  replyToId,
52
+ callerSignal: req.signal,
51
53
  })
54
+ abortRun = run.abort
52
55
 
53
56
  log.info('chat', `Enqueued session run ${run.runId}`, {
54
57
  sessionId: id,
@@ -86,7 +89,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
86
89
  })
87
90
  },
88
91
  cancel() {
89
- // Client disconnected; subsequent writes should be ignored.
92
+ // Client disconnected abort the run so the LLM stream is cancelled.
93
+ abortRun?.()
90
94
  },
91
95
  })
92
96
 
@@ -11,7 +11,7 @@ import { ensureMainSessionFlag, isProtectedMainSession } from '@/lib/server/main
11
11
  export const dynamic = 'force-dynamic'
12
12
 
13
13
 
14
- export async function GET(_req: Request) {
14
+ export async function GET(req: Request) {
15
15
  const sessions = loadSessions()
16
16
  for (const id of Object.keys(sessions)) {
17
17
  const run = getSessionRunState(id)
@@ -19,7 +19,16 @@ export async function GET(_req: Request) {
19
19
  sessions[id].queuedCount = run.queueLength
20
20
  sessions[id].currentRunId = run.runningRunId || null
21
21
  }
22
- return NextResponse.json(sessions)
22
+
23
+ const { searchParams } = new URL(req.url)
24
+ const limitParam = searchParams.get('limit')
25
+ if (!limitParam) return NextResponse.json(sessions)
26
+
27
+ const limit = Math.max(1, Number(limitParam) || 50)
28
+ const offset = Math.max(0, Number(searchParams.get('offset')) || 0)
29
+ const all = Object.values(sessions).sort((a, b) => (b.lastActiveAt ?? b.createdAt) - (a.lastActiveAt ?? a.createdAt))
30
+ const items = all.slice(offset, offset + limit)
31
+ return NextResponse.json({ items, total: all.length, hasMore: offset + limit < all.length })
23
32
  }
24
33
 
25
34
  export async function DELETE(req: Request) {
@@ -10,6 +10,7 @@ import { notify } from '@/lib/server/ws-hub'
10
10
  import { createNotification } from '@/lib/server/create-notification'
11
11
  import { enqueueSystemEvent } from '@/lib/server/system-events'
12
12
  import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
13
+ import { validateDag, cascadeUnblock } from '@/lib/server/dag-validation'
13
14
 
14
15
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
15
16
  // Keep completed queue integrity even if daemon is not running.
@@ -29,6 +30,17 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
29
30
 
30
31
  const prevStatus = tasks[id].status
31
32
 
33
+ // DAG validation: reject if proposed blockedBy would create a cycle
34
+ if (Array.isArray(body.blockedBy)) {
35
+ const dagResult = validateDag(tasks, id, body.blockedBy)
36
+ if (!dagResult.valid) {
37
+ return NextResponse.json(
38
+ { error: 'Dependency cycle detected', cycle: dagResult.cycle },
39
+ { status: 400 },
40
+ )
41
+ }
42
+ }
43
+
32
44
  // Support atomic comment append to avoid race conditions
33
45
  if (body.appendComment) {
34
46
  if (!tasks[id].comments) tasks[id].comments = []
@@ -114,20 +126,13 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
114
126
  }
115
127
  }
116
128
 
117
- // When a task is completed, auto-unblock dependent tasks
129
+ // When a task is completed, cascade unblock dependent tasks
118
130
  if (tasks[id].status === 'completed') {
119
- const blockedIds = Array.isArray(tasks[id].blocks) ? tasks[id].blocks as string[] : []
120
- for (const blockedId of blockedIds) {
121
- const blocked = tasks[blockedId]
122
- if (!blocked) continue
123
- const deps = Array.isArray(blocked.blockedBy) ? blocked.blockedBy as string[] : []
124
- const allDone = deps.every((depId: string) => tasks[depId]?.status === 'completed')
125
- if (allDone && (blocked.status === 'backlog' || blocked.status === 'todo')) {
126
- blocked.status = 'queued'
127
- blocked.queuedAt = Date.now()
128
- blocked.updatedAt = Date.now()
129
- saveTasks(tasks)
130
- enqueueTask(blockedId)
131
+ const unblockedIds = cascadeUnblock(tasks, id)
132
+ if (unblockedIds.length > 0) {
133
+ saveTasks(tasks)
134
+ for (const uid of unblockedIds) {
135
+ enqueueTask(uid)
131
136
  }
132
137
  }
133
138
  }
@@ -1,6 +1,8 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
3
  import { loadTasks, saveTasks, loadSettings, loadAgents, logActivity } from '@/lib/server/storage'
4
+ import { TaskCreateSchema, formatZodError } from '@/lib/validation/schemas'
5
+ import { z } from 'zod'
4
6
  import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
5
7
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
6
8
  import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
@@ -8,6 +10,7 @@ import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
8
10
  import { notify } from '@/lib/server/ws-hub'
9
11
  import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
10
12
  import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
13
+ import { validateDag } from '@/lib/server/dag-validation'
11
14
 
12
15
  export async function GET(req: Request) {
13
16
  // Keep completed queue integrity even if daemon is not running.
@@ -55,7 +58,12 @@ export async function DELETE(req: Request) {
55
58
  }
56
59
 
57
60
  export async function POST(req: Request) {
58
- const body = await req.json()
61
+ const raw = await req.json()
62
+ const parsed = TaskCreateSchema.safeParse(raw)
63
+ if (!parsed.success) {
64
+ return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
65
+ }
66
+ const body = { ...raw, ...parsed.data }
59
67
  const id = genId()
60
68
  const now = Date.now()
61
69
  const tasks = loadTasks()
@@ -66,6 +74,17 @@ export async function POST(req: Request) {
66
74
  const retryBackoffSec = Number.isFinite(Number(body.retryBackoffSec))
67
75
  ? Math.max(1, Math.min(3600, Math.trunc(Number(body.retryBackoffSec))))
68
76
  : Math.max(1, Math.min(3600, Math.trunc(Number(settings.taskRetryBackoffSec ?? 30))))
77
+ // DAG validation: reject if proposed blockedBy would create a cycle
78
+ if (Array.isArray(body.blockedBy) && body.blockedBy.length > 0) {
79
+ const dagResult = validateDag(tasks, id, body.blockedBy)
80
+ if (!dagResult.valid) {
81
+ return NextResponse.json(
82
+ { error: 'Dependency cycle detected', cycle: dagResult.cycle },
83
+ { status: 400 },
84
+ )
85
+ }
86
+ }
87
+
69
88
  // Resolve @mentions in description to auto-assign agent
70
89
  const resolvedAgentId = body.description
71
90
  ? resolveTaskAgentFromDescription(body.description, body.agentId || '', loadAgents())
@@ -84,6 +103,30 @@ export async function POST(req: Request) {
84
103
  sessionId: typeof body.sessionId === 'string' ? body.sessionId : null,
85
104
  result: typeof body.result === 'string' ? body.result : null,
86
105
  error: typeof body.error === 'string' ? body.error : null,
106
+ outputFiles: Array.isArray(body.outputFiles)
107
+ ? body.outputFiles.filter((entry: unknown) => typeof entry === 'string').slice(0, 24)
108
+ : [],
109
+ artifacts: Array.isArray(body.artifacts)
110
+ ? body.artifacts
111
+ .filter((artifact: unknown) => artifact && typeof artifact === 'object')
112
+ .map((artifact: unknown) => {
113
+ const row = artifact as {
114
+ url?: unknown
115
+ type?: unknown
116
+ filename?: unknown
117
+ }
118
+ const normalizedType = String(row.type || '')
119
+ return {
120
+ url: String(row.url || ''),
121
+ type: ['image', 'video', 'pdf', 'file'].includes(normalizedType)
122
+ ? (normalizedType as 'image' | 'video' | 'pdf' | 'file')
123
+ : 'file',
124
+ filename: String(row.filename || ''),
125
+ }
126
+ })
127
+ .filter((artifact: { url: string; filename: string }) => artifact.url && artifact.filename)
128
+ .slice(0, 24)
129
+ : [],
87
130
  createdAt: now,
88
131
  updatedAt: now,
89
132
  queuedAt: null,
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadUsage } from '@/lib/server/storage'
2
+ import { loadUsage, loadSessions, loadAgents } from '@/lib/server/storage'
3
3
  import type { UsageRecord } from '@/types'
4
4
  export const dynamic = 'force-dynamic'
5
5
 
@@ -41,10 +41,14 @@ export async function GET(req: Request) {
41
41
  }
42
42
  }
43
43
 
44
+ // Build session→agent lookup
45
+ const sessions = loadSessions() as Record<string, { agentId?: string }>
46
+ const agents = loadAgents() as Record<string, { name?: string }>
47
+
44
48
  // Compute summaries
45
49
  let totalTokens = 0
46
50
  let totalCost = 0
47
- const byAgent: Record<string, { tokens: number; cost: number }> = {}
51
+ const byAgent: Record<string, { name: string; cost: number; tokens: number; count: number }> = {}
48
52
  const byProvider: Record<string, { tokens: number; cost: number }> = {}
49
53
  const bucketMap: Record<string, { tokens: number; cost: number }> = {}
50
54
 
@@ -60,11 +64,16 @@ export async function GET(req: Request) {
60
64
  byProvider[prov].tokens += tokens
61
65
  byProvider[prov].cost += cost
62
66
 
63
- // by agent (using sessionId as proxy agents map to sessions)
64
- const agentKey = r.sessionId || 'unknown'
65
- if (!byAgent[agentKey]) byAgent[agentKey] = { tokens: 0, cost: 0 }
66
- byAgent[agentKey].tokens += tokens
67
- byAgent[agentKey].cost += cost
67
+ // by agent resolve sessionId agentId agent name
68
+ const session = r.sessionId ? sessions[r.sessionId] : undefined
69
+ const agentId = session?.agentId || 'unknown'
70
+ const agentName = agentId !== 'unknown' && agents[agentId]?.name
71
+ ? agents[agentId].name
72
+ : agentId
73
+ if (!byAgent[agentId]) byAgent[agentId] = { name: agentName, cost: 0, tokens: 0, count: 0 }
74
+ byAgent[agentId].cost += cost
75
+ byAgent[agentId].tokens += tokens
76
+ byAgent[agentId].count += 1
68
77
 
69
78
  // time series bucketing
70
79
  const bk = bucketKey(r.timestamp || now, range)
@@ -0,0 +1,62 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadWallets, loadWalletTransactions, upsertWalletTransaction } from '@/lib/server/storage'
3
+ import { sendSol } from '@/lib/server/solana'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ import type { AgentWallet, WalletTransaction } from '@/types'
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 wallets = loadWallets() as Record<string, AgentWallet>
11
+ const wallet = wallets[id]
12
+ if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
13
+
14
+ const body = await req.json()
15
+ const transactionId = typeof body.transactionId === 'string' ? body.transactionId.trim() : ''
16
+ const decision = body.decision as 'approve' | 'deny'
17
+
18
+ if (!transactionId) {
19
+ return NextResponse.json({ error: 'transactionId is required' }, { status: 400 })
20
+ }
21
+ if (decision !== 'approve' && decision !== 'deny') {
22
+ return NextResponse.json({ error: 'decision must be "approve" or "deny"' }, { status: 400 })
23
+ }
24
+
25
+ const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
26
+ const tx = allTxs[transactionId]
27
+ if (!tx || tx.walletId !== id) {
28
+ return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })
29
+ }
30
+ if (tx.status !== 'pending_approval') {
31
+ return NextResponse.json({ error: `Transaction is already ${tx.status}` }, { status: 409 })
32
+ }
33
+
34
+ if (decision === 'deny') {
35
+ tx.status = 'denied'
36
+ tx.approvedBy = 'user'
37
+ upsertWalletTransaction(transactionId, tx)
38
+ notify('wallets')
39
+ return NextResponse.json({ status: 'denied', transactionId })
40
+ }
41
+
42
+ // Approve — sign and submit
43
+ try {
44
+ const { signature, fee } = await sendSol(wallet.encryptedPrivateKey, tx.toAddress, tx.amountLamports)
45
+ tx.status = 'confirmed'
46
+ tx.signature = signature
47
+ tx.feeLamports = fee
48
+ tx.approvedBy = 'user'
49
+ upsertWalletTransaction(transactionId, tx)
50
+ notify('wallets')
51
+ return NextResponse.json({ status: 'confirmed', transactionId, signature })
52
+ } catch (err: unknown) {
53
+ tx.status = 'failed'
54
+ upsertWalletTransaction(transactionId, tx)
55
+ notify('wallets')
56
+ return NextResponse.json({
57
+ error: err instanceof Error ? err.message : String(err),
58
+ transactionId,
59
+ status: 'failed',
60
+ }, { status: 500 })
61
+ }
62
+ }
@@ -0,0 +1,18 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadWallets, loadWalletBalanceHistory } from '@/lib/server/storage'
3
+ import type { AgentWallet, WalletBalanceSnapshot } from '@/types'
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
7
+ const { id } = await params
8
+ const wallets = loadWallets() as Record<string, AgentWallet>
9
+ const wallet = wallets[id]
10
+ if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
11
+
12
+ const allSnapshots = loadWalletBalanceHistory() as Record<string, WalletBalanceSnapshot>
13
+ const walletSnapshots = Object.values(allSnapshots)
14
+ .filter((s) => s.walletId === id)
15
+ .sort((a, b) => a.timestamp - b.timestamp)
16
+
17
+ return NextResponse.json(walletSnapshots)
18
+ }