@swarmclawai/swarmclaw 1.2.0 → 1.2.1

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 (123) hide show
  1. package/README.md +10 -0
  2. package/package.json +4 -1
  3. package/src/app/api/chats/[id]/deploy/route.ts +11 -6
  4. package/src/app/api/chats/[id]/devserver/route.ts +5 -2
  5. package/src/app/api/chats/[id]/messages/route.ts +7 -1
  6. package/src/app/api/credentials/[id]/route.ts +4 -1
  7. package/src/app/api/extensions/marketplace/route.ts +5 -2
  8. package/src/app/api/memory/maintenance/route.ts +5 -2
  9. package/src/app/api/preview-server/route.ts +14 -11
  10. package/src/app/api/system/status/route.ts +11 -0
  11. package/src/app/api/upload/route.ts +4 -1
  12. package/src/cli/index.js +7 -0
  13. package/src/cli/spec.js +1 -0
  14. package/src/components/agents/agent-files-editor.tsx +44 -32
  15. package/src/components/agents/personality-builder.tsx +13 -7
  16. package/src/components/agents/trash-list.tsx +1 -1
  17. package/src/components/chat/message-bubble.tsx +1 -0
  18. package/src/components/chat/message-list.tsx +25 -39
  19. package/src/components/chat/swarm-status-card.tsx +10 -3
  20. package/src/components/layout/daemon-indicator.tsx +7 -8
  21. package/src/components/layout/update-banner.tsx +8 -13
  22. package/src/components/logs/log-list.tsx +1 -1
  23. package/src/components/memory/memory-card.tsx +3 -1
  24. package/src/components/org-chart/org-chart-view.tsx +4 -0
  25. package/src/components/projects/project-list.tsx +4 -2
  26. package/src/components/projects/tabs/overview-tab.tsx +3 -2
  27. package/src/components/secrets/secret-sheet.tsx +1 -1
  28. package/src/components/secrets/secrets-list.tsx +1 -1
  29. package/src/components/shared/agent-switch-dialog.tsx +12 -6
  30. package/src/components/shared/dir-browser.tsx +22 -18
  31. package/src/components/skills/skill-sheet.tsx +2 -3
  32. package/src/components/tasks/task-list.tsx +1 -1
  33. package/src/components/tasks/task-sheet.tsx +1 -1
  34. package/src/hooks/use-openclaw-gateway.ts +46 -27
  35. package/src/instrumentation.ts +10 -7
  36. package/src/lib/chat/chat.ts +18 -2
  37. package/src/lib/providers/anthropic.ts +6 -3
  38. package/src/lib/providers/claude-cli.ts +9 -3
  39. package/src/lib/providers/cli-utils.ts +15 -0
  40. package/src/lib/providers/codex-cli.ts +9 -3
  41. package/src/lib/providers/gemini-cli.ts +6 -2
  42. package/src/lib/providers/index.ts +4 -1
  43. package/src/lib/providers/ollama.ts +5 -2
  44. package/src/lib/providers/openai.ts +8 -5
  45. package/src/lib/providers/opencode-cli.ts +6 -2
  46. package/src/lib/server/agents/agent-registry.ts +20 -3
  47. package/src/lib/server/agents/main-agent-loop.ts +4 -3
  48. package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
  49. package/src/lib/server/chat-execution/chat-execution.ts +14 -2
  50. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
  51. package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
  52. package/src/lib/server/chat-execution/message-classifier.ts +5 -2
  53. package/src/lib/server/chat-execution/post-stream-finalization.ts +4 -1
  54. package/src/lib/server/chat-execution/prompt-builder.ts +11 -1
  55. package/src/lib/server/chat-execution/prompt-sections.ts +52 -9
  56. package/src/lib/server/chat-execution/response-completeness.ts +5 -2
  57. package/src/lib/server/chat-execution/stream-agent-chat.ts +42 -12
  58. package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
  59. package/src/lib/server/connectors/bluebubbles.ts +7 -4
  60. package/src/lib/server/connectors/connector-inbound.ts +16 -13
  61. package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
  62. package/src/lib/server/connectors/connector-outbound.ts +6 -3
  63. package/src/lib/server/connectors/discord.ts +10 -7
  64. package/src/lib/server/connectors/email.ts +17 -14
  65. package/src/lib/server/connectors/googlechat.ts +7 -4
  66. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
  67. package/src/lib/server/connectors/matrix.ts +6 -3
  68. package/src/lib/server/connectors/openclaw.ts +20 -17
  69. package/src/lib/server/connectors/outbox.ts +4 -1
  70. package/src/lib/server/connectors/runtime-state.ts +19 -0
  71. package/src/lib/server/connectors/session-consolidation.ts +5 -2
  72. package/src/lib/server/connectors/signal.ts +9 -6
  73. package/src/lib/server/connectors/slack.ts +13 -10
  74. package/src/lib/server/connectors/teams.ts +8 -5
  75. package/src/lib/server/connectors/telegram.ts +15 -12
  76. package/src/lib/server/connectors/whatsapp.ts +32 -29
  77. package/src/lib/server/embeddings.ts +4 -1
  78. package/src/lib/server/link-understanding.ts +4 -1
  79. package/src/lib/server/memory/memory-abstract.ts +59 -0
  80. package/src/lib/server/memory/memory-db.ts +40 -14
  81. package/src/lib/server/missions/mission-service.ts +6 -3
  82. package/src/lib/server/openclaw/gateway.ts +8 -5
  83. package/src/lib/server/project-utils.ts +13 -0
  84. package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
  85. package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
  86. package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
  87. package/src/lib/server/provider-health.ts +18 -0
  88. package/src/lib/server/query-expansion.ts +4 -1
  89. package/src/lib/server/runtime/alert-dispatch.ts +7 -6
  90. package/src/lib/server/runtime/daemon-state.ts +189 -50
  91. package/src/lib/server/runtime/heartbeat-service.ts +23 -0
  92. package/src/lib/server/runtime/idle-window.ts +4 -1
  93. package/src/lib/server/runtime/perf.ts +4 -1
  94. package/src/lib/server/runtime/process-manager.ts +7 -4
  95. package/src/lib/server/runtime/queue.ts +31 -28
  96. package/src/lib/server/runtime/scheduler.ts +9 -6
  97. package/src/lib/server/runtime/session-run-manager.ts +3 -0
  98. package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
  99. package/src/lib/server/sandbox/novnc-auth.ts +10 -0
  100. package/src/lib/server/session-tools/context.ts +14 -0
  101. package/src/lib/server/session-tools/discovery.ts +9 -6
  102. package/src/lib/server/session-tools/index.ts +3 -1
  103. package/src/lib/server/session-tools/platform.ts +1 -1
  104. package/src/lib/server/session-tools/subagent.ts +23 -2
  105. package/src/lib/server/session-tools/wallet.ts +4 -1
  106. package/src/lib/server/skills/clawhub-client.ts +4 -1
  107. package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
  108. package/src/lib/server/skills/skill-eligibility.ts +6 -0
  109. package/src/lib/server/solana.ts +6 -0
  110. package/src/lib/server/storage-auth.ts +5 -5
  111. package/src/lib/server/storage-normalization.ts +4 -0
  112. package/src/lib/server/storage.ts +19 -8
  113. package/src/lib/server/tasks/task-followups.ts +4 -1
  114. package/src/lib/server/tool-loop-detection.ts +8 -3
  115. package/src/lib/server/tool-planning.ts +226 -0
  116. package/src/lib/server/tool-retry.ts +4 -3
  117. package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
  118. package/src/lib/server/ws-hub.ts +5 -2
  119. package/src/lib/strip-internal-metadata.test.ts +44 -4
  120. package/src/lib/strip-internal-metadata.ts +20 -6
  121. package/src/stores/use-approval-store.ts +7 -1
  122. package/src/stores/use-chat-store.ts +5 -1
  123. package/src/types/index.ts +6 -0
