@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
@@ -29,3 +29,73 @@ export function estimateCost(model: string, inputTokens: number, outputTokens: n
29
29
  export function getModelCosts(): Record<string, [number, number]> {
30
30
  return { ...MODEL_COSTS }
31
31
  }
32
+
33
+ // --- Agent Monthly Budget ---
34
+
35
+ import { loadUsage, loadSessions } from './storage'
36
+ import type { Agent, UsageRecord } from '@/types'
37
+
38
+ /**
39
+ * Sum the estimated cost for an agent in the current calendar month.
40
+ * Usage records are keyed by sessionId; we resolve agentId through sessions.
41
+ */
42
+ export function getAgentMonthlySpend(agentId: string): number {
43
+ const sessions = loadSessions()
44
+ // Build a set of sessionIds linked to this agent
45
+ const agentSessionIds = new Set<string>()
46
+ for (const [sid, session] of Object.entries(sessions)) {
47
+ if (session?.agentId === agentId) agentSessionIds.add(sid)
48
+ }
49
+ if (agentSessionIds.size === 0) return 0
50
+
51
+ const now = new Date()
52
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).getTime()
53
+
54
+ const usage = loadUsage()
55
+ let total = 0
56
+ for (const sid of agentSessionIds) {
57
+ const records = usage[sid]
58
+ if (!Array.isArray(records)) continue
59
+ for (const record of records) {
60
+ const r = record as UsageRecord
61
+ if (typeof r.timestamp !== 'number' || r.timestamp < monthStart) continue
62
+ if (typeof r.estimatedCost === 'number' && Number.isFinite(r.estimatedCost) && r.estimatedCost > 0) {
63
+ total += r.estimatedCost
64
+ }
65
+ }
66
+ }
67
+ return total
68
+ }
69
+
70
+ export interface BudgetCheckResult {
71
+ ok: boolean
72
+ spend: number
73
+ budget: number
74
+ message?: string
75
+ }
76
+
77
+ /**
78
+ * Check whether an agent is within its monthly budget.
79
+ * Returns ok: true if no budget is set or spend is under the cap.
80
+ */
81
+ export function checkBudget(agent: Agent): BudgetCheckResult {
82
+ const budget = typeof agent.monthlyBudget === 'number' && Number.isFinite(agent.monthlyBudget) && agent.monthlyBudget > 0
83
+ ? agent.monthlyBudget
84
+ : 0
85
+
86
+ if (budget <= 0) {
87
+ return { ok: true, spend: 0, budget: 0 }
88
+ }
89
+
90
+ const spend = getAgentMonthlySpend(agent.id)
91
+ if (spend >= budget) {
92
+ return {
93
+ ok: false,
94
+ spend,
95
+ budget,
96
+ message: `Agent "${agent.name}" has reached its monthly budget: $${spend.toFixed(4)} spent of $${budget.toFixed(2)} cap.`,
97
+ }
98
+ }
99
+
100
+ return { ok: true, spend, budget }
101
+ }
@@ -1,6 +1,7 @@
1
1
  import { genId } from '@/lib/id'
2
2
  import { saveNotification, hasUnreadNotificationWithKey } from '@/lib/server/storage'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
+ import { dispatchAlert } from '@/lib/server/alert-dispatch'
4
5
  import type { AppNotification } from '@/types'
5
6
 
6
7
  /**
@@ -38,5 +39,6 @@ export function createNotification(opts: {
38
39
  }
39
40
  saveNotification(id, notification)
40
41
  notify('notifications')
42
+ dispatchAlert(notification).catch(() => {})
41
43
  return notification
42
44
  }
@@ -10,6 +10,7 @@ import {
10
10
  sendConnectorMessage,
11
11
  startConnector,
12
12
  getConnectorStatus,
13
+ checkConnectorHealth,
13
14
  } from './connectors/manager'
14
15
  import { startHeartbeatService, stopHeartbeatService, getHeartbeatServiceStatus } from './heartbeat-service'
15
16
  import { hasOpenClawAgents, ensureGatewayConnected, disconnectGateway, getGateway } from './openclaw-gateway'
@@ -82,6 +83,10 @@ const ds: {
82
83
  /** Session IDs we've already alerted as stale (alert-once semantics). */
