@swarmclawai/swarmclaw 0.7.7 → 0.8.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 (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -18,14 +18,14 @@ import { getProvider } from '@/lib/providers'
18
18
  import { estimateCost, checkAgentBudgetLimits } from './cost'
19
19
  import { log } from './logger'
20
20
  import { logExecution } from './execution-log'
21
- import { streamAgentChat } from './stream-agent-chat'
21
+ import { buildToolDisciplineLines, streamAgentChat } from './stream-agent-chat'
22
22
  import { runLinkUnderstanding } from './link-understanding'
23
23
  import { buildSessionTools } from './session-tools'
24
24
  import type { StructuredToolInterface } from '@langchain/core/tools'
25
25
  import type { Session } from '@/types'
26
26
  import { stripMainLoopMetaForPersistence } from './main-agent-loop'
27
27
  import { getPluginManager } from './plugins'
28
- import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
28
+ import { isLocalOpenClawEndpoint, normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
29
29
  import { routeTaskIntent } from './capability-router'
30
30
  import { notify } from './ws-hub'
31
31
  import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
@@ -38,6 +38,7 @@ import {
38
38
  setCachedLlmResponse,
39
39
  type LlmResponseCacheKeyInput,
40
40
  } from './llm-response-cache'
41
+ import { genId } from '@/lib/id'
41
42
  import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
42
43
  import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
43
44
  import { isHeartbeatSource, isInternalHeartbeatRun } from './heartbeat-source'
@@ -46,14 +47,19 @@ import { buildIdentityContinuityContext, refreshSessionIdentityState } from './i
46
47
  import { syncSessionArchiveMemory } from './session-archive-memory'
47
48
  import { evaluateSessionFreshness, resetSessionRuntime, resolveSessionResetPolicy } from './session-reset-policy'
48
49
  import { pruneStreamingAssistantArtifacts, upsertStreamingAssistantArtifact } from '@/lib/chat-streaming-state'
50
+ import { resolveActiveProjectContext } from './project-context'
51
+ import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from './assistant-control'
52
+ import { buildToolEventAssistantSummary } from '@/lib/tool-event-summary'
53
+ import { buildAgentDisabledMessage, isAgentDisabled } from './agent-availability'
49
54
  type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
50
55
 
51
56
  /** Slice history from the most recent context-clear marker forward */
52
57
  function applyContextClearBoundary(messages: Message[]): Message[] {
58
+ const filterModelHistory = (items: Message[]) => items.filter((message) => message.historyExcluded !== true)
53
59
  for (let i = messages.length - 1; i >= 0; i--) {
54
- if (messages[i].kind === 'context-clear') return messages.slice(i + 1)
60
+ if (messages[i].kind === 'context-clear') return filterModelHistory(messages.slice(i + 1))
55
61
  }
56
- return messages
62
+ return filterModelHistory(messages)
57
63
  }
58
64
 
59
65
  interface SessionWithTools {
@@ -119,6 +125,7 @@ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
119
125
  previous
120
126
  && previous.name === (ev.toolName || 'unknown')
121
127
  && previous.input === (ev.toolInput || '')
128
+ && previous.toolCallId === (ev.toolCallId || previous.toolCallId)
122
129
  && !previous.output
123
130
  ) {
124
131
  return
@@ -126,11 +133,14 @@ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
126
133
  bag.push({
127
134
  name: ev.toolName || 'unknown',
128
135
  input: ev.toolInput || '',
136
+ toolCallId: ev.toolCallId,
129
137
  })
130
138
  return
131
139
  }
132
140
  if (ev.t === 'tool_result') {
133
- const idx = bag.findLastIndex((e) => e.name === (ev.toolName || 'unknown') && !e.output)
141
+ const idx = ev.toolCallId
142
+ ? bag.findLastIndex((e) => e.toolCallId === ev.toolCallId && !e.output)
143
+ : bag.findLastIndex((e) => e.name === (ev.toolName || 'unknown') && !e.output)
134
144
  if (idx === -1) return
135
145
  const output = ev.toolOutput || ''
136
146
  bag[idx] = {
@@ -141,6 +151,25 @@ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
141
151
  }
142
152
  }
143
153
 
154
+ function escapeRegExp(value: string): string {
155
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
156
+ }
157
+
158
+ function hasExplicitToolMention(message: string, toolName: string): boolean {
159
+ const escaped = escapeRegExp(toolName)
160
+ const negated = new RegExp(`\\b(?:do not|don't|dont|avoid|skip|without|never)\\s+(?:use\\s+|call\\s+|invoke\\s+)?(?:the\\s+)?\`?${escaped}\`?(?:\\s+tool)?\\b`, 'i')
161
+ if (negated.test(message)) return false
162
+ const boundary = new RegExp(`(^|[^a-z0-9_])\`?${escaped}\`?([^a-z0-9_]|$)`, 'i')
163
+ return boundary.test(message)
164
+ }
165
+
166
+ function hasExplicitGenericToolRequest(message: string, toolName: string): boolean {
167
+ const escaped = escapeRegExp(toolName)
168
+ const negated = new RegExp(`\\b(?:do not|don't|dont|avoid|skip|without|never)\\s+(?:use\\s+|call\\s+|invoke\\s+)?(?:the\\s+)?${escaped}(?:\\s+tool)?\\b`, 'i')
169
+ if (negated.test(message)) return false
170
+ return new RegExp(`(^|[\\s(])\`${escaped}\`([\\s).,!?]|$)|\\b${escaped}\\s+tool\\b|\\buse\\s+(?:the\\s+)?${escaped}\\b|\\bcall\\s+(?:the\\s+)?${escaped}\\b|\\binvoke\\s+(?:the\\s+)?${escaped}\\b`, 'i').test(message)
171
+ }
172
+
144
173
  export function dedupeConsecutiveToolEvents(events: MessageToolEvent[]): MessageToolEvent[] {
145
174
  const sameEvent = (left: MessageToolEvent, right: MessageToolEvent): boolean => (
146
175
  left.name === right.name
@@ -177,6 +206,26 @@ export function dedupeConsecutiveToolEvents(events: MessageToolEvent[]): Message
177
206
  return deduped
178
207
  }
179
208
 
209
+ export function deriveTerminalRunError(params: {
210
+ errorMessage?: string
211
+ fullResponse: string
212
+ streamErrors: string[]
213
+ toolEvents: MessageToolEvent[]
214
+ internal: boolean
215
+ }): string | undefined {
216
+ if (params.errorMessage) return params.errorMessage
217
+
218
+ if (params.streamErrors.length > 0 && !params.fullResponse.trim()) {
219
+ return params.streamErrors[params.streamErrors.length - 1]
220
+ }
221
+
222
+ if (!params.internal && !params.fullResponse.trim() && params.toolEvents.length === 0) {
223
+ return 'Run completed without any response text, tool calls, or explicit error details. Check the provider configuration and try again.'
224
+ }
225
+
226
+ return undefined
227
+ }
228
+
180
229
  function extractDelegateResponse(outputText: string): string | null {
181
230
  try {
182
231
  const parsed = JSON.parse(outputText) as Record<string, unknown>
@@ -191,6 +240,8 @@ function extractDelegateResponse(outputText: string): string | null {
191
240
  const MANAGE_PLATFORM_RESOURCE_TO_TOOL: Record<string, string> = {
192
241
  agent: 'manage_agents',
193
242
  agents: 'manage_agents',
243
+ project: 'manage_projects',
244
+ projects: 'manage_projects',
194
245
  task: 'manage_tasks',
195
246
  tasks: 'manage_tasks',
196
247
  schedule: 'manage_schedules',
@@ -355,13 +406,32 @@ function shouldReplaceRecentAssistantMessage(params: {
355
406
  return prevTools === 0
356
407
  }
357
408
 
409
+ function hasPersistableAssistantPayload(text: string, thinking: string, toolEvents: MessageToolEvent[]): boolean {
410
+ return text.trim().length > 0 || thinking.trim().length > 0 || toolEvents.length > 0
411
+ }
412
+
413
+ function getPersistedAssistantText(text: string, toolEvents: MessageToolEvent[]): string {
414
+ const trimmed = text.trim()
415
+ if (trimmed) return trimmed
416
+ return buildToolEventAssistantSummary(toolEvents)
417
+ }
418
+
419
+ function getToolEventsSnapshotKey(toolEvents: MessageToolEvent[]): string {
420
+ return JSON.stringify(toolEvents.map((event) => [
421
+ event.name,
422
+ event.input,
423
+ event.output || '',
424
+ event.error === true,
425
+ event.toolCallId || '',
426
+ ]))
427
+ }
428
+
358
429
  export function pruneSuppressedHeartbeatStreamMessage(messages: Message[]): boolean {
359
430
  return pruneStreamingAssistantArtifacts(messages)
360
431
  }
361
432
 
362
433
  export function requestedToolNamesFromMessage(message: string): string[] {
363
- const lower = message.toLowerCase()
364
- const candidates = [
434
+ const explicitCandidates = [
365
435
  'delegate_to_claude_code',
366
436
  'delegate_to_codex_cli',
367
437
  'delegate_to_opencode_cli',
@@ -389,35 +459,106 @@ export function requestedToolNamesFromMessage(message: string): string[] {
389
459
  'wallet_tool',
390
460
  'http_request',
391
461
  'send_file',
462
+ 'sandbox_exec',
463
+ 'sandbox_list_runtimes',
464
+ 'schedule_wake',
465
+ 'spawn_subagent',
466
+ 'ask_human',
467
+ 'context_status',
468
+ 'context_summarize',
469
+ 'openclaw_nodes',
470
+ 'openclaw_workspace',
471
+ ]
472
+ const genericCandidates = [
392
473
  'browser',
393
474
  'web',
394
475
  'shell',
395
476
  'files',
396
477
  'edit_file',
397
- 'sandbox_exec',
398
- 'sandbox_list_runtimes',
399
478
  'git',
400
479
  'canvas',
401
- 'schedule_wake',
402
- 'spawn_subagent',
403
480
  'mailbox',
404
- 'ask_human',
405
481
  'document',
406
482
  'extract',
407
483
  'table',
408
484
  'crawl',
409
- 'context_status',
410
- 'context_summarize',
411
- 'openclaw_nodes',
412
- 'openclaw_workspace',
485
+ 'email',
413
486
  ]
414
- const requested = candidates.filter((name) => lower.includes(name.toLowerCase()))
415
- if (/(^|[\s(])`delegate`([\s).,!?]|$)|\bdelegate tool\b|\buse delegate\b/.test(lower)) {
487
+ const requested = explicitCandidates.filter((name) => hasExplicitToolMention(message, name))
488
+ for (const name of genericCandidates) {
489
+ if (hasExplicitGenericToolRequest(message, name)) requested.push(name)
490
+ }
491
+ if (hasExplicitGenericToolRequest(message, 'delegate')) {
416
492
  requested.push('delegate')
417
493
  }
418
494
  return Array.from(new Set(requested))
419
495
  }
420
496
 
497
+ function parseToolJsonObject(raw: string): Record<string, unknown> | null {
498
+ const trimmed = raw.trim()
499
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) return null
500
+ try {
501
+ const parsed = JSON.parse(trimmed)
502
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
503
+ ? parsed as Record<string, unknown>
504
+ : null
505
+ } catch {
506
+ return null
507
+ }
508
+ }
509
+
510
+ function summarizeConnectorToolFailure(output: string): string {
511
+ const trimmed = output.trim()
512
+ const withoutPrefix = trimmed.replace(/^Error:\s*/i, '')
513
+ const parsed = parseToolJsonObject(withoutPrefix) || parseToolJsonObject(trimmed)
514
+ if (parsed) {
515
+ const detail = parsed.detail
516
+ if (detail && typeof detail === 'object' && !Array.isArray(detail)) {
517
+ const detailRecord = detail as Record<string, unknown>
518
+ const message = typeof detailRecord.message === 'string' ? detailRecord.message.trim() : ''
519
+ if (message) return message
520
+ const code = typeof detailRecord.code === 'string' ? detailRecord.code.trim() : ''
521
+ const status = typeof detailRecord.status === 'string' ? detailRecord.status.trim() : ''
522
+ if (code && status) return `${code}: ${status}`
523
+ if (code) return code
524
+ if (status) return status
525
+ }
526
+ const message = typeof parsed.message === 'string' ? parsed.message.trim() : ''
527
+ if (message) return message
528
+ const error = typeof parsed.error === 'string' ? parsed.error.trim() : ''
529
+ if (error) return error
530
+ }
531
+ return withoutPrefix.replace(/\s+/g, ' ').trim() || 'Connector delivery failed.'
532
+ }
533
+
534
+ function connectorToolEventSucceeded(event: MessageToolEvent): boolean {
535
+ if (!event.output) return false
536
+ const parsed = parseToolJsonObject(event.output)
537
+ const status = typeof parsed?.status === 'string' ? parsed.status.trim().toLowerCase() : ''
538
+ return status === 'sent' || status === 'voice_sent' || status === 'scheduled'
539
+ }
540
+
541
+ const POSITIVE_CONNECTOR_DELIVERY_RE = /\b(?:i(?:'ve| have)?(?: successfully)? sent|i sent|successfully sent|sent to your|voice note (?:has been|was) sent|message (?:has been|was) sent)\b/i
542
+
543
+ export function reconcileConnectorDeliveryText(text: string, events: MessageToolEvent[]): string {
544
+ const trimmed = text.trim()
545
+ if (!trimmed || !POSITIVE_CONNECTOR_DELIVERY_RE.test(trimmed)) return text
546
+
547
+ const connectorEvents = dedupeConsecutiveToolEvents(events).filter((event) => event.name === 'connector_message_tool')
548
+ if (connectorEvents.length === 0) return text
549
+ if (connectorEvents.some((event) => connectorToolEventSucceeded(event))) return text
550
+
551
+ const latestFailure = [...connectorEvents]
552
+ .reverse()
553
+ .find((event) => event.error === true && typeof event.output === 'string' && event.output.trim())
554
+
555
+ const failureSummary = latestFailure?.output
556
+ ? summarizeConnectorToolFailure(latestFailure.output)
557
+ : 'I could not confirm that the connector actually sent anything.'
558
+
559
+ return `I couldn't send that through the configured connector. ${failureSummary}`.trim()
560
+ }
561
+
421
562
  function parseKeyValueArgs(raw: string): Record<string, string> {
422
563
  const out: Record<string, string> = {}
423
564
  const regex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*("([^"]*)"|'([^']*)'|[^\s,]+)/g
@@ -560,6 +701,17 @@ function hasToolEnabled(session: SessionWithTools, toolName: string): boolean {
560
701
  return pluginIdMatches(session?.plugins || session?.tools || [], toolName)
561
702
  }
562
703
 
704
+ export function hasDirectLocalCodingTools(session: SessionWithTools): boolean {
705
+ return [
706
+ 'shell',
707
+ 'execute_command',
708
+ 'files',
709
+ 'edit_file',
710
+ 'openclaw_workspace',
711
+ 'sandbox',
712
+ ].some((toolName) => hasToolEnabled(session, toolName))
713
+ }
714
+
563
715
  function enabledDelegationTools(session: SessionWithTools): DelegateTool[] {
564
716
  const tools: DelegateTool[] = []
565
717
  if (hasToolEnabled(session, 'claude_code') || hasToolEnabled(session, 'delegate')) tools.push('delegate_to_claude_code')
@@ -671,8 +823,39 @@ function syncSessionFromAgent(sessionId: string): void {
671
823
  }
672
824
  const isShortcutChat = session.shortcutForAgentId === agent.id || agent.threadSessionId === sessionId
673
825
  if (isShortcutChat) {
826
+ const desiredPlugins = Array.isArray(agent.plugins) ? [...agent.plugins] : []
827
+ const currentPlugins = Array.isArray(session.plugins) ? [...session.plugins] : []
828
+ if (JSON.stringify(currentPlugins) !== JSON.stringify(desiredPlugins)) {
829
+ session.plugins = desiredPlugins
830
+ changed = true
831
+ }
674
832
  if (session.shortcutForAgentId !== agent.id) { session.shortcutForAgentId = agent.id; changed = true }
675
833
  if (session.name !== agent.name) { session.name = agent.name; changed = true }
834
+ const desiredHeartbeatEnabled = agent.heartbeatEnabled ?? false
835
+ if ((session.heartbeatEnabled ?? false) !== desiredHeartbeatEnabled) {
836
+ session.heartbeatEnabled = desiredHeartbeatEnabled
837
+ changed = true
838
+ }
839
+ const desiredHeartbeatIntervalSec = agent.heartbeatIntervalSec ?? null
840
+ if ((session.heartbeatIntervalSec ?? null) !== desiredHeartbeatIntervalSec) {
841
+ session.heartbeatIntervalSec = desiredHeartbeatIntervalSec
842
+ changed = true
843
+ }
844
+ const desiredMemoryScopeMode = agent.memoryScopeMode ?? null
845
+ if ((((session as unknown as Record<string, unknown>).memoryScopeMode as string | null | undefined) ?? null) !== desiredMemoryScopeMode) {
846
+ ;(session as unknown as Record<string, unknown>).memoryScopeMode = desiredMemoryScopeMode
847
+ changed = true
848
+ }
849
+ const desiredMemoryTierPreference = agent.memoryTierPreference ?? null
850
+ if ((((session as unknown as Record<string, unknown>).memoryTierPreference as string | null | undefined) ?? null) !== desiredMemoryTierPreference) {
851
+ ;(session as unknown as Record<string, unknown>).memoryTierPreference = desiredMemoryTierPreference
852
+ changed = true
853
+ }
854
+ const desiredProjectId = agent.projectId ?? null
855
+ if ((session.projectId ?? null) !== desiredProjectId) {
856
+ session.projectId = desiredProjectId
857
+ changed = true
858
+ }
676
859
  }
677
860
 
678
861
  if (changed) {
@@ -728,6 +911,15 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
728
911
  }
729
912
  }
730
913
 
914
+ // 5b. Workspace context files (HEARTBEAT.md, IDENTITY.md, AGENTS.md, etc.)
915
+ try {
916
+ const { buildWorkspaceContext } = require('./workspace-context')
917
+ const wsCtx = buildWorkspaceContext({ cwd: session.cwd })
918
+ if (wsCtx.block) parts.push(wsCtx.block)
919
+ } catch {
920
+ // Workspace context is non-critical
921
+ }
922
+
731
923
  // 6. Thinking & Output Format (OpenClaw Style)
732
924
  const thinkingHint = [
733
925
  '## Output Format',
@@ -737,6 +929,14 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
737
929
  ]
738
930
  parts.push(thinkingHint.join('\n'))
739
931
 
932
+ const enabledPlugins = Array.isArray(session.plugins) ? session.plugins : (Array.isArray(agent.plugins) ? agent.plugins : [])
933
+ const toolDisciplineLines = buildToolDisciplineLines(enabledPlugins)
934
+ if (toolDisciplineLines.length > 0) parts.push(['## Tool Discipline', ...toolDisciplineLines].join('\n'))
935
+ const operatingGuidance = getPluginManager().collectOperatingGuidance(enabledPlugins)
936
+ if (operatingGuidance.length > 0) parts.push(['## Tool Guidance', ...operatingGuidance].join('\n'))
937
+ const capabilityLines = getPluginManager().collectCapabilityDescriptions(enabledPlugins)
938
+ if (capabilityLines.length > 0) parts.push(['## Tool Capabilities', ...capabilityLines].join('\n'))
939
+
740
940
  // 7. Heartbeat Guidance
741
941
  parts.push([
742
942
  '## Heartbeats',
@@ -826,8 +1026,34 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
826
1026
 
827
1027
  const appSettings = loadSettings()
828
1028
  const agentForSession = session.agentId ? loadAgents()[session.agentId] : null
1029
+ if (isAgentDisabled(agentForSession)) {
1030
+ const disabledError = buildAgentDisabledMessage(agentForSession, 'run chats')
1031
+ onEvent?.({ t: 'err', text: disabledError })
1032
+
1033
+ let persisted = false
1034
+ if (!internal) {
1035
+ session.messages.push({
1036
+ role: 'assistant',
1037
+ text: disabledError,
1038
+ time: Date.now(),
1039
+ })
1040
+ session.lastActiveAt = Date.now()
1041
+ saveSessions(sessions)
1042
+ persisted = true
1043
+ }
1044
+
1045
+ return {
1046
+ runId,
1047
+ sessionId,
1048
+ text: disabledError,
1049
+ persisted,
1050
+ toolEvents: [],
1051
+ error: disabledError,
1052
+ }
1053
+ }
829
1054
  const toolPolicy = resolveSessionToolPolicy(session.plugins, appSettings)
830
1055
  const isHeartbeatRun = isInternalHeartbeatRun(internal, source)
1056
+ const isAutonomousInternalRun = internal && source !== 'chat'
831
1057
  const isAutoRunNoHistory = isHeartbeatRun
832
1058
  const heartbeatStatusOnly = false
833
1059
  if (shouldApplySessionFreshnessReset(source)) {
@@ -847,6 +1073,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
847
1073
  saveSessions(sessions)
848
1074
  }
849
1075
  }
1076
+ if (isAutonomousInternalRun) {
1077
+ try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* archive sync is best-effort */ }
1078
+ }
850
1079
  const pluginsForRun = heartbeatStatusOnly ? [] : toolPolicy.enabledPlugins
851
1080
  let sessionForRun = pluginsForRun === session.plugins
852
1081
  ? session
@@ -1017,9 +1246,72 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1017
1246
 
1018
1247
  let thinkingText = ''
1019
1248
  let streamingPartialText = ''
1249
+ let lastPartialSaveAt = 0
1250
+ let lastPartialSnapshotKey = ''
1251
+ let partialSaveTimeout: ReturnType<typeof setTimeout> | null = null
1252
+
1253
+ const persistStreamingAssistantArtifact = () => {
1254
+ partialSaveTimeout = null
1255
+ const persistedToolEvents = toolEvents.length ? dedupeConsecutiveToolEvents([...toolEvents]) : []
1256
+ if (!hasPersistableAssistantPayload(streamingPartialText, thinkingText, persistedToolEvents)) return
1257
+
1258
+ const snapshotKey = JSON.stringify([
1259
+ streamingPartialText,
1260
+ thinkingText,
1261
+ getToolEventsSnapshotKey(persistedToolEvents),
1262
+ ])
1263
+ if (snapshotKey === lastPartialSnapshotKey) return
1264
+
1265
+ lastPartialSnapshotKey = snapshotKey
1266
+ lastPartialSaveAt = Date.now()
1267
+
1268
+ try {
1269
+ const fresh = loadSessions()
1270
+ const current = fresh[sessionId]
1271
+ if (!current) return
1272
+ current.messages = Array.isArray(current.messages) ? current.messages : []
1273
+ const partialMsg: Message = {
1274
+ role: 'assistant',
1275
+ text: streamingPartialText,
1276
+ time: Date.now(),
1277
+ streaming: true,
1278
+ thinking: thinkingText || undefined,
1279
+ toolEvents: persistedToolEvents.length ? persistedToolEvents : undefined,
1280
+ }
1281
+ upsertStreamingAssistantArtifact(current.messages, partialMsg, {
1282
+ minIndex: runMessageStartIndex,
1283
+ minTime: runStartedAt,
1284
+ })
1285
+ fresh[sessionId] = current
1286
+ saveSessions(fresh)
1287
+ notify(`messages:${sessionId}`)
1288
+ } catch { /* partial save is best-effort */ }
1289
+ }
1290
+
1291
+ const queuePartialAssistantPersist = (immediate = false) => {
1292
+ const now = Date.now()
1293
+ const minIntervalMs = 400
1294
+ if (immediate || now - lastPartialSaveAt >= minIntervalMs) {
1295
+ if (partialSaveTimeout) {
1296
+ clearTimeout(partialSaveTimeout)
1297
+ partialSaveTimeout = null
1298
+ }
1299
+ persistStreamingAssistantArtifact()
1300
+ return
1301
+ }
1302
+ if (partialSaveTimeout) return
1303
+ partialSaveTimeout = setTimeout(() => {
1304
+ persistStreamingAssistantArtifact()
1305
+ }, minIntervalMs - (now - lastPartialSaveAt))
1306
+ }
1307
+
1020
1308
  const emit = (ev: SSEEvent) => {
1309
+ let shouldPersistPartial = false
1310
+ let immediatePartialPersist = false
1021
1311
  if (ev.t === 'd' && typeof ev.text === 'string') {
1022
1312
  streamingPartialText += ev.text
1313
+ shouldPersistPartial = true
1314
+ immediatePartialPersist = streamingPartialText.length === ev.text.length
1023
1315
  }
1024
1316
  if (ev.t === 'err' && typeof ev.text === 'string') {
1025
1317
  const trimmed = ev.text.trim()
@@ -1030,6 +1322,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1030
1322
  }
1031
1323
  if (ev.t === 'thinking' && ev.text) {
1032
1324
  thinkingText += ev.text
1325
+ shouldPersistPartial = true
1033
1326
  }
1034
1327
  if (ev.t === 'md' && ev.text) {
1035
1328
  try {
@@ -1043,36 +1336,18 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1043
1336
  } catch { /* ignore non-JSON md events */ }
1044
1337
  }
1045
1338
  collectToolEvent(ev, toolEvents)
1339
+ if (ev.t === 'tool_call' || ev.t === 'tool_result') {
1340
+ shouldPersistPartial = true
1341
+ immediatePartialPersist = true
1342
+ }
1343
+ if (shouldPersistPartial) queuePartialAssistantPersist(immediatePartialPersist)
1046
1344
  onEvent?.(ev)
1047
1345
  }
1048
1346
 
1049
1347
  // Periodic partial save so a browser refresh doesn't lose the in-flight response.
1050
- let lastPartialSaveLen = 0
1051
- const PARTIAL_SAVE_INTERVAL_MS = 5000
1348
+ const PARTIAL_SAVE_INTERVAL_MS = 2000
1052
1349
  const partialSaveTimer = setInterval(() => {
1053
- if (streamingPartialText.length > lastPartialSaveLen) {
1054
- lastPartialSaveLen = streamingPartialText.length
1055
- try {
1056
- const fresh = loadSessions()
1057
- const current = fresh[sessionId]
1058
- if (!current) return
1059
- current.messages = Array.isArray(current.messages) ? current.messages : []
1060
- const partialMsg: Message = {
1061
- role: 'assistant',
1062
- text: streamingPartialText,
1063
- time: Date.now(),
1064
- streaming: true,
1065
- toolEvents: toolEvents.length ? dedupeConsecutiveToolEvents([...toolEvents]) : undefined,
1066
- }
1067
- upsertStreamingAssistantArtifact(current.messages, partialMsg, {
1068
- minIndex: runMessageStartIndex,
1069
- minTime: runStartedAt,
1070
- })
1071
- fresh[sessionId] = current
1072
- saveSessions(fresh)
1073
- notify(`messages:${sessionId}`)
1074
- } catch { /* partial save is best-effort */ }
1075
- }
1350
+ persistStreamingAssistantArtifact()
1076
1351
  }, PARTIAL_SAVE_INTERVAL_MS)
1077
1352
 
1078
1353
  const parseAndEmit = (raw: string) => {
@@ -1105,7 +1380,10 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1105
1380
  const responseCacheConfig = resolveLlmResponseCacheConfig(appSettings)
1106
1381
  let responseCacheHit = false
1107
1382
  let responseCacheInput: LlmResponseCacheKeyInput | null = null
1108
- const hasPlugins = !!(sessionForRun.plugins?.length || sessionForRun.tools?.length) && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
1383
+ const useLocalOpenClawNativeRuntime = providerType === 'openclaw' && isLocalOpenClawEndpoint(sessionForRun.apiEndpoint)
1384
+ const hasPlugins = !!(sessionForRun.plugins?.length || sessionForRun.tools?.length)
1385
+ && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
1386
+ && !useLocalOpenClawNativeRuntime
1109
1387
 
1110
1388
  let durationMs = 0
1111
1389
  const startTs = Date.now()
@@ -1117,9 +1395,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1117
1395
  ? getSessionMessages(sessionId).slice(-6)
1118
1396
  : undefined
1119
1397
 
1120
- console.log(`[chat-execution] provider=${providerType}, hasPlugins=${hasPlugins}, imagePath=${imagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, plugins=${(sessionForRun.plugins || sessionForRun.tools || []).length}`)
1398
+ console.log(`[chat-execution] provider=${providerType}, hasPlugins=${hasPlugins}, localOpenClawNative=${useLocalOpenClawNativeRuntime}, imagePath=${imagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, plugins=${(sessionForRun.plugins || sessionForRun.tools || []).length}`)
1121
1399
  if (hasPlugins) {
1122
- fullResponse = (await streamAgentChat({
1400
+ const result = await streamAgentChat({
1123
1401
  session: sessionForRun,
1124
1402
  message: effectiveMessage,
1125
1403
  imagePath,
@@ -1129,7 +1407,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1129
1407
  write: (raw) => parseAndEmit(raw),
1130
1408
  history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
1131
1409
  signal: abortController.signal,
1132
- })).fullText
1410
+ })
1411
+ fullResponse = result.finalResponse || result.fullText
1133
1412
  } else {
1134
1413
  const directHistorySnapshot = isAutoRunNoHistory
1135
1414
  ? getSessionMessages(sessionId).slice(-6)
@@ -1201,6 +1480,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1201
1480
  })
1202
1481
  } finally {
1203
1482
  clearInterval(partialSaveTimer)
1483
+ if (partialSaveTimeout) clearTimeout(partialSaveTimeout)
1204
1484
  active.delete(sessionId)
1205
1485
  if (signal) signal.removeEventListener('abort', abortFromOutside)
1206
1486
  }
@@ -1261,12 +1541,18 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1261
1541
  return false
1262
1542
  }
1263
1543
  const agent = session.agentId ? loadAgents()[session.agentId] : null
1544
+ const activeProjectContext = resolveActiveProjectContext(session)
1264
1545
  const { tools, cleanup } = await buildSessionTools(session.cwd, sessionForRun.plugins || sessionForRun.tools || [], {
1265
1546
  agentId: session.agentId || null,
1266
1547
  sessionId,
1267
1548
  platformAssignScope: agent?.platformAssignScope || 'self',
1268
1549
  mcpServerIds: agent?.mcpServerIds,
1269
1550
  mcpDisabledTools: agent?.mcpDisabledTools,
1551
+ projectId: activeProjectContext.projectId,
1552
+ projectRoot: activeProjectContext.projectRoot,
1553
+ projectName: activeProjectContext.project?.name || null,
1554
+ projectDescription: activeProjectContext.project?.description || null,
1555
+ memoryScopeMode: (((session as unknown as Record<string, unknown>).memoryScopeMode as string | null | undefined) ?? agent?.memoryScopeMode ?? null),
1270
1556
  })
1271
1557
  try {
1272
1558
  const directTool = tools.find((t) => t?.name === toolName) as StructuredToolInterface | undefined
@@ -1277,10 +1563,11 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1277
1563
  const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
1278
1564
  if (!selectedTool?.invoke) return false
1279
1565
  const toolInput = JSON.stringify(translated.args)
1280
- emit({ t: 'tool_call', toolName, toolInput })
1566
+ const toolCallId = genId()
1567
+ emit({ t: 'tool_call', toolName, toolInput, toolCallId })
1281
1568
  const toolOutput = await selectedTool.invoke(translated.args)
1282
1569
  const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
1283
- emit({ t: 'tool_result', toolName, toolOutput: outputText })
1570
+ emit({ t: 'tool_result', toolName, toolOutput: outputText, toolCallId })
1284
1571
  const delegateResponse = (
1285
1572
  toolName === 'delegate'
1286
1573
  || toolName.startsWith('delegate_to_')
@@ -1334,6 +1621,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1334
1621
  const shouldAutoDelegateCoding = (!internal && source === 'chat')
1335
1622
  && enabledDelegateTools.length > 0
1336
1623
  && !hasDelegationCall
1624
+ && calledNames.size === 0
1625
+ && !requestedToolNames.length
1626
+ && !hasDirectLocalCodingTools(sessionForRun)
1337
1627
  && routingDecision?.intent === 'coding'
1338
1628
 
1339
1629
  if (shouldAutoDelegateCoding) {
@@ -1422,10 +1712,28 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1422
1712
  }
1423
1713
  }
1424
1714
 
1425
- if (!errorMessage && streamErrors.length > 0 && !(fullResponse || '').trim()) {
1426
- errorMessage = streamErrors[streamErrors.length - 1]
1715
+ const terminalError = deriveTerminalRunError({
1716
+ errorMessage,
1717
+ fullResponse: fullResponse || '',
1718
+ streamErrors,
1719
+ toolEvents,
1720
+ internal,
1721
+ })
1722
+ if (terminalError && terminalError !== errorMessage) {
1723
+ if (!errorMessage) {
1724
+ log.warn('chat-run', `Run ended without a visible response for session ${sessionId}`, {
1725
+ runId,
1726
+ source,
1727
+ internal,
1728
+ provider: providerType,
1729
+ messagePreview: effectiveMessage.slice(0, 200),
1730
+ inferredError: terminalError,
1731
+ })
1732
+ }
1733
+ errorMessage = terminalError
1427
1734
  }
1428
1735
 
1736
+ const persistedToolEvents = dedupeConsecutiveToolEvents(toolEvents)
1429
1737
  let finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
1430
1738
  if (pluginsForRun.length > 0 && finalText && !isHeartbeatRun) {
1431
1739
  try {
@@ -1436,27 +1744,30 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1436
1744
  )
1437
1745
  } catch { /* outbound transforms are non-critical */ }
1438
1746
  }
1747
+ finalText = reconcileConnectorDeliveryText(finalText, persistedToolEvents)
1439
1748
  finalText = normalizeAssistantArtifactLinks(finalText, session.cwd)
1440
- const textForPersistence = stripMainLoopMetaForPersistence(finalText)
1441
- const persistedToolEvents = dedupeConsecutiveToolEvents(toolEvents)
1749
+ const rawTextForPersistence = stripMainLoopMetaForPersistence(finalText)
1750
+ const hiddenControlOnly = shouldSuppressHiddenControlText(rawTextForPersistence)
1751
+ const textForPersistence = stripHiddenControlTokens(rawTextForPersistence)
1752
+ const persistedText = getPersistedAssistantText(textForPersistence, persistedToolEvents)
1442
1753
 
1443
- if (isHeartbeatRun && finalText) {
1444
- const heartbeatStatus = extractHeartbeatStatus(finalText)
1754
+ if (isHeartbeatRun && rawTextForPersistence) {
1755
+ const heartbeatStatus = extractHeartbeatStatus(rawTextForPersistence)
1445
1756
  if (heartbeatStatus) emit({ t: 'status', text: JSON.stringify(heartbeatStatus) })
1446
1757
  }
1447
1758
 
1448
1759
  // HEARTBEAT_OK suppression
1449
1760
  const heartbeatConfig = input.heartbeatConfig
1450
1761
  let heartbeatClassification: 'suppress' | 'strip' | 'keep' | null = null
1451
- if (isHeartbeatRun && textForPersistence.length > 0) {
1452
- heartbeatClassification = classifyHeartbeatResponse(textForPersistence, heartbeatConfig?.ackMaxChars ?? 300, toolEvents.length > 0)
1762
+ if (isHeartbeatRun && rawTextForPersistence.length > 0) {
1763
+ heartbeatClassification = classifyHeartbeatResponse(rawTextForPersistence, heartbeatConfig?.ackMaxChars ?? 300, toolEvents.length > 0)
1453
1764
 
1454
1765
  // Deduplication logic from OpenClaw (nagging prevention)
1455
1766
  // If the model repeats itself exactly within 24h, suppress the heartbeat alert.
1456
1767
  if (heartbeatClassification !== 'suppress' && !toolEvents.length) {
1457
1768
  const prevText = session.lastHeartbeatText || ''
1458
1769
  const prevSentAt = session.lastHeartbeatSentAt || 0
1459
- const isDuplicate = prevText.trim() === textForPersistence.trim()
1770
+ const isDuplicate = prevText.trim() === persistedText.trim()
1460
1771
  && (Date.now() - prevSentAt) < 24 * 60 * 60 * 1000
1461
1772
  if (isDuplicate) {
1462
1773
  heartbeatClassification = 'suppress'
@@ -1469,7 +1780,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1469
1780
  notify(`heartbeat:agent:${session.agentId}`)
1470
1781
  }
1471
1782
 
1472
- const shouldPersistAssistant = textForPersistence.length > 0
1783
+ const shouldPersistAssistant = !hiddenControlOnly
1784
+ && hasPersistableAssistantPayload(persistedText, thinkingText, persistedToolEvents)
1473
1785
  && heartbeatClassification !== 'suppress'
1474
1786
 
1475
1787
  const normalizeResumeId = (value: unknown): string | null =>
@@ -1480,16 +1792,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1480
1792
  if (current) {
1481
1793
  current.messages = Array.isArray(current.messages) ? current.messages : []
1482
1794
  const currentAgent = current.agentId ? loadAgents()[current.agentId] : null
1483
- let changed = false
1484
- changed = pruneStreamingAssistantArtifacts(current.messages, {
1795
+ pruneStreamingAssistantArtifacts(current.messages, {
1485
1796
  minIndex: runMessageStartIndex,
1486
1797
  minTime: runStartedAt,
1487
- }) || changed
1798
+ })
1488
1799
  const persistField = (key: string, value: unknown) => {
1489
1800
  const normalized = normalizeResumeId(value)
1490
1801
  if ((current as Record<string, unknown>)[key] !== normalized) {
1491
1802
  ;(current as Record<string, unknown>)[key] = normalized
1492
- changed = true
1493
1803
  }
1494
1804
  }
1495
1805
 
@@ -1508,18 +1818,15 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1508
1818
  claudeCode: normalizeResumeId(sr.claudeCode ?? cr.claudeCode),
1509
1819
  codex: normalizeResumeId(sr.codex ?? cr.codex),
1510
1820
  opencode: normalizeResumeId(sr.opencode ?? cr.opencode),
1821
+ gemini: normalizeResumeId(sr.gemini ?? cr.gemini),
1511
1822
  }
1512
1823
  if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
1513
1824
  current.delegateResumeIds = nextResume
1514
- changed = true
1515
1825
  }
1516
1826
  }
1517
1827
 
1518
1828
  if (shouldPersistAssistant) {
1519
1829
  const persistedKind = isHeartbeatRun ? 'heartbeat' : 'chat'
1520
- const persistedText = heartbeatClassification === 'strip'
1521
- ? textForPersistence.replace(/HEARTBEAT_OK/gi, '').trim()
1522
- : textForPersistence
1523
1830
  const nowTs = Date.now()
1524
1831
  const nextAssistantMessage: Message = {
1525
1832
  role: 'assistant',
@@ -1544,7 +1851,6 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1544
1851
  current.lastHeartbeatText = persistedText
1545
1852
  current.lastHeartbeatSentAt = nowTs
1546
1853
  }
1547
- changed = true
1548
1854
  try {
1549
1855
  await getPluginManager().runHook('onMessage', { session: current, message: nextAssistantMessage }, { enabledIds: pluginsForRun })
1550
1856
  } catch { /* onMessage hooks are non-critical */ }
@@ -1603,7 +1909,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1603
1909
  }
1604
1910
  }
1605
1911
  if (isHeartbeatRun && heartbeatClassification === 'suppress') {
1606
- changed = pruneSuppressedHeartbeatStreamMessage(current.messages) || changed
1912
+ pruneSuppressedHeartbeatStreamMessage(current.messages)
1607
1913
  }
1608
1914
 
1609
1915
  // Fire afterChatTurn hook for all enabled plugins (memory auto-save, logging, etc.)
@@ -1614,6 +1920,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1614
1920
  response: textForPersistence,
1615
1921
  source,
1616
1922
  internal,
1923
+ toolEvents: persistedToolEvents,
1617
1924
  }, { enabledIds: pluginsForRun })
1618
1925
  } catch { /* afterChatTurn hooks are non-critical */ }
1619
1926
 
@@ -1623,10 +1930,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1623
1930
  }
1624
1931
 
1625
1932
  refreshSessionIdentityState(current, currentAgent)
1626
- changed = true
1627
1933
  try {
1628
- const archiveSync = syncSessionArchiveMemory(current, { agent: currentAgent })
1629
- if (archiveSync.stored) changed = true
1934
+ syncSessionArchiveMemory(current, { agent: currentAgent })
1630
1935
  } catch { /* archive sync is best-effort */ }
1631
1936
  fresh[sessionId] = current
1632
1937
  saveSessions(fresh)
@@ -1636,7 +1941,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1636
1941
  return {
1637
1942
  runId,
1638
1943
  sessionId,
1639
- text: finalText,
1944
+ text: hiddenControlOnly ? '' : textForPersistence,
1640
1945
  persisted: shouldPersistAssistant,
1641
1946
  toolEvents: persistedToolEvents,
1642
1947
  error: errorMessage,