@@ -1157,3 +1157,26 @@ export function pruneHeartbeatState(liveSessionIds: Set<string>): number {
1157
1157
  }
1158
1158
  return removed
1159
1159
  }
1160
+
1161
+ /**
1162
+ * Remove orchestrator tracking entries for agents that no longer exist.
1163
+ * Called periodically by the daemon health sweep.
1164
+ */
1165
+ export function pruneOrchestratorState(liveAgentIds: Set<string>): number {
1166
+ let removed = 0
1167
+ for (const agentId of orchestratorState.lastWakeByAgent.keys()) {
1168
+ if (!liveAgentIds.has(agentId)) { orchestratorState.lastWakeByAgent.delete(agentId); removed++ }
1169
+ }
1170
+ for (const agentId of orchestratorState.failures.keys()) {
1171
+ if (!liveAgentIds.has(agentId)) { orchestratorState.failures.delete(agentId); removed++ }
1172
+ }
1173
+ const todayStr = new Date().toISOString().slice(0, 10)
1174
+ for (const key of orchestratorState.dailyCycles.keys()) {
1175
+ const agentId = key.slice(0, key.lastIndexOf(':'))
1176
+ if (!liveAgentIds.has(agentId) || key.slice(key.lastIndexOf(':') + 1) !== todayStr) {
1177
+ orchestratorState.dailyCycles.delete(key)
1178
+ removed++
1179
+ }
1180
+ }
1181
+ return removed
1182
+ }
@@ -1,5 +1,8 @@
1
1
  import { loadSessions } from '@/lib/server/storage'
2
2
  import type { Session } from '@/types'
3
+ import { log } from '@/lib/server/logger'
4
+
5
+ const TAG = 'idle-window'
3
6
 
4
7
  const DEFAULT_IDLE_THRESHOLD_MS = 120_000 // 2 minutes
5
8
  const DAILY_GUARANTEE_MS = 24 * 60 * 60 * 1000
@@ -73,7 +76,7 @@ export async function drainIdleWindowCallbacks(): Promise<void> {
73
76
  try {
74
77
  await cb()
75
78
  } catch (err) {
76
- console.warn('[idle-window] Callback failed:', err instanceof Error ? err.message : String(err))
79
+ log.warn(TAG, 'Callback failed:', err instanceof Error ? err.message : String(err))
77
80
  }
78
81
  }
79
82
  }
@@ -15,6 +15,9 @@
15
15
  */
16
16
 
17
17
  import { hmrSingleton } from '@/lib/shared-utils'
18
+ import { log } from '@/lib/server/logger'
19
+
20
+ const TAG = 'perf'
18
21
 
19
22
  interface PerfEntry {
20
23
  category: string
@@ -44,7 +47,7 @@ function emitEntry(entry: PerfEntry): void {
44
47
  const metaStr = entry.meta && Object.keys(entry.meta).length > 0
45
48
  ? ' ' + JSON.stringify(entry.meta)
46
49
  : ''
47
- console.log(`[perf] ${entry.category}/${entry.label} ${entry.durationMs}ms${metaStr}`)
50
+ log.info(TAG, `${entry.category}/${entry.label} ${entry.durationMs}ms${metaStr}`)
48
51
  }
49
52
 
50
53
  const _noopEnd = () => 0
@@ -2,6 +2,9 @@ import { genId } from '@/lib/id'
2
2
  import { hmrSingleton, sleep } from '@/lib/shared-utils'
3
3
  import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
4
4
  import { detectDocker } from '@/lib/server/sandbox/docker-detect'
5
+ import { log } from '@/lib/server/logger'
6
+
7
+ const TAG = 'process-manager'
5
8
 
6
9
  const MAX_LOG_CHARS = 200_000
7
10
  const DEFAULT_BACKGROUND_YIELD_MS = 10_000
@@ -127,9 +130,9 @@ function cleanupSandboxContainer(containerName: string) {
127
130
  if (!detectDocker().available) return
128
131
  try {
129
132
  const child = spawn('docker', ['rm', '-f', containerName], { stdio: 'ignore', detached: true })
130
- child.on('error', (err) => { console.warn(`[process-manager] Docker cleanup error for ${containerName}:`, err.message) })
133
+ child.on('error', (err) => { log.warn(TAG, `Docker cleanup error for ${containerName}:`, err.message) })
131
134
  child.unref()
132
- } catch (err: unknown) { console.warn(`[process-manager] Docker cleanup spawn failed for ${containerName}:`, err instanceof Error ? err.message : String(err)) }
135
+ } catch (err: unknown) { log.warn(TAG, `Docker cleanup spawn failed for ${containerName}:`, err instanceof Error ? err.message : String(err)) }
133
136
  }
