@swarmclawai/swarmclaw 0.6.8 → 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 (166) hide show
  1. package/README.md +70 -45
  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 +18 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  9. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  10. package/src/app/api/memory/route.ts +36 -5
  11. package/src/app/api/notifications/route.ts +3 -0
  12. package/src/app/api/plugins/install/route.ts +57 -5
  13. package/src/app/api/plugins/marketplace/route.ts +73 -22
  14. package/src/app/api/plugins/route.ts +61 -1
  15. package/src/app/api/plugins/ui/route.ts +34 -0
  16. package/src/app/api/settings/route.ts +62 -0
  17. package/src/app/api/setup/doctor/route.ts +22 -5
  18. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  19. package/src/app/api/tasks/[id]/route.ts +11 -3
  20. package/src/app/api/tasks/route.ts +8 -2
  21. package/src/app/globals.css +27 -0
  22. package/src/app/page.tsx +10 -5
  23. package/src/cli/index.js +13 -0
  24. package/src/components/activity/activity-feed.tsx +9 -2
  25. package/src/components/agents/agent-avatar.tsx +5 -1
  26. package/src/components/agents/agent-card.tsx +55 -9
  27. package/src/components/agents/agent-sheet.tsx +86 -29
  28. package/src/components/agents/inspector-panel.tsx +1 -1
  29. package/src/components/auth/access-key-gate.tsx +63 -54
  30. package/src/components/auth/user-picker.tsx +37 -32
  31. package/src/components/chat/chat-area.tsx +11 -0
  32. package/src/components/chat/chat-header.tsx +69 -25
  33. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  34. package/src/components/chat/code-block.tsx +3 -1
  35. package/src/components/chat/exec-approval-card.tsx +8 -1
  36. package/src/components/chat/message-bubble.tsx +164 -4
  37. package/src/components/chat/message-list.tsx +30 -4
  38. package/src/components/chat/session-approval-card.tsx +80 -0
  39. package/src/components/chat/streaming-bubble.tsx +6 -5
  40. package/src/components/chat/thinking-indicator.tsx +48 -12
  41. package/src/components/chat/tool-request-banner.tsx +39 -20
  42. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  43. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  44. package/src/components/connectors/connector-list.tsx +33 -11
  45. package/src/components/connectors/connector-sheet.tsx +29 -6
  46. package/src/components/home/home-view.tsx +20 -14
  47. package/src/components/input/chat-input.tsx +22 -1
  48. package/src/components/knowledge/knowledge-list.tsx +17 -18
  49. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  50. package/src/components/layout/app-layout.tsx +73 -21
  51. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  52. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  53. package/src/components/memory/memory-list.tsx +20 -13
  54. package/src/components/plugins/plugin-list.tsx +213 -59
  55. package/src/components/plugins/plugin-sheet.tsx +119 -24
  56. package/src/components/projects/project-list.tsx +17 -9
  57. package/src/components/providers/provider-list.tsx +21 -6
  58. package/src/components/providers/provider-sheet.tsx +42 -25
  59. package/src/components/runs/run-list.tsx +17 -13
  60. package/src/components/schedules/schedule-card.tsx +10 -3
  61. package/src/components/schedules/schedule-list.tsx +2 -2
  62. package/src/components/schedules/schedule-sheet.tsx +19 -7
  63. package/src/components/secrets/secret-sheet.tsx +7 -2
  64. package/src/components/secrets/secrets-list.tsx +18 -5
  65. package/src/components/sessions/new-session-sheet.tsx +183 -376
  66. package/src/components/sessions/session-card.tsx +10 -2
  67. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  68. package/src/components/shared/command-palette.tsx +13 -5
  69. package/src/components/shared/empty-state.tsx +20 -8
  70. package/src/components/shared/notification-center.tsx +134 -86
  71. package/src/components/shared/profile-sheet.tsx +4 -0
  72. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  73. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  74. package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
  75. package/src/components/skills/clawhub-browser.tsx +1 -0
  76. package/src/components/skills/skill-list.tsx +31 -12
  77. package/src/components/skills/skill-sheet.tsx +20 -7
  78. package/src/components/tasks/approvals-panel.tsx +170 -66
  79. package/src/components/tasks/task-board.tsx +20 -12
  80. package/src/components/tasks/task-card.tsx +21 -7
  81. package/src/components/tasks/task-column.tsx +4 -3
  82. package/src/components/tasks/task-list.tsx +1 -1
  83. package/src/components/tasks/task-sheet.tsx +130 -1
  84. package/src/components/ui/dialog.tsx +1 -0
  85. package/src/components/ui/sheet.tsx +1 -0
  86. package/src/components/usage/metrics-dashboard.tsx +66 -64
  87. package/src/components/wallets/wallet-panel.tsx +65 -41
  88. package/src/components/wallets/wallet-section.tsx +9 -3
  89. package/src/components/webhooks/webhook-list.tsx +21 -12
  90. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  91. package/src/lib/approval-display.test.ts +45 -0
  92. package/src/lib/approval-display.ts +62 -0
  93. package/src/lib/clipboard.ts +38 -0
  94. package/src/lib/memory.ts +8 -0
  95. package/src/lib/providers/claude-cli.ts +5 -3
  96. package/src/lib/providers/index.ts +67 -21
  97. package/src/lib/runtime-loop.ts +3 -2
  98. package/src/lib/server/approvals.ts +150 -0
  99. package/src/lib/server/chat-execution.ts +223 -62
  100. package/src/lib/server/clawhub-client.ts +82 -6
  101. package/src/lib/server/connectors/manager.ts +27 -1
  102. package/src/lib/server/cost.test.ts +73 -0
  103. package/src/lib/server/cost.ts +165 -34
  104. package/src/lib/server/daemon-state.ts +42 -0
  105. package/src/lib/server/data-dir.ts +18 -1
  106. package/src/lib/server/integrity-monitor.ts +208 -0
  107. package/src/lib/server/llm-response-cache.test.ts +102 -0
  108. package/src/lib/server/llm-response-cache.ts +227 -0
  109. package/src/lib/server/main-agent-loop.ts +1 -1
  110. package/src/lib/server/main-session.ts +6 -3
  111. package/src/lib/server/mcp-conformance.test.ts +18 -0
  112. package/src/lib/server/mcp-conformance.ts +233 -0
  113. package/src/lib/server/memory-db.ts +180 -17
  114. package/src/lib/server/memory-retrieval.test.ts +56 -0
  115. package/src/lib/server/orchestrator-lg.ts +4 -1
  116. package/src/lib/server/orchestrator.ts +4 -3
  117. package/src/lib/server/plugins.ts +650 -142
  118. package/src/lib/server/process-manager.ts +18 -0
  119. package/src/lib/server/queue.ts +253 -11
  120. package/src/lib/server/runtime-settings.ts +9 -0
  121. package/src/lib/server/session-run-manager.test.ts +23 -0
  122. package/src/lib/server/session-run-manager.ts +11 -1
  123. package/src/lib/server/session-tools/canvas.ts +85 -50
  124. package/src/lib/server/session-tools/chatroom.ts +130 -127
  125. package/src/lib/server/session-tools/connector.ts +233 -454
  126. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  127. package/src/lib/server/session-tools/crud.ts +84 -7
  128. package/src/lib/server/session-tools/delegate.ts +351 -752
  129. package/src/lib/server/session-tools/discovery.ts +198 -0
  130. package/src/lib/server/session-tools/edit_file.ts +82 -0
  131. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  132. package/src/lib/server/session-tools/file.ts +257 -425
  133. package/src/lib/server/session-tools/git.ts +87 -47
  134. package/src/lib/server/session-tools/http.ts +85 -33
  135. package/src/lib/server/session-tools/index.ts +205 -160
  136. package/src/lib/server/session-tools/memory.ts +152 -265
  137. package/src/lib/server/session-tools/monitor.ts +126 -0
  138. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  139. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  140. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  141. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  142. package/src/lib/server/session-tools/platform.ts +86 -0
  143. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  144. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  145. package/src/lib/server/session-tools/sandbox.ts +175 -148
  146. package/src/lib/server/session-tools/schedule.ts +66 -31
  147. package/src/lib/server/session-tools/session-info.ts +104 -410
  148. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  149. package/src/lib/server/session-tools/shell.ts +171 -143
  150. package/src/lib/server/session-tools/subagent.ts +77 -77
  151. package/src/lib/server/session-tools/wallet.ts +182 -106
  152. package/src/lib/server/session-tools/web.ts +179 -349
  153. package/src/lib/server/storage.ts +24 -0
  154. package/src/lib/server/stream-agent-chat.ts +301 -244
  155. package/src/lib/server/task-quality-gate.test.ts +44 -0
  156. package/src/lib/server/task-quality-gate.ts +67 -0
  157. package/src/lib/server/task-validation.test.ts +78 -0
  158. package/src/lib/server/task-validation.ts +67 -2
  159. package/src/lib/server/tool-aliases.ts +68 -0
  160. package/src/lib/server/tool-capability-policy.ts +23 -5
  161. package/src/lib/tasks.ts +7 -1
  162. package/src/lib/tool-definitions.ts +23 -23
  163. package/src/lib/validation/schemas.ts +12 -0
  164. package/src/lib/view-routes.ts +2 -24
  165. package/src/stores/use-app-store.ts +23 -1
  166. package/src/types/index.ts +121 -7
