@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,4 +1,5 @@
1
1
  import fs from 'fs'
2
+ import os from 'os'
2
3
  import {
3
4
  loadSessions,
4
5
  saveSessions,
@@ -13,18 +14,28 @@ import {
13
14
  active,
14
15
  } from './storage'
15
16
  import { getProvider } from '@/lib/providers'
16
- import { estimateCost, checkBudget } from './cost'
17
+ import { estimateCost, checkAgentBudgetLimits } from './cost'
17
18
  import { log } from './logger'
18
19
  import { logExecution } from './execution-log'
19
20
  import { streamAgentChat } from './stream-agent-chat'
21
+ import { runLinkUnderstanding } from './link-understanding'
20
22
  import { buildSessionTools } from './session-tools'
23
+ import type { StructuredToolInterface } from '@langchain/core/tools'
24
+ import type { Session } from '@/types'
21
25
  import { stripMainLoopMetaForPersistence } from './main-agent-loop'
22
26
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
23
27
  import { getMemoryDb } from './memory-db'
24
28
  import { routeTaskIntent } from './capability-router'
25
29
  import { notify } from './ws-hub'
26
30
  import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
31
+ import { toolIdMatches } from './tool-aliases'
27
32
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
33
+ import {
34
+ getCachedLlmResponse,
35
+ resolveLlmResponseCacheConfig,
36
+ setCachedLlmResponse,
37
+ type LlmResponseCacheKeyInput,
38
+ } from './llm-response-cache'
28
39
  import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
29
40
  import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
30
41
  import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
@@ -74,6 +85,9 @@ export interface ExecuteChatTurnResult {
74
85
  persisted: boolean
75
86
  toolEvents: MessageToolEvent[]
76
87
  error?: string
88
+ inputTokens?: number
89
+ outputTokens?: number
90
+ estimatedCost?: number
77
91
  }
78
92
 
79
93
  function extractEventJson(line: string): SSEEvent | null {
@@ -143,20 +157,32 @@ function requestedToolNamesFromMessage(message: string): string[] {
143
157
  'manage_connectors',
144
158
  'manage_sessions',
145
159
  'manage_secrets',
160
+ 'manage_capabilities',
161
+ 'manage_platform',
162
+ 'manage_chatrooms',
163
+ 'search_marketplace',
164
+ 'monitor_tool',
165
+ 'plugin_creator_tool',
146
166
  'memory_tool',
167
+ 'wallet_tool',
168
+ 'http_request',
169
+ 'send_file',
147
170
  'browser',
148
- 'web_search',
149
- 'web_fetch',
150
- 'execute_command',
151
- 'read_file',
152
- 'write_file',
153
- 'list_files',
154
- 'copy_file',
155
- 'move_file',
156
- 'delete_file',
171
+ 'web',
172
+ 'shell',
173
+ 'files',
157
174
  'edit_file',
158
- 'send_file',
159
- 'process_tool',
175
+ 'sandbox_exec',
176
+ 'sandbox_list_runtimes',
177
+ 'git',
178
+ 'canvas',
179
+ 'delegate',
180
+ 'schedule_wake',
181
+ 'spawn_subagent',
182
+ 'context_status',
183
+ 'context_summarize',
184
+ 'openclaw_nodes',
185
+ 'openclaw_workspace',
160
186
  ]
161
187
  return candidates.filter((name) => lower.includes(name.toLowerCase()))
162
188
  }
@@ -300,12 +326,12 @@ function extractDelegationTask(message: string, toolName: string): string | null
300
326
  }
301
327
 
302
328
  function hasToolEnabled(session: SessionWithTools, toolName: string): boolean {
303
- return Array.isArray(session?.tools) && session.tools.includes(toolName)
329
+ return toolIdMatches(session?.tools || [], toolName)
304
330
  }
305
331
 