134
137
 
135
138
  function normalizeLines(text: string): string[] {
@@ -264,7 +267,7 @@ export async function startManagedProcess(opts: StartProcessOptions): Promise<St
264
267
  try { child.kill('SIGTERM') } catch { /* noop */ }
265
268
  const escalationTimer = setTimeout(() => {
266
269
  if (!state.children.has(id)) return
267
- console.warn(`[process-manager] Process ${id} (pid=${rec.pid}) did not exit after SIGTERM, sending SIGKILL`)
270
+ log.warn(TAG, `Process ${id} (pid=${rec.pid}) did not exit after SIGTERM, sending SIGKILL`)
268
271
  appendLog(id, '\n[process] SIGKILL escalation — process ignored SIGTERM.\n')
269
272
  try { child.kill('SIGKILL') } catch { /* noop */ }
270
273
  }, SIGKILL_ESCALATION_MS)
@@ -491,7 +494,7 @@ export async function reapOrphanedSandboxContainers(): Promise<number> {
491
494
  (r) => r.sandboxContainerName === name && r.status === 'running',
492
495
  )
493
496
  if (!isTracked) {
494
- console.warn(`[process-manager] Reaping orphaned sandbox container: ${name}`)
497
+ log.warn(TAG, `Reaping orphaned sandbox container: ${name}`)
495
498
  cleanupSandboxContainer(name)
496
499
  reaped++
497
500
  }
@@ -1,3 +1,4 @@
1
+ import { log } from '@/lib/server/logger'
1
2
  import { matchesCapabilities, filterAgentsByCapabilities, capabilityMatchScore } from '@/lib/server/agents/capability-match'
2
3
  import { genId } from '@/lib/id'
3
4
  import { dedup, hmrSingleton, jitteredBackoff } from '@/lib/shared-utils'
@@ -44,6 +45,8 @@ import {
44
45
  } from '@/lib/server/tasks/task-lifecycle'
45
46
  import { noteMissionTaskFinished, noteMissionTaskStarted } from '@/lib/server/missions/mission-service'
46
47
 
48
+ const TAG = 'queue'
49
+
47
50
  export const collectTaskConnectorFollowupTargets = collectTaskConnectorFollowupTargetsImpl
48
51
  export const resolveTaskOriginConnectorFollowupTarget = resolveTaskOriginConnectorFollowupTargetImpl
49
52
 
@@ -649,7 +652,7 @@ function queueTaskAutonomyObservation(input: {
649
652
  toolEvents: input.toolEvents,
650
653
  sourceMessage: input.sourceMessage,
651
654
  }).catch((err: unknown) => {
652
- console.warn(`[queue] Autonomy observation failed for ${input.runId}:`, err)
655
+ log.warn(TAG, `[queue] Autonomy observation failed for ${input.runId}:`, err)
653
656
  })
654
657
  }
655
658
 