@@ -14,19 +14,28 @@ import {
14
14
  active,
15
15
  } from './storage'
16
16
  import { getProvider } from '@/lib/providers'
17
- import { estimateCost, checkBudget } from './cost'
17
+ import { estimateCost, checkAgentBudgetLimits } from './cost'
18
18
  import { log } from './logger'
19
19
  import { logExecution } from './execution-log'
20
20
  import { streamAgentChat } from './stream-agent-chat'
21
21
  import { runLinkUnderstanding } from './link-understanding'
22
22
  import { buildSessionTools } from './session-tools'
23
+ import type { StructuredToolInterface } from '@langchain/core/tools'
24
+ import type { Session } from '@/types'
23
25
  import { stripMainLoopMetaForPersistence } from './main-agent-loop'
24
26
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
25
27
  import { getMemoryDb } from './memory-db'
26
28
  import { routeTaskIntent } from './capability-router'
27
29
  import { notify } from './ws-hub'
28
30
  import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
31
+ import { toolIdMatches } from './tool-aliases'
29
32
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
33
+ import {
34
+ getCachedLlmResponse,
35
+ resolveLlmResponseCacheConfig,
36
+ setCachedLlmResponse,
37
+ type LlmResponseCacheKeyInput,
38
+ } from './llm-response-cache'
30
39
  import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