306
332
  function enabledDelegationTools(session: SessionWithTools): DelegateTool[] {
307
333
  const tools: DelegateTool[] = []
308
- if (hasToolEnabled(session, 'claude_code')) tools.push('delegate_to_claude_code')
334
+ if (hasToolEnabled(session, 'claude_code') || hasToolEnabled(session, 'delegate')) tools.push('delegate_to_claude_code')
309
335
  if (hasToolEnabled(session, 'codex_cli')) tools.push('delegate_to_codex_cli')
310
336
  if (hasToolEnabled(session, 'opencode_cli')) tools.push('delegate_to_opencode_cli')
311
337
  return tools
@@ -329,9 +355,10 @@ function getTodaySpendUsd(): number {
329
355
  let total = 0
330
356
  for (const records of Object.values(usage)) {
331
357
  for (const record of records || []) {
332
- const ts = typeof (record as any)?.timestamp === 'number' ? (record as any).timestamp : 0
358
+ const rec = record as Record<string, unknown>
359
+ const ts = typeof rec?.timestamp === 'number' ? rec.timestamp : 0
333
360
  if (ts < minTs) continue
334
- const cost = typeof (record as any)?.estimatedCost === 'number' ? (record as any).estimatedCost : 0
361
+ const cost = typeof rec?.estimatedCost === 'number' ? rec.estimatedCost : 0
335
362
  if (Number.isFinite(cost) && cost > 0) total += cost
336
363
  }
337
364
  }
@@ -385,22 +412,43 @@ function syncSessionFromAgent(sessionId: string): void {
385
412
  }
386
413
  }
387
414
 
388
- function buildAgentSystemPrompt(session: any): string | undefined {
415
+ function buildAgentSystemPrompt(session: Session): string | undefined {
389
416
  if (!session.agentId) return undefined
390
417
  const agents = loadAgents()
391
418
  const agent = agents[session.agentId]
419
+ if (!agent) return undefined
392
420
 
393
421
  const settings = loadSettings()
394
422
  const parts: string[] = []
395
- // Identity block — agent needs to know who it is
396
- const identityLines = [`## My Identity`, `My name is ${agent.name}.`]
397
- if (agent.description) identityLines.push(agent.description)
398
- identityLines.push('I should always refer to myself by this name. I am not "Assistant" — I have my own name and identity.')
399
- parts.push(identityLines.join(' '))
400
- if (settings.userPrompt) parts.push(settings.userPrompt)
423
+
424
+ // 1. Identity & Persona (Grounded OpenClaw Style)
425
+ const identityLines = [`## My Identity`]
426
+ identityLines.push(`Name: ${agent.name}`)
427
+ if (agent.emoji) identityLines.push(`Emoji: ${agent.emoji}`)
428
+ if (agent.creature) identityLines.push(`Creature: ${agent.creature}`)
429
+ if (agent.vibe) identityLines.push(`Vibe: ${agent.vibe}`)
430
+ if (agent.theme) identityLines.push(`Theme: ${agent.theme}`)
431
+ if (agent.description) identityLines.push(`Description: ${agent.description}`)
432
+ identityLines.push('I should always embody this identity. I am not just an "Assistant" — I am a persistent companion with my own soul and presence.')
433
+ parts.push(identityLines.join('\n'))
434
+
435
+ // 2. Runtime & Capabilities (OpenClaw Style)
436
+ const runtimeLines = [
437
+ '## Runtime',
438
+ `os=${process.platform} | host=${os.hostname()} | agent=${agent.id} | provider=${agent.provider} | model=${agent.model}`,
439
+ `capabilities=tools,heartbeats,autonomous_loop,multi_agent_chat`,
440
+ ]
441
+ parts.push(runtimeLines.join('\n'))
442
+
443
+ // 3. User & DateTime Context
444
+ if (settings.userPrompt) parts.push(`## User Instructions\n${settings.userPrompt}`)
401
445
  parts.push(buildCurrentDateTimePromptContext())
402
- if (agent.soul) parts.push(agent.soul)
403
- if (agent.systemPrompt) parts.push(agent.systemPrompt)
446
+
447
+ // 4. Soul & Core Instructions
448
+ if (agent.soul) parts.push(`## Soul\n${agent.soul}`)
449
+ if (agent.systemPrompt) parts.push(`## System Prompt\n${agent.systemPrompt}`)
450
+
451
+ // 5. Skills (SwarmClaw Core)
404
452
  if (agent.skillIds?.length) {
405
453
  const allSkills = loadSkills()
406
454
  for (const skillId of agent.skillIds) {
@@ -408,7 +456,22 @@ function buildAgentSystemPrompt(session: any): string | undefined {
408
456
  if (skill?.content) parts.push(`## Skill: ${skill.name}\n${skill.content}`)
409
457
  }
410
458
  }
411
- if (!parts.length) return undefined
459
+
460
+ // 6. Thinking & Output Format (OpenClaw Style)
461
+ const thinkingHint = [
462
+ '## Output Format',
463
+ 'If your model supports internal reasoning/thinking, put all internal analysis inside <think>...</think> tags.',
464
+ 'Your final response to the user should be clear and concise.',
465
+ 'When you have nothing to say, respond with ONLY: NO_MESSAGE',
466
+ ]
467
+ parts.push(thinkingHint.join('\n'))
468
+
469
+ // 7. Heartbeat Guidance
470
+ parts.push([
471
+ '## Heartbeats',
472
+ 'You run on an autonomous heartbeat. If you receive a heartbeat poll and nothing needs attention, reply exactly: HEARTBEAT_OK',
473
+ ].join('\n'))
474
+
412
475
  return parts.join('\n\n')
413
476
  }
414
477
 
@@ -473,7 +536,7 @@ function normalizeMemoryText(value: string): string {
473
536
  }
474
537
 
475
538
  function shouldStoreAutoMemoryNote(opts: {
476
- session: any
539
+ session: Session
477
540
  source: string
478
541
  internal: boolean
479
542
  message: string
@@ -496,7 +559,7 @@ function shouldStoreAutoMemoryNote(opts: {
496
559
  }
497
560
 
498
561
  function storeAutoMemoryNote(opts: {
499
- session: any
562
+ session: Session
500
563
  message: string
501
564
  response: string
502
565
  source: string
@@ -523,12 +586,12 @@ function storeAutoMemoryNote(opts: {
523
586
  }
524
587
  }
525
588
  const created = db.add({
526
- agentId: session.agentId,
527
- sessionId: session.id,
589
+ agentId: session.agentId as string,
590
+ sessionId: session.id as string,
528
591
  category: 'execution',
529
592
  title,
530
593
  content,
531
- } as any)
594
+ })
532
595
  session.lastAutoMemoryAt = now
533
596
  return created?.id || null
534
597
  } catch {
@@ -537,9 +600,9 @@ function storeAutoMemoryNote(opts: {
537
600
  }
538
601
 
539
602
  export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promise<ExecuteChatTurnResult> {
603
+ const { message } = input
540
604
  const {
541
605
  sessionId,
542
- message,
543
606
  imagePath,
544
607
  imageUrl,
545
608
  attachedFiles,
@@ -582,16 +645,17 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
582
645
  onEvent?.({ t: 'err', text: `Capability policy blocked tools for this run: ${blockedSummary}` })
583
646
  }
584
647
 
585
- // --- Agent monthly budget enforcement ---
648
+ // --- Agent spend-limit enforcement (hourly/daily/monthly) ---
586
649
  if (session.agentId) {
587
650
  const agentsMap = loadAgents()
588
651
  const agent = agentsMap[session.agentId]
589
- if (agent?.monthlyBudget && agent.monthlyBudget > 0) {
590
- const budgetResult = checkBudget(agent)
591
- if (!budgetResult.ok) {
592
- const action = agent.budgetAction || 'warn'
652
+ if (agent) {
653
+ const budgetCheck = checkAgentBudgetLimits(agent)
654
+ const action = agent.budgetAction || 'warn'
655
+
656
+ if (budgetCheck.exceeded.length > 0) {
657
+ const budgetError = budgetCheck.exceeded.map((entry) => entry.message).join(' ')
593
658
  if (action === 'block') {
594
- const budgetError = budgetResult.message || `Agent budget exceeded: $${budgetResult.spend.toFixed(4)} / $${budgetResult.budget.toFixed(2)}`
595
659
  onEvent?.({ t: 'err', text: budgetError })
596
660
 
597
661
  let persisted = false
@@ -616,7 +680,10 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
616
680
  }
617
681
  }
618
682
  // budgetAction === 'warn': emit a warning but continue
619
- onEvent?.({ t: 'status', text: JSON.stringify({ budgetWarning: budgetResult.message }) })
683
+ onEvent?.({ t: 'status', text: JSON.stringify({ budgetWarning: budgetError }) })
684
+ } else if (budgetCheck.warnings.length > 0) {
685
+ const warningText = budgetCheck.warnings.map((entry) => entry.message).join(' ')
686
+ onEvent?.({ t: 'status', text: JSON.stringify({ budgetWarning: warningText }) })
620
687
  }
621
688
  }
622
689
  }
@@ -676,6 +743,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
676
743
  const apiKey = resolveApiKeyForSession(session, provider)
677
744
 
678
745
  if (!internal) {
746
+ const linkAnalysis = await runLinkUnderstanding(message)
679
747
  session.messages.push({
680
748
  role: 'user',
681
749
  text: message,
@@ -685,6 +753,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
685
753
  attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
686
754
  replyToId: input.replyToId || undefined,
687
755
  })
756
+ if (linkAnalysis.length > 0) {
757
+ session.messages.push({
758
+ role: 'assistant',
759
+ kind: 'system',
760
+ text: `[Automated Link Analysis]\n${linkAnalysis.join('\n\n')}`,
761
+ time: Date.now(),
762
+ })
763
+ }
688
764
  session.lastActiveAt = Date.now()
689
765
  saveSessions(sessions)
690
766
  }
@@ -692,9 +768,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
692
768
  const systemPrompt = buildAgentSystemPrompt(session)
693
769
  const toolEvents: MessageToolEvent[] = []
694
770
  const streamErrors: string[] = []
771
+ const accumulatedUsage = { inputTokens: 0, outputTokens: 0, estimatedCost: 0 }
695
772
 
696
773
  let thinkingText = ''
774
+ let streamingPartialText = ''
697
775
  const emit = (ev: SSEEvent) => {
776
+ if (ev.t === 'd' && typeof ev.text === 'string') {
777
+ streamingPartialText += ev.text
778
+ }
698
779
  if (ev.t === 'err' && typeof ev.text === 'string') {
699
780
  const trimmed = ev.text.trim()
700
781
  if (trimmed) {
@@ -705,10 +786,51 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
705
786
  if (ev.t === 'thinking' && ev.text) {
706
787
  thinkingText += ev.text
707
788
  }
789
+ if (ev.t === 'md' && ev.text) {
790
+ try {
791
+ const mdPayload = JSON.parse(ev.text) as Record<string, unknown>
792
+ const usage = mdPayload.usage as { inputTokens?: number; outputTokens?: number; estimatedCost?: number } | undefined
793
+ if (usage) {
794
+ if (typeof usage.inputTokens === 'number') accumulatedUsage.inputTokens += usage.inputTokens
795
+ if (typeof usage.outputTokens === 'number') accumulatedUsage.outputTokens += usage.outputTokens
796
+ if (typeof usage.estimatedCost === 'number') accumulatedUsage.estimatedCost += usage.estimatedCost
797
+ }
798
+ } catch { /* ignore non-JSON md events */ }
799
+ }
708
800
  collectToolEvent(ev, toolEvents)
709
801
  onEvent?.(ev)
710
802
  }
711
803
 
804
+ // Periodic partial save so a browser refresh doesn't lose the in-flight response.
805
+ let lastPartialSaveLen = 0
806
+ const PARTIAL_SAVE_INTERVAL_MS = 5000
807
+ const partialSaveTimer = setInterval(() => {
808
+ if (streamingPartialText.length > lastPartialSaveLen) {
809
+ lastPartialSaveLen = streamingPartialText.length
810
+ try {
811
+ const fresh = loadSessions()
812
+ const current = fresh[sessionId]
813
+ if (!current) return
814
+ const partialMsg: Message = {
815
+ role: 'assistant',
816
+ text: streamingPartialText,
817
+ time: Date.now(),
818
+ streaming: true,
819
+ toolEvents: toolEvents.length ? [...toolEvents] : undefined,
820
+ }
821
+ const lastMsg = current.messages.at(-1)
822
+ if (lastMsg?.streaming) {
823
+ current.messages[current.messages.length - 1] = partialMsg
824
+ } else {
825
+ current.messages.push(partialMsg)
826
+ }
827
+ fresh[sessionId] = current
828
+ saveSessions(fresh)
829
+ notify(`messages:${sessionId}`)
830
+ } catch { /* partial save is best-effort */ }
831
+ }
832
+ }, PARTIAL_SAVE_INTERVAL_MS)
833
+
712
834
  const parseAndEmit = (raw: string) => {
713
835
  const lines = raw.split('\n').filter(Boolean)
714
836
  for (const line of lines) {
@@ -736,8 +858,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
736
858
  // Capture provider-reported usage for the direct (non-tools) path.
737
859
  // Uses a mutable object because TS can't track callback mutations on plain variables.
738
860
  const directUsage = { inputTokens: 0, outputTokens: 0, received: false }
861
+ const responseCacheConfig = resolveLlmResponseCacheConfig(appSettings)
862
+ let responseCacheHit = false
863
+ let responseCacheInput: LlmResponseCacheKeyInput | null = null
739
864
  const hasTools = !!sessionForRun.tools?.length && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
740
865
 
866
+ let durationMs = 0
867
+ const startTs = Date.now()
741
868
  try {
742
869
  // Heartbeat runs get a small tail of recent messages so the agent can see
743
870
  // prior findings and avoid repeating the same searches. Full history is
@@ -747,33 +874,78 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
747
874
  : undefined
748
875
 
749
876
  console.log(`[chat-execution] provider=${providerType}, hasTools=${hasTools}, imagePath=${imagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, tools=${(sessionForRun.tools || []).length}`)
750
-
751
- fullResponse = hasTools
752
- ? (await streamAgentChat({
753
- session: sessionForRun,
754
- message,
755
- imagePath,
756
- attachedFiles,
757
- apiKey,
758
- systemPrompt,
759
- write: (raw) => parseAndEmit(raw),
760
- history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
761
- signal: abortController.signal,
762
- })).fullText
763
- : await provider.handler.streamChat({
877
+ if (hasTools) {
878
+ fullResponse = (await streamAgentChat({
879
+ session: sessionForRun,
880
+ message: message,
881
+ imagePath,
882
+ attachedFiles,
883
+ apiKey,
884
+ systemPrompt,
885
+ write: (raw) => parseAndEmit(raw),
886
+ history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
887
+ signal: abortController.signal,
888
+ })).fullText
889
+ } else {
890
+ const directHistorySnapshot = isAutoRunNoHistory
891
+ ? getSessionMessages(sessionId).slice(-6)
892
+ : applyContextClearBoundary(getSessionMessages(sessionId))
893
+ responseCacheInput = {
894
+ provider: providerType,
895
+ model: sessionForRun.model,
896
+ apiEndpoint: sessionForRun.apiEndpoint || '',
897
+ systemPrompt,
898
+ message: message,
899
+ imagePath,
900
+ imageUrl,
901
+ attachedFiles,
902
+ history: directHistorySnapshot,
903
+ }
904
+ const canUseResponseCache = !internal && responseCacheConfig.enabled
905
+ const cached = canUseResponseCache
906
+ ? getCachedLlmResponse(responseCacheInput, responseCacheConfig)
907
+ : null
908
+ if (cached) {
909
+ responseCacheHit = true
910
+ fullResponse = cached.text
911
+ emit({
912
+ t: 'md',
913
+ text: JSON.stringify({
914
+ cache: {
915
+ hit: true,
916
+ ageMs: cached.ageMs,
917
+ provider: cached.provider,
918
+ model: cached.model,
919
+ },
920
+ }),
921
+ })
922
+ emit({ t: 'd', text: cached.text })
923
+ } else {
924
+ fullResponse = await provider.handler.streamChat({
764
925
  session: sessionForRun,
765
- message,
926
+ message: message,
766
927
  imagePath,
767
928
  apiKey,
768
929
  systemPrompt,
769
930
  write: (raw: string) => parseAndEmit(raw),
770
931
  active,
771
- loadHistory: isAutoRunNoHistory ? () => getSessionMessages(sessionId).slice(-6) : (sid: string) => applyContextClearBoundary(getSessionMessages(sid)),
932
+ loadHistory: (sid: string) => {
933
+ if (sid === sessionId) return directHistorySnapshot
934
+ return isAutoRunNoHistory
935
+ ? getSessionMessages(sid).slice(-6)
936
+ : applyContextClearBoundary(getSessionMessages(sid))
937
+ },
772
938
  onUsage: (u) => { directUsage.inputTokens = u.inputTokens; directUsage.outputTokens = u.outputTokens; directUsage.received = true },
773
939
  signal: abortController.signal,
774
940
  })
775
- } catch (err: any) {
776
- errorMessage = err?.message || String(err)
941
+ if (canUseResponseCache && responseCacheInput && fullResponse) {
942
+ setCachedLlmResponse(responseCacheInput, fullResponse, responseCacheConfig)
943
+ }
944
+ }
945
+ }
946
+ durationMs = Date.now() - startTs
947
+ } catch (err: unknown) {
948
+ errorMessage = err instanceof Error ? err.message : String(err)
777
949
  const failureText = errorMessage || 'Run failed.'
778
950
  markProviderFailure(providerType, failureText)
779
951
  emit({ t: 'err', text: failureText })
@@ -784,6 +956,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
784
956
  error: failureText,
785
957
  })
786
958
  } finally {
959
+ clearInterval(partialSaveTimer)
787
960
  active.delete(sessionId)
788
961
  if (signal) signal.removeEventListener('abort', abortFromOutside)
789
962
  }
@@ -794,7 +967,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
794
967
 
795
968
  // Record usage for the direct (non-tools) streamChat path.
796
969
  // streamAgentChat already calls appendUsage internally for the tools path.
797
- if (!hasTools && fullResponse && !errorMessage) {
970
+ if (!hasTools && fullResponse && !errorMessage && !responseCacheHit) {
798
971
  const inputTokens = directUsage.received ? directUsage.inputTokens : Math.ceil(message.length / 4)
799
972
  const outputTokens = directUsage.received ? directUsage.outputTokens : Math.ceil(fullResponse.length / 4)
800
973
  const totalTokens = inputTokens + outputTokens
@@ -811,6 +984,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
811
984
  totalTokens,
812
985
  estimatedCost: cost,
813
986
  timestamp: Date.now(),
987
+ durationMs,
814
988
  }
815
989
  appendUsage(sessionId, usageRecord)
816
990
  emit({
@@ -828,6 +1002,54 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
828
1002
  : null
829
1003
  const calledNames = new Set((toolEvents || []).map((t) => t.name))
830
1004
 
1005
+ const translateToolInvocation = (
1006
+ requestedName: string,
1007
+ rawArgs: Record<string, unknown>,
1008
+ ): { toolName: string; args: Record<string, unknown> } => {
1009
+ if (requestedName === 'web_search') {
1010
+ return {
1011
+ toolName: 'web',
1012
+ args: {
1013
+ action: 'search',
1014
+ query: typeof rawArgs.query === 'string' ? rawArgs.query : message.trim(),
1015
+ maxResults: typeof rawArgs.maxResults === 'number' ? rawArgs.maxResults : 5,
1016
+ },
1017
+ }
1018
+ }
1019
+ if (requestedName === 'web_fetch') {
1020
+ return {
1021
+ toolName: 'web',
1022
+ args: {
1023
+ action: 'fetch',
1024
+ url: rawArgs.url,
1025
+ },
1026
+ }
1027
+ }
1028
+ if (requestedName === 'delegate_to_claude_code') {
1029
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
1030
+ }
1031
+ if (requestedName === 'delegate_to_codex_cli') {
1032
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'codex' } }
1033
+ }
1034
+ if (requestedName === 'delegate_to_opencode_cli') {
1035
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'opencode' } }
1036
+ }
1037
+
1038
+ const managePrefix = 'manage_'
1039
+ if (requestedName.startsWith(managePrefix) && requestedName !== 'manage_platform') {
1040
+ const resource = requestedName.slice(managePrefix.length)
1041
+ if (resource) {
1042
+ const { action, id, data, ...rest } = rawArgs
1043
+ return {
1044
+ toolName: 'manage_platform',
1045
+ args: { resource, action, id, data, ...rest },
1046
+ }
1047
+ }
1048
+ }
1049
+
1050
+ return { toolName: requestedName, args: rawArgs }
1051
+ }
1052
+
831
1053
  const invokeSessionTool = async (toolName: string, args: Record<string, unknown>, failurePrefix: string): Promise<boolean> => {
832
1054
  const blockedReason = resolveConcreteToolPolicyBlock(toolName, toolPolicy, appSettings)
833
1055
  if (blockedReason) {
@@ -851,11 +1073,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
851
1073
  mcpDisabledTools: agent?.mcpDisabledTools,
852
1074
  })
853
1075
  try {
854
- const selectedTool = tools.find((t: any) => t?.name === toolName) as any
1076
+ const translated = translateToolInvocation(toolName, args)
1077
+ const selectedTool = tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
855
1078
  if (!selectedTool?.invoke) return false
856
- const toolInput = JSON.stringify(args)
1079
+ const toolInput = JSON.stringify(translated.args)
857
1080
  emit({ t: 'tool_call', toolName, toolInput })
858
- const toolOutput = await selectedTool.invoke(args)
1081
+ const toolOutput = await selectedTool.invoke(translated.args)
859
1082
  const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
860
1083
  emit({ t: 'tool_result', toolName, toolOutput: outputText })
861
1084
  // Don't overwrite fullResponse with raw tool output — it's already captured
@@ -867,8 +1090,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
867
1090
  }
868
1091
  calledNames.add(toolName)
869
1092
  return true
870
- } catch (forceErr: any) {
871
- emit({ t: 'err', text: `${failurePrefix}: ${forceErr?.message || String(forceErr)}` })
1093
+ } catch (forceErr: unknown) {
1094
+ emit({ t: 'err', text: `${failurePrefix}: ${forceErr instanceof Error ? forceErr.message : String(forceErr)}` })
872
1095
  return false
873
1096
  } finally {
874
1097
  await cleanup()
@@ -1024,6 +1247,18 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1024
1247
  let heartbeatClassification: 'suppress' | 'strip' | 'keep' | null = null
1025
1248
  if (isHeartbeatRun && textForPersistence.length > 0) {
1026
1249
  heartbeatClassification = classifyHeartbeatResponse(textForPersistence, heartbeatConfig?.ackMaxChars ?? 300, toolEvents.length > 0)
1250
+
1251
+ // Deduplication logic from OpenClaw (nagging prevention)
1252
+ // If the model repeats itself exactly within 24h, suppress the heartbeat alert.
1253
+ if (heartbeatClassification !== 'suppress' && !toolEvents.length) {
1254
+ const prevText = session.lastHeartbeatText || ''
1255
+ const prevSentAt = session.lastHeartbeatSentAt || 0
1256
+ const isDuplicate = prevText.trim() === textForPersistence.trim()
1257
+ && (Date.now() - prevSentAt) < 24 * 60 * 60 * 1000
1258
+ if (isDuplicate) {
1259
+ heartbeatClassification = 'suppress'
1260
+ }
1261
+ }
1027
1262
  }
1028
1263
 
1029
1264
  // Emit WS notification for every heartbeat completion so UI can show pulse
@@ -1043,8 +1278,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1043
1278
  let changed = false
1044
1279
  const persistField = (key: string, value: unknown) => {
1045
1280
  const normalized = normalizeResumeId(value)
1046
- if ((current as any)[key] !== normalized) {
1047
- ;(current as any)[key] = normalized
1281
+ if ((current as Record<string, unknown>)[key] !== normalized) {
1282
+ ;(current as Record<string, unknown>)[key] = normalized
1048
1283
  changed = true
1049
1284
  }
1050
1285
  }
@@ -1058,10 +1293,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1058
1293
  const currentResume = (current.delegateResumeIds && typeof current.delegateResumeIds === 'object')
1059
1294
  ? current.delegateResumeIds
1060
1295
  : {}
1296
+ const sr = sourceResume as Record<string, unknown>
1297
+ const cr = currentResume as Record<string, unknown>
1061
1298
  const nextResume = {
1062
- claudeCode: normalizeResumeId((sourceResume as any).claudeCode ?? (currentResume as any).claudeCode),
1063
- codex: normalizeResumeId((sourceResume as any).codex ?? (currentResume as any).codex),
1064
- opencode: normalizeResumeId((sourceResume as any).opencode ?? (currentResume as any).opencode),
1299
+ claudeCode: normalizeResumeId(sr.claudeCode ?? cr.claudeCode),
1300
+ codex: normalizeResumeId(sr.codex ?? cr.codex),
1301
+ opencode: normalizeResumeId(sr.opencode ?? cr.opencode),
1065
1302
  }
1066
1303
  if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
1067
1304
  current.delegateResumeIds = nextResume
@@ -1070,7 +1307,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1070
1307
  }
1071
1308
 
1072
1309
  if (shouldPersistAssistant) {
1073
- const persistedKind = internal && source !== 'session-awakening' ? 'heartbeat' : 'chat'
1310
+ const persistedKind = internal && source === 'heartbeat' ? 'heartbeat' : 'chat'
1074
1311
  const persistedText = heartbeatClassification === 'strip'
1075
1312
  ? textForPersistence.replace(/HEARTBEAT_OK/gi, '').trim()
1076
1313
  : textForPersistence
@@ -1084,7 +1321,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1084
1321
  kind: persistedKind,
1085
1322
  }
1086
1323
  const previous = current.messages.at(-1)
1087
- if (shouldReplaceRecentAssistantMessage({
1324
+ if (previous?.streaming || shouldReplaceRecentAssistantMessage({
1088
1325
  previous,
1089
1326
  nextToolEvents: toolEvents,
1090
1327
  nextKind: persistedKind,
@@ -1094,6 +1331,10 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1094
1331
  } else {
1095
1332
  current.messages.push(nextAssistantMessage)
1096
1333
  }
1334
+ if (isHeartbeatRun) {
1335
+ current.lastHeartbeatText = persistedText
1336
+ current.lastHeartbeatSentAt = nowTs
1337
+ }
1097
1338
  changed = true
1098
1339
 
1099
1340
  // Conversation tone detection
@@ -1107,12 +1348,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1107
1348
  // Target routing for non-suppressed heartbeat alerts
1108
1349
  if (isHeartbeatRun && heartbeatConfig?.target && heartbeatConfig.target !== 'none' && heartbeatConfig.showAlerts !== false) {
1109
1350
  try {
1351
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1110
1352
  const { listRunningConnectors, sendConnectorMessage } = require('./connectors/manager')
1111
1353
  let connectorId: string | undefined
1112
1354
  let channelId: string | undefined
1113
1355
  if (heartbeatConfig.target === 'last') {
1114
1356
  const running = listRunningConnectors()
1115
- const first = running.find((c: any) => c.recentChannelId)
1357
+ const first = running.find((c: { recentChannelId?: string }) => c.recentChannelId)
1116
1358
  if (first) {
1117
1359
  connectorId = first.id
1118
1360
  channelId = first.recentChannelId
@@ -1153,14 +1395,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1153
1395
  session: current,
1154
1396
  source,
1155
1397
  internal,
1156
- message,
1398
+ message: message,
1157
1399
  response: textForPersistence,
1158
1400
  now: Date.now(),
1159
1401
  })
1160
1402
  if (autoMemoryEligible) {
1161
1403
  const storedId = storeAutoMemoryNote({
1162
1404
  session: current,
1163
- message,
1405
+ message: message,
1164
1406
  response: textForPersistence,
1165
1407
  source,
1166
1408
  now: Date.now(),
@@ -1169,7 +1411,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1169
1411
  }
1170
1412
 
1171
1413
  // Don't extend idle timeout for heartbeat runs — only user-initiated activity counts
1172
- if (source !== 'heartbeat' && source !== 'main-loop-followup') {
1414
+ if (source !== 'heartbeat' && source !== 'heartbeat-wake' && source !== 'main-loop-followup') {
1173
1415
  current.lastActiveAt = Date.now()
1174
1416
  }
1175
1417
  fresh[sessionId] = current
@@ -1184,5 +1426,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1184
1426
  persisted: shouldPersistAssistant,
1185
1427
  toolEvents,
1186
1428
  error: errorMessage,
1429
+ inputTokens: accumulatedUsage.inputTokens || undefined,
1430
+ outputTokens: accumulatedUsage.outputTokens || undefined,
1431
+ estimatedCost: accumulatedUsage.estimatedCost || undefined,
1187
1432
  }
1188
1433
  }