@@ -739,7 +742,7 @@ async function executeTaskRun(
739
742
  try {
740
743
  const followupBudget = checkAgentBudgetLimits(typedAgentForBudget)
741
744
  if (!followupBudget.ok) {
742
- console.warn(`[queue] Budget exceeded for "${typedAgentForBudget.name}" during follow-up, stopping.`)
745
+ log.warn(TAG, `[queue] Budget exceeded for "${typedAgentForBudget.name}" during follow-up, stopping.`)
743
746
  break
744
747
  }
745
748
  } catch {}
@@ -967,7 +970,7 @@ export function disableSessionHeartbeat(sessionId: string | null | undefined) {
967
970
  session.heartbeatEnabled = false
968
971
  session.lastActiveAt = Date.now()
969
972
  saveSessions(sessions)
970
- console.log(`[queue] Disabled heartbeat on session ${sessionId} (task finished)`)
973
+ log.info(TAG, `[queue] Disabled heartbeat on session ${sessionId} (task finished)`)
971
974
  }
972
975
 
973
976
  export function enqueueTask(taskId: string) {
@@ -1062,7 +1065,7 @@ export function validateCompletedTasksQueue() {
1062
1065
  if (tasksDirty) { saveTasks(tasks); notify('tasks') }
1063
1066
  if (sessionsDirty) saveSessions(sessions)
1064
1067
  if (demoted > 0) {
1065
- console.warn(`[queue] Demoted ${demoted} invalid completed task(s) to failed after validation audit`)
1068
+ log.warn(TAG, `[queue] Demoted ${demoted} invalid completed task(s) to failed after validation audit`)
1066
1069
  }
1067
1070
  return { checked, demoted }
1068
1071
  }
@@ -1208,7 +1211,7 @@ export async function processNext() {
1208
1211
  let recovered = false
1209
1212
  for (const [id, t] of Object.entries(allTasks) as [string, BoardTask][]) {
1210
1213
  if (t.status === 'queued' && !queueSet.has(id)) {
1211
- console.log(`[queue] Recovering orphaned queued task: "${t.title}" (${id})`)
1214
+ log.info(TAG, `[queue] Recovering orphaned queued task: "${t.title}" (${id})`)
1212
1215
  pushQueueUnique(currentQueue, id)
1213
1216
  recovered = true
1214
1217
  }
@@ -1243,7 +1246,7 @@ export async function processNext() {
1243
1246
  // Put it back in the queue and skip
1244
1247
  pushQueueUnique(queue, taskId)
1245
1248
  saveQueue(queue)
1246
- console.log(`[queue] Skipping task "${task.title}" (${taskId}) — blocked by incomplete dependencies`)
1249
+ log.info(TAG, `[queue] Skipping task "${task.title}" (${taskId}) — blocked by incomplete dependencies`)
1247
1250
  return
1248
1251
  }
1249
1252
  }
@@ -1277,7 +1280,7 @@ export async function processNext() {
1277
1280
  return a.name.localeCompare(b.name)
1278
1281
  })
1279
1282
  const rerouted = candidates[0]
1280
- console.log(`[queue] Rerouting task "${task.title}" (${taskId}) from agent "${agent.name}" to "${rerouted.name}" — capability match`)
1283
+ log.info(TAG, `[queue] Rerouting task "${task.title}" (${taskId}) from agent "${agent.name}" to "${rerouted.name}" — capability match`)
1281
1284
  task.agentId = rerouted.id
1282
1285
  agent = rerouted
1283
1286
  } else {
@@ -1473,7 +1476,7 @@ export async function processNext() {
1473
1476
  saveSessions(sessions)
1474
1477
  }
1475
1478
 
1476
- console.log(`[queue] Running task "${task.title}" (${taskId}) with ${agent.name}`)
1479
+ log.info(TAG, `[queue] Running task "${task.title}" (${taskId}) with ${agent.name}`)
1477
1480
 
1478
1481
  try {
1479
1482
  const taskRunId = `${taskId}:attempt-${(task.attempts || 0) + 1}`
@@ -1508,7 +1511,7 @@ export async function processNext() {
1508
1511
  toolEvents: taskRun.toolEvents,
1509
1512
  sourceMessage: task.description || task.title,
1510
1513
  })
1511
- console.warn(`[queue] Task "${task.title}" cancelled during execution`)
1514
+ log.warn(TAG, `[queue] Task "${task.title}" cancelled during execution`)
1512
1515
  return
1513
1516
  }
1514
1517
  if (t2[taskId]) {
@@ -1596,10 +1599,10 @@ export async function processNext() {
1596
1599
  else if (opencodeId) t2[taskId].cliProvider = 'opencode-cli'
1597
1600
  else if (geminiId) t2[taskId].cliProvider = 'gemini-cli'
1598
1601
  }
1599
- console.log(`[queue] CLI resume IDs for task ${taskId}: claude=${claudeId}, codex=${codexId}, opencode=${opencodeId}, gemini=${geminiId}`)
1602
+ log.info(TAG, `[queue] CLI resume IDs for task ${taskId}: claude=${claudeId}, codex=${codexId}, opencode=${opencodeId}, gemini=${geminiId}`)
1600
1603
  }
1601
1604
  } catch (e) {
1602
- console.warn(`[queue] Failed to extract CLI resume IDs for task ${taskId}:`, e)
1605
+ log.warn(TAG, `[queue] Failed to extract CLI resume IDs for task ${taskId}:`, e)
1603
1606
  }
1604
1607
 
1605
1608
  saveTasks(t2)
@@ -1640,7 +1643,7 @@ export async function processNext() {
1640
1643
  cleanupTerminalOneOffSchedule(doneTask)
1641
1644
  // Clean up LangGraph checkpoints for completed tasks
1642
1645
  getCheckpointSaver().deleteThread(taskId).catch((e) =>
1643
- console.warn(`[queue] Failed to clean up checkpoints for task ${taskId}:`, e)
1646
+ log.warn(TAG, `[queue] Failed to clean up checkpoints for task ${taskId}:`, e)
1644
1647
  )
1645
1648
  // Cascade unblock: auto-queue tasks whose blockers are all done
1646
1649
  const latestTasks = loadTasks()
@@ -1649,7 +1652,7 @@ export async function processNext() {
1649
1652
  saveTasks(latestTasks)
1650
1653
  for (const uid of unblockedIds) {
1651
1654
  enqueueTask(uid)
1652
- console.log(`[queue] Auto-unblocked task "${latestTasks[uid]?.title}" (${uid})`)
1655
+ log.info(TAG, `[queue] Auto-unblocked task "${latestTasks[uid]?.title}" (${uid})`)
1653
1656
  }
1654
1657
  notify('tasks')
1655
1658
  }
@@ -1659,15 +1662,15 @@ export async function processNext() {
1659
1662
  const { wakeProtocolRunFromTaskCompletion } = await import('@/lib/server/protocols/protocol-service')
1660
1663
  wakeProtocolRunFromTaskCompletion(taskId)
1661
1664
  } catch (e) {
1662
- console.warn(`[queue] Failed to wake protocol run for task ${taskId}:`, e)
1665
+ log.warn(TAG, `[queue] Failed to wake protocol run for task ${taskId}:`, e)
1663
1666
  }
1664
1667
  }