31
40
  import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
32
41
  import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
@@ -148,20 +157,32 @@ function requestedToolNamesFromMessage(message: string): string[] {
148
157
  'manage_connectors',
149
158
  'manage_sessions',
150
159
  'manage_secrets',
160
+ 'manage_capabilities',
161
+ 'manage_platform',
162
+ 'manage_chatrooms',
163
+ 'search_marketplace',
164
+ 'monitor_tool',
165
+ 'plugin_creator_tool',
151
166
  'memory_tool',
167
+ 'wallet_tool',
168
+ 'http_request',
169
+ 'send_file',
152
170
  'browser',
153
- 'web_search',
154
- 'web_fetch',
155
- 'execute_command',
156
- 'read_file',
157
- 'write_file',
158
- 'list_files',
159
- 'copy_file',
160
- 'move_file',
161
- 'delete_file',
171
+ 'web',
172
+ 'shell',
173
+ 'files',
162
174
  'edit_file',
163
- 'send_file',
164
- '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',
165
186
  ]
166
187
  return candidates.filter((name) => lower.includes(name.toLowerCase()))
167
188
  }
@@ -305,12 +326,12 @@ function extractDelegationTask(message: string, toolName: string): string | null
305
326
  }
306
327
 
307
328
  function hasToolEnabled(session: SessionWithTools, toolName: string): boolean {
308
- return Array.isArray(session?.tools) && session.tools.includes(toolName)
329
+ return toolIdMatches(session?.tools || [], toolName)
309
330
  }
310
331
 
311
332
  function enabledDelegationTools(session: SessionWithTools): DelegateTool[] {
312
333
  const tools: DelegateTool[] = []
313
- 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')
314
335
  if (hasToolEnabled(session, 'codex_cli')) tools.push('delegate_to_codex_cli')
315
336
  if (hasToolEnabled(session, 'opencode_cli')) tools.push('delegate_to_opencode_cli')
316
337
  return tools
@@ -334,9 +355,10 @@ function getTodaySpendUsd(): number {
334
355
  let total = 0
335
356
  for (const records of Object.values(usage)) {
336
357
  for (const record of records || []) {
337
- 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
338
360
  if (ts < minTs) continue
339
- const cost = typeof (record as any)?.estimatedCost === 'number' ? (record as any).estimatedCost : 0
361
+ const cost = typeof rec?.estimatedCost === 'number' ? rec.estimatedCost : 0
340
362
  if (Number.isFinite(cost) && cost > 0) total += cost
341
363
  }
342
364
  }
@@ -390,7 +412,7 @@ function syncSessionFromAgent(sessionId: string): void {
390
412
  }
391
413
  }
392
414
 
393
- function buildAgentSystemPrompt(session: any): string | undefined {
415
+ function buildAgentSystemPrompt(session: Session): string | undefined {
394
416
  if (!session.agentId) return undefined
395
417
  const agents = loadAgents()
396
418
  const agent = agents[session.agentId]
@@ -514,7 +536,7 @@ function normalizeMemoryText(value: string): string {
514
536
  }
515
537
 
516
538
  function shouldStoreAutoMemoryNote(opts: {
517
- session: any
539
+ session: Session
518
540
  source: string
519
541
  internal: boolean
520
542
  message: string
@@ -537,7 +559,7 @@ function shouldStoreAutoMemoryNote(opts: {
537
559
  }
538
560
 
539
561
  function storeAutoMemoryNote(opts: {
540
- session: any
562
+ session: Session
541
563
  message: string
542
564
  response: string
543
565
  source: string
@@ -564,12 +586,12 @@ function storeAutoMemoryNote(opts: {
564
586
  }
565
587
  }
566
588
  const created = db.add({
567
- agentId: session.agentId,
568
- sessionId: session.id,
589
+ agentId: session.agentId as string,
590
+ sessionId: session.id as string,
569
591
  category: 'execution',
570
592
  title,
571
593
  content,
572
- } as any)
594
+ })
573
595
  session.lastAutoMemoryAt = now
574
596
  return created?.id || null
575
597
  } catch {
@@ -578,9 +600,9 @@ function storeAutoMemoryNote(opts: {
578
600
  }
579
601
 
580
602
  export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promise<ExecuteChatTurnResult> {
603
+ const { message } = input
581
604
  const {
582
605
  sessionId,
583
- message,
584
606
  imagePath,
585
607
  imageUrl,
586
608
  attachedFiles,
@@ -623,16 +645,17 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
623
645
  onEvent?.({ t: 'err', text: `Capability policy blocked tools for this run: ${blockedSummary}` })
624
646
  }
625
647
 
626
- // --- Agent monthly budget enforcement ---
648
+ // --- Agent spend-limit enforcement (hourly/daily/monthly) ---
627
649
  if (session.agentId) {
628
650
  const agentsMap = loadAgents()
629
651
  const agent = agentsMap[session.agentId]
630
- if (agent?.monthlyBudget && agent.monthlyBudget > 0) {
631
- const budgetResult = checkBudget(agent)
632
- if (!budgetResult.ok) {
633
- 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(' ')
634
658
  if (action === 'block') {
635
- const budgetError = budgetResult.message || `Agent budget exceeded: $${budgetResult.spend.toFixed(4)} / $${budgetResult.budget.toFixed(2)}`
636
659
  onEvent?.({ t: 'err', text: budgetError })
637
660
 
638
661
  let persisted = false
@@ -657,7 +680,10 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
657
680
  }
658
681
  }
659
682
  // budgetAction === 'warn': emit a warning but continue
660
- 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 }) })
661
687
  }
662
688
  }
663
689
  }
@@ -745,7 +771,11 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
745
771
  const accumulatedUsage = { inputTokens: 0, outputTokens: 0, estimatedCost: 0 }
746
772
 
747
773
  let thinkingText = ''
774
+ let streamingPartialText = ''
748
775
  const emit = (ev: SSEEvent) => {
776
+ if (ev.t === 'd' && typeof ev.text === 'string') {
777
+ streamingPartialText += ev.text
778
+ }
749
779
  if (ev.t === 'err' && typeof ev.text === 'string') {
750
780
  const trimmed = ev.text.trim()
751
781
  if (trimmed) {
@@ -771,6 +801,36 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
771
801
  onEvent?.(ev)
772
802
  }
773
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
+
774
834
  const parseAndEmit = (raw: string) => {
775
835
  const lines = raw.split('\n').filter(Boolean)
776
836
  for (const line of lines) {
@@ -798,6 +858,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
798
858
  // Capture provider-reported usage for the direct (non-tools) path.
799
859
  // Uses a mutable object because TS can't track callback mutations on plain variables.
800
860
  const directUsage = { inputTokens: 0, outputTokens: 0, received: false }
861
+ const responseCacheConfig = resolveLlmResponseCacheConfig(appSettings)
862
+ let responseCacheHit = false
863
+ let responseCacheInput: LlmResponseCacheKeyInput | null = null
801
864
  const hasTools = !!sessionForRun.tools?.length && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
802
865
 
803
866
  let durationMs = 0
@@ -811,30 +874,75 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
811
874
  : undefined
812
875
 
813
876
  console.log(`[chat-execution] provider=${providerType}, hasTools=${hasTools}, imagePath=${imagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, tools=${(sessionForRun.tools || []).length}`)
814
- fullResponse = hasTools
815
- ? (await streamAgentChat({
816
- session: sessionForRun,
817
- message,
818
- imagePath,
819
- attachedFiles,
820
- apiKey,
821
- systemPrompt,
822
- write: (raw) => parseAndEmit(raw),
823
- history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
824
- signal: abortController.signal,
825
- })).fullText
826
- : 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({
827
925
  session: sessionForRun,
828
- message,
926
+ message: message,
829
927
  imagePath,
830
928
  apiKey,
831
929
  systemPrompt,
832
930
  write: (raw: string) => parseAndEmit(raw),
833
931
  active,
834
- 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
+ },
835
938
  onUsage: (u) => { directUsage.inputTokens = u.inputTokens; directUsage.outputTokens = u.outputTokens; directUsage.received = true },
836
939
  signal: abortController.signal,
837
940
  })
941
+ if (canUseResponseCache && responseCacheInput && fullResponse) {
942
+ setCachedLlmResponse(responseCacheInput, fullResponse, responseCacheConfig)
943
+ }
944
+ }
945
+ }
838
946
  durationMs = Date.now() - startTs
839
947
  } catch (err: unknown) {
840
948
  errorMessage = err instanceof Error ? err.message : String(err)
@@ -848,6 +956,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
848
956
  error: failureText,
849
957
  })
850
958
  } finally {
959
+ clearInterval(partialSaveTimer)
851
960
  active.delete(sessionId)
852
961
  if (signal) signal.removeEventListener('abort', abortFromOutside)
853
962
  }
@@ -858,7 +967,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
858
967
 
859
968
  // Record usage for the direct (non-tools) streamChat path.
860
969
  // streamAgentChat already calls appendUsage internally for the tools path.
861
- if (!hasTools && fullResponse && !errorMessage) {
970
+ if (!hasTools && fullResponse && !errorMessage && !responseCacheHit) {
862
971
  const inputTokens = directUsage.received ? directUsage.inputTokens : Math.ceil(message.length / 4)
863
972
  const outputTokens = directUsage.received ? directUsage.outputTokens : Math.ceil(fullResponse.length / 4)
864
973
  const totalTokens = inputTokens + outputTokens
@@ -893,6 +1002,54 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
893
1002
  : null
894
1003
  const calledNames = new Set((toolEvents || []).map((t) => t.name))
895
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
+
896
1053
  const invokeSessionTool = async (toolName: string, args: Record<string, unknown>, failurePrefix: string): Promise<boolean> => {
897
1054
  const blockedReason = resolveConcreteToolPolicyBlock(toolName, toolPolicy, appSettings)
898
1055
  if (blockedReason) {
@@ -916,11 +1073,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
916
1073
  mcpDisabledTools: agent?.mcpDisabledTools,
917
1074
  })
918
1075
  try {
919
- 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
920
1078
  if (!selectedTool?.invoke) return false
921
- const toolInput = JSON.stringify(args)
1079
+ const toolInput = JSON.stringify(translated.args)
922
1080
  emit({ t: 'tool_call', toolName, toolInput })
923
- const toolOutput = await selectedTool.invoke(args)
1081
+ const toolOutput = await selectedTool.invoke(translated.args)
924
1082
  const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
925
1083
  emit({ t: 'tool_result', toolName, toolOutput: outputText })
926
1084
  // Don't overwrite fullResponse with raw tool output — it's already captured
@@ -932,8 +1090,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
932
1090
  }
933
1091
  calledNames.add(toolName)
934
1092
  return true
935
- } catch (forceErr: any) {
936
- 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)}` })
937
1095
  return false
938
1096
  } finally {
939
1097
  await cleanup()
@@ -1120,8 +1278,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1120
1278
  let changed = false
1121
1279
  const persistField = (key: string, value: unknown) => {
1122
1280
  const normalized = normalizeResumeId(value)
1123
- if ((current as any)[key] !== normalized) {
1124
- ;(current as any)[key] = normalized
1281
+ if ((current as Record<string, unknown>)[key] !== normalized) {
1282
+ ;(current as Record<string, unknown>)[key] = normalized
1125
1283
  changed = true
1126
1284
  }
1127
1285
  }
@@ -1135,10 +1293,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1135
1293
  const currentResume = (current.delegateResumeIds && typeof current.delegateResumeIds === 'object')
1136
1294
  ? current.delegateResumeIds
1137
1295
  : {}
1296
+ const sr = sourceResume as Record<string, unknown>
1297
+ const cr = currentResume as Record<string, unknown>
1138
1298
  const nextResume = {
1139
- claudeCode: normalizeResumeId((sourceResume as any).claudeCode ?? (currentResume as any).claudeCode),
1140
- codex: normalizeResumeId((sourceResume as any).codex ?? (currentResume as any).codex),
1141
- 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),
1142
1302
  }
1143
1303
  if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
1144
1304
  current.delegateResumeIds = nextResume
@@ -1147,7 +1307,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1147
1307
  }
1148
1308
 
1149
1309
  if (shouldPersistAssistant) {
1150
- const persistedKind = internal && source !== 'session-awakening' ? 'heartbeat' : 'chat'
1310
+ const persistedKind = internal && source === 'heartbeat' ? 'heartbeat' : 'chat'
1151
1311
  const persistedText = heartbeatClassification === 'strip'
1152
1312
  ? textForPersistence.replace(/HEARTBEAT_OK/gi, '').trim()
1153
1313
  : textForPersistence
@@ -1161,7 +1321,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1161
1321
  kind: persistedKind,
1162
1322
  }
1163
1323
  const previous = current.messages.at(-1)
1164
- if (shouldReplaceRecentAssistantMessage({
1324
+ if (previous?.streaming || shouldReplaceRecentAssistantMessage({
1165
1325
  previous,
1166
1326
  nextToolEvents: toolEvents,
1167
1327
  nextKind: persistedKind,
@@ -1188,12 +1348,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1188
1348
  // Target routing for non-suppressed heartbeat alerts
1189
1349
  if (isHeartbeatRun && heartbeatConfig?.target && heartbeatConfig.target !== 'none' && heartbeatConfig.showAlerts !== false) {
1190
1350
  try {
1351
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1191
1352
  const { listRunningConnectors, sendConnectorMessage } = require('./connectors/manager')
1192
1353
  let connectorId: string | undefined
1193
1354
  let channelId: string | undefined
1194
1355
  if (heartbeatConfig.target === 'last') {
1195
1356
  const running = listRunningConnectors()
1196
- const first = running.find((c: any) => c.recentChannelId)
1357
+ const first = running.find((c: { recentChannelId?: string }) => c.recentChannelId)
1197
1358
  if (first) {
1198
1359
  connectorId = first.id
1199
1360
  channelId = first.recentChannelId
@@ -1234,14 +1395,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1234
1395
  session: current,
1235
1396
  source,
1236
1397
  internal,
1237
- message,
1398
+ message: message,
1238
1399
  response: textForPersistence,
1239
1400
  now: Date.now(),
1240
1401
  })
1241
1402
  if (autoMemoryEligible) {
1242
1403
  const storedId = storeAutoMemoryNote({
1243
1404
  session: current,
1244
- message,
1405
+ message: message,
1245
1406
  response: textForPersistence,
1246
1407
  source,
1247
1408
  now: Date.now(),
@@ -1250,7 +1411,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1250
1411
  }
1251
1412
 
1252
1413
  // Don't extend idle timeout for heartbeat runs — only user-initiated activity counts
1253
- if (source !== 'heartbeat' && source !== 'main-loop-followup') {
1414
+ if (source !== 'heartbeat' && source !== 'heartbeat-wake' && source !== 'main-loop-followup') {
1254
1415
  current.lastActiveAt = Date.now()
1255
1416
  }
1256
1417
  fresh[sessionId] = current
@@ -4,23 +4,99 @@ export interface ClawHubSearchResult {
4
4
  skills: ClawHubSkill[]
5
5
  total: number
6
6
  page: number
7
+ nextCursor?: string | null
7
8
  }
8
9
 
9
- const CLAWHUB_BASE_URL = process.env.CLAWHUB_API_URL || 'https://clawhub.openclaw.dev/api'
10
+ const CLAWHUB_BASE_URL = process.env.CLAWHUB_API_URL || 'https://clawhub.ai/api/v1'
11
+
12
+ /**
13
+ * Raw shape returned by the ClawHub `/skills` endpoint.
14
+ * Fields are mapped to our internal `ClawHubSkill` type.
15
+ */
16
+ interface ClawHubRawItem {
17
+ slug: string
18
+ displayName?: string
19
+ name?: string
20
+ summary?: string
21
+ description?: string
22
+ author?: string | { name?: string }
23
+ tags?: Record<string, string> | string[]
24
+ stats?: { downloads?: number; installsAllTime?: number; stars?: number }
25
+ latestVersion?: { version?: string; changelog?: string }
26
+ metadata?: Record<string, unknown> | null
27
+ url?: string
28
+ createdAt?: number
29
+ updatedAt?: number
30
+ }
31
+
32
+ function mapRawToSkill(raw: ClawHubRawItem): ClawHubSkill {
33
+ const name = raw.displayName || raw.name || raw.slug
34
+ const description = raw.summary || raw.description || ''
35
+ const author = typeof raw.author === 'string'
36
+ ? raw.author
37
+ : raw.author?.name || 'community'
38
+ const tags = Array.isArray(raw.tags)
39
+ ? raw.tags
40
+ : raw.tags ? Object.keys(raw.tags) : []
41
+ const downloads = raw.stats?.installsAllTime ?? raw.stats?.downloads ?? 0
42
+ const version = raw.latestVersion?.version || '1.0.0'
43
+ return {
44
+ id: raw.slug,
45
+ name,
46
+ description,
47
+ author,
48
+ tags,
49
+ downloads,
50
+ url: raw.url || `https://clawhub.ai/skills/${raw.slug}`,
51
+ version,
52
+ }
53
+ }
10
54
 
11
55
  export async function searchClawHub(query: string, page = 1, limit = 20): Promise<ClawHubSearchResult> {
12
56
  try {
13
- const url = `${CLAWHUB_BASE_URL}/skills?q=${encodeURIComponent(query)}&page=${page}&limit=${limit}`
14
- const res = await fetch(url)
57
+ const params = new URLSearchParams({ limit: String(limit) })
58
+ if (query) params.set('q', query)
59
+ if (page > 1) params.set('page', String(page))
60
+
61
+ const url = `${CLAWHUB_BASE_URL}/skills?${params}`
62
+ const res = await fetch(url, { signal: AbortSignal.timeout(8000) })
15
63
  if (!res.ok) throw new Error(`ClawHub responded with ${res.status}`)
16
- return await res.json()
17
- } catch {
64
+
65
+ const data = await res.json() as { items?: ClawHubRawItem[]; skills?: ClawHubRawItem[]; nextCursor?: string | null; total?: number }
66
+
67
+ // ClawHub v1 returns { items, nextCursor }; fall back to { skills, total } for compat
68
+ const rawItems = data.items || data.skills || []
69
+ const skills = rawItems.map(mapRawToSkill)
70
+ const total = data.total ?? (data.nextCursor ? skills.length + 1 : skills.length)
71
+
72
+ return { skills, total, page, nextCursor: data.nextCursor }
73
+ } catch (err: unknown) {
74
+ console.warn('[clawhub] search failed:', err instanceof Error ? err.message : String(err))
18
75
  return { skills: [], total: 0, page }
19
76
  }
20
77
  }
21
78
 
22
79
  export async function fetchSkillContent(rawUrl: string): Promise<string> {
23
- const res = await fetch(rawUrl)
80
+ // ClawHub skill pages are at /skills/<slug> — try raw content endpoint first
81
+ let contentUrl = rawUrl
82
+ if (contentUrl.startsWith('https://clawhub.ai/skills/') && !contentUrl.includes('/raw')) {
83
+ const slug = contentUrl.replace('https://clawhub.ai/skills/', '').replace(/\/$/, '')
84
+ // Try the raw content API first
85
+ const rawApiUrl = `${CLAWHUB_BASE_URL}/skills/${slug}/content`
86
+ try {
87
+ const res = await fetch(rawApiUrl, { signal: AbortSignal.timeout(8000) })
88
+ if (res.ok) {
89
+ const data = await res.json() as { content?: string }
90
+ if (data.content) return data.content
91
+ }
92
+ } catch {
93
+ // Fall through to direct fetch
94
+ }
95
+ // Try the raw endpoint pattern
96
+ contentUrl = `https://clawhub.ai/skills/${slug}/raw`
97
+ }
98
+
99
+ const res = await fetch(contentUrl, { signal: AbortSignal.timeout(10000) })
24
100
  if (!res.ok) throw new Error(`Failed to fetch skill content: ${res.status}`)
25
101
  return res.text()
26
102
  }
@@ -335,6 +335,7 @@ export function isCurrentGeneration(connectorId: string, gen: number): boolean {
335
335
 
336
336
  /** Get platform implementation lazily */
337
337
  export async function getPlatform(platform: string) {
338
+ // 1. Check Built-ins
338
339
  switch (platform) {
339
340
  case 'discord': return (await import('./discord')).default
340
341
  case 'telegram': return (await import('./telegram')).default
@@ -347,8 +348,33 @@ export async function getPlatform(platform: string) {
347
348
  case 'googlechat': return (await import('./googlechat')).default
348
349
  case 'matrix': return (await import('./matrix')).default
349
350
  case 'email': return (await import('./email')).default
350
- default: throw new Error(`Unknown platform: ${platform}`)
351
351
  }
352
+
353
+ // 2. Check Plugin-provided connectors
354
+ try {
355
+ const { getPluginManager } = await import('../plugins')
356
+ const manager = getPluginManager()
357
+ const pluginConnectors = manager.getConnectors()
358
+ const found = pluginConnectors.find(c => c.id === platform)
359
+
360
+ if (found) {
361
+ return {
362
+ start: async (connector: Connector, token: string, onMessage: (msg: InboundMessage) => Promise<string>) => {
363
+ const stop = found.startListener ? await found.startListener(onMessage) : () => {}
364
+ return {
365
+ connector,
366
+ stop: async () => { if (stop) await stop() },
367
+ sendMessage: found.sendMessage,
368
+ authenticated: true,
369
+ }
370
+ }
371
+ }
372
+ }
373
+ } catch (err: unknown) {
374
+ console.warn(`[connector] Failed to check plugins for platform "${platform}":`, err instanceof Error ? err.message : String(err))
375
+ }
376
+
377
+ throw new Error(`Unknown platform: ${platform}`)
352
378
  }
353
379
 
354
380
  export function formatMediaLine(media: InboundMedia): string {