83
84
  staleSessionIds: Set<string>
84
85
  connectorRestartState: Map<string, { lastAttemptAt: number; failCount: number; wakeAttempts: number }>
86
+ /** OpenClaw gateway agent IDs currently considered down. */
87
+ openclawDownAgentIds: Set<string>
88
+ /** Per-agent auto-repair state for OpenClaw gateways. */
89
+ openclawRepairState: Map<string, { attempts: number; lastAttemptAt: number; cooldownUntil: number }>
85
90
  manualStopRequested: boolean
86
91
  running: boolean
87
92
  lastProcessedAt: number | null
@@ -94,6 +99,8 @@ const ds: {
94
99
  memoryConsolidationIntervalId: null,
95
100
  staleSessionIds: new Set<string>(),
96
101
  connectorRestartState: new Map<string, { lastAttemptAt: number; failCount: number; wakeAttempts: number }>(),
102
+ openclawDownAgentIds: new Set<string>(),
103
+ openclawRepairState: new Map<string, { attempts: number; lastAttemptAt: number; cooldownUntil: number }>(),
97
104
  manualStopRequested: false,
98
105
  running: false,
99
106
  lastProcessedAt: null,
@@ -102,6 +109,8 @@ const ds: {
102
109
  // Backfill fields for hot-reloaded daemon state objects from older code versions.
103
110
  if (!ds.staleSessionIds) ds.staleSessionIds = new Set<string>()
104
111
  if (!ds.connectorRestartState) ds.connectorRestartState = new Map<string, { lastAttemptAt: number; failCount: number; wakeAttempts: number }>()
112
+ if (!ds.openclawDownAgentIds) ds.openclawDownAgentIds = new Set<string>()
113
+ if (!ds.openclawRepairState) ds.openclawRepairState = new Map<string, { attempts: number; lastAttemptAt: number; cooldownUntil: number }>()
105
114
  // Migrate from old issueLastAlertAt map if present (HMR across code versions)
106
115
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
116
  if ((ds as any).issueLastAlertAt) delete (ds as any).issueLastAlertAt
@@ -247,6 +256,13 @@ async function sendHealthAlert(text: string) {
247
256
  }
248
257
 
249
258
  async function runConnectorHealthChecks(now: number) {
259
+ // First, check isAlive() on running instances and attempt reconnection for dead ones
260
+ try {
261
+ await checkConnectorHealth()
262
+ } catch (err: unknown) {
263
+ console.error('[health] Connector isAlive check failed:', err instanceof Error ? err.message : String(err))
264
+ }
265
+
250
266
  const connectors = loadConnectors()
251
267
  for (const connector of Object.values(connectors) as Record<string, unknown>[]) {
252
268
  if (!connector?.id || typeof connector.id !== 'string') continue
@@ -533,6 +549,107 @@ async function runProviderHealthChecks() {
533
549
  }
534
550
  }
535
551
 
552
+ const OPENCLAW_REPAIR_MAX_ATTEMPTS = 3
553
+ const OPENCLAW_REPAIR_COOLDOWN_MS = 300_000 // 5 minutes
554
+
555
+ async function runOpenClawGatewayHealthChecks() {
556
+ const agents = loadAgents()
557
+ const credentials = loadCredentials()
558
+
559
+ // Build deduplicated OpenClaw agent tuples
560
+ const seen = new Set<string>()
561
+ const tuples: { agentId: string; endpoint: string; credentialId: string; credentialName: string }[] = []
562
+
563
+ for (const agent of Object.values(agents) as Record<string, unknown>[]) {
564
+ if (!agent?.id || typeof agent.id !== 'string') continue
565
+ if (agent.provider !== 'openclaw') continue
566
+
567
+ const key = `openclaw:${agent.id}`
568
+ if (seen.has(key)) continue
569
+ seen.add(key)
570
+
571
+ const credentialId = typeof agent.credentialId === 'string' ? agent.credentialId : ''
572
+ const endpoint = typeof agent.apiEndpoint === 'string' ? agent.apiEndpoint : ''
573
+ const cred = credentialId ? (credentials[credentialId] as Record<string, unknown> | undefined) : undefined
574
+ const credName = typeof cred?.name === 'string' ? cred.name : 'openclaw'
575
+
576
+ tuples.push({ agentId: agent.id, endpoint, credentialId, credentialName: credName })
577
+ }
578
+
579
+ if (!tuples.length) return
580
+
581
+ const { probeOpenClawHealth } = await import('./openclaw-health')
582
+
583
+ for (const tuple of tuples) {
584
+ let token: string | undefined
585
+ if (tuple.credentialId) {
586
+ const cred = credentials[tuple.credentialId] as Record<string, unknown> | undefined
587
+ if (cred?.encryptedKey && typeof cred.encryptedKey === 'string') {
588
+ try { token = decryptKey(cred.encryptedKey) } catch { continue }
589
+ }
590
+ }
591
+
592
+ const result = await probeOpenClawHealth({
593
+ endpoint: tuple.endpoint || undefined,
594
+ token,
595
+ timeoutMs: 10_000,
596
+ })
597
+
598
+ const now = Date.now()
599
+
600
+ if (result.ok) {
601
+ // Recovered
602
+ if (ds.openclawDownAgentIds.has(tuple.agentId)) {
603
+ ds.openclawDownAgentIds.delete(tuple.agentId)
604
+ ds.openclawRepairState.delete(tuple.agentId)
605
+ createNotification({
606
+ type: 'success',
607
+ title: 'OpenClaw gateway recovered',
608
+ message: `Gateway for ${tuple.credentialName} is reachable again.`,
609
+ dedupKey: `openclaw-gw-down:${tuple.agentId}`,
610
+ })
611
+ }
612
+ continue
613
+ }
614
+
615
+ // Unhealthy
616
+ const repair = ds.openclawRepairState.get(tuple.agentId) || { attempts: 0, lastAttemptAt: 0, cooldownUntil: 0 }
617
+
618
+ // In cooldown — skip
619
+ if (repair.cooldownUntil > now) continue
620
+
621
+ // Cooldown expired — reset
622
+ if (repair.cooldownUntil > 0 && repair.cooldownUntil <= now) {
623
+ repair.attempts = 0
624
+ repair.cooldownUntil = 0
625
+ }
626
+
627
+ ds.openclawDownAgentIds.add(tuple.agentId)
628
+
629
+ if (repair.attempts < OPENCLAW_REPAIR_MAX_ATTEMPTS) {
630
+ try {
631
+ const { runOpenClawDoctor } = await import('./openclaw-doctor')
632
+ await runOpenClawDoctor({ fix: true })
633
+ } catch (err: unknown) {
634
+ console.warn('[daemon] openclaw doctor --fix failed:', err instanceof Error ? err.message : String(err))
635
+ }
636
+ repair.attempts += 1
637
+ repair.lastAttemptAt = now
638
+ } else {
639
+ repair.cooldownUntil = now + OPENCLAW_REPAIR_COOLDOWN_MS
640
+ }
641
+
642
+ ds.openclawRepairState.set(tuple.agentId, repair)
643
+
644
+ createNotification({
645
+ type: 'error',
646
+ title: `OpenClaw gateway unreachable: ${tuple.credentialName}`,
647
+ message: result.error || 'Health check failed',
648
+ dedupKey: `openclaw-gw-down:${tuple.agentId}`,
649
+ })
650
+ }
651
+ }
652
+
536
653
  async function runHealthChecks() {
537
654
  // Continuously keep the completed queue honest.
538
655
  validateCompletedTasksQueue()
@@ -601,6 +718,13 @@ async function runHealthChecks() {
601
718
  console.error('[daemon] Provider health check failed:', err instanceof Error ? err.message : String(err))
602
719
  }
603
720
 
721
+ // OpenClaw gateway health checks + auto-repair
722
+ try {
723
+ await runOpenClawGatewayHealthChecks()
724
+ } catch (err: unknown) {
725
+ console.error('[daemon] OpenClaw gateway health check failed:', err instanceof Error ? err.message : String(err))
726
+ }
727
+
604
728
  // Process webhook retry queue
605
729
  try {
606
730
  await processWebhookRetries()
@@ -0,0 +1,115 @@
1
+ import type { BoardTask } from '@/types'
2
+
3
+ interface DagResult {
4
+ valid: boolean
5
+ cycle?: string[]
6
+ }
7
+
8
+ /**
9
+ * Validate that adding `proposedBlockedBy` to `taskId` would not create a cycle
10
+ * in the task dependency graph. Uses DFS to check if `taskId` is reachable from
11
+ * any of its proposed blockers (which would mean a cycle).
12
+ */
13
+ export function validateDag(
14
+ tasks: Record<string, BoardTask>,
15
+ taskId: string,
16
+ proposedBlockedBy: string[],
17
+ ): DagResult {
18
+ // Build adjacency: task -> tasks it is blocked by (its dependencies)
19
+ // We temporarily add the proposed edges: taskId is blocked by proposedBlockedBy
20
+ // A cycle exists if we can reach taskId by following blockedBy edges from any
21
+ // of the proposed blockers.
22
+
23
+ // DFS from each proposed blocker, following existing blockedBy edges.
24
+ // If we reach taskId, we have a cycle.
25
+ for (const startId of proposedBlockedBy) {
26
+ if (startId === taskId) {
27
+ return { valid: false, cycle: [taskId, taskId] }
28
+ }
29
+
30
+ const visited = new Set<string>()
31
+ const path: string[] = []
32
+ const found = dfs(tasks, startId, taskId, visited, path)
33
+ if (found) {
34
+ // path contains the route from startId to taskId
35
+ // The full cycle is: taskId -> startId -> ... -> taskId
36
+ return { valid: false, cycle: [taskId, ...path] }
37
+ }
38
+ }
39
+
40
+ return { valid: true }
41
+ }
42
+
43
+ /**
44
+ * DFS through the blockedBy graph starting from `current`, looking for `target`.
45
+ * Returns true if target is found, and populates `path` with the route.
46
+ */
47
+ function dfs(
48
+ tasks: Record<string, BoardTask>,
49
+ current: string,
50
+ target: string,
51
+ visited: Set<string>,
52
+ path: string[],
53
+ ): boolean {
54
+ if (visited.has(current)) return false
55
+ visited.add(current)
56
+ path.push(current)
57
+
58
+ const task = tasks[current]
59
+ if (!task) {
60
+ path.pop()
61
+ return false
62
+ }
63
+
64
+ const blockers = Array.isArray(task.blockedBy) ? task.blockedBy : []
65
+ for (const blockerId of blockers) {
66
+ if (blockerId === target) {
67
+ path.push(blockerId)
68
+ return true
69
+ }
70
+ if (dfs(tasks, blockerId, target, visited, path)) {
71
+ return true
72
+ }
73
+ }
74
+
75
+ path.pop()
76
+ return false
77
+ }
78
+
79
+ /**
80
+ * After a task completes, find all tasks that were blocked by it and check
81
+ * if all their blockers are now done. If so, auto-queue them.
82
+ * Returns the IDs of tasks that were unblocked.
83
+ */
84
+ export function cascadeUnblock(
85
+ tasks: Record<string, BoardTask>,
86
+ completedTaskId: string,
87
+ ): string[] {
88
+ const completedTask = tasks[completedTaskId]
89
+ if (!completedTask || completedTask.status !== 'completed') return []
90
+
91
+ const unblocked: string[] = []
92
+ const blockedIds = Array.isArray(completedTask.blocks) ? completedTask.blocks : []
93
+
94
+ for (const blockedId of blockedIds) {
95
+ const blocked = tasks[blockedId]
96
+ if (!blocked) continue
97
+ // Only auto-queue tasks that are in backlog (waiting on dependencies)
98
+ if (blocked.status !== 'backlog') continue
99
+
100
+ const deps = Array.isArray(blocked.blockedBy) ? blocked.blockedBy : []
101
+ const allDone = deps.every((depId) => {
102
+ const dep = tasks[depId]
103
+ return dep?.status === 'completed'
104
+ })
105
+
106
+ if (allDone) {
107
+ blocked.status = 'queued'
108
+ blocked.queuedAt = Date.now()
109
+ blocked.updatedAt = Date.now()
110
+ unblocked.push(blockedId)
111
+ }
112
+ }
113
+
114
+ return unblocked
115
+ }
@@ -1192,21 +1192,26 @@ export function addKnowledge(params: {
1192
1192
  agentIds?: string[]
1193
1193
  createdByAgentId?: string | null
1194
1194
  createdBySessionId?: string | null
1195
+ source?: string
1196
+ sourceUrl?: string
1195
1197
  }): MemoryEntry {
1196
1198
  const db = getMemoryDb()
1199
+ const metadata: Record<string, unknown> = {
1200
+ tags: params.tags || [],
1201
+ scope: params.scope || 'global',
1202
+ agentIds: params.scope === 'agent' ? (params.agentIds || []) : [],
1203
+ createdByAgentId: params.createdByAgentId || null,
1204
+ createdBySessionId: params.createdBySessionId || null,
1205
+ }
1206
+ if (params.source) metadata.source = params.source
1207
+ if (params.sourceUrl) metadata.sourceUrl = params.sourceUrl
1197
1208
  return db.add({
1198
1209
  agentId: null,
1199
1210
  sessionId: null,
1200
1211
  category: 'knowledge',
1201
1212
  title: params.title,
1202
1213
  content: params.content,
1203
- metadata: {
1204
- tags: params.tags || [],
1205
- scope: params.scope || 'global',
1206
- agentIds: params.scope === 'agent' ? (params.agentIds || []) : [],
1207
- createdByAgentId: params.createdByAgentId || null,
1208
- createdBySessionId: params.createdBySessionId || null,
1209
- },
1214
+ metadata,
1210
1215
  })
1211
1216
  }
1212
1217
 
@@ -0,0 +1,48 @@
1
+ import { execFile } from 'child_process'
2
+ import { promisify } from 'util'
3
+ import * as path from 'path'
4
+ import * as os from 'os'
5
+ import { loadSettings } from './storage'
6
+
7
+ const execFileAsync = promisify(execFile)
8
+
9
+ export interface DoctorResult {
10
+ ok: boolean
11
+ output: string
12
+ fixed: boolean
13
+ }
14
+
15
+ function resolveWorkspacePath(override?: string): string {
16
+ if (override) return override
17
+ const settings = loadSettings()
18
+ if (typeof settings.openclawWorkspacePath === 'string' && settings.openclawWorkspacePath.trim()) {
19
+ return settings.openclawWorkspacePath.trim()
20
+ }
21
+ return path.join(os.homedir(), '.openclaw', 'workspace')
22
+ }
23
+
24
+ export async function runOpenClawDoctor(opts?: { fix?: boolean; workspace?: string }): Promise<DoctorResult> {
25
+ const workspace = resolveWorkspacePath(opts?.workspace)
26
+ const args = ['doctor']
27
+ if (opts?.fix) args.push('--fix')
28
+
29
+ try {
30
+ const { stdout, stderr } = await execFileAsync('openclaw', args, {
31
+ cwd: workspace,
32
+ timeout: 30_000,
33
+ maxBuffer: 256 * 1024,
34
+ })
35
+ return {
36
+ ok: true,
37
+ output: (stdout + stderr).trim(),
38
+ fixed: !!opts?.fix,
39
+ }
40
+ } catch (err: unknown) {
41
+ const execErr = err as { code?: number; stdout?: string; stderr?: string; message?: string }
42
+ return {
43
+ ok: false,
44
+ output: ((execErr.stdout || '') + (execErr.stderr || '') || execErr.message || String(err)).trim(),
45
+ fixed: false,
46
+ }
47
+ }
48
+ }
@@ -124,7 +124,12 @@ async function executeSubTaskViaCli(agent: Agent, task: string, parentSessionId:
124
124
  }
125
125
  ss(sessions)
126
126
 
127
- const result = await callProvider(agent, agent.systemPrompt, [{ role: 'user', text: task }])
127
+ // Build system prompt with identity
128
+ const subPromptParts: string[] = []
129
+ subPromptParts.push(`## My Identity\nMy name is ${agent.name}.${agent.description ? ' ' + agent.description : ''} I should always refer to myself by this name.`)
130
+ if (agent.soul) subPromptParts.push(agent.soul)
131
+ if (agent.systemPrompt) subPromptParts.push(agent.systemPrompt)
132
+ const result = await callProvider(agent, subPromptParts.join('\n\n'), [{ role: 'user', text: task }])
128
133
 
129
134
  const s2 = ls()
130
135
  if (s2[childId]) {
@@ -348,9 +353,14 @@ export async function executeLangGraphOrchestrator(
348
353
  apiKey: engine.apiKey,
349
354
  apiEndpoint: engine.apiEndpoint,
350
355
  })
351
- // Build system message: [userPrompt] \n\n [soul] \n\n [systemPrompt] \n\n [orchestrator context]
356
+ // Build system message: [identity] \n\n [userPrompt] \n\n [soul] \n\n [systemPrompt] \n\n [orchestrator context]
352
357
  const settings = loadSettings()
353
358
  const promptParts: string[] = []
359
+ // Identity block
360
+ const orchIdentity = [`## My Identity`, `My name is ${orchestrator.name}.`]
361
+ if (orchestrator.description) orchIdentity.push(orchestrator.description)
362
+ orchIdentity.push('I should always refer to myself by this name.')
363
+ promptParts.push(orchIdentity.join(' '))
354
364
  if (settings.userPrompt) promptParts.push(settings.userPrompt)
355
365
  promptParts.push(buildCurrentDateTimePromptContext())
356
366
  if (orchestrator.soul) promptParts.push(orchestrator.soul)
@@ -296,7 +296,12 @@ async function executeSubTask(
296
296
  saveSessions(sessions)
297
297
 
298
298
  const history = [{ role: 'user', text: task }]
299
- const result = await callProvider(agent, agent.systemPrompt, history)
299
+ // Build system prompt with identity so the agent knows who it is
300
+ const promptParts: string[] = []
301
+ promptParts.push(`## My Identity\nMy name is ${agent.name}.${agent.description ? ' ' + agent.description : ''} I should always refer to myself by this name.`)
302
+ if (agent.soul) promptParts.push(agent.soul)
303
+ if (agent.systemPrompt) promptParts.push(agent.systemPrompt)
304
+ const result = await callProvider(agent, promptParts.join('\n\n'), history)
300
305
 
301
306
  childSession.messages.push({ role: 'user', text: task, time: Date.now() })
302
307
  childSession.messages.push({ role: 'assistant', text: result, time: Date.now() })