1665
- console.log(`[queue] Task "${task.title}" completed`)
1668
+ log.info(TAG, `[queue] Task "${task.title}" completed`)
1666
1669
  } else if (doneTask?.status === 'cancelled') {
1667
- console.warn(`[queue] Task "${task.title}" cancelled during execution`)
1670
+ log.warn(TAG, `[queue] Task "${task.title}" cancelled during execution`)
1668
1671
  } else {
1669
1672
  if (doneTask?.status === 'queued') {
1670
- console.warn(`[queue] Task "${task.title}" scheduled for retry`)
1673
+ log.warn(TAG, `[queue] Task "${task.title}" scheduled for retry`)
1671
1674
  } else {
1672
1675
  pushMainLoopEventToMainSessions({
1673
1676
  type: 'task_failed',
@@ -1678,12 +1681,12 @@ export async function processNext() {
1678
1681
  handleTerminalTaskResultDeliveries(doneTask)
1679
1682
  cleanupTerminalOneOffSchedule(doneTask)
1680
1683
  }
1681
- console.warn(`[queue] Task "${task.title}" failed completion validation`)
1684
+ log.warn(TAG, `[queue] Task "${task.title}" failed completion validation`)
1682
1685
  }
1683
1686
  }
1684
1687
  } catch (err: unknown) {
1685
1688
  const errMsg = err instanceof Error ? err.message : String(err || 'Unknown error')
1686
- console.error(`[queue] Task "${task.title}" failed:`, errMsg)
1689
+ log.error(TAG, `[queue] Task "${task.title}" failed:`, errMsg)
1687
1690
  const taskRunId = `${taskId}:attempt-${(task.attempts || 0) + 1}`
1688
1691
  const t2 = loadTasks()
1689
1692
  if (isCancelledTask(t2[taskId])) {
@@ -1699,7 +1702,7 @@ export async function processNext() {
1699
1702
  error: t2[taskId].error || errMsg,
1700
1703
  sourceMessage: task.description || task.title,
1701
1704
  })
1702
- console.warn(`[queue] Task "${task.title}" aborted because it was cancelled`)
1705
+ log.warn(TAG, `[queue] Task "${task.title}" aborted because it was cancelled`)
1703
1706
  return
1704
1707
  }
1705
1708
  if (t2[taskId]) {
@@ -1720,9 +1723,9 @@ export async function processNext() {
1720
1723
  source: 'task-repair',
1721
1724
  runId: repairRunId,
1722
1725
  })
1723
- console.log(`[queue] Repair turn completed for task "${task.title}" (${taskId})`)
1726
+ log.info(TAG, `[queue] Repair turn completed for task "${task.title}" (${taskId})`)
1724
1727
  } catch (repairErr: unknown) {
1725
- console.warn(`[queue] Repair turn failed for task "${task.title}":`, repairErr instanceof Error ? repairErr.message : String(repairErr))
1728
+ log.warn(TAG, `[queue] Repair turn failed for task "${task.title}":`, repairErr instanceof Error ? repairErr.message : String(repairErr))
1726
1729
  // If repair fails, attempt guardian recovery
1727
1730
  const taskCwd = t2[taskId].cwd || WORKSPACE_DIR
1728
1731
  prepareGuardianRecovery({
@@ -1784,9 +1787,9 @@ export async function processNext() {
1784
1787
  })
1785
1788
  const latest = loadTasks()[taskId] as BoardTask | undefined
1786
1789
  if (latest?.status === 'queued') {
1787
- console.warn(`[queue] Task "${task.title}" queued for retry after error`)
1790
+ log.warn(TAG, `[queue] Task "${task.title}" queued for retry after error`)
1788
1791
  } else if (latest?.status === 'cancelled') {
1789
- console.warn(`[queue] Task "${task.title}" stayed cancelled after abort`)
1792
+ log.warn(TAG, `[queue] Task "${task.title}" stayed cancelled after abort`)
1790
1793
  } else {
1791
1794
  pushMainLoopEventToMainSessions({
1792
1795
  type: 'task_failed',
@@ -1828,7 +1831,7 @@ export function cleanupFinishedTaskSessions() {
1828
1831
  }
1829
1832
  if (cleaned > 0) {
1830
1833
  saveSessions(sessions)
1831
- console.log(`[queue] Disabled heartbeat on ${cleaned} session(s) with finished tasks`)
1834
+ log.info(TAG, `[queue] Disabled heartbeat on ${cleaned} session(s) with finished tasks`)
1832
1835
  }
1833
1836
  }
1834
1837
 
@@ -1976,7 +1979,7 @@ export function resumeQueue() {
1976
1979
  for (const task of Object.values(tasks) as BoardTask[]) {
1977
1980
  if (task.status === 'queued' && !queue.includes(task.id)) {
1978
1981
  applyTaskPolicyDefaults(task)
1979
- console.log(`[queue] Recovering stuck queued task: "${task.title}" (${task.id})`)
1982
+ log.info(TAG, `[queue] Recovering stuck queued task: "${task.title}" (${task.id})`)
1980
1983
  queue.push(task.id)
1981
1984
  task.queuedAt = task.queuedAt || Date.now()
1982
1985
  modified = true
@@ -2004,7 +2007,7 @@ export function resumeQueue() {
2004
2007
  recovered++
2005
2008
  }
2006
2009
  if (recovered > 0) {
2007
- console.log(`[queue] Recovered ${recovered} orphaned running task(s) on boot`)
2010
+ log.info(TAG, `[queue] Recovered ${recovered} orphaned running task(s) on boot`)
2008
2011
  }
2009
2012
 
2010
2013
  if (modified) {
@@ -2013,7 +2016,7 @@ export function resumeQueue() {
2013
2016
  }
2014
2017
 
2015
2018
  if (queue.length > 0) {
2016
- console.log(`[queue] Resuming ${queue.length} queued task(s) on boot`)
2019
+ log.info(TAG, `[queue] Resuming ${queue.length} queued task(s) on boot`)
2017
2020
  processNext()
2018
2021
  }
2019
2022
  }
@@ -11,8 +11,11 @@ import { ensureAgentThreadSession } from '@/lib/server/agents/agent-thread-sessi
11
11
  import { ensureMissionForTask, noteScheduleMissionTriggered } from '@/lib/server/missions/mission-service'
12
12
  import { hasActiveProtocolRunForSchedule, launchProtocolRunForSchedule } from '@/lib/server/protocols/protocol-service'
13
13
  import { hmrSingleton } from '@/lib/shared-utils'
14
+ import { log } from '@/lib/server/logger'
14
15
  import type { Schedule } from '@/types'
15
16
 
17
+ const TAG = 'scheduler'
18
+
16
19
  const TICK_INTERVAL = 60_000 // 60 seconds
17
20
  const schedulerState = hmrSingleton('__swarmclaw_scheduler_state__', () => ({
18
21
  intervalId: null as ReturnType<typeof setInterval> | null,
@@ -45,7 +48,7 @@ function shouldLaunchScheduleProtocol(schedule: Schedule): boolean {
45
48
 
46
49
  export function startScheduler() {
47
50
  if (schedulerState.intervalId) return
48
- console.log('[scheduler] Starting scheduler engine (60s tick)')
51
+ log.info(TAG, 'Starting scheduler engine (60s tick)')
49
52
 
50
53
  // Compute initial nextRunAt for cron schedules missing it
51
54
  computeNextRuns()
@@ -57,7 +60,7 @@ export function stopScheduler() {
57
60
  if (schedulerState.intervalId) {
58
61
  clearInterval(schedulerState.intervalId)
59
62
  schedulerState.intervalId = null
60
- console.log('[scheduler] Stopped scheduler engine')
63
+ log.info(TAG, 'Stopped scheduler engine')
61
64
  }
62
65
  }
63
66
 
@@ -75,7 +78,7 @@ function computeNextRuns() {
75
78
  schedule.nextRunAt = interval.next().getTime()
76
79
  changedEntries.push([schedule.id, schedule])
77
80
  } catch (err) {
78
- console.error(`[scheduler] Invalid cron for ${schedule.id}:`, err)
81
+ log.error(TAG, `Invalid cron for ${schedule.id}:`, err)
79
82
  schedule.status = 'failed'
80
83
  changedEntries.push([schedule.id, schedule])
81
84
  }
@@ -133,7 +136,7 @@ async function tick(now = Date.now()) {
133
136
 
134
137
  const agent = agents[schedule.agentId]
135
138
  if (!agent) {
136
- console.error(`[scheduler] Agent ${schedule.agentId} not found for schedule ${schedule.id}`)
139
+ log.error(TAG, `Agent ${schedule.agentId} not found for schedule ${schedule.id}`)
137
140
  schedule.status = 'failed'
138
141
  upsertSchedule(schedule.id, schedule)
139
142
  pushMainLoopEventToMainSessions({
@@ -143,7 +146,7 @@ async function tick(now = Date.now()) {
143
146
  continue
144
147
  }
145
148
  if (isAgentDisabled(agent)) {
146
- console.warn(`[scheduler] Skipping schedule "${schedule.name}" (${schedule.id}) because agent ${schedule.agentId} is disabled`)
149
+ log.warn(TAG, `Skipping schedule "${schedule.name}" (${schedule.id}) because agent ${schedule.agentId} is disabled`)
147
150
  advanceSchedule(schedule)
148
151
  upsertSchedule(schedule.id, schedule)
149
152
  pushMainLoopEventToMainSessions({
@@ -153,7 +156,7 @@ async function tick(now = Date.now()) {
153
156
  continue
154
157
  }
155
158
 
156
- console.log(`[scheduler] Firing schedule "${schedule.name}" (${schedule.id})`)
159
+ log.info(TAG, `Firing schedule "${schedule.name}" (${schedule.id})`)
157
160
  schedule.lastRunAt = now
158
161
  schedule.runNumber = (schedule.runNumber || 0) + 1
159
162
  // Compute next run
@@ -818,6 +818,9 @@ async function drainExecution(executionKey: string): Promise<void> {
818
818
  next.run.missionId = result.missionId || next.run.missionId || null
819
819
  finishedMissionId = next.run.missionId || null
820
820
  next.run.resultPreview = result.text?.slice(0, 280)
821
+ if (typeof result.inputTokens === 'number') next.run.totalInputTokens = result.inputTokens
822
+ if (typeof result.outputTokens === 'number') next.run.totalOutputTokens = result.outputTokens
823
+ if (typeof result.estimatedCost === 'number') next.run.estimatedCost = result.estimatedCost
821
824
  syncRunRecord(next.run)
822
825
  emitRunMeta(next, next.run.status, {
823
826
  persisted: result.persisted,
@@ -3,10 +3,16 @@ type BridgeAuth = {
3
3
  password?: string
4
4
  }
5
5
 
6
+ const AUTH_BY_PORT_MAX = 200
6
7
  const authByPort = new Map<number, BridgeAuth>()
7
8
 
8
9
  export function setBridgeAuthForPort(port: number, auth: BridgeAuth): void {
9
10
  if (!Number.isFinite(port) || port <= 0) return
11
+ // FIFO eviction at cap
12
+ if (!authByPort.has(port) && authByPort.size >= AUTH_BY_PORT_MAX) {
13
+ const firstKey = authByPort.keys().next().value
14
+ if (firstKey !== undefined) authByPort.delete(firstKey)
15
+ }
10
16
  const token = typeof auth.token === 'string' ? auth.token.trim() : ''
11
17
  const password = typeof auth.password === 'string' ? auth.password.trim() : ''
12
18
  authByPort.set(port, {
@@ -15,12 +15,22 @@ export type NoVncObserverTokenPayload = {
15
15
  password?: string
16
16
  }
17
17
 
18
+ const NOVNC_TOKEN_MAX = 500
18
19
  const noVncObserverTokens = new Map<string, NoVncObserverTokenEntry>()
19
20
 
20
21
  function pruneExpiredObserverTokens(now: number): void {
21
22
  for (const [token, entry] of noVncObserverTokens) {
22
23
  if (entry.expiresAt <= now) noVncObserverTokens.delete(token)
23
24
  }
25
+ // Hard cap as safety net
26
+ if (noVncObserverTokens.size > NOVNC_TOKEN_MAX) {
27
+ const excess = noVncObserverTokens.size - NOVNC_TOKEN_MAX
28
+ const iter = noVncObserverTokens.keys()
29
+ for (let i = 0; i < excess; i++) {
30
+ const k = iter.next().value
31
+ if (k !== undefined) noVncObserverTokens.delete(k)
32
+ }
33
+ }
24
34
  }
25
35
 
26
36
  export function isNoVncEnabled(params: { enableNoVnc: boolean; headless: boolean }): boolean {
@@ -164,6 +164,7 @@ export function extractResumeIdentifier(text: string): string | null {
164
164
  return null
165
165
  }
166
166
 
167
+ const BINARY_LOOKUP_CACHE_MAX = 100
167
168
  const binaryLookupCache = new Map<string, { checkedAt: number; path: string | null }>()
168
169
  const BINARY_LOOKUP_TTL_MS = 30_000
169
170
  const isWindows = process.platform === 'win32'
@@ -173,6 +174,19 @@ export function findBinaryOnPath(binaryName: string): string | null {
173
174
  const cached = binaryLookupCache.get(binaryName)
174
175
  if (cached && now - cached.checkedAt < BINARY_LOOKUP_TTL_MS) return cached.path
175
176
 
177
+ // Prune expired + cap
178
+ for (const [k, v] of binaryLookupCache) {
179
+ if (now - v.checkedAt > BINARY_LOOKUP_TTL_MS) binaryLookupCache.delete(k)
180
+ }
181
+ if (binaryLookupCache.size > BINARY_LOOKUP_CACHE_MAX) {
182
+ const excess = binaryLookupCache.size - BINARY_LOOKUP_CACHE_MAX
183
+ const iter = binaryLookupCache.keys()
184
+ for (let i = 0; i < excess; i++) {
185
+ const k = iter.next().value
186
+ if (k !== undefined) binaryLookupCache.delete(k)
187
+ }
188
+ }
189
+
176
190
  const { spawnSync } = require('child_process')
177
191
  const probe = isWindows
178
192
  ? spawnSync('where', [binaryName], { encoding: 'utf-8', timeout: 2000, stdio: 'pipe' })
@@ -11,6 +11,9 @@ import { loadSessions, patchAgent, patchSession } from '../storage'
11
11
  import { inferExtensionPublisherSourceFromUrl } from '@/lib/extension-sources'
12
12
  import { errorMessage } from '@/lib/shared-utils'
13
13
  import { getEnabledCapabilityIds, isExternalExtensionId, normalizeCapabilitySelection } from '@/lib/capability-selection'
14
+ import { log } from '@/lib/server/logger'
15
+
16
+ const TAG = 'session-tools-discovery'
14
17
 
15
18
  function grantCapabilitySelection(current: {
16
19
  tools?: string[] | null
@@ -62,7 +65,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
62
65
  const q = typeof normalized.query === 'string' ? normalized.query : ''
63
66
  const extensionId = explicitExtensionId || (action === 'request_access' ? q.trim() : '')
64
67
 
65
- console.log('[discovery] Executing action:', action, { query: q, extensionId })
68
+ log.info(TAG, 'Executing action:', { action, query: q, extensionId })
66
69
 
67
70
  try {
68
71
  switch (action) {
@@ -87,7 +90,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
87
90
  const results: Record<string, unknown>[] = []
88
91
 
89
92
  try {
90
- console.log('[discovery] Searching ClawHub...')
93
+ log.info(TAG, 'Searching ClawHub...')
91
94
  const hubResults = await searchClawHub(q)
92
95
  if (hubResults && hubResults.skills) {
93
96
  results.push(...hubResults.skills.map((s) => ({
@@ -101,11 +104,11 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
101
104
  })))
102
105
  }
103
106
  } catch (err: unknown) {
104
- console.error('[discovery] ClawHub search failed:', errorMessage(err))
107
+ log.error(TAG, 'ClawHub search failed:', errorMessage(err))
105
108
  }
106
109
 
107
110
  try {
108
- console.log('[discovery] Searching SwarmClaw registry...')
111
+ log.info(TAG, 'Searching SwarmClaw registry...')
109
112
  const registryResults = new Map<string, Record<string, unknown>>()
110
113
  const registries = [
111
114
  { url: 'https://swarmclaw.ai/registry/extensions.json', catalogSource: 'swarmclaw-site' },
@@ -133,7 +136,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
133
136
  }
134
137
  results.push(...registryResults.values())
135
138
  } catch (err: unknown) {
136
- console.error('[discovery] SC Registry search failed:', errorMessage(err))
139
+ log.error(TAG, 'SC Registry search failed:', errorMessage(err))
137
140
  }
138
141
 
139
142
  if (results.length === 0) {
@@ -226,7 +229,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
226
229
  }
227
230
  } catch (err: unknown) {
228
231
  const msg = errorMessage(err)
229
- console.error('[discovery] executeDiscoveryAction failed:', msg)
232
+ log.error(TAG, 'executeDiscoveryAction failed:', msg)
230
233
  return `Error: ${msg}`
231
234
  }
232
235
  }
@@ -57,6 +57,8 @@ import {
57
57
  export type { ToolContext, SessionToolsResult }
58
58
  export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
59
59
 
60
+ const TAG = 'session-tools'
61
+
60
62
  const DELEGATION_TOOL_NAMES = new Set([
61
63
  'delegate',
62
64
  'spawn_subagent',
@@ -455,7 +457,7 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
455
457
  abortSignalRef,
456
458
  }
457
459
  } catch (err: any) {
458
- console.error('[session-tools] buildSessionTools critical failure:', err.message)
460
+ log.error(TAG, 'buildSessionTools critical failure:', err.message)
459
461
  throw err
460
462
  }
461
463
  }
@@ -226,7 +226,7 @@ const PlatformExtension: Extension = {
226
226
  description: 'Unified management of agents, projects, tasks, schedules, skills, documents, and secrets.',
227
227
  hooks: {
228
228
  getCapabilityDescription: () => 'I can manage durable execution context across agents, projects, tasks, schedules, documents, skills, webhooks, connectors, sessions, and encrypted secrets.',
229
- getOperatingGuidance: () => ['Use projects to hold longer-lived goals, objectives, and credential requirements.', 'Create/update tasks for long-lived goals to track progress.', 'Use schedules for follow-ups and heartbeat-style check-ins. Check existing schedules before creating new ones.', 'Inspect existing chats before creating duplicates.'],
229
+ getOperatingGuidance: () => ['Use projects to hold longer-lived goals, objectives, and credential requirements.', 'Create/update tasks for long-lived goals to track progress.', 'When work on a task is finished, update its status to completed with a concrete result summary. Cancel or archive tasks that are no longer relevant. Stale open tasks block project progress.', 'Use schedules for follow-ups and heartbeat-style check-ins. Check existing schedules before creating new ones.', 'Inspect existing chats before creating duplicates.'],
230
230
  } as ExtensionHooks,
231
231
  tools: [
232
232
  {
@@ -5,6 +5,7 @@ import type { Extension, ExtensionHooks } from '@/types'
5
5
  import { registerNativeCapability } from '../native-capabilities'
6
6
  import { normalizeToolInputArgs } from './normalize-tool-args'
7
7
  import { errorMessage, sleep } from '@/lib/shared-utils'
8
+ import { loadAgents } from '@/lib/server/storage'
8
9
  import {
9
10
  cancelDelegationJob,
10
11
  getDelegationJob,
@@ -99,7 +100,12 @@ function validateAllowedSubagentTarget(agentId: string, ctx: ActionContext): str
99
100
  ? ctx.delegationTargetAgentIds.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
100
101
  : []
101
102
  if (allowedAgentIds.length === 0 || allowedAgentIds.includes(agentId)) return null
102
- return `Error: agent "${agentId}" is not in the allowed delegate agent list.`
103
+
104
+ const agents = loadAgents()
105
+ const allowedNames = allowedAgentIds
106
+ .map(id => agents[id]?.name ? `${agents[id].name} [${id}]` : id)
107
+ .join(', ')
108
+ return `Error: agent "${agentId}" is not in your allowed delegation list. You may only delegate to: ${allowedNames}. Do not retry with this agent.`
103
109
  }
104
110
 
105
111
  function parseBooleanLike(value: unknown): boolean | unknown {
@@ -538,6 +544,21 @@ registerNativeCapability('subagent', SubagentExtension)
538
544
  */
539
545
  export function buildSubagentTools(bctx: ToolBuildContext): StructuredToolInterface[] {
540
546
  if (!bctx.ctx?.delegationEnabled || !bctx.hasExtension('spawn_subagent')) return []
547
+
548
+ let description = SubagentExtension.tools![0].description
549
+ if (bctx.ctx?.delegationTargetMode === 'selected') {
550
+ const allowedIds = (bctx.ctx.delegationTargetAgentIds || []).filter(
551
+ (id): id is string => typeof id === 'string' && id.trim().length > 0,
552
+ )
553
+ if (allowedIds.length > 0) {
554
+ const agents = loadAgents()
555
+ const allowedSummary = allowedIds
556
+ .map(id => agents[id]?.name ? `${agents[id].name} [${id}]` : id)
557
+ .join(', ')
558
+ description += ` DELEGATION RESTRICTED: You may ONLY delegate to these agents: ${allowedSummary}. Attempts to delegate to any other agent will be rejected.`
559
+ }
560
+ }
561
+
541
562
  return [
542
563
  tool(
543
564
  async (args) => executeSubagentAction(args, {
@@ -548,7 +569,7 @@ export function buildSubagentTools(bctx: ToolBuildContext): StructuredToolInterf
548
569
  }),
549
570
  {
550
571
  name: 'spawn_subagent',
551
- description: SubagentExtension.tools![0].description,
572
+ description,
552
573
  schema: subagentToolSchema
553
574
  }
554
575
  )
@@ -39,6 +39,9 @@ import {
39
39
  simulateSolanaTransaction,
40
40
  } from '../solana'
41
41
  import { TOOL_CAPABILITY } from '../tool-planning'
42
+ import { log } from '@/lib/server/logger'
43
+
44
+ const TAG = 'wallet'
42
45
  import { clearWalletPortfolioCache } from '@/lib/server/wallet/wallet-portfolio'
43
46
  import {
44
47
  createAgentWallet,
@@ -1042,7 +1045,7 @@ async function executeWalletAction(args: unknown, context: { agentId?: string |
1042
1045
  } catch (err: unknown) {
1043
1046
  const msg = errorMessage(err)
1044
1047
  if (msg.includes('429') || msg.toLowerCase().includes('too many requests')) {
1045
- console.warn('[wallet] Solana RPC rate-limited. Consider using a dedicated RPC endpoint (SOLANA_RPC_URL env var).')
1048
+ log.warn(TAG, 'Solana RPC rate-limited. Consider using a dedicated RPC endpoint (SOLANA_RPC_URL env var).')
1046
1049
  }
1047
1050
  return JSON.stringify({ error: msg })
1048
1051
  }
@@ -1,5 +1,8 @@
1
1
  import type { ClawHubSkill } from '@/types'
2
2
  import { errorMessage } from '@/lib/shared-utils'
3
+ import { log } from '@/lib/server/logger'
4
+
5
+ const TAG = 'clawhub-client'
3
6
 
4
7
  export interface ClawHubSearchResult {
5
8
  skills: ClawHubSkill[]
@@ -177,7 +180,7 @@ export async function searchClawHub(query: string, page = 1, limit = 20, cursor?
177
180
  return { skills, total, page, nextCursor: data.nextCursor }
178
181
  } catch (err: unknown) {
179
182
  const error = errorMessage(err)
180
- console.warn('[clawhub] search failed:', error)
183
+ log.warn(TAG, 'search failed:', error)
181
184
  return { skills: [], total: 0, page, error }
182
185
  }
183
186
  }