@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.
- package/README.md +62 -30
- package/package.json +10 -1
- package/src/app/api/agents/[id]/clone/route.ts +40 -0
- package/src/app/api/agents/route.ts +39 -14
- package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +34 -2
- package/src/app/api/chatrooms/route.ts +26 -3
- package/src/app/api/connectors/[id]/health/route.ts +64 -0
- package/src/app/api/connectors/route.ts +17 -2
- package/src/app/api/knowledge/route.ts +6 -1
- package/src/app/api/openclaw/doctor/route.ts +17 -0
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/sessions/[id]/chat/route.ts +5 -1
- package/src/app/api/sessions/route.ts +11 -2
- package/src/app/api/tasks/[id]/route.ts +18 -13
- package/src/app/api/tasks/route.ts +44 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/cli/index.js +20 -0
- package/src/cli/index.ts +223 -39
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +38 -6
- package/src/components/agents/agent-chat-list.tsx +79 -3
- package/src/components/agents/agent-sheet.tsx +191 -26
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +24 -9
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +17 -16
- package/src/components/chat/message-list.tsx +6 -5
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +165 -23
- package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +25 -3
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +35 -4
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +112 -17
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/hooks/use-swipe.ts +49 -0
- package/src/lib/providers/anthropic.ts +16 -2
- package/src/lib/providers/claude-cli.ts +7 -1
- package/src/lib/providers/index.ts +7 -0
- package/src/lib/providers/ollama.ts +16 -2
- package/src/lib/providers/openai.ts +7 -2
- package/src/lib/providers/openclaw.ts +6 -1
- package/src/lib/providers/provider-defaults.ts +7 -0
- package/src/lib/schedule-templates.ts +115 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +76 -4
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +86 -12
- package/src/lib/server/chatroom-routing.ts +65 -0
- package/src/lib/server/connectors/discord.ts +3 -0
- package/src/lib/server/connectors/email.ts +267 -0
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +239 -5
- package/src/lib/server/connectors/openclaw.ts +3 -0
- package/src/lib/server/connectors/slack.ts +6 -0
- package/src/lib/server/connectors/telegram.ts +18 -0
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +17 -5
- package/src/lib/server/cost.ts +70 -0
- package/src/lib/server/create-notification.ts +2 -0
- package/src/lib/server/daemon-state.ts +124 -0
- package/src/lib/server/dag-validation.ts +115 -0
- package/src/lib/server/memory-db.ts +12 -7
- package/src/lib/server/openclaw-doctor.ts +48 -0
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +238 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +8 -2
- package/src/lib/server/session-tools/memory.ts +23 -4
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +158 -6
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/stores/use-app-store.ts +15 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +98 -2
- package/tsconfig.json +2 -1
package/src/lib/server/cost.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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() })
|