@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.
Files changed (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -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
- 'o3': [10, 40],
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
- // --- Agent Monthly Budget ---
34
-
35
- import { loadUsage, loadSessions } from './storage'
36
- import type { Agent, UsageRecord } from '@/types'
68
+ export interface AgentSpendWindows {
69
+ hourly: number
70
+ daily: number
71
+ monthly: number
72
+ }
37
73
 
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)
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 now = new Date()
52
- const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).getTime()
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 records = usage[sid]
58
- if (!Array.isArray(records)) continue
59
- for (const record of records) {
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
- 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
- }
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
- return total
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
- * Check whether an agent is within its monthly budget.
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 = typeof agent.monthlyBudget === 'number' && Number.isFinite(agent.monthlyBudget) && agent.monthlyBudget > 0
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
- export const WORKSPACE_DIR = path.join(DATA_DIR, 'workspace')
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
+ }