@swarmclawai/swarmclaw 0.6.7 → 0.7.0
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 +82 -39
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +19 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/graph/route.ts +46 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +16 -3
- package/src/app/api/tasks/route.ts +10 -2
- package/src/app/api/usage/route.ts +9 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +37 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +112 -34
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/activity-moment.tsx +2 -0
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/checkpoint-timeline.tsx +112 -0
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +46 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +37 -7
- package/src/components/home/home-view.tsx +54 -24
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +87 -19
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +214 -60
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +28 -9
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/hint-tip.tsx +31 -0
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +224 -0
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +72 -48
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +319 -74
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +112 -1
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +115 -16
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +193 -19
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +7 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +662 -132
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +280 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +32 -2
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +95 -33
- package/src/lib/server/session-tools/index.ts +217 -138
- package/src/lib/server/session-tools/memory.ts +154 -239
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +78 -0
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +181 -327
- package/src/lib/server/storage.ts +36 -0
- package/src/lib/server/stream-agent-chat.ts +348 -242
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +24 -5
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +24 -23
- package/src/lib/validation/schemas.ts +13 -0
- package/src/lib/view-routes.ts +2 -23
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +155 -10
package/src/lib/server/cost.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { Agent, UsageRecord } from '@/types'
|
|
2
|
+
import { loadSessions, loadUsage } from './storage'
|
|
3
|
+
|
|
1
4
|
// Model cost table: [inputCostPer1M, outputCostPer1M] in USD
|
|
2
5
|
const MODEL_COSTS: Record<string, [number, number]> = {
|
|
3
6
|
// Anthropic
|
|
@@ -11,7 +14,7 @@ const MODEL_COSTS: Record<string, [number, number]> = {
|
|
|
11
14
|
'gpt-4.1': [2, 8],
|
|
12
15
|
'gpt-4.1-mini': [0.4, 1.6],
|
|
13
16
|
'gpt-4.1-nano': [0.1, 0.4],
|
|
14
|
-
|
|
17
|
+
o3: [10, 40],
|
|
15
18
|
'o3-mini': [1.1, 4.4],
|
|
16
19
|
'o4-mini': [1.1, 4.4],
|
|
17
20
|
// OpenAI embeddings
|
|
@@ -19,6 +22,38 @@ const MODEL_COSTS: Record<string, [number, number]> = {
|
|
|
19
22
|
'text-embedding-3-large': [0.13, 0],
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
const ONE_HOUR_MS = 60 * 60 * 1000
|
|
26
|
+
const WARNING_RATIO = 0.8
|
|
27
|
+
|
|
28
|
+
type GenericRecord = Record<string, unknown>
|
|
29
|
+
type SessionsMap = Record<string, GenericRecord>
|
|
30
|
+
type UsageMap = Record<string, unknown>
|
|
31
|
+
|
|
32
|
+
function parsePositiveBudget(value: unknown): number | null {
|
|
33
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return null
|
|
34
|
+
return value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toDateBoundaries(now: number) {
|
|
38
|
+
const d = new Date(now)
|
|
39
|
+
const dayStart = new Date(d)
|
|
40
|
+
dayStart.setHours(0, 0, 0, 0)
|
|
41
|
+
const monthStart = new Date(d.getFullYear(), d.getMonth(), 1)
|
|
42
|
+
return {
|
|
43
|
+
hourStartTs: now - ONE_HOUR_MS,
|
|
44
|
+
dayStartTs: dayStart.getTime(),
|
|
45
|
+
monthStartTs: monthStart.getTime(),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getAgentSessionIds(agentId: string, sessions: SessionsMap): Set<string> {
|
|
50
|
+
const ids = new Set<string>()
|
|
51
|
+
for (const [sid, session] of Object.entries(sessions)) {
|
|
52
|
+
if (session?.agentId === agentId) ids.add(sid)
|
|
53
|
+
}
|
|
54
|
+
return ids
|
|
55
|
+
}
|
|
56
|
+
|
|
22
57
|
export function estimateCost(model: string, inputTokens: number, outputTokens: number): number {
|
|
23
58
|
const costs = MODEL_COSTS[model]
|
|
24
59
|
if (!costs) return 0
|
|
@@ -30,41 +65,141 @@ export function getModelCosts(): Record<string, [number, number]> {
|
|
|
30
65
|
return { ...MODEL_COSTS }
|
|
31
66
|
}
|
|
32
67
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
68
|
+
export interface AgentSpendWindows {
|
|
69
|
+
hourly: number
|
|
70
|
+
daily: number
|
|
71
|
+
monthly: number
|
|
72
|
+
}
|
|
37
73
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const sessions = loadSessions()
|
|
44
|
-
|
|
45
|
-
const agentSessionIds =
|
|
46
|
-
|
|
47
|
-
|
|
74
|
+
export function getAgentSpendWindows(
|
|
75
|
+
agentId: string,
|
|
76
|
+
now = Date.now(),
|
|
77
|
+
opts?: { sessions?: SessionsMap; usage?: UsageMap },
|
|
78
|
+
): AgentSpendWindows {
|
|
79
|
+
const sessions = opts?.sessions ?? (loadSessions() as SessionsMap)
|
|
80
|
+
const usage = opts?.usage ?? (loadUsage() as UsageMap)
|
|
81
|
+
const agentSessionIds = getAgentSessionIds(agentId, sessions)
|
|
82
|
+
if (agentSessionIds.size === 0) {
|
|
83
|
+
return { hourly: 0, daily: 0, monthly: 0 }
|
|
48
84
|
}
|
|
49
|
-
if (agentSessionIds.size === 0) return 0
|
|
50
85
|
|
|
51
|
-
const
|
|
52
|
-
const
|
|
86
|
+
const { hourStartTs, dayStartTs, monthStartTs } = toDateBoundaries(now)
|
|
87
|
+
const spend: AgentSpendWindows = { hourly: 0, daily: 0, monthly: 0 }
|
|
53
88
|
|
|
54
|
-
const usage = loadUsage()
|
|
55
|
-
let total = 0
|
|
56
89
|
for (const sid of agentSessionIds) {
|
|
57
|
-
const
|
|
58
|
-
if (!Array.isArray(
|
|
59
|
-
for (const record of
|
|
90
|
+
const raw = usage[sid]
|
|
91
|
+
if (!Array.isArray(raw)) continue
|
|
92
|
+
for (const record of raw) {
|
|
60
93
|
const r = record as UsageRecord
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
94
|
+
const ts = typeof r?.timestamp === 'number' ? r.timestamp : 0
|
|
95
|
+
if (ts <= 0) continue
|
|
96
|
+
const cost = typeof r?.estimatedCost === 'number' ? r.estimatedCost : 0
|
|
97
|
+
if (!Number.isFinite(cost) || cost <= 0) continue
|
|
98
|
+
|
|
99
|
+
if (ts >= monthStartTs) spend.monthly += cost
|
|
100
|
+
if (ts >= dayStartTs) spend.daily += cost
|
|
101
|
+
if (ts >= hourStartTs) spend.hourly += cost
|
|
65
102
|
}
|
|
66
103
|
}
|
|
67
|
-
|
|
104
|
+
|
|
105
|
+
return spend
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getAgentMonthlySpend(
|
|
109
|
+
agentId: string,
|
|
110
|
+
now = Date.now(),
|
|
111
|
+
opts?: { sessions?: SessionsMap; usage?: UsageMap },
|
|
112
|
+
): number {
|
|
113
|
+
return getAgentSpendWindows(agentId, now, opts).monthly
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getAgentDailySpend(
|
|
117
|
+
agentId: string,
|
|
118
|
+
now = Date.now(),
|
|
119
|
+
opts?: { sessions?: SessionsMap; usage?: UsageMap },
|
|
120
|
+
): number {
|
|
121
|
+
return getAgentSpendWindows(agentId, now, opts).daily
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getAgentHourlySpend(
|
|
125
|
+
agentId: string,
|
|
126
|
+
now = Date.now(),
|
|
127
|
+
opts?: { sessions?: SessionsMap; usage?: UsageMap },
|
|
128
|
+
): number {
|
|
129
|
+
return getAgentSpendWindows(agentId, now, opts).hourly
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type AgentBudgetWindow = 'hourly' | 'daily' | 'monthly'
|
|
133
|
+
|
|
134
|
+
export interface AgentBudgetStatus {
|
|
135
|
+
window: AgentBudgetWindow
|
|
136
|
+
spend: number
|
|
137
|
+
budget: number
|
|
138
|
+
ratio: number
|
|
139
|
+
message: string
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface AgentBudgetCheckSummary {
|
|
143
|
+
ok: boolean
|
|
144
|
+
spend: AgentSpendWindows
|
|
145
|
+
exceeded: AgentBudgetStatus[]
|
|
146
|
+
warnings: AgentBudgetStatus[]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function budgetWindowLabel(window: AgentBudgetWindow): string {
|
|
150
|
+
if (window === 'hourly') return 'hourly'
|
|
151
|
+
if (window === 'daily') return 'daily'
|
|
152
|
+
return 'monthly'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildBudgetStatus(
|
|
156
|
+
agentName: string,
|
|
157
|
+
window: AgentBudgetWindow,
|
|
158
|
+
spend: number,
|
|
159
|
+
budget: number,
|
|
160
|
+
exceeded: boolean,
|
|
161
|
+
): AgentBudgetStatus {
|
|
162
|
+
const ratio = budget > 0 ? spend / budget : 0
|
|
163
|
+
const label = budgetWindowLabel(window)
|
|
164
|
+
const message = exceeded
|
|
165
|
+
? `Agent "${agentName}" has reached its ${label} budget: $${spend.toFixed(4)} spent of $${budget.toFixed(2)} cap.`
|
|
166
|
+
: `Agent "${agentName}" is nearing its ${label} budget: $${spend.toFixed(4)} of $${budget.toFixed(2)} (${Math.round(ratio * 100)}%).`
|
|
167
|
+
return { window, spend, budget, ratio, message }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function checkAgentBudgetLimits(
|
|
171
|
+
agent: Agent,
|
|
172
|
+
now = Date.now(),
|
|
173
|
+
opts?: { sessions?: SessionsMap; usage?: UsageMap },
|
|
174
|
+
): AgentBudgetCheckSummary {
|
|
175
|
+
const budgets: Partial<Record<AgentBudgetWindow, number>> = {
|
|
176
|
+
hourly: parsePositiveBudget(agent.hourlyBudget) ?? undefined,
|
|
177
|
+
daily: parsePositiveBudget(agent.dailyBudget) ?? undefined,
|
|
178
|
+
monthly: parsePositiveBudget(agent.monthlyBudget) ?? undefined,
|
|
179
|
+
}
|
|
180
|
+
const spend = getAgentSpendWindows(agent.id, now, opts)
|
|
181
|
+
const exceeded: AgentBudgetStatus[] = []
|
|
182
|
+
const warnings: AgentBudgetStatus[] = []
|
|
183
|
+
|
|
184
|
+
for (const window of ['hourly', 'daily', 'monthly'] as const) {
|
|
185
|
+
const budget = budgets[window]
|
|
186
|
+
if (!budget) continue
|
|
187
|
+
const windowSpend = spend[window]
|
|
188
|
+
if (windowSpend >= budget) {
|
|
189
|
+
exceeded.push(buildBudgetStatus(agent.name, window, windowSpend, budget, true))
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
if (windowSpend >= budget * WARNING_RATIO) {
|
|
193
|
+
warnings.push(buildBudgetStatus(agent.name, window, windowSpend, budget, false))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
ok: exceeded.length === 0,
|
|
199
|
+
spend,
|
|
200
|
+
exceeded,
|
|
201
|
+
warnings,
|
|
202
|
+
}
|
|
68
203
|
}
|
|
69
204
|
|
|
70
205
|
export interface BudgetCheckResult {
|
|
@@ -75,14 +210,10 @@ export interface BudgetCheckResult {
|
|
|
75
210
|
}
|
|
76
211
|
|
|
77
212
|
/**
|
|
78
|
-
*
|
|
79
|
-
* Returns ok: true if no budget is set or spend is under the cap.
|
|
213
|
+
* Backwards-compatible monthly-only budget check.
|
|
80
214
|
*/
|
|
81
215
|
export function checkBudget(agent: Agent): BudgetCheckResult {
|
|
82
|
-
const budget =
|
|
83
|
-
? agent.monthlyBudget
|
|
84
|
-
: 0
|
|
85
|
-
|
|
216
|
+
const budget = parsePositiveBudget(agent.monthlyBudget) ?? 0
|
|
86
217
|
if (budget <= 0) {
|
|
87
218
|
return { ok: true, spend: 0, budget: 0 }
|
|
88
219
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadQueue, loadSchedules, loadSessions, saveSessions, loadConnectors, saveConnectors, loadWebhookRetryQueue, upsertWebhookRetry, deleteWebhookRetry, loadWebhooks, loadAgents, appendWebhookLog, loadCredentials, decryptKey } from './storage'
|
|
1
|
+
import { loadQueue, loadSchedules, loadSessions, saveSessions, loadConnectors, saveConnectors, loadWebhookRetryQueue, upsertWebhookRetry, deleteWebhookRetry, loadWebhooks, loadAgents, loadSettings, appendWebhookLog, loadCredentials, decryptKey } from './storage'
|
|
2
2
|
import { notify } from './ws-hub'
|
|
3
3
|
import { processNext, cleanupFinishedTaskSessions, validateCompletedTasksQueue, recoverStalledRunningTasks } from './queue'
|
|
4
4
|
import { startScheduler, stopScheduler } from './scheduler'
|
|
@@ -17,9 +17,11 @@ import { hasOpenClawAgents, ensureGatewayConnected, disconnectGateway, getGatewa
|
|
|
17
17
|
import { enqueueSessionRun } from './session-run-manager'
|
|
18
18
|
import { WORKSPACE_DIR } from './data-dir'
|
|
19
19
|
import { genId } from '@/lib/id'
|
|
20
|
+
import path from 'node:path'
|
|
20
21
|
import type { WebhookRetryEntry } from '@/types'
|
|
21
22
|
import { createNotification } from '@/lib/server/create-notification'
|
|
22
23
|
import { pingProvider, OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
|
|
24
|
+
import { runIntegrityMonitor } from '@/lib/server/integrity-monitor'
|
|
23
25
|
|
|
24
26
|
const QUEUE_CHECK_INTERVAL = 30_000 // 30 seconds
|
|
25
27
|
const BROWSER_SWEEP_INTERVAL = 60_000 // 60 seconds
|
|
@@ -80,6 +82,7 @@ const ds: {
|
|
|
80
82
|
healthIntervalId: ReturnType<typeof setInterval> | null
|
|
81
83
|
memoryConsolidationTimeoutId: ReturnType<typeof setTimeout> | null
|
|
82
84
|
memoryConsolidationIntervalId: ReturnType<typeof setInterval> | null
|
|
85
|
+
evalSchedulerIntervalId: ReturnType<typeof setInterval> | null
|
|
83
86
|
/** Session IDs we've already alerted as stale (alert-once semantics). */
|
|
84
87
|
staleSessionIds: Set<string>
|
|
85
88
|
connectorRestartState: Map<string, { lastAttemptAt: number; failCount: number; wakeAttempts: number }>
|
|
@@ -87,6 +90,8 @@ const ds: {
|
|
|
87
90
|
openclawDownAgentIds: Set<string>
|
|
88
91
|
/** Per-agent auto-repair state for OpenClaw gateways. */
|
|
89
92
|
openclawRepairState: Map<string, { attempts: number; lastAttemptAt: number; cooldownUntil: number }>
|
|
93
|
+
lastIntegrityCheckAt: number | null
|
|
94
|
+
lastIntegrityDriftCount: number
|
|
90
95
|
manualStopRequested: boolean
|
|
91
96
|
running: boolean
|
|
92
97
|
lastProcessedAt: number | null
|
|
@@ -97,10 +102,13 @@ const ds: {
|
|
|
97
102
|
healthIntervalId: null,
|
|
98
103
|
memoryConsolidationTimeoutId: null,
|
|
99
104
|
memoryConsolidationIntervalId: null,
|
|
105
|
+
evalSchedulerIntervalId: null,
|
|
100
106
|
staleSessionIds: new Set<string>(),
|
|
101
107
|
connectorRestartState: new Map<string, { lastAttemptAt: number; failCount: number; wakeAttempts: number }>(),
|
|
102
108
|
openclawDownAgentIds: new Set<string>(),
|
|
103
109
|
openclawRepairState: new Map<string, { attempts: number; lastAttemptAt: number; cooldownUntil: number }>(),
|
|
110
|
+
lastIntegrityCheckAt: null,
|
|
111
|
+
lastIntegrityDriftCount: 0,
|
|
104
112
|
manualStopRequested: false,
|
|
105
113
|
running: false,
|
|
106
114
|
lastProcessedAt: null,
|
|
@@ -111,6 +119,8 @@ if (!ds.staleSessionIds) ds.staleSessionIds = new Set<string>()
|
|
|
111
119
|
if (!ds.connectorRestartState) ds.connectorRestartState = new Map<string, { lastAttemptAt: number; failCount: number; wakeAttempts: number }>()
|
|
112
120
|
if (!ds.openclawDownAgentIds) ds.openclawDownAgentIds = new Set<string>()
|
|
113
121
|
if (!ds.openclawRepairState) ds.openclawRepairState = new Map<string, { attempts: number; lastAttemptAt: number; cooldownUntil: number }>()
|
|
122
|
+
if (ds.lastIntegrityCheckAt === undefined) ds.lastIntegrityCheckAt = null
|
|
123
|
+
if (ds.lastIntegrityDriftCount === undefined) ds.lastIntegrityDriftCount = 0
|
|
114
124
|
// Migrate from old issueLastAlertAt map if present (HMR across code versions)
|
|
115
125
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
126
|
if ((ds as any).issueLastAlertAt) delete (ds as any).issueLastAlertAt
|
|
@@ -118,6 +128,7 @@ if (ds.healthIntervalId === undefined) ds.healthIntervalId = null
|
|
|
118
128
|
if (ds.manualStopRequested === undefined) ds.manualStopRequested = false
|
|
119
129
|
if (ds.memoryConsolidationTimeoutId === undefined) ds.memoryConsolidationTimeoutId = null
|
|
120
130
|
if (ds.memoryConsolidationIntervalId === undefined) ds.memoryConsolidationIntervalId = null
|
|
131
|
+
if (ds.evalSchedulerIntervalId === undefined) ds.evalSchedulerIntervalId = null
|
|
121
132
|
|
|
122
133
|
export function ensureDaemonStarted(source = 'unknown'): boolean {
|
|
123
134
|
if (ds.running) return false
|
|
@@ -140,6 +151,7 @@ export function startDaemon(options?: { source?: string; manualStart?: boolean }
|
|
|
140
151
|
startHealthMonitor()
|
|
141
152
|
startHeartbeatService()
|
|
142
153
|
startMemoryConsolidation()
|
|
154
|
+
startEvalScheduler()
|
|
143
155
|
return
|
|
144
156
|
}
|
|
145
157
|
ds.running = true
|
|
@@ -155,6 +167,7 @@ export function startDaemon(options?: { source?: string; manualStart?: boolean }
|
|
|
155
167
|
startHealthMonitor()
|
|
156
168
|
startHeartbeatService()
|
|
157
169
|
startMemoryConsolidation()
|
|
170
|
+
startEvalScheduler()
|
|
158
171
|
} catch (err: unknown) {
|
|
159
172
|
ds.running = false
|
|
160
173
|
notify('daemon')
|
|
@@ -182,6 +195,7 @@ export function stopDaemon(options?: { source?: string; manualStop?: boolean })
|
|
|
182
195
|
stopHealthMonitor()
|
|
183
196
|
stopHeartbeatService()
|
|
184
197
|
stopMemoryConsolidation()
|
|
198
|
+
stopEvalScheduler()
|
|
185
199
|
stopAllConnectors().catch(() => {})
|
|
186
200
|
}
|
|
187
201
|
|
|
@@ -725,6 +739,35 @@ async function runHealthChecks() {
|
|
|
725
739
|
console.error('[daemon] OpenClaw gateway health check failed:', err instanceof Error ? err.message : String(err))
|
|
726
740
|
}
|
|
727
741
|
|
|
742
|
+
// Integrity drift monitoring for identity/config/plugin files.
|
|
743
|
+
try {
|
|
744
|
+
const integrity = runIntegrityMonitor(loadSettings())
|
|
745
|
+
ds.lastIntegrityCheckAt = integrity.checkedAt
|
|
746
|
+
ds.lastIntegrityDriftCount = integrity.drifts.length
|
|
747
|
+
if (integrity.drifts.length > 0) {
|
|
748
|
+
for (const drift of integrity.drifts) {
|
|
749
|
+
const rel = path.relative(process.cwd(), drift.filePath)
|
|
750
|
+
const shortPath = rel && !rel.startsWith('..') ? rel : drift.filePath
|
|
751
|
+
const action = drift.type === 'created'
|
|
752
|
+
? 'created'
|
|
753
|
+
: drift.type === 'deleted'
|
|
754
|
+
? 'deleted'
|
|
755
|
+
: 'modified'
|
|
756
|
+
createNotification({
|
|
757
|
+
type: drift.type === 'deleted' ? 'error' : 'warning',
|
|
758
|
+
title: `Integrity drift detected (${drift.kind})`,
|
|
759
|
+
message: `${shortPath} was ${action}.`,
|
|
760
|
+
dedupKey: `integrity:${drift.id}:${drift.nextHash || 'missing'}`,
|
|
761
|
+
entityType: 'session',
|
|
762
|
+
entityId: drift.id,
|
|
763
|
+
})
|
|
764
|
+
}
|
|
765
|
+
await sendHealthAlert(`Integrity monitor detected ${integrity.drifts.length} file drift event(s).`)
|
|
766
|
+
}
|
|
767
|
+
} catch (err: unknown) {
|
|
768
|
+
console.error('[daemon] Integrity monitor check failed:', err instanceof Error ? err.message : String(err))
|
|
769
|
+
}
|
|
770
|
+
|
|
728
771
|
// Process webhook retry queue
|
|
729
772
|
try {
|
|
730
773
|
await processWebhookRetries()
|
|
@@ -785,6 +828,69 @@ function stopMemoryConsolidation() {
|
|
|
785
828
|
}
|
|
786
829
|
}
|
|
787
830
|
|
|
831
|
+
// --- Eval scheduler ---
|
|
832
|
+
|
|
833
|
+
const EVAL_DEFAULT_INTERVAL_MS = 24 * 3600_000 // 24 hours
|
|
834
|
+
|
|
835
|
+
function parseCronToMs(cron: string | null | undefined): number | null {
|
|
836
|
+
if (!cron || typeof cron !== 'string') return null
|
|
837
|
+
// Simple heuristic: extract hours from common cron patterns like "0 */6 * * *"
|
|
838
|
+
const hourMatch = cron.match(/\*\/(\d+)/)
|
|
839
|
+
if (hourMatch) return parseInt(hourMatch[1], 10) * 3600_000
|
|
840
|
+
return EVAL_DEFAULT_INTERVAL_MS
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function runEvalSchedulerTick() {
|
|
844
|
+
try {
|
|
845
|
+
const settings = loadSettings()
|
|
846
|
+
if (!settings.autonomyEvalEnabled) return
|
|
847
|
+
|
|
848
|
+
const { runEvalSuite } = await import('./eval/runner')
|
|
849
|
+
const agents = loadAgents()
|
|
850
|
+
const heartbeatAgentIds = Object.keys(agents).filter(
|
|
851
|
+
(id) => agents[id].heartbeatEnabled === true,
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
for (const agentId of heartbeatAgentIds) {
|
|
855
|
+
try {
|
|
856
|
+
const result = await runEvalSuite(agentId)
|
|
857
|
+
console.log(
|
|
858
|
+
`[daemon:eval] Agent ${agents[agentId].name}: ${result.percentage}% (${result.totalScore}/${result.maxScore})`,
|
|
859
|
+
)
|
|
860
|
+
createNotification({
|
|
861
|
+
title: `Eval: ${agents[agentId].name} scored ${result.percentage}%`,
|
|
862
|
+
message: `${result.runs.length} scenarios, ${result.totalScore}/${result.maxScore} points`,
|
|
863
|
+
type: result.percentage >= 60 ? 'info' : 'warning',
|
|
864
|
+
})
|
|
865
|
+
} catch (err: unknown) {
|
|
866
|
+
console.error(`[daemon:eval] Failed for agent ${agentId}:`, err instanceof Error ? err.message : String(err))
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
} catch (err: unknown) {
|
|
870
|
+
console.error('[daemon:eval] Scheduler tick error:', err instanceof Error ? err.message : String(err))
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function startEvalScheduler() {
|
|
875
|
+
if (ds.evalSchedulerIntervalId) return
|
|
876
|
+
try {
|
|
877
|
+
const settings = loadSettings()
|
|
878
|
+
if (!settings.autonomyEvalEnabled) return
|
|
879
|
+
const intervalMs = parseCronToMs(settings.autonomyEvalCron) || EVAL_DEFAULT_INTERVAL_MS
|
|
880
|
+
ds.evalSchedulerIntervalId = setInterval(runEvalSchedulerTick, intervalMs)
|
|
881
|
+
console.log(`[daemon:eval] Eval scheduler started (interval=${Math.round(intervalMs / 3600_000)}h)`)
|
|
882
|
+
} catch {
|
|
883
|
+
// Eval scheduling is optional — don't block daemon start
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function stopEvalScheduler() {
|
|
888
|
+
if (ds.evalSchedulerIntervalId) {
|
|
889
|
+
clearInterval(ds.evalSchedulerIntervalId)
|
|
890
|
+
ds.evalSchedulerIntervalId = null
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
788
894
|
export async function runDaemonHealthCheckNow() {
|
|
789
895
|
await runHealthChecks()
|
|
790
896
|
}
|
|
@@ -823,6 +929,11 @@ export function getDaemonStatus() {
|
|
|
823
929
|
staleSessions: ds.staleSessionIds.size,
|
|
824
930
|
connectorsInBackoff: ds.connectorRestartState.size,
|
|
825
931
|
checkIntervalSec: Math.trunc(HEALTH_CHECK_INTERVAL / 1000),
|
|
932
|
+
integrity: {
|
|
933
|
+
enabled: loadSettings().integrityMonitorEnabled !== false,
|
|
934
|
+
lastCheckedAt: ds.lastIntegrityCheckAt,
|
|
935
|
+
lastDriftCount: ds.lastIntegrityDriftCount,
|
|
936
|
+
},
|
|
826
937
|
},
|
|
827
938
|
webhookRetry: {
|
|
828
939
|
pendingRetries,
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
|
+
import os from 'os'
|
|
3
|
+
import fs from 'fs'
|
|
2
4
|
|
|
3
5
|
export const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data')
|
|
4
|
-
|
|
6
|
+
|
|
7
|
+
// Workspace lives outside the project directory to avoid triggering Next.js HMR
|
|
8
|
+
// when agents create/modify files. Falls back to data/workspace for Docker/CI.
|
|
9
|
+
function resolveWorkspaceDir(): string {
|
|
10
|
+
if (process.env.WORKSPACE_DIR) return process.env.WORKSPACE_DIR
|
|
11
|
+
const external = path.join(os.homedir(), '.swarmclaw', 'workspace')
|
|
12
|
+
try {
|
|
13
|
+
fs.mkdirSync(external, { recursive: true })
|
|
14
|
+
return external
|
|
15
|
+
} catch {
|
|
16
|
+
// If we can't create the external dir (permissions, etc.), fall back to in-project
|
|
17
|
+
return path.join(DATA_DIR, 'workspace')
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const WORKSPACE_DIR = resolveWorkspaceDir()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
|
+
import type { EvalScenario, EvalRun, EvalSuiteResult } from './types'
|
|
3
|
+
import { getScenario, EVAL_SCENARIOS } from './scenarios'
|
|
4
|
+
import { scoreCriteria } from './scorer'
|
|
5
|
+
import { saveEvalRun } from './store'
|
|
6
|
+
import { loadSessions, saveSessions, loadAgents, loadCredentials, decryptKey } from '../storage'
|
|
7
|
+
import { executeSessionChatTurn } from '../chat-execution'
|
|
8
|
+
import type { Session } from '@/types'
|
|
9
|
+
|
|
10
|
+
export async function runEvalScenario(scenarioId: string, agentId: string): Promise<EvalRun> {
|
|
11
|
+
const scenario = getScenario(scenarioId)
|
|
12
|
+
if (!scenario) throw new Error(`Unknown eval scenario: ${scenarioId}`)
|
|
13
|
+
|
|
14
|
+
const agents = loadAgents() as Record<string, Record<string, unknown>>
|
|
15
|
+
const agent = agents[agentId]
|
|
16
|
+
if (!agent) throw new Error(`Unknown agent: ${agentId}`)
|
|
17
|
+
|
|
18
|
+
const runId = genId()
|
|
19
|
+
const sessionId = `eval-${runId}`
|
|
20
|
+
const now = Date.now()
|
|
21
|
+
|
|
22
|
+
const run: EvalRun = {
|
|
23
|
+
id: runId,
|
|
24
|
+
scenarioId,
|
|
25
|
+
agentId,
|
|
26
|
+
status: 'running',
|
|
27
|
+
startedAt: now,
|
|
28
|
+
score: 0,
|
|
29
|
+
maxScore: scenario.scoringCriteria.reduce((sum, c) => sum + c.weight, 0),
|
|
30
|
+
details: [],
|
|
31
|
+
sessionId,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Create temporary eval session
|
|
35
|
+
const sessions = loadSessions() as Record<string, Session>
|
|
36
|
+
const evalSession: Session = {
|
|
37
|
+
id: sessionId,
|
|
38
|
+
name: `Eval: ${scenario.name}`,
|
|
39
|
+
cwd: process.cwd(),
|
|
40
|
+
user: 'eval-runner',
|
|
41
|
+
provider: (agent.provider as Session['provider']) ?? 'anthropic',
|
|
42
|
+
model: (agent.model as string) ?? '',
|
|
43
|
+
credentialId: (agent.credentialId as string | null) ?? null,
|
|
44
|
+
apiEndpoint: (agent.apiEndpoint as string | null) ?? null,
|
|
45
|
+
claudeSessionId: null,
|
|
46
|
+
agentId,
|
|
47
|
+
tools: scenario.tools,
|
|
48
|
+
messages: [],
|
|
49
|
+
createdAt: now,
|
|
50
|
+
lastActiveAt: now,
|
|
51
|
+
}
|
|
52
|
+
sessions[sessionId] = evalSession
|
|
53
|
+
saveSessions(sessions)
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result = await executeSessionChatTurn({
|
|
57
|
+
sessionId,
|
|
58
|
+
message: scenario.userMessage,
|
|
59
|
+
internal: true,
|
|
60
|
+
source: 'eval',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const judgeProvider = typeof agent.provider === 'string' ? agent.provider : undefined
|
|
64
|
+
const judgeModel = typeof agent.model === 'string' ? agent.model : undefined
|
|
65
|
+
let judgeApiKey: string | null = null
|
|
66
|
+
if (typeof agent.credentialId === 'string' && agent.credentialId) {
|
|
67
|
+
const creds = loadCredentials()
|
|
68
|
+
const cred = creds[agent.credentialId]
|
|
69
|
+
if (cred) {
|
|
70
|
+
try { judgeApiKey = decryptKey(cred.encryptedKey) } catch { /* skip undecryptable */ }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const judgeOpts = judgeProvider && judgeModel ? {
|
|
74
|
+
provider: judgeProvider,
|
|
75
|
+
model: judgeModel,
|
|
76
|
+
apiKey: judgeApiKey,
|
|
77
|
+
apiEndpoint: typeof agent.apiEndpoint === 'string' ? agent.apiEndpoint : undefined,
|
|
78
|
+
} : undefined
|
|
79
|
+
|
|
80
|
+
run.details = await scoreCriteria(
|
|
81
|
+
scenario.scoringCriteria,
|
|
82
|
+
result.text,
|
|
83
|
+
result.toolEvents || [],
|
|
84
|
+
judgeOpts,
|
|
85
|
+
)
|
|
86
|
+
run.score = run.details.reduce((sum, d) => sum + d.score, 0)
|
|
87
|
+
run.status = 'completed'
|
|
88
|
+
run.endedAt = Date.now()
|
|
89
|
+
} catch (err: unknown) {
|
|
90
|
+
run.status = 'failed'
|
|
91
|
+
run.error = err instanceof Error ? err.message : String(err)
|
|
92
|
+
run.endedAt = Date.now()
|
|
93
|
+
} finally {
|
|
94
|
+
// Clean up eval session
|
|
95
|
+
const currentSessions = loadSessions() as Record<string, Session>
|
|
96
|
+
delete currentSessions[sessionId]
|
|
97
|
+
saveSessions(currentSessions)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
saveEvalRun(run)
|
|
101
|
+
return run
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function runEvalSuite(agentId: string, categories?: string[]): Promise<EvalSuiteResult> {
|
|
105
|
+
const scenarios: EvalScenario[] = categories
|
|
106
|
+
? EVAL_SCENARIOS.filter(s => categories.includes(s.category))
|
|
107
|
+
: EVAL_SCENARIOS
|
|
108
|
+
|
|
109
|
+
const runs: EvalRun[] = []
|
|
110
|
+
for (const scenario of scenarios) {
|
|
111
|
+
const evalRun = await runEvalScenario(scenario.id, agentId)
|
|
112
|
+
runs.push(evalRun)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const totalScore = runs.reduce((sum, r) => sum + r.score, 0)
|
|
116
|
+
const maxScore = runs.reduce((sum, r) => sum + r.maxScore, 0)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
agentId,
|
|
120
|
+
totalScore,
|
|
121
|
+
maxScore,
|
|
122
|
+
percentage: maxScore > 0 ? Math.round((totalScore / maxScore) * 100) : 0,
|
|
123
|
+
runs,
|
|
124
|
+
completedAt: Date.now(),
|
|
125
|
+
}
|
|
126
|
+